На самом деле так выглядел бы Ассемблер, если бы он был оружием, но с C тоже надо быть предельно аккуратным

От переводчика:
Данная публикация является переводом статьи-ответа на текст «How to C in 2016». Перевод последнего был опубликован мной в пятницу и вызвал, местами, неоднозначную реакцию сообщества. Наводку на данный «ответ», для поддержания обсуждения вопроса уже в рамках Хабра, дал пользователь CodeRush, за что ему отдельное спасибо.


Ранее в сети была опубликована статья «Программирование на С в 2016 году» с множеством полезных советов, среди которых, увы, были и не очень удачные идеи. Именно поэтому я решил прокомментировать соответствующие моменты. Пока я готовил новый материал, кто-то заметил, что за работу на C должны браться только ответственные программисты, в то время как безответственным хватит и других языков, в рамках которых имеется больше возможностей для совершенствования имеющихся навыков. Давайте разбираться в секретах специалистов своего дела.

Используйте отладчик


Пункт №1, который вы, наверняка, игнорируете, а зря, — запускать для каждой строки кода отладчик уровня ядра прямо на этапе написания. Если вы задействуете потенциал данного инструмента только для решения особо сложных задач, вы, определенно, допускаете ошибку.

Я имею в виду использование таких IDE, как Visual Studio, XCode, или Eclipse. Если в данном случае вы работаете только с редактором (без возможности отладки), значит, вы плохо делаете свое дело. Я упоминаю об этом нюансе, потому что очень многие пишут код в редакторах, в которых не предусмотрена функция отладки. И я не исключение.

Это важно при программировании на всех языках, но на С особенно. При повреждении памяти вам понадобится дамп структур и содержимого памяти для того, чтобы обнаружить ошибку. Почему Х выдает какое-то странное 37653? Команда отладки в стиле printf() не прояснит ситуацию, но заглянув в данные утилиты hexdump в стеке, вы обязательно поймете, как переписывался определенный объем информации.

Не забывайте об отладке своего кода


Поскольку в языке C не предусмотрена защита памяти, ошибка, допущенная в одном месте, может появиться и в другом, даже если эта часть кода не будет связана с поврежденной. Вот почему так трудно проводить отладку некоторых проблемных участков кода. В таких случаях многие программисты рвут на себе волосы, кричат: «Я не могу это исправить» и умоляют коллег помочь.

Не наступайте на те же грабли. Пару раз столкнувшись с подобными «неразрешимыми» задачами, вы научитесь писать более качественный код. Это будет самотестирующейся код, который оперативно выявляет баги, или в ходе написания будут использоваться эффективные проверочные утилиты для целых блоков данных, охватывающие пограничные случаи.

Активно защищайте код


Однажды мне довелось работать над проектом, руководители которого во избежание сбоя в программе решили ставить catch(...) (в C ++ ) везде. Исключения и даже случаи повреждения памяти просто незаметно маскировались, и программа продолжала функционировать. Разработчикам казалось, что они снижали уязвимость кода. Они думали, что защитный стиль программирования – отличное решение.

Но это не защита, а глупость. Где логика в том, чтобы скрывать ошибки, обнаружить которые впоследствии гораздо сложнее?

Делаете по-другому. Вам нужен принцип атаки, код, который будет выявлять недочеты в максимально сжатые сроки.

Одним из способов воплотить этот подход в реальность является assert() — двойные проверочные предположения, благодаря которым вы, наверняка, все сделаете правильно. Данная функция обнаруживает баги прежде чем они навредят памяти. Я серьезно: для отладки незаметных ошибок на С я просто вставляю assert() везде, где они могут спровоцировать сбой (только не увлекайтесь предложенной командой).

Если мы говорим о кодах, лучший способ «напасть заранее» — это модульное тестирование. Как только у вас возникают сомнения, пишите модульные тесты для проверки неоднозначных параметров. Существует мнение, будто, работая на С, легко запутаться, когда изначально все идет по плану, а потом всплывают странности – для таких случаев тоже неплохо иметь в запасе подходящие тесты.

Код должен быть качественным


