Comments 169
Спасибо за статью.У меня есть вопрос, по поводу вендоринга в C и в C++, какая лучше всего практика по использованию и подключению сторонних библиотек и контроля их версий? И смотрели ли Вы в сторону таких молодых языков как Rust или Go?
В крупных проектах (во всяком случае, в тех, в которых я участвовал) все необходимые библиотеки (включая libc-библиотеки) содержатся в исходниках и компилируются при сборке.
То есть, в проекте присутствует только одна версия библиотеки. Хочешь другую — нужно озаботиться этим и установить её в проекте.

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

А вообще есть clib, но я с ней не работал, ничего сказать о ней не могу:)
Ну или, вот это менее популярное решение.

С Rust я пока еще не знаком, с Go недавно начал возиться. Пока все очень нравится — система импорта пакетов после сей не перестает меня радовать :)
Да, но Go пока позволяет использовать только последнюю версию библиотеки.
А вот как раз Cargo в Rust позволяет выбирать любую версию.
Так что, советую ознакомиться, если будет время.
Мне, например, очень нравится подход, который они используют.
Ну, я только начал знакомство с Go, у меня еще не было проектов на нем, где требовалась бы не последняя версия библиотеки:)
Ну, надеюсь, что все еще впереди.

Rust недавно расхваливал один хороший знакомый, так что рано или поздно я к нему приду, думаю.
А Cargo — уже как получится. Пока вообще не представляю, что это за язык:)
Rust недавно расхваливал один хороший знакомый, так что рано или поздно я к нему приду, думаю.
А Cargo — уже как получится. Пока вообще не представляю, что это за язык:)

Если что, cargo это пакетный менеджер раста.

Cargo — это не язык, это утилита для Rust — пакетный менеджер, который отвечает за структуру проекта, сборку и за библиотеки.
Вендоринг в go позволяет заморозить библиотеку на нужном коммите (если только там не разветвленная цепь зависимостей, которые вытянуть проблематично. Еще есть мой любимый gopkg.in (от создателей mgo), позволяющий использовать даже несколько версий одного пакета в одном приложении. Но тут проблема доверия и оф.поддержки для многих может стать решающим фактором.

Область применения Rust или Go несколько отличается от системного программирования, особенно Go.

Интересно, почему область применения rust — не системное ПО? Он в этой нише, в первую очередь, и интересен.

Если в проекте используется CMake, то есть ещё один вариант: таки указать в CMakeLists.txt репозитории и конкретные версии, которые нужно тянуть. К примеру, в мануалах ко многим библиотекам от Google рекомендуется подключать их именно так.
Если уж упоминается restrict, то можно было бы вспомнить и про inline-функции, константы (в разделе про макросы), про именованую инициализацию структур и расширеную массивов (особенно в последнем разделе) etc.
А вот register на этом фоне смотрится уже какой-то тенью прошлого :)

За valgrind отдельное спасибо, почему-то до сих пор есть люди, которые про него не знают.
Да, возможно. Выбрал довольно абстрактную тему для статьи — сложно охватить все и разом. Правда, inline я вижу постоянно, а те же volatile и restrict — довольно редко, даже в тех местах, где их использование напрашивается.
Скажем так, узнать про inline проще, чем про restrict и volatile — вот я и решил это упомянуть :)

А register — да, потихоньку выходит из использования. Тем не менее, иногда — полезная вещь.
register вообще ни на что не влияет в современных компиляторах при уровне оптимизации -O1 и выше. (современные — это где-то с конца 1980х). Но на -O0 может влиять. Вот например обсуждение в рассылке GCC: https://gcc.gnu.org/ml/gcc/2010-05/msg00098.html
Когда писал программы на streetinterview (сейчас hackerrank.com) моя программа не вкладывалась по времени буквально на процент в отведенный лимит. Я исключительно с помощью register (скорее методом итеративного поиска, чем логически, потому что были и очень не очевидные последствия) подобрал так чтобы программа на моем AMD работала процентов на 5 быстрее. Отправил программу на streetinterview и увидел падание производительности на все 10% на их XEON. Для убедительности запустил пару раз и с тех пор register не использую, чтобы не снился.
Да, только он только на однопоточный простейших приложений и работает. В более-менее сложном чем-то он тупо замедляет все раз в 100 примерно и ничего толком не работает. Мне больше нравится вариант использовать jemalloc с опцией дебага heap-а. Там можно и в рантайпе профили снимать, графы смотреть, что откуда пришло. Различные графики строить и тд. Ну и утечки тоже ищет. И оно почти не замедляет ничего(по крайней мере приложение приемлемо работает с ним), памяти только больше жрет, что ожидаемо.
https://github.com/jemalloc/jemalloc/wiki/Use-Case%3A-Heap-Profiling
Правильно ли я понимаю, что для отлова утечек/выходов за границы нужно сначала полностью пересобрать проект с другой версией библиотеки, а потом разглядывать логи? Не назвал бы это удобным подходом.

Но в любом случае попробую попозже, интересно проверить насколько оно справится с теми случаями, которые valgrind точно отслеживает.
Можно без перезборки.
Достаточно через LD_PRELOAD подгрузить so-ку, jemalloc-а собранного с поддержкой поиска утечек.
Так через переменные окружения можно настроить, а настроек там тьма.
Как я понимаю jmalloc это нестандартный аллокатор и отладчик для хипа. А valgrind это проверка всего и сразу. Хотя часть можно отключить. Он проверяет переполнения, обращения по некорректным адресам, утечки и д.р.

Еще есть более быстрый вариант. Это asan/ubsan. Он может проверять уже кое что, чего valgrind не может, но в тоже время чуть менее надежно.
Для использования *san надо собирать приложение с нужными флагами. Работа замедляется в несколько раз, но получается почти c/c++ с проверкой корректности работы с памятью.
Он может проверять уже кое что, чего valgrind не может,

Например?
Для использования *san надо собирать приложение с нужными флагами
В valgrind мне нравится как раз то, что для проверок не нужны никакие вмешательства в сборку и прочие дополнительные телодвижения. Но при этом он удобно интегрируется в IDE. Например, в том же Eclipse (да-да, я им вовсю пользуюсь) живёт в профайлерах:

image

С остальными ещё пока руки не дошли поразбираться, но интересно.
Например?


Скажем у вас есть структура что-то вроде
struct
{
  int a[15];
  int b;
};


a[16] для valgrind скорее всего будет корректен, если не вставится паддинг после a. А ubsan или asan выведет ошибку.
Или тоже самое, только уже разные структуры или массивы, но по указателю из первой структуры через переполнение индекса вы попадете, во вторую. Тут тоже asan/ubsan должен сработать.
Пропустить может если там будут указатели а не массивы.

Другие случаи не помню, но они тоже есть.

Ну, если в лоб, то gcc и сам отловит :) И если индекс задаётся переменной, значение которой компилятору известно. Но идея понятна, спасибо.

P.S. А вот cppcheck не поймал, даже если индекс явно 16 влепить. Надо ещё PVS замучить, но позже.

P.P.S. Кстати, немного не про это, но про С. Вроде бы мелочь, но приятно, чёрт возьми :)
да, конечно я имел в виду что индекс задается в рантайме. Скажем читается из файлы, или как-то вычисляется.
P.S. А вот cppcheck не поймал, даже если индекс явно 16 влепить.

Возможно баг, или информации анализатору не хватает. Вообще он такое ловит:


(error) Array 's.a[15]' accessed at index 16, which is out of bounds.


Насчёт sanitizers — отличные инструменты. Замедляют программу значительно меньше valgrind, а находят зачастую больше проблем, хотя бы за счёт большего объёма информации о программе. Пересобирать надо, да, но оно того стоит.

Объявляйте переменные в начале функции.

Совет так себе. "Совершенный код" рекомендует объявлять локальные переменные как можно ближе к месту использования, чтобы при чтении не приходилось "вспоминать" типы увиденных переменных.


По возможности инициализируйте переменные при объявлении. Численные с помощью нуля, указатели — NULL.

В основном совет справедлив только для языков/компиляторов, где компилятор не проверяет инициализацию переменных. Небольшой оффтоп, но например в Java так лучше не делать (я понимаю, что эта статья для С-шников, но сейчас мало кто пишет только на С, а статью могут читать все).


примеры (java)

Не инициализируем сразу:


int a;
if(something){
    a = 3;
    b = 5;
    c = 9;
}else{
    // тут забыли присвоить a=1;
    b = 2;
    c = 1;   
}
f(a,b); // не компилируется (a не инициализирована), сразу исправляем.

Инициализируем сразу:


int a=0;
if(something){
    a = 3;
    b = 5;
    c = 9;
}else{
    // тут забыли присвоить a=1;
    b = 2;
    c = 1;   
}
f(a,b); // компилируется (a инициализирована, но не тем, чем надо), ловим баги в рантайме.

Не уверен, но мне кажется, что в настоящее время ключевое слово register не влияет ни на что, по крайней мере на самых распространенных платформах.

Объявляйте переменные в начале функции.

Как минимум это релевантно в Си, ибо не перечит стандарту ansi

В ansi c, насколько я помню, "не в начале" объявить даже нельзя, в С99 — можно. Если уже есть компилятор С99, не всегда нужно следовать ansi c.

Более того, хватает компиляторов с поддержкой c11
Но как правило последний стандарт не отражает стандарт который используется (требуется поддерживать)

объявлять локальные переменные как можно ближе к месту использования, чтобы при чтении не приходилось «вспоминать» типы увиденных переменных.

Если функция не растягивается на хрелиард строк, то ничего вспоминать и не придётся. Одна из причин такого объявления в C++ или Java в том, что для не-POD-типов существуют конструкторы и деструкторы. И нет никакого смысла их дёргать (а они могут быть далеко на самыми лёгкими), если код пойдёт по ветке, в которой эти переменные просто не используются. В C такой проблемы нет (если не учитывать инициализацию). Привычка, скорее, тянется из ISO C :-)

И да, кстати


Не уверен, но мне кажется, что в настоящее время ключевое слово register не влияет ни на что, по крайней мере на самых распространенных платформах.

src без register

int sqr(int a) {
return a*a;
}


image


src c register

int sqr(register int a) {
return a*a;
}


image

Не писал тэг "сарказм", думал и так поймут. Естественно, при включенной оптимизации будет что-то вида


mov eax, edi
imul eax, eax
ret

или функциональный аналог.

Нет (: пример с оптимизацией будет придумать посложнее

К тому же, в соответствии с ABI, указание register в параметрах функции вообще бессмысленно. А если брать «игрушечные» примеры, то и вовсе:

static int sqr(int a) {
    return a*a;
}

int main(void) {
    return sqr(123);
}

превратится в:

main:
.LFB61:
.cfi_startproc
movl $15129, %eax
ret


Или это:

int main(int argv, char **argc) {
    return sqr(argv);
}

станет

main:
.LFB61:
.cfi_startproc
movl %edi, %eax
imull %edi, %eax
ret

Не уверен, но мне кажется, что в настоящее время ключевое слово register не влияет ни на что, по крайней мере на самых распространенных платформах.
По крайей мере нельзя взять адрес регистровой переменной, независимо от того, поместил ли ее компилятор в регистр.
Дельное замечание.
На самом деле, в си есть такая вещь, как предварительное объявление функции.
То есть, если ты уверен, что это будет полезно для читаемости, ты можешь объявить переменную в начале, а инициализировать в коде, но так же с объявлением типа.
Например:
int foo;
/* ... спустя n строк в той же области видимости...*/
int foo = VALUE_FOR_FOO;

Конечно, это противоречить пункту о инициализации при объявлении, но каждый сам решает, чему отдавать приоритет.
Что-то я не припомню аналога прототипов функций, но для переменных. Это какое то дополнение в стандарте позволяющее два раза объявить переменную?
не, нельзя так, автор сморозил просто.
будет либо «error: non-extern declaration of 'foo' follows extern declaration»
либо «error: 'extern' variable cannot have an initializer»
Почему же? Вот вполне валидный код:

#include <stdio.h>

extern int foo;
/* ... спустя n строк в той же области видимости...*/
int foo = 0xDEAFBEEF;

int main(void) 
{
    return printf( "%08X\n", foo );
}

Лучше вообще стараться иметь иммутабельные переменные, везде где можно (слово можно тут надо понимать с разных точек зрения). Это упрощает поведение кода и в итоге его анализ.


А это предполагает автоматически, что инициализация при объявлении, один раз и навсегда. Так что совет-то как раз годный. И для всех языков осмысленный. И именно в java есть смысл так делать почти всегда, добавив еще и final.

чтобы при чтении не приходилось «вспоминать» типы увиденных переменных.

IDE же подскажет типы при наведении.

Но нужно делать лишние действия с каждой такой переменной

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

Мне не нравится идея писать макросы-функции капсом. Я рассуждаю логически (логика и программирование — это же стороны одной монеты, не так ли?): макрос-функция скрывает в себе некое законченное действие, т.е. по смыслу неотличим от функции. Так зачем выделять внешним видом его из ряда функций? Только для того, чтобы знать, что это макрос? И что дает это знание? Рано или поздно может возникнуть ситуация, когда макрос на самом деле придется заменить на функцию, и тогда придется править тонны кода, меняя капс-идентификатор на некапс-идентификатор…
С моей т.з. логично и правильно отличать идентификаторы переменных от констант, для чего и служит капс-выделение. Отличать функционально завешенный блок кода, решающий часто повторяющуюся или логически обособленную задачу, оформленный макросом или функцией смысла не вижу.

Что касается других «рекомендаций», то увы и ах, при работе с встраиваемыми системами некоторые «правильные» принципы не работают: «красиво завершиться, напоследок сказав, что именно пошло не по плану» программа попросту не может… Например, популярный нынче квадрокоптер: в случае ошибки «красиво завершиться» — это значит, перед падением на асфальт сделать мертвую петлю? Или осколки разбросать в виде кода ошибки? Во многом количестве встраиваемых систем НЕТ ОС, которая могла бы воспользоваться кодами ошибок. И так или иначе приходится писать программы, умело игнорирующие ошибки, т.е. допускающие деление на ноль без видимых для пользователя последствий.
Хотя сам принцип -да, разумен. Но не абсолютен.

И, таки-да, автор сам себе противоречит, выделяя функции-обертки капсом…
Так зачем выделять внешним видом его из ряда функций?
Явное лучше неявного. Макрос — не функция (например, его нельзя передавать как аргумент в другие функции), и нет никакого смысла обманывать читающего код разработчика, делая вид, что это не так.

Рано или поздно может возникнуть ситуация, когда макрос на самом деле придется заменить на функцию
Не надо писать макросов, которые можно заменить на функции.
Функции с результатом void тоже нельзя передавать, как аргумент в другие функции, — и что?
Любая попытка давать ОДНОЗНАЧНЫЕ рекомендации будет ошибочной.
Функции, макросы — это все абстракции, призванные что-то от читающего скрыть, упростить, уменьшить количество анализируемых сущностей. С точки зрения человека их смысл именно в этом. Поэтому сущности с одинаковым смыслом (в человеческом понимании) не стоит явно разделять.
Вот объясните мне логику создателей GCC, которые спокойно сделали два макроса _BV(x) и bit_is_set(a,b) — какой логикой они руководствовались, делая один заглавными, а второй нет?
Функции с результатом void можно передавать. Я имел в виду что-то вроде
void do_things(void) {
    ...
}

void do_async(void (*what)(void)) {
    ...
}

...

do_async(do_things);

Макросы (в современном языке) нужны только там, где функций недостаточно. Там, где нужно генерировать названия функций, например. Или как-то ещё расширять синтаксис языка.

Логику создателей GCC я вам объяснить не смогу, я не принадлежу к их числу.
Обидно, что мои слова вы поняли буквально.
«Передавать функцию» я, разумеется, имел ввиду «передавать результат функции», а не ее адрес.
UFO landed and left these words here
Вот чесслово, не понимаю: зачем в одном языке делать как в другом. Эдак можно договориться и до упрощённого клингонского, или того круче, «ку» и «кю» :-)
UFO landed and left these words here
В других языках это понятно зачем. Но ответа на вопрос «зачем это в C» вы так и не дали.
UFO landed and left these words here
UFO landed and left these words here
Нет, мне не кажется. Я знаю C, я много пишу на нём, и не понимаю зачем притягивать за уши юзкейсы из других языков и совсем других областей применения.