Что касается этого пункта, такие вещи, как модульное, регрессионное тестирование и даже фаззинг постепенно становятся нормой. Если у вас есть проект с открытым исходным кодом, вам просто необходимо запросить опцию make test, чтобы провести его качественный анализ. С ее помощью вы сможете запустить модульное тестирование кода с высоким покрытием. Такой подход считается стандартом для крупных проектов с открытым исходным кодом. Я не шучу: модульное тестирование с высоким покрытием кода должно быть отправной точкой для каждого нового проекта. Вы заметите это во всех моих серьезных разработках с открытым исходным кодом. Тут я начинаю писать модульные тексты уже на начальном этапе, причем один за другим (правда, я ленивый, и поэтому не могу похвастаться высоким покрытием кода).

Фаззинг посредством AFL – явление относительно новое, но, протестировав данный механизм, вы поймете, насколько он эффективен в процессе идентификации багов в различных проектах с открытым кодом. Язык программирования С бьет все рекорды, когда речь заходит о парсинге из вне. Раньше нередко возникали сбои в программах из-за плохо отформатированных файлов или никудышных пакетов сети. Но в 2016 году никто не станет терпеть подобную ерунду. Если вы не уверены в том, что, независимо от вводимых данных, программа, наверняка, будет работать корректно, значит, вы где-то ошиблись.

И, если вам кажется, что о качестве должен заботиться кто-то другой, вы снова заблуждаетесь.

Забудьте о глобальных переменных


Когда я работаю над проектами с открытым исходным кодом на C/C ++, именно глобальные переменные не дают мне жить. Вам трудно проводить отладку проекта и задавать параметры многопоточности, потому что вы устроили настоящий рассадник глобальных переменных. Минимизируйте их использование – и рефакторинг кода станет куда эффективнее.

Да, если мы говорим о системе отладки/ регистрации статуса, обращаться к глобальным переменным даже нужно, а в остальных случаях – просто забудьте об их существовании.


Немного ООП, добавим функциональное программирование и чуть-чуть Java


Нередко шутят, что «я могу программировать на FORTRAN на любом ЯП», ссылаясь на то, что программисты часто используют тот или иной язык не по назначению, пытаясь свести его к знакомым инструментам. Но это как сказать, что немые программисты остаются немыми, с каким бы языком программирования не столкнулись. Хотя иногда универсальные свойства, присущие различным системам программирования тоже отлично работают.

В объектно-ориентированном программировании нас интересует то, как концепция структуры объединяет данные и методы, которые необходимы для обработки информации. Для struct Fooba вы создаете ряд функций, которые сводятся к foo_xxxx(). К вашим услугам конструктор foo_create(), деструктор foo_destroy() и море функций, определяющих структуру.

Самое главное: описывать struct Foobar в файле C, а не в файле заголовка. Пусть функции будут общедоступными, но вот точный формат структуры, желательно, скрыть. Как правило, на структуры даются прямые ссылки, что особенно важно для библиотек, экспорт заголовков которых сказывается на совместимости бинарных интерфейсов приложений (так как меняется размер структуры). Если необходимо экспортировать структуру, в качестве первого параметра указывайте версию или размер.

Нет, я не собираюсь вдаваться в подробности ООП вплоть до наследования и полиморфизма. Наоборот, хотелось просто отметить, что можно взять на заметку преимущества модульного программирования, которые, в принципе, схожи с ООП.

Кроме того, никто не отменял несколько хороших идей и в функциональном программировании, а именно те из присущих ему функций, которые не имеют «побочных эффектов». Это опции, рассчитанные на то, что есть исходные данные и определенная информация на выходе. Никакой самодеятельности. Большинство написанных вами функций должны выглядеть именно так. Если же вы сочиняете что-то вроде void foobar(void); — ждите неприятностей.

В списке инструментов, заметно усложняющих жизнь, упоминаются и глобальные переменные. В эту же категорию попадают и вызовы системы, снижающие ее производительность. Глобальные переменные, по сути, похожи на переменные, скрытые глубоко в теле структуры, которые вы вызываете через int foobar(struct Xyz *p);. В результате приходится копаться в недрах р, чтобы найти необходимые параметры. Проще, когда все это лежит на поверхности, тогда запрос формируется по типу foobar(p->length, p->socket->status, p->bbb). Да, в этом случае приходится работать с длинными, раздражающими списками параметров, но так вместо сложной структуры функция foobar() зависит от простых типов.