Функции могут возвращать void
Нет, не могут. void-функция в C не возвращает ничего, никакого «возвращаемого значения» у неё нет, и сама мысль о его использовани не должна возникать, by design.

P.S. Не путать с void *
Я не нес в своей статье посыл «Пишите только так и никак иначе» — каждый волен выбирать, какого стиля придерживаться. Те рекомендации по оформлению кода, что я привел — сборка наиболее часто используемых принципов как из проектов компании, где я работаю, так и из opensource проектов.

У новичка, читающего данную статью, появится представление о том, как обычно делают более опытные ребята в самых разных проектах :)

А по поводу оберток — они нужны не в эксплуатации — конечный пользователь не будет смотреть логи работы программы -, а в отладке. Надо же тестерам и разрабам понимать, что и где упало в случае ошибки.
Да, кстати, капсом я обертки не выделяю
о_О
Я пишу их с заглавной буквы.
Кто то использует _ перед именем обертки, кто-то — другие обозначения. Тут тоже есть свобода выбора :)
О_о
Если у вас есть сомнения: написать макрос или функцию — пишите функцию, не прогадаете. При соблюдении этого простого правила макрос никогда в жизни не выродится в функцию.
Увы, и Ваша рекомендация не панацея. Частенько приходится делать как переход от макроса к функции, так и наоборот. Это же не С++ с его шаблонами… Простой пример
#define max(x,y) ((x)>(y))?(x):(y)
В Си это не раз выручает при сравнении чисел любых типов, в то время как функций пришлось бы писать гораздо больше и с не такими лаконичными именами
Или в «малых» встраиваемых системах: работу с портом ввода-вывода выгоднее делать макросом в силу большей скорости исполнения кода…
Ага, а потом кто-то пишет
x = max(a++, b);

И удивляется результатам. Ну или правило о том, что так нельзя делать, пишется в Code Style Guide, который в итоге раздувается.
Еще один пример того, что абсолютно правильных рекомендаций не бывает.
Или в «малых» встраиваемых системах: работу с портом ввода-вывода выгоднее делать макросом в силу большей скорости исполнения кода…
Всегда пишу такие макросы функции со словом static, это позволяет компилятору самому принимать решение, как оптимальнее — заинлайнить как макрос или сделать функцией. Только что сравнил:
#define CLR(a,b) ((a) &=~(1<<(b)))
static void clr(volatile uint8_t *PORT,uint8_t bitN)
	{	*PORT &= ~ (1<<bitN);	}
Результат один к одному. (Правда при использовании функции приходится писать лишний амперсант при вызове функции. В данной статье от разработчиков микроконтроллеров конечно сказано что макрос работает быстрее… Но я объявил функцию static и компилятор имеет полное право ее заоптимизировать, что он и сделал. Убрав static — получаю гораздо большую программу.)

А с учетом что в коде у меня (и не только, не нахожу статью на хабре), присутствуют далее такие короткие функции:
static void Led_Off(void)
	{	clr( &DDRC, 3 );
		clr( &PORTC, 3 );	}

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

P.S. В «малых» встраиваемых системах у меня все функции static (кроме main), сборка простая — в основной файл сначала все .h файлы инклудяются, потом все .c — все для того чтобы у компилятора были все возможности.
Убрав static — получаю гораздо большую программу.)

Ну так это понятно. У меня тоже давно выработан автопилот: все функции внутри единицы компиляции объявляются как static, если внешнее связывание не требуется явно. И это не зависит от цели сборки.
Зуд происходит неспроста. Человек который +- постоянно пишет на си, прекрасно знает, какое там раздолье на стрельбу по конечностям.

#define cmp(x, y) (x - y)

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

По поводу операционных систем: скоро линукс и его собратья будут в каждом чайнике стоять. И это прекрасно. В mbed есть тоже есть всякие RTOS. Причем ошибки все равно обрабатываются, нередко для этого заводят обработчики по systick.
Системщикам неплохо бы помнить, что C является основным языком при программировании встраиваемых систем и узлов IoT на малых микроконтроллерах.
И там совсем другие подходы.
Поэтому статьи по C надо начинать с дифференциации.
Т.е. сразу предупредить, о программировании каких платформ идет речь и о каком организационном подходе к проектированию.

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


Как я писал в комментариях выше, я не говорю, что нужно писать только так. Это — рекомендации, сделанные на основе самых распространенных практик оформления кода в самых разных проектах.

А по поводу адаптации к рефакторингу… Это очень сложная тема.
Когда я только начинал учить Си, меня часто тюкали по голове за то, что я пишу по-своему, игнорируя общепринятые вещи. Тогда же мне довелось участвовать в небольшом проекте, на последней стадии разработки которого одному опытному программисту поручили выполнить рефакторинг моего кода, ибо он не соответствовал кодстайлу проекта. А кодстайл проекта мы взяли такой же, как в ядре Linux.

Однако, каждый волен сам выбирать свой кодстайл, главное чтоб он не противоречил соглашениям, принятым в проекте:)

Разрешите поинтересоваться, это какие же подходы используются при разработке для малых микроконтроллеров, что указанные автором подходы не приенимы?

Не знаю что имел в виду автор, но встречал как «тяп ляп и впродакшн» так и «я художник, я так вижу» (ну с очень индивидуальным подходом в программировании). Такой код write only и стоит его почитать, но ровно с той целью с которой стоит посмотреть картины авангардизма.

Хотя я такой подход не приветствую, но по своему опыту скажу что можно сдать код который «пора бы отрефакторить», но можно прицепить еще один костыль и будет работать.

Правда бывают и требовательные заказчики, один у меня уже обновления в течении пяти лет заказывает, вот там в исходнике — красота, хоть на выставку.

Редко, когда я, почти, полностью согласен с содержанием статьи. Автору спасибо!

потихоньку был вытеснен из десктопа и энтерпрайза

Да неужели?
https://habrahabr.ru/company/hh/blog/318450/
Да, у Java там почти в два раза отрыв по сравнению с С++, но что-то подсказывает, что это не десктоп с энтерпрайзом.
Я вот сейчас пишу с ПК на котором одно приложение на C# и штук пять на Java. И это не что-то такое особенное, вполне себе мейнстримовая ОС. Остальное, С++ и C. Странно, да?

И как много современных десктопных приложений написано на чистом С?

С или С++?
На С написано мало. Но вытеснил его отнюдь не Java и C#. А С++

Просто в статье и написано, что был вытеснен именно C. C++ от C уже совсем далеко ушел, их даже не стоит рядом упоминать.

В статье:
«был вытеснен из десктопа и энтерпрайза такими высокоуровневыми гигантами как Java и C#»
А С++ он не был вытеснен. Он был достаточно плавно заменен, причем часто с сохранением кодовой базы.
UFO landed and left these words here

Там ещё и ошибка была, вероятно. Если только в make текущий каталог не сбрасывается при переходе к следующей строке блока в Makefile, то make будет вызван в sound/, sound/graphics/, sound/graphics/engine/.

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

Я бы, всё же, очень попросил исправить вас этот Makefile к виду с вызовами $(MAKE) вместо make. Иначе это ломает многопоточную сборку.


А ещё лучше — совместить это с makefile'ом от Sirikid. :)

В make каждая строка выполняется в отдельном сабшелле, поэтому все сбрасывается.

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

Альтернативный вариант — исходники раскидать, а объектники свалить в одну кучу, используя VPATH. Но это опять вопрос сборки, не относящийся напрямую к языку.
UFO landed and left these words here
С конкретно такими «подпроектами» сильно красиво не получается, поэтому через шелл циклы:

SUBDIRS = sound graphics engine
.PHONY: all build clean

all: build

build clean:
    for dir in $(SUBDIRS); do \
        make -C $$dir $@; \
    done


Или с макросами
SUBDIRS=sound graphics engine

define SUB
	# DO NOT REMOVE THIS LINE
	$(MAKE) -C $(1) $(2)
endef

.PHONY: all build clean

all: build

build clean:
	$(foreach dir,$(SUBDIRS),$(call SUB, $(dir),$@))