Отчасти подобный функциональный подход к программированию обусловлен использованием констант, где указатели задаются через const, а, значит, функция не может их изменить. Зато ясно, где результаты (возвращаемое значение и неконстантные указатели), а где исходные данные.

С – язык программирования систем низкого уровня, но, если не считать явно сложных случаев, постарайтесь им не злоупотреблять. Вместо конкретных приемов С используйте инструменты, с помощью которых ваш код будет жизнеспособным в более широкой среде, пусть даже и похожей на условия С. Так вы сможете интегрировать его с JavaScript, Java, C # и др.

Придется обойтись без арифметики указателей. Да, в 1980-х она позволила существенно ускорить код, но с 1990-х от нее больше никакой пользы, особенно, в случае с современными оптимизирующими компиляторами. Арифметика указателей только снижает читабельность кода. Почти всякий раз, когда проект с открытым исходным кодом подвергается воздействию кибермошенников (Hearbleed (по всей видимости, имелось ввиду Heartbleed прим.), Shellshock, и т.д.), ищите причину в коде с арифметикой указателей. В качестве альтернативы обратите внимание на переменные целочисленных индексов и проанализируйте соответствующие массивы данных, как если бы вы писали это на Java.

Такой идеальный подход к написанию кода подразумевает и отказ от парсинга сетевых протоколов/форматов файлов для структур/целых. Да, пособие по сетевому протоколированию советует использовать что-то вроде noths(*(short*)p), только вот совет этот уже во время написания книги был не самым удачным, а сегодня и вовсе неуместен. Анализируйте целочисленные параметры, как на Java: p[0] * 256 + p[1]. Вам кажется, что закрепив упакованную структуру на поверхности, ее проще будет анализировать – не тут-то было.

Блокируйте небезопасные функции


Хватит использовать устаревшие функции из разряда strcpy() и sprintf(). Если я нахожу уязвимости, скорее всего, они именно здесь. Более того, с таким набором аудит вашего кода стоит гораздо дороже, ведь нужно просмотреть каждую из вышеупомянутых функций, чтобы убедиться, что буфер не переполнен. Возможно, вы уверены, что с буфером все в порядке, но мне придется долго и нудно это проверять. Нет, чтобы писать strlcpy()/strcpy_s(), и snprintf()/sprintf_s().

В целом, вы действительно должны отдавать себе отчет в том, что такое переполнение буфера и переполнение размеров переменной. Посмотрите, как на OpenBSD используется reallocarray(), разберитесь, почему данная опция позволяет решить проблему переполнения размеров переменной, а затем постарайтесь использовать ее во всех своих кодах вместо malloc(). Если придется, скопируйте исходный reallocarray() из OpenBSD и придерживайтесь этой функции в ваших программах.

Вы знаете, почему код вдруг дают сбой при вводе определенных данных? Может, дело в их безопасности. И, кстати, почему ваш код взламывают злоумышленники? Будете все делать правильно – больше о подобных проблемах беспокоиться не придется.

В статье «Программирование на С в 2016 году» прозвучал совет везде использовать calloc(). Не спешите ему следовать, ведь в этом случае на многих платформах вы все равно столкнетесь с переполнением размеров переменной. Кроме того, привыкайте к функциям вроде realloc(), а заодно и reallocarray().

Существует множество правил написания безопасного кода, но если вы выполните то, что я сказал, решите большинство проблем в этой сфере. И, да, скептически воспринимайте любые исходные данные, даже если речь идет о локальном файле или USB-порте, который, казалось бы, под контролем.

Долой странный код


Чего не хватает всем компаниям, специализирующимся на разработке программного обеспечения, так это собраний после работы, на которые может прийти любой желающий и озвучить предложения по единому стилю создаваемых кодов. Затем просто увольняем всех, кто выпендривается. Звучит глупо.

Единственное, что можно назвать правильным «стилем» — сходство кода с его соратниками из Интернета. Это относится и к личным кодам, и к проектам с открытым исходным кодом, над которыми вы обычно работаете. Вам нужно всего лишь выбрать один из существующих известных стилей, вроде предлагаемых Linux, BSD, WebKit, или Gnu.