Вобщем все печально, если не поступить примерно так:
SUBDIRS=sound graphics engine
SOURCES=$(foreach dir, $(SUBDIRS), $(wildcard $(dir)/*.c))
OBJECTS=$(subst .c,.o,$(SOURCES))

.PHONY: all clean

all: program

program: $(OBJECTS)
	$(LD) -o $@ $^

clean:
	-rm $(OBJECTS)


Мы не можем использовать имена каталогов в качестве таргетов (то есть можем, но будем при этом страдать), потому что они с одной стороны существуют, а с другой стороны ни от чего не зависят. Можно было бы для каждого каталога определить связанную с ним статическую либу (sound -> libsound.a), которую бы и собирал соответствующий sub-make, но это не спасет от необходимости делать clean все равно по каталогам (find -name '*.o;' это тоже не самый правильный путь).

Я бы предпочел иметь всю сборку в одном мэйкфайле, чтобы там иметь и список всех исходников, и список всех объектников и все на свете. Но это лично мои предпочтения. Так-то можно и automake использовать и иметь в каждом каталоге совсем крошечные Makefile.am.
UFO landed and left these words here
Потому что make умеет из коробки в параллельность и инкрементальные сборки. А мне почему-то регулярно приходится разбирать конструкции вида «тут мы из мэйка вызовем шелл, а в нем мэйк, а там опять шелл, а уже он окончательно последний мэйк».

Я бы очень хотел, чтобы описанная выше архитектура была преувеличением. Но нет, это суровая реальность на моем текущем месте работы.

Да-да, как раз хотел кинуть это. Пишут Makefile рекурсивные и потом жалуются на скорость работы make.

Тут не скорость работы make-а, а проблема отслеживания зависимостей. Тк make тупо не знает про вложенные и не может правильно понять, что нужно пересобрать.
Знает, если ему объяснить. В приведенном примере make ничего не объяснили о зависимостях.
А каким образом ему можно сообщить? Простой пример.
mybin: bin.c libsound.a:
    cc $(^) -o $(@)

libsound/libsound.a:
    make -C libsound


Поменяли файл локальный bin.c, не из libsound/. В этом примере make попытается пересобрать libsound.a, тк на этом уровне он не знает, что libsound.a не зависит от bin.c.
Если сделать это без вложенного make в libsound можно сделать полный граф зависимостей и тогда make-у будет понятно, что libsound.a не зависит от bin.c и его трогать не нужно.
Как можно тут указать правильно все от чего зависит libsound.a не дублирую Makefile из libsound?
Он зайдет в libsound, запустит там make, увидит, что делать ничего не надо, выйдет. Да, уйти в sub-make придется, но и все.

Есть вариант, который используется в некоторых проектах, поставить libsound.a в зависимость libsound/stamp, на который делается touch в конце сборки libsound.a, а при любой правке в каталоге libsound придется делать touch на этот stamp. Такое имеет смысл, если предполагается, что содержимое libsound будет меняться редко и вообще это сторонний код, которому мы верим.
Если представить проект, где, например, с десяток таких библиотек и они по-разном зависят от каких то хедеров. Если Makefile общий с include вместо вызова вложенного make, тогда есть полный граф зависимостей и будет правильно работать -j например. Сразу будет понятно, что можно распараллелить, а что нет. В случае с раздельными же, нужно либо както этот -j передавать ниже, либо всегда с ним запускать. Так же, подобные внешние вызовы будут своего рода барьерами, мешая make-у работать параллельно иногда и ожидая вложенного завершения. Короче в статье, которую привели выше все хорошо описано и почему не нужно так делать, если хочется нормальной сборки. Минусов у единого Makefile, если это один проект, я не вижу. Некой модульности можно достичить include-ами, если хочется собрать только одну либо, то можно написать make libsound/libsound.a
Я просто сталкивался в относительно большом проекте(2М строк), как раз с реккурсивным make и неправильным отслеживанием зависимостей отчасти из-за этого. Сборка была очень медленной(С++ с шаблонами, бустами всякими и подобным шлаком), плохо параллелилась и делала много лишнего на каждый чих, когда не нужно.
-j передается в sub-make автоматически (в отличие от некоторых других флагов), если внешний make понял, что это sub-make. Для этого стоит либо использовать $(MAKE) вместо make, либо (надежнее) ставить + перед соответствующей строкой. На самом деле в этом случае sub-make даже идет параллельно с «внешним», джобы общие и вообще все шикарно работает.

На самом деле я всеми руками за один единственный Makefile. Просто его чуть труднее правильно готовить, а многие разработчики, которых я видел, почему-то не хотят научиться это делать как следует.
-j передается в sub-make автоматически

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

Как раз наоборот.

The ‘-j’ option is a special case (see Parallel Execution). If you set it to some numeric value ‘N’ and your operating system supports it (most any UNIX system will; others typically won’t), the parent make and all the sub-makes will communicate to ensure that there are only ‘N’ jobs running at the same time between them all. Note that any job that is marked recursive (see Instead of Executing Recipes) doesn’t count against the total jobs (otherwise we could get ‘N’ sub-makes running and have no slots left over for any real work!)
тыц
char c[IP_UDP_DHCP_SIZE == 576 ? 1 : -1];

Лично у меня, когда я это увидел, повис вопрос: «Чеееееееее?».

Это абсолютно необходимая проверка для компиляции на разные архитектуры и разными компиляторами. Дело в том, что отключение выравнивания в разных компиляторах выполняется по-разному. Опять-таки архитектуры бывают разные, вплоть до 32битных байтов.

Второй момент — почему так странно, а не просто assert. Дело в том, что обычный assert отрабатывает во время исполнения, а такой — во время компиляции. Сейчас обычно делают макрос CCASSERT, но тут, похоже, код древний, и на общепринятый макрос просто не перешли.

Если интересно, то вот код макроса:
#define _x_CCASERT_LINE_CAT(predicate, line) typedef char constraint_violated_on_line_##line[2*((predicate) != 0)-1];
#define CCASSERT(predicate) _x_CCASERT_LINE_CAT(predicate, __LINE__)

// Usage: CCASSERT(1) to pass; CCASSERT(0) to fail
/*
typedef struct {
long x;
long y;
}foo ;
CCASSERT(sizeof(foo) < 10) // will not complain
*/



Это — пример того, как делать не стоит. Никогда. Иначе случится насилие. Рано или поздно.

В целом, когда у нас есть внутренняя структура, являющаяся образом какого-то внешнего пакета, то является хорошим тоном проверить хотя бы её длину (в идеале — ещё и смещение до ключевых полей).

Если вы когда-нибудь будете портировать ваш проект на пяток архитектур и пяток разных компиляторов, то сами нарветесь на проблемы со структурами. И на своем горьком опыте начнете ставить assert и CCASSERT.

Костыль, созданный с одной целью — вставить его, как палку, в колеса тому, кто будет расширять этот код.

Ну кто же мог подумать, что расширять код будет человек, который впервые сталкивается с проблемами портирования? :-))) Кстати, совет — держитесь в рамках ANSI C-88, все более новое, могут и не принять в код. Не на все платформы есть компиляторы, понимающие более новые стандарты Си.
Не на все платформы есть компиляторы, понимающие более новые стандарты Си.

C99 уже 18 лет… Допускаю, что есть такие компиляторы, но примеры можно?
MS-DOS, МСВС 3.0 (там gcc 2.95.4 и обновлять нельзя), думаю что PDP-11, VAX-11 и так далее… Плюс вагон военных машинок с нестандартной архитектурой…
Под МСВС собирал последний раз году в 2005, неужели они с тех пор так и заморожены? Вот это стабильность :)

Впрочем, привычка кодировать ближе к ISO всё равно осталась. Но без некоторых новых вещей уже обходиться не хочется :)
Суть в том, что в МСВС сертифицируется конкретная сборка под конкретную машину. Поставить более новую версию — это заново потратить год (и пару миллионов) на сертификацию. Так что если сертифицирована 3.0 — себе дороже затевать сертификацию 5.0.
Ну да, у нас для сертификации программы считали MD5 исходников/утилит/мейкфайлов etc, потом контрольные суммы того, что получилось после сборки. Плюс-минус байт уже не катит. Но когда это было и какое тогда было железо — с тех пор же многое изменилось. Неужели у нх не возникло желание использовать ОС на чём-то посвежей?
Новые версии МСВС есть. Но кто будет сертифицировать новую версию на то железо, на которое уже сертифицирована старая? А сертифицируется именно бинарная сборка, если пересобрали из сорцов -уже сертификации нет. Принесли любой бинарнкик — опять сертификация слетела. Провели компиляцию не на МСВС — опять сертификации нет.

Из рассказов коллег:
— И предоставьте исходный код в распечатанном виде.
— Там 600 тысяч строк, будет 15 тысяч листов, вес листа 80 грамм — итого 1200кг. Грузовик дадите?
— Ладно, давайте на CD.

:-)

Ну в общем, если сертификация важнее всего, то остальным можно пожертвовать.
Если в одну стопку сложить, то при стандартной плотности (80 г — толщина 0.1 мм) получится около 1.5 метров — тоже никак не гузовик. Может, у вас листов было в 10 раз больше?
Не, 1.5 метра — это до распечатки. Далее до такой толщины обратно в условиях офиса не сжать. Прессов-то нет…

Но даже 5 метров — в багажник влезет струдом. Газель нужна.

По размеру это 6 коробок размером порядка 0.3х0.3х0.3 м (если спрессовать так плотно, как A4 обычно лежит в пачках). То есть, 30 пачек A4 по 500 листов.

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

А с тех пор и железо другое, и софтины сильно меняются, не sh ведь :) Но хозяин — барин, конечно.
Ну у нас Linux, FreeBSD, MS-DOS, Windows, QNX. Из компиляторов — gcc, lcc, clang, BC++, BCB, VC++… В итоге ровно к такому же решению и пришли. И именно потому, что не раз налетали.
В использовании оберток есть небольшой минус, который, если захотеть, можно решить костылем. А что это за минус — можете предположить в комментариях :)

Там минус очень существенный — на каком бы сокете не произошел сбой — сообщение об ошибке будет одинаковое. Так что надо как минимум добавлять вызываемые параметры. А ещё лучше — лишний параметр в обертке — текстовая строка с назначением сокета, которая выводится в сообщений.

А «небольшой» минус — это то, что как закроются сокеты — зависит от библиотеки. Если успели сделать connect по TCP/IP — сокет скорее всего закроется жестко и вторая сторона не будет в курсе, что мы уже отвалились. Так что лучше использовать atexit.
Да, все абсолютно верно.
Пометил себе, как появится заряд — с меня плюс в карму :)
Храните заголовочники в директории include.

А не могли бы вы объяснить, какой смысл делать отдельную директорию include? Я в этом вижу только минусы:
  • нужно прописывать еще один путь в настройках проекта
  • дерево проекта увеличивается вдвое, ведь внутри папки include приходится повторять всю структуру папок с файлами .c
  • в файле xx.c нельзя просто написать include «xx.h», приходится писать полный путь до него — #include «yyy/zzz/xx.h»
  • чтобы скопировать какой-то «модуль», вам приходится копировать два файла из разных мест

Гораздо удобнее, на мой взгляд, группировать файлы в папке по принципу «модулей». Допустим, модуль Common — это отдельная папка, в ней common.c и common.h. Этот модуль приобретает «позиционную независимость», т.е. когда вы его копируете в другой проект, вам не надо переписывать инклуд в файле common.c. Ну и все минусы, описанные выше, пропадают.
Возможно, я выбрал не очень удачный пример в статье. Да, группировка — очень хорошая практика. И она прекрасно дополняет метод с include'ом.

Я уже писал — ко всему надо подходить с умом. Заголовки общего пользования, библиотечные заголовки, которые должны быть доступны во всем проекте. include — для них самое подходящее место.

Возьмем тот же busybox. В исходниках все группируется именно так, как вы сказали. Каждая утилита в отдельной папке. И .c, и локальные заголовки. Но тем не менее, директория include там тоже есть, и все библиотечные хэдеры живут там:)

Самый популярный файл во всем busybox — обитающий там заголовочник с интригующим названием «Libbb.h»

По поводу полного пути — тут вы не совсем правы. Когда мы указываем директорию include, то поиск заголовочников при линковке будет происходить в ней. Так что можно обойтись указанием имени файла в кавычках :)
По поводу полного пути — тут вы не совсем правы

Я просто подумал, что вы предлагаете в папке include дублировать всю структуру каталогов для исходников, а не держать там только глобальные заголовочники. Если в папку include не совать вообще все заголовочные файлы, а только глобальные, то никаких возражений у меня нет.
В папку include нужно класть только внешние заголовки библиотеки. Все внутренние заголовки удобнее держать рядом с компилируемыми файлами (.c), тут вы правы. Делается это для того, чтобы избежать роста сложности в настройках проектов использующих ваш модуль. Представьте, что в ваших внешних includa-x есть ссылки на include-ы сторонних библиотек, которые вы используете. В таком случае, пользователь вашей библиотеки просто не сможет использовать ее без настойки правильных путей к внешним библиотекам.
Например, если есть module1 использующий module2 который, в свою очередь использует module3, а во внешних include-ах всех модулей присутствуют включения друг-друга, то module1 не получится использовать без include-ов модулей 2 и 3, хотя на самом деле достаточно только .lib файлов.

Не совсем понятно почему часть про кодстайл описана в контексте Си. Сейчас, на мой взгляд, кодстайл должен быть неотъемлимой частью любого проекта.


Не понимаю в чем профит группирования заголовочных файлов в одном месте, если это не какая-то библиотека. Когда работал с одним самописным игровым движком (не моим) был бинарник и куча .h файов, который служили интерфейсами дял подключения. Когда не видишь реализации — это удобно. В остальных случаях скачки фокуса между папками include и src довольно неудобны и в чем польза такой группировки мне непонятно.


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

Не совсем понятно почему возникает необходимость использовать volatile переменные как в примере.


Там явно написано почему, в первом же предложении. Во втором — более развёрнуто.
Скажу снова: по поводу оформления кода — это не «абсолютные истины», а рекомендации. Практика, которая встречается чаще всего. Кодстайл всегда зависит от конкретного проекта.

По поводу заголовочников стоит написать отдельную статью. В идеале, все заголовочники должны быть оформлены так, чтобы из них можно было понять как можно больше интерфейсов взаимодействия внутри программы.
+ надо понимать, что ничего не стоит делать бездумно. В том же busybox заголовочники бывают 2х типов: общего пользования и локальные. Общего пользования лежат в include в корне, а локальные — в папках с утилитами (подпроектами).
Очень правильный комментарий вот тут, группировка — очень хорошая практика. Однако в системе вынос в инклуд я встречаю гораздо чаще.

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

Тогда любое изменение этих переменных, которое будет сделано из другого, будет неявным для компилятора. Отсутствие volatile в этом случае может положить весь механизм IPC в приложении.

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

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

В C++ использование volatile в многопоточной среде — это очень вредный совет! Он просто не имеет свойств, которые могут быть нужны в многопоточной среде — например, атомарности изменений. Подробное обсуждение есть на stackoverflow
могут читать люди, пишущие на разных языках

В C++

Настоящий фидошник Subject не читает?
Вам смешно, а начинающий разработчик прочитает совет, подумает «ну C++ — это почти тот же C», и будет бездумно применять volatile там, где не надо. Навстречался я уже с такими, так что всегда не лишним будет предупредить лишний раз.
Вы не правы. Модификатор volatile не имеет к многопоточности никакого отношения. Это абсолютно перпендикулярные вещи. В общем случае, кроме чтения/записи байта, вы не сможете атомарно менять переменную, не используя специальные функции (примитивы) предназначенные для этого. volatile никак не влияет на барьеры памяти в С, в отличие, например, от Java-ы.

Ещё забавно, как сочетается volatile и переупорядочивание строк компилятором. Тут можно придумать что-то кроме вставки на асме или специфичных для процессора инструкций?