Среди преимуществ других языков программирования, особенно Python, можно отметить весьма небольшое количество общепринятых стилей, чего не скажешь о С. Так, например, при анализе уязвимости Hearbleed выяснилось, что OpenSSL использует скобки Whitesmiths – стиля, который в свое время был общепринятым, но сейчас встречается редко и выглядит странно. LibreSSL преобразовала его в формат BSD. Весьма неплохое решение: если ваш стиль на С слишком витиеват/устарел, возможно, пора сменить его на что-то распространенное/привычное.

Вы уверены, что все станут использовать те же крутые штуки, что и вы, как только увидят, как клево они смотрятся в вашем коде? Нет уж, избавьтесь от них, они только раздражают людей. Или, если такие инструменты жизненно необходимы (бывает и такое), задокументируйте свой опыт.

Будущее за многоядерными процессорами


Процессоры вряд ли станут быстрее. Как видите, просто появляется все больше и больше ядер. Нет, это не значит, что вам не нужно писать многопотоковый код, но, вероятно, стоит задуматься и об их будущем.

Скажите нет мьютексам и критическим секциям – они только усложняют ваш код. Да, это повышает безопасность продукта, но ведь страдает производительность. Так ваш код может летать на 2 или 3 ядрах, но если их больше, программа начинает тормозить. Важнее масштабируемости под разное количество ядер в рамках программирования на языке С может быть только обеспечение достойного уровня безопасности продукта.

Думаю, очень скоро вы познакомитесь с огромной статьей, посвященной масштабируемости под различные системы, а пока просто делайте, как я уже говорил: избавьтесь от глобальных переменных и скрытых функций обмена данными встроенных структур. Потом, когда нужно будет заняться рефакторингом кода и его масштабом, работать будет в разы проще.

Забудьте о true/false для success/failure


В статье «Программирование на С в 2016 году» сказано, что success всегда равняется true. Чушь. True это true, а success это success. Не ставьте между ними знак равенства. Чтобы получить 0 в случае успешного результата и другое значение при отказе функции, придется написать сложнейший код.

Да, вот такая ерунда: стандарта нет, и вряд ли когда-либо появится. Вместо того, чтобы прислушиваться к вредному совету из «Программирования на С в 2016 году», посмотрите, как doc прекрасно справляется с задачей «долой странный код». Автор считает, что если бы мы научили других делать так же, если бы не запутывали собственный код, подавая тем самым хороший пример, тогда от проблемы не осталось бы и следа. Наивно. Программисты никогда не согласятся с этим стандартом. Вашему коду придется выживать в мире, полном неоднозначности, где и true, и 0 означают success, несмотря на изначально противоположные значения. Создание стандарта возможно только при однозначном определении показателей SUCCESS и FAILURE.

Если код выглядит так:

if (foobar(x,y)) {
      ...;
   } else {
      ...;
   }

Читая его, я ни за что не разберусь, что здесь success, а что failure. Вот вам и разнообразие стандартов. Лучше пишите так:  
 
  if (foobar(x,y) == Success) {
      ...;
   } else {
      ...;
   }


Немного о целых величинах


Автор статьи «Программирование на С в 2016 году» утверждает, что нет никаких оснований использовать классические int или unsigned, а вместо них лучше обращаться к int32_t и uint32_t. Нонсенс! Команды int и long являются общепризнанными для ввода исходных данных в большинстве функций библиотек, причем именно они обеспечивают своего рода защиту типов и уведомляют пользователей даже при запросе различных типов одного размера.

Честно говоря, задать неверные значения целых несложно, в том числе и на 64 и 32-битных системах. Да, использование int для управления указателем сломает 64-битный код (вместо этого пишите intptr_t, ptrdiff_t или size_t), но, вы не поверите, как редко это случается на практике. Просто задайте mmap() для 4 первых гигабайт, пометив, что при загрузке эти страницы недействительны, проведите модульное/регрессионное тестирование — и вы быстро решите любую проблему. Причем мне вовсе не нужно вам объяснять, как это лучше сделать.
Что раздражает в коде больше всего, так это то, что программисты вечно спешат повторно определить типы целых величин. Завязывайте. Я понимаю, что u32 придает особый шарм коду, но меня этот элемент просто выводит из себя. А ведь именно мне придется читать код. Пожалуйста, замените его на что-нибудь стандартное, например, uint32_t или unsigned int. И, о ужас, хватит произвольно создавать типы целых вроде filesize. Я знаю, что вы хотите придать выбранному целому новый смысл, но не забывайте, что программирование на C рассчитано на «низкий уровень», а потому программисты просто умирают в процессе проверки таких перлов.

Используйте статический и динамический анализ


Если раньше специфика C сводилась к «уровням предупреждений» и опции «линт», на сегодняшний момент их место занял «статический анализ». С помощью Clang компиляторы радуют пользователей все новыми и новыми сообщениями, да и gcc старается на отставать. И, хотя многие не в курсе, компиляторы Microsoft также предлагают статический анализ на уровне Clang. Возможности XCode в сфере визуализации анализа Clang действительно впечатляют, хотя речь идет о тех же механизмах, что и в самом Clang.

Но это только общий статический анализ. А ведь есть еще много инструментов для обеспечения безопасности, которые поднимают статический анализ на более высокий уровень — Coverity, Veracode и HP Fortify.

Тут не обходится без принципа «ошибочного допуска», а ведь это неправильное определение. Прописывая в коде такие команды, вы его «подчищаете», а, значит, получаете куда более надежные результаты. Другими словами, попдная схема позволяет довести код до совершенства, удаляя ненужные элементы. Написание кода под четким надзором статического анализатора совершенствует навыки программирования.

Эти ужасные зависимости


После нескольких лет существования корпораций проекты появляются только на текущей системе. А все потому что накапливается огромное множество несистематизированных зависимостей. В одной из компаний, где я работал шутили, что им придется поделиться материалами с конкурентами, так как иначе они ни за что не вспомнят, как осилили те или иные разработки.

И по весьма веским причинам такая практика закрепляется навсегда. В другой компании предложили унифицировать версии компиляторов во избежание проблем с интеграцией, возникающих при использовании командами сотрудников разных компиляторов. Но такой подход решает относительно незначительную проблему, на смену которой приходит куда более серьезная. Устраняя вопросы с интеграцией, вы проводите своеобразную санацию кода.

Открытый исходный код тоже подразумевает определенные сложности. Зависимости редко документируют полностью, а вот недочетов в них – хоть отбавляй. В большинстве подобных случаев, в итоге, вы тратите море времени на установку двух несовместимых версий одной зависимости, чтобы, о чудо, скомпилировать необходимый код.

Чем меньше зависимостей, тем популярнее становится код. Для этого вам нужно всего лишь:

  • Удалить зависимости. Как правило, лишь 1% зависимостей приносит программисту пользу, остальные 99% — затрудняют работу.
  • Использовать только необходимый исходный файл. Вместо того, чтобы завязать все процессы на целой библиотеке OpenSSL (и ее зависимостях), просто добавьте файл sha2.c, если, конечно, вам не понадобятся другие функции OpenSSL.
  • Пусть источник всех зависимостей будет прямо в дереве. Например, Lua – шикарный скриптовый язык в 25kloc, которому просто необходимы обновления. Вместо того, чтобы бросать пользователей на произвол судьбы в погоне за соответствующей зависимостью lua-dev, укажите источник Lua в своем дереве.
  • Загружайте библиотеки в процессе запуска программы через dlopen(), не забывая про файлы их интерфейсов .h, которые также являются частью источника вашего проекта. Это значит, что у вас не будет проблем с той или иной зависимостью, пока она не нужна для функционирования библиотек. Или, если без нее никак, можете подготовить уведомления об ошибках с предусмотренными инструкциями по исправлению неполадок зависимости.


Разберитесь с неопределенными поведением на C


Скорее всего, вы не совсем понимаете, как работает язык C. Рассмотрим выражение (х + 1 < х). Действительный результат в этом случае 5. А все потому, что С не задает действия для x, если данная переменная получает максимальное целое значение и вы прибавляете 1, что приводит к ее переполнению. Многие компиляторы идентифицируют подобные явления, как переполнение в двоичном коде, так же, как и другие языки программирования (например, Java). Но, как известно, некоторые компиляторы автоматически классифицируют такого рода ситуации, как невозможные, и просто удаляют весь соответствующий код.