Вы веткой не ошиблись?
Если нет, то, видимо, это я выразился неясно. Собственно, я то и имел в виду, что использовать volatile для доступа к разделяемой памяти без какой-либо внешней синхронизации небезопасно по большому количеству различных причин, и нужно применять существующие примитивы.
Я прочитал ваш комментарий.
Да, я не прав. Многопоточное приложение тут не при чем, компилятор в состоянии отслеживать изменения переменных в потоках.
А вот когда несколько процессов могут менять одну переменную — другое дело :)
Когда речь идет о изменении переменной извне процесса, то тогда оно будет неявным для компилятора.
Да. Но если совсем точно, то компилятор ничего не знает про потоки, абсолютно. Не знаю как в C, а в C++ только-только стали появляться TLS в стандарте, а вся работа с многопоточностью реализована в библиотеке. В любом случае на месте обращения к разделяемой потоками переменной будет нетривиальная функция или intrinsinc и компилятор нечего не выкинет. С volatile все по старинке.
Вы неправы. Точнее не совсем правы. Компилятор в состоянии отследить изменение в разных потоках.
volatiole нужен в том случае если переменная изменяется снаружи.
Например у нас есть обработчик прерываний. И его вызова нету в тексте программы, так-как его вызывает(передает управление на него) процессор/контроллер в произвольный момент времени.
Или например у нас есть переменная которая связанна с некоторым физическим адресом(регистром периферии) в SoC.
И нам нужно записать а потом сразу считать значение так-как этот регистр обрабатывается особым образом.
И если мы напишем что-то типа

reg = 0x10;
if(reg == 1)
{
do_somthing;
}

и reg будет без volatile то копилятор вправе выкинуть этот код.
Или например мы пишем вот так
reg = 0x10;
reg = 0x11;
..
reg = 0x35;

Без volatile опять компилятор выкинет предыдущие операции и оставит только последнюю.
… а слова в названиях отделяются нижним подчеркиванием

А какие еще бывают подчеркивания?

Никакие :)
Отчего-то все называют этот символ именно так :)
Изначально хотел написать «нижний дефис», но меня раскритиковали.
У меня вызывает внутреннее противление приведённая в статье реализация функции
static int CheckModemConnection()
На первый взгляд, всё выглядит понятным. На это и рассчитано.
Возвращает 1, если параметры, связанные с модемным соединением изменились, и 0, если нет.
Насколько я помню, это традиционно для Си возвращать именно такие значения, чтобы в месте вызова можно было бы реализовать обработку для случая произошедших изменений. Было бы логичнее иметь в качестве кода возврата bool, но это же Си! (NB: надо свериться с последним стандартом!)

Далее, название функции: CheckModemConnection. А если придётся проверять ещё чего-нибудь, то надо будет писать отдельную функцию для вот этого самого чего-нибудь? Мне бы ужасно захотелось бы обобщить и иметь общую для всех функцию проверки. Даже, если в Си нет классов. (Всегда можно ввести.) К тому же, для того, чтобы иметь возможность проверять состояние, неплохо бы как-то формализовать сие понятие и сделать так, чтобы, например, пробегать по списку необходимых свойств в цикле, а не создавать длинное условие для оператора if.

В конце-концов, было бы крайне любопытно (и эффективно?) иметь текстовую строку, описывающую текущее состояние объекта, каждый символ которой связан с некоторым элементом описания модемного соединения. Делая простой проход по этой строке, можно было бы сразу получать ответ на нужный вопрос. А, если само символьное представление делать более сложным (XML?), то можно было бы автоматизировать выдачу диагностических сообщений (например, соединяя друг с другом строки состояния для различных элементов в единую строку).
Было бы логичнее иметь в качестве кода возврата bool

Для C, скорее, традиционней ERROR_SUCCESS (0 в общем случае) или код ошибки (!= 0), или что изменилось, etc.

NB: надо свериться с последним стандартом!

C99 — stdbool.h

Если бы надо было проверить что-то еще — да, мы бы написали отдельную функцию.
И она бы вызывалась вместе со всеми проверками. CheckModemConnection() — одна из сотен функций, которые вызываются при обработке пришедшей конфигурации. То есть все — и сеть, и voip, и iptv имеет подобные проверки.
Нужно это, чтобы группировать параметры, и, когда надо добавить новый, знать, где находится проверка группы связанных параметров.

Конфигурация приходит в виде дерева, по конкретным нодам в цикле не пройтись. Разве что, можно создать массив/перечисление с указателями на параметры, и идти по нему… Но это не слишком целесообразно, ибо будет гораздо сложнее найти источник и сразу не скажешь (не смотря на массив), какие параметры подвергаются проверке.
Сказанное Вами побуждает меня попросить Вас сообщить новые подробности. Всё это воспринимается как задачка, при решении которой можно было бы проверить в деле много разных «гитик».
Эта функция — одна из рядовых проверок в одном крупном проекте.
Так как пункт про комменты самый первый и ориентирован больше для новичков, то я просто взял эту функцию (в которой изначально комментариев не было), и для наглядности придумал пару поясняющих комментариев.

"Традиционно" для Си функции действия add, get, write_ и т. д. должны возвращать отрицательный код ошибки, 0 или положительное значение в случае успеха. А функции предиката аналог bool — 0 и 1.


В данном примере CheckModemConnection() из названия должна возвращать 0 — всё ок или отрицательный код ошибки. Поэтому лучше её было бы назвать как-то так: isModemConnectionChanged(), тогда сразу бы было ясно, что она "Возвращает 1, если параметры, связанные с модемным соединением изменились, и 0, если нет.".


Слово традиционно я намеренно заключил в кавычки, т.к. если говорить о традициях, то язык Си неразрывно связан с историей Unix. Поэтому приверженцам традиций лучше следовать Linux kernel code style — пункт 16, либо KNF.

Костыль, созданный с одной целью — вставить его, как палку, в колеса тому, кто будет расширять этот код.
Вернее, не так.
Это — костыль, который ясно вам скажет: не надо повторять мою реализацию, хочешь пакет больше — указатели, динамическая память и динамическое вычисление размера тебе в помощь!
Это — пример того, как делать не стоит. Никогда. Иначе случится насилие. Рано или поздно.

Вы не поняли, и делаете неправильные выводы.
Это проверка, что компилятор корректно упаковал структуру с нужным размером.
См. https://git.busybox.net/busybox/commit/?id=6884f665bd7bc101f56ff9047afaffbc06dc99e2


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

Это бессмысленное усложнение, т.к. нужна структура/буфер под максимальный размер DHCP пакета. Динамика — оверкил в условиях, когда пакеты обрабатываются строго последовательно и максимальный размер пакета заранее известен.
Если DHCP клиент не обозначил серверу максимальный размер (57 опция), то сервер не имеет права по RFC отвечать пакетами бОльшего размера. Однако, это только в идеальном мире, поэтому есть опция UDHCP_SLACK_FOR_BUGGY_SERVERS, в которой задается размер дополнительного места в буфере с максимумом 924, что дает возможность принимать пакеты до 1500 байт.
См. https://git.busybox.net/busybox/commit/?id=72e76044cfda377486a5199a0d35d71edf669a42