Таким образом, вам придется исходить не из того, как работает текущая версия вашего компилятора на С, а вникнуть в дополнительные аспекты, проанализировав, как на код могут отреагировать другие компиляторы на С.

Заключение


Не беритесь за программирование на С, если вы не привыкли брать на себя ответственность. Важно хорошенько разобраться с такими понятиями, как переполнение буфера, переполнение размеров переменной, синхронизация потоков, неопределенное поведение и др. Ответственность предполагает создание качественного кода, рассчитанного на раннее обнаружение багов. Именно в таких условиях программирование на C будет развиваться в 2016 году.

Комментарии (28)


  1. ragequit
    25.01.2016 11:42

    Ну и как обычно это бывает с большими текстами, обо всех «косяках», ошибках и прочей ереси, которую вы, как специалист, могли заметить в простыне выше, прошу сообщать в ЛС, буду стараться оперативно править по мере сил и возможностей. Уверен, что-то ускользнуло. Спасибо.


  1. defuz
    25.01.2016 11:56
    +6

    В 2016 году я бы сказал так:

    Не беритесь за программирование на С, если вы не хотите брать на себя ответственность за то, что является заботой компилятора.
    И изучите наконец Rust.


    1. defuz
      25.01.2016 12:29
      +10

      Постараюсь обосновать свое утверждение, чтобы мой комментарий не казался таким голословным:

      Активно защищайте код
      Rust просто не позволит вам просто так упустить ошибку. Вам придется либо обработать ее, либо пробросить ее наверх вызывающему, либо явно указать, что ошибка должна привести к завершению потока.
      Забудьте о глобальных переменных
      В Rust нет проблем с глобальным переменными. Любое глобальное значение является неизменяемым и доступным из любого количества потоков. Изменяемым оно может быть только внутри unsafe кода.
      Немного ООП
      В Rust есть типажи и параметрический полиморфизм, которые дают очень широкие возможности по написании абстрактного кода, и все это работает без каких-либо накладных расходов во время выполнения.
      Блокируйте небезопасные функции
      В Rust есть строгое разделение на безопасные и unsafe-функции. Последние можно вызывать только внутри unsafe-блоков.
      Долой странный код
      У Rust есть rustfmt. Более того, проверки по именованию констант/переменных/типов встроены в качестве предупреждений в сам компилятор.
      Будущее за многоядерными процессорами
      Полностью поддерживаю. Вот только С никаким образом не помогает писать писать параллельный код. В отличии от Rust, который будет дотошно следить за использованием переменных и передачей данных между вашими потоками. И, опять же, все это работает без каких-либо накладных расходов.
      Забудьте о true/false для success/failure
      Используйте вместо этого тип-суммы:
      enum Result<T, E> {
          Ok(T),
          Err(E)
      }
      

      Немного о целых величинах
      В Rust есть строгое разделение на числа определенного размера (u8, u16, u32, u64, i8, i16, i32, i64) и системно-зависимые числа (usize, isize).
      Используйте статический и динамический анализ
      А еще лучше, когда он происходит сразу на этапе компиляции, как в Rust.
      Эти ужасные зависимости
      Вовсе не ужасны, если у вас есть нормальный менеджер пакетов с поддержкой зависимостей и семантического версионирования, например Cargo.
      Разберитесь с неопределенными переменным на C
      Rust всегда проверяет арифметические переполнения, если только вы не попросите его явно этого не делать.


    1. DrLivesey
      25.01.2016 12:33
      +2

      Я заинтересовался Rust еще до его релиза, но только сейчас начал писать что-то в рамках его изучения, и нахожусь в самом начале этого пути.

      Основная проблема для меня оказалась в его непривычности. Какие-то вещи, как например связные списки вообще внезапно оказались не так «просты» как в С++ или С.

      Но и другая проблема — огромное количество Legacy, которое переводить на какой-то другой язык просто нерентабельно.


      1. defuz
        25.01.2016 12:39
        +3

        Переписывать все на Rust действительно не имеет особого смысла, хотя некоторые бросаются даже в такие крайности (видимо от безысходности).

        Но что можно делать уже сегодня, так это писать на Rust, используя уже существующие наработки на C. Разработчики Rust приложили очень много усилий для того, чтобы была возможность по максимуму использовать существующее наследие, так что связка Rust-C работает очень хорошо.


    1. Keroro
      25.01.2016 12:35
      +1

      Во встраиваемых системах, например, Си всё еще на коне (хотя «плюсы» в последние годы понемногу растут, но до доли Си им еще очень далеко).


      1. defuz
        25.01.2016 12:42
        +3

        И у Rust есть очень неплохие шансы начать его оттуда потихоньку вытеснять. К встраиваемым системам обычно предоставляются достаточно серьезные требования не только по производительности, но и по надежности. Поскольку ни С, ни С++ никак особо не помогают удовлетворять такие требования, переход Rust рано или поздно станет просто вопросом коммерческой выгоды.


        1. Keroro
          25.01.2016 12:52
          +1

          Поживём-увидим. Много кто обещал похоронить Си. На вскидку вспоминаются Ada, Forth и Дракон, наверняка их было гораздо больше. Но воз и ныне там.


          1. DrLivesey
            25.01.2016 12:57
            +2

            Си умрет только тогда, когда умрет последний Си-программист.


          1. Halt
            26.01.2016 12:48
            +2

            Ни Ада, ни Форт ни Дракон не решали фундаментальных проблем — они решали то, что считалось проблемой на тот момент. Работа с памятью же оставалась по сути той же. Раст — решает, причем не сдвигом парадигмы (как делает ФП), а грамотными контрактами. Во-вторых, он не берет платы от программиста за сахарок.


        1. CodeRush
          25.01.2016 13:11
          +7

          Торжественно обещаю Хабру попробовать написать DXE-драйвер на Rust, несмотря на пару неудачных попыток ранее. Язык выглядит действительно вкусно для написания компонентов прошивки, не являющихся критически важными, но при этом имеющими большую и запутанную кодовую базу, к примеру — драйверов PCIe и USB, которым по 5 лет уже, а баги в них до сих пор вылезают постоянно, и конца этому процессу не видно.
          К знатокам такой вопрос: можно писать на Rust без стека? А без кучи? А есть у вас все содержимое памяти — read-only, кроме стека (так происходит, если исполнять PEI-модули прямо из SPI-чипа, не копируя в оперативную память, которой пока еще нет, а стек выделять из кэша второго уровня в Non-Eviction Mode)? На С — можно, если можно и на Rust — на нем можно будет написать UEFI-совместимую прошивку, минимально разбавив ассемблером. Если нет — значит, только некоторые ее компоненты.


          1. Googolplex
            25.01.2016 13:18

            Без кучи — точно можно, без стека — не уверен. Также думаю, что если вся память кроме стека read-only, тоже будет нормально.


          1. defuz
            25.01.2016 13:20
            +3

            Писать на Rust без кучи можно – на нем уже сейчас пишут операционные системы, во время запуска которых никакой кучи еще вообще нет. Более того, любое выделение памяти в куче происходит явно, так что у вас не получится случайно попытаться это сделать.

            По-поводу стека не совсем уверен, что правильно вас понял. Как вы вообще представляете себе программирование без стека? Где хранить значения переменных? Стек – это вроде бы единственная вещь, от которой в Rust не получится отказаться. Все остальное – куча, раскрутка исключений, потоки, libstd – можно отключить. Но стек у вас будет работать хоть на микроволновке.


            1. CodeRush
              25.01.2016 13:28
              +1

              Без стека можно писать на ассемблере, используя для хранения переменных регистры, а для передачи управления — ближние и дальние переходы. На С писать по настоящему без стека нельзя, конечно, но можно минимизировать его использование, тоже передавая параметры через регистры и заменяя вызовы через call на переходы, ядро PEI и некоторые ранние модули так и работают. Конечно, это уже не совсем C, но я готов и на «не совсем Rust», была бы возможность. Если нет — тоже не беда, весь этот кусок можно писать как раньше, кода там совсем немного.


              1. defuz
                25.01.2016 13:39
                +3

                В Rust есть возможность делать ассемблерные вставки, думаю вам это пригодится.

                Rust построен на базе LLVM, который сам по себе является виртуальной машиной с бесконечным количеством регистров. Так что я думаю что у вас не получится, как и в С, контролировать марринг значений в регистры.

                В аналогичном вопросе по-поводу gcc предлагают произвести базовую инициализацию стека на ассемблере, и сразу передать управление в «настоящую» функцию. Возможно в вашем случае такое решение будет приемлемо.


                1. CodeRush
                  25.01.2016 13:55
                  +1

                  Вставки — это хорошо, но решение MS не поддерживать ключевое слово __asm в 64-битном режиме компиляции в MSVC и требования совместимости отучили от вставок и разработчиков, поэтому сейчас я весь код на ассемблере выношу в отдельные файлы .asm/.s и вызываю как обыкновенные функции с соглашением MS x64.

                  LLVM — ничего страшного, если вставки работают правильно, то их хватит.

                  Про стек — я так и думал, и меня такой ответ вполне устраивает. Большое спасибо.


  1. MacIn
    25.01.2016 12:52
    +2

    Набо трюизмов по сути. 2016 год — работать без отладчика ассертов или отладки как таковой… orly? Нет, я знал и студентов, которые писали на С и на С++, не пользуясь отладчиком вообще, а потом узнавали с удивлением, что такая штука есть. Но…

    Да, у вас ссылки на статьи/переводы перепутались — дважды ссылка на один и тот же перевод статьи про «С в 2016» и ни одной на оригинал. А, да, оригинал есть внизу статьи, просто странно получается с 3 ссылками.


    1. ragequit
      25.01.2016 12:59

      важды ссылка на один и тот же перевод статьи про «С в 2016» и ни одной на оригинал.


      Ссылка на оригинал этой статьи — в подвале, где и должна быть: Перевод: Robert Graham. В тексте: две ссылки на оригинал How to C in 2016 и две на перевод этой же статьи на Хабре, все правильно.


      1. MacIn
        25.01.2016 13:02
        +2

        А, уже поправили, сначала «сети была опубликована статья «Программирование на С в 2016 году» с» вела туда же, на перевод предыдущей.

        Да, мне кажется вот этот момент

        Действительный результат в этом случае 5. А все потому, что С не задает действия для x, если данная переменная получает максимальное целое значение и вы прибавляете 1, что приводит к ее переполнению.

        требует прояснения.


    1. CodeRush
      25.01.2016 13:05
      +3

      По поводу работы без отладчика — половина реального процесса BIOS development / board bring-up проходит без отладчика, т.к. подключение текстового вывода в UART, не говоря уже об отладке на уровне исходного кода, сильно замедляет загрузку и сбивает все тайминги. Иногда это вполне терпимо, но в других случаях — совершенно неприемлимо. Если вмешиваться в процесс загрузки релизной прошивки нельзя, а отлаживать её надо, единственным доступным средством является включение декодирования порта CPU IO 0x80 на LPC или PCI и сбор последовательности POST-кодов.


      1. MacIn
        25.01.2016 13:09
        +1

        Спору нет — для этих случаев (железо) разговор особый, и отладчика может не быть.


        1. saboteur_kiev
          25.01.2016 17:13

          Так в нынешнее время, Си в основном в embedded и юзается, что затрудняет его отладку там, где стоит IDE.


          1. MacIn
            25.01.2016 18:26
            +1

            Правомерность использования слов «в основном» сомнительна.
            Если есть посыл «используйте отладчик», значит речь идет о том программировании, где его использование принципиально возможно.


  1. ateraefectus
    25.01.2016 15:58
    +1

    Забудьте о true/false для success/failure

    Читая его, я ни за что не разберусь, что здесь success, а что failure. Вот вам и разнообразие стандартов. Лучше пишите так:

    Я считаю, что вместо всех этих рудиментов достаточно взять за правило строить условия таким образом, чтобы блок if всегда соответствовал true/success, а else всегда соответствовал false/failure. В таком случае недоразумения исключены.

    P.S. Не-С программист


    1. StrangerInRed
      25.01.2016 16:04

      Просто все что !=0 — true, все что == 0 — false


      1. MacIn
        25.01.2016 16:59
        +1

        Да, но 0 (false) ка успех как раз нелогично, и именно для таких функций стоит прописывать сравнение явно, чтобы подчеркнуть «что-то здесь не так». Для тех, где успех — это ожидаемое true это просто лишнее нагромождение.


  1. SadhooKlay
    26.01.2016 12:14

    Замечательный перевод. Спасибо