Такой небольшой размер — прямое противоречие RFC с описанной в нем 57й опцией.

Никакого противоречия нет. Такой небольшой размер находится в полном соответствии с минимальным MTU, ниже которого быть не может. Соответственно, т.к DHCP протокол основан на UDP без подтверждения доставки, этот небольшой размер дает "гарантию", что DHCP пакеты не потеряются по пути из-за более меньшего MTU.
Чтобы информировать сервер о максимально поддерживаемом размере, добавляется 57 опция, с размером — IP_UDP_DHCP_SIZE == 576, вне зависимости от дополнительного буфера UDHCP_SLACK_FOR_BUGGY_SERVERS.
См. коммиты 2007 и 2010 года:
https://git.busybox.net/busybox/commit/?id=35ff74676b54b1cae5a6324d2517568393fedbc8
https://git.busybox.net/busybox/commit/?id=b3af65b95de883e9be403e065f57b867d8ea8d43


Таким образом, чтобы "законно" получать пакеты больше стандартного размера, нужно
1) увеличить размер UDHCP_SLACK_FOR_BUGGY_SERVERS до максимально возможного
2) уведомлять сервер о реально поддерживаемом размере 57й опцией с учетом текущего MTU интерфейса.
И тут, всё давно придумано
https://github.com/wl500g/wl500g/commit/57fd93bd29399d6b08643bb79a3e41f330b6cd9a

Спасибо за проделанную работу по поиску информации по теме.

Да, вы абсолютно правы, я не правильно понял замысел и интерфейсы bb.
Извиняюсь.
Сейчас отредактирую статью.
Объявляйте переменные в начале функции.

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

Даже Google думает не так, как написано:
C++ allows you to declare variables anywhere in a function. We encourage you to declare them in as local a scope as possible, and as close to the first use as possible.


Вы можете обьяснить зачем это делать?
Чтобы код компилировался на старом компиляторе, например.
Или если вы по какой-то причине считаете, что именно в этой функции вам важнее уметь находить все переменные функции за один раз, чем удобство просмотра инициализации какой-нибудь одной.

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

Жизнь очень многогранна, то, что для одного — укуренный бред, для другого — реальная ситуация.
Как минимум для совместимости с ANSI / ISO стандартами.

Даже Google думает не так

И о другом языке.

не нужно искать тип переменной

https://habrahabr.ru/post/325678/#comment_10157022
Если мы говорим о совместимости, то стоит добавить советы типа «использовать битовый сдвиг вместо умножения»
Для современных (и даже не очень) компиляторов совет абсолютно бесполезный — они сами разберутся как оптимизировать арифметику. А к совместимости он вообще не имеет никакого отношения.
Сюда же в правило обычно добавляються переменные в `for loop`-ах. От этого горит еще больше.

ИМХО такое объявление добавляет читабельности: сразу прочитав список переменных, уже примерно понимаешь, с чем имеешь дело и что делает код.

Ну как читабельности… Если функцию можно разделить на логические части, а для каждой части свои переменные. То читабельнее будет объявлять переменные хотя-бы перед каждой частью, но уж точно не в начале функции.
Отличное руководство. Как раз для таких начинающих как я. Спасибо большое.
По поводу инициализации переменных не соглашусь. Если переменные не инициализировать, компилятор будет ругаться, если она была использована без инициализации. Несколько раз это серьёзно выручало при написании конечных автоматов на case-ах.

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

А ещё, начинающим сишникам я бы порекомендовал поизучать код nginx, отличный код, интересные решения.
Да, я бы порекомендовал вместо инициализации переменных включить это (и не только) в сообщения об ошибках и реагировать на них!
Если вы постоянно работаете с трекерами (вроде RedMine), то при внесении правок в код можно указать номер задачи, в рамках которой эти правки были внесены. Если у кого-то при просмотре кода возникнет вопрос а-ля «Зачем тут этот функционал?», ему не придется далеко ходить. В нашей компании еще пишут фамилию программиста, чтобы если что знать, к кому идти с расспросами.
/* Muraviyov: #66770 */


Плохой совет. Плохой он оттого, что комментарии никто не поддерживает.
Мировой опыт (выраженный в книжках МакКоннелла, Р. Мартина и пр.), а равно и мой скромный, говорит, что комментарии редко бывают актуальными, тем более, такие.
«Кто, когда и почему?» — на эти вопросы достоверно отвечает система контроля версий (должна отвечать… у вас-то какая?).
Когда мы делаем git blame в каком-нибудь огромном файле, в который каммитили все, кому не лень, найти изменения по какой-то конкретной фигне может быть довольно долго.
+ даже зная, кто и каким каммитом добавил функционал, не всегда можно понять, в рамках какой задачи это было сделано. (не все и не везде указывают).
Да и в любом случае, просто вбить заранее указанный номер задачи в трекер будет в разы быстрее, чем сначала искать «кто, где и зачем».

По поводу того, что комментарии не поддерживают — не соглашусь.
Я бы посмотрел на того, кто бы взялся сопровождать тот же bb, не будь он так хорошо задокументирован)
Если комментарии не нужны, то почему так много рекомендаций из самой различной литературы и интернета отмечают умение грамотно писать комментарии одним из правил хорошего тона?
Когда мы делаем git blame в каком-нибудь огромном файле

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

это ничем не отличается от наличия/отсутствия (обсуждаемого нами) коммента

это вопрос дисциплины и гайдов оформления текста коммита, только вот в отличие от отсутствия комментария с причинами «забыл/лень/не хочу/не знаю» текст коммита легко валидируется хуками, что способствует гайдам

просто вбить заранее указанный номер задачи в трекер будет в разы быстрее

с «быстрее вбить» спорить даже не собираюсь (я сам люблю one-click переходы)
но Вы забываете про достоверность

Я бы посмотрел на того, кто бы взялся сопровождать тот же bb, не будь он так хорошо задокументирован)

я не знаю, что такое bb
но с высоты своего опыта поддержки многолетних активно меняющихся legacy-продуктов могу утверждать, что такие комментарии бессмысленны чуть менее, чем совсем

отмечают умение грамотно писать комментарии одним из правил хорошего тона

я выделил ключевое слово ;)
Соглашусь с Zapped. Очень уж часто у моей практике такие комментарии протухали. Или трекер менялся, и получалось как на bash:
комментарий перед злобной реализацией некого алгоритма на несколько страниц: «описание алгоритма смотри в тетрадке у Чуня».
Only those users with full accounts are able to leave comments. Log in, please.