Этот текст адресован когорте программистов на С(ях). Это не академические атрибуты из учебников это скорее правила буравчика оформления сорцов из реального prod(а). Некоторые приемы совпали с MISRA, некоторые с CERT-C. А кое-что является результатом множества итераций инспекций программ и перестроек после реальных инцидентов. В общем тут представлен обогащенный концентрат полезных практик программирования на С(ях).
*1–Все функции должны быть менее 45 строк. Так каждая функция сможет уместиться на одном экране. Это позволит легко анализировать алгоритм и управлять модульностью кода.
*2–Не допускать всяческих магических чисел в коде. Это уничтожает читаемость кода. Все константы надо определять в перечисления заглавными буквами.
*3–На все сборки должна быть одна общая кодовая база (общак, репа). Модификация в одном компоненте должна отражаться на всех сборках организации, использующих компонент (например алгоритмы CRC). Это позволит сэкономить время на создание новых проектов для новых программ.
*4–Все .с файлы должны быть оснащены одноименным .h файлом. Так эффективнее переносить, анализировать и мигрировать проекты на очередные аппаратные платформы. И сразу понятно, где следует искать прототипы функций из *.c файлов.
*5–Аппаратно-зависимый код должен быть отделен от аппаратно независимого кода по разным файлам и разным папкам. Так можно тестировать на другой архитектуре платформо-независимые функции и алгоритмы. Всякую математику, калькуляторы всяческих CRC(шек) и работу со строчками.
6--Константы следует определять при помощи перечислений enum в большей степени, чем препроцессором. Так можно собрать константы из одной темы в одном месте и они не будут разбросаны по всему проекту.
7–Не вставлять функции внутрь if() . Коды возврата приходится анализировать отладчиком до проверки условия.
это очень плохо:
if (MmGet(ID_IPv4_ROLE, tmp, 1, &tmp_len) != MM_RET_CODE_OK) {
return ERROR_CODE_HARDWARE_FAULT;
}
Надо писать код так, чтобы было возможно его проверять пошаговым отладчиком. Поэтому каждое элементарное действие должно быть на одной строке. Вот так уже гораздо лучше.
int ret = MmGet(ID_IPv4_ROLE, tmp, 1, &tmp_len);
if (ret != MM_RET_CODE_OK) {
return ERROR_CODE_HARDWARE_FAULT;
}
8–Использовать static функции везде, где только можно. Это повысит модульность.
*9–Используй препроцессорный #error для предупреждения о нарушении зависимостей между компонентами.
#ifndef ADC_DRV_H
#define ADC_DRV_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include "adc_bsp.h"
#include "adc_types.h"
#ifndef HAS_MCU
#error "+ HAS_MCU"
#endif
#ifndef HAS_ADC
#error "+ HAS_ADC"
#endif
bool adc_init_channel(uint8_t adc_num, AdcChannel_t adc_channel);
bool adc_init(void);
bool adc_proc(void);
bool adc_channel_read(uint8_t adc_num, uint16_t adc_channel, uint32_t* code);
#ifdef __cplusplus
}
#endif
#endif /* ADC_DRV_H */
*10--Если что-то можно проверить на этапе make файлов, то это надо проверить на этапе make файлов. Каждый компонент должен проверять, что подключены нужные зависимости. Это можно сделать через условные операторы make файлов.
$(info I2S_MK_INC=$(I2S_MK_INC))
ifneq ($(I2S_MK_INC),Y)
I2S_MK_INC=Y
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
$(info Build $(mkfile_path) )
I2S_DIR = $(WORKSPACE_LOC)bsp/bsp_stm32f4/i2s
#@echo $(error I2S_DIR=$(I2S_DIR))
INCDIR += -I$(I2S_DIR)
OPT += -DHAS_I2S
SOURCES_C += $(I2S_DIR)/i2s_drv.c
ifeq ($(DIAG),Y)
SOURCES_C += $(I2S_DIR)/i2s_diag.c
endif
ifeq ($(CLI),Y)
ifeq ($(I2S_COMMANDS),Y)
OPT += -DHAS_I2S_COMMANDS
SOURCES_C += $(I2S_DIR)/i2s_commands.c
endif
endif
endif
*11--Если что то можно проверить на этапе препроцессора, то это надо проверить на этапе препроцессора. Каждый компонент должен проверять, что подключены нужные зависимости. Это можно сделать через макросы компонентов.
*12–Если что-то можно проверить на этапе компиляции, то это надо проверить на этапе компиляции (static_assert(ы)). Например можно проверить, что в конфигурациях скорость UART не равна нулю. В RunTime не должно быть проверок, которые можно произвести на этапе компиляции, препроцессора или make файлов.
*13–Каждой set функции должна быть поставлена в соответствие get функция. Это позволит написать модульный тест для данного параметра.
*14–Если переменная это физическая величина, то в суффиксе указывать размерность (timeout_ms). Это увеличивает понятность кода.
*15–Все Си-функции должны всегда возвращать код ошибки. Минимум тип bool или числовой код ошибки. Так можно понять, где именно что-то пошло не так. Проще говоря, не должно быть функций, которые возвращают void. Функции void это, по факту, бомбы с часовым механизмом. В один день они отработают ошибочно, а вы об этом ничего даже не узнаете.
*16–Для каждого программного компонента создавать несколько *.с *.h файлов:
Файл |
h |
c |
файл констант компонента |
* |
|
файл типов данных для данного компонента |
* |
|
файл команд CLI |
* |
* |
файлы конфигурации компонента |
* |
* |
файлы диагностики |
* |
* |
файлы самого драйвера |
* |
* |
Это позволит ориентироваться в коде и управлять модульностью.
17–Если функция получает указатель, то пусть сразу проверяет на нуль значение указателя. Так прошивки не будут падать при получении нулевых указателей. Это повысит надежность кода. Вы же не знаете как и кто этот код будет испытывать. Хорошая функция всегда проверяет то, что ей дают.
18–Если есть конечный автомат, то добавить счетчик циклов. Так можно будет проверить, что автомат вообще вертится.
19–В идеале все переменные должны иметь разные имена. Так было бы очень удобно делать поиск по grep. Но тут надо искать компромисс с наглядностью.
20–У каждой функции должен быть только 1 return. Это позволит дописать какой-то функционал в конце, зная, что он точно вызовется.
21–Не использовать операторы >, >= Вместо них использовать <, <= просто поменяв местами аргументы там, где это нужно. Это позволит интуитивно проще анализировать логику по коду. Человеку еще со времен школьной математики понятнее, когда то, что слева - то меньше, а то, что справа - то больше. Так как ось X стрелкой показывала вправо. Особенно удобно при проверке переменной на принадлежность интервалу. Получается, что > и >= это вообще два бессмысленных оператора в языке С.
*22–В проекте обязательно должны быть модульные тесты. Тесты это просто функции, которые вызывают другие функции в run-time. Это позволит сделать безболезненную перестройку кода, когда архитектура начнет скрипеть. Тесты можно вызывать как до запуска приложения, так и по команде из UART- CLI.
*23–Избегайте бесконечных циклов while (1) при блокирующем ожидании чего-либо. Например ожидание прерывания по окончании отправки в UART. Прерывания могут и не произойти из-за сбоя. while (1) это просто капкан в программировании. Всегда должен быть предусмотрен аварийный механизм выхода по TimeOut(у).
24–Использовать макрофункции препроцессора для кодогенерации одинаковых функций или пишите кодогенераторы, если препроцессор запрещен. Копипаста - причина программных ошибок №1.
*25--Все высокоуровневые функции в конец .с файла. Это избавит от нужды указывать отдельно прототипы static функций.
26–Скрывать область видимости локальных переменных по максимуму.
27–Если код не используется, то этот код не должен собираться. Это уменьшит размер артефактов. Уменьшит вероятность ошибок.
28–Если вы в С передаете что-то через указатель или возвращаете через указатель, то указываете направление движения данных приставками in, io или out.
Например:
void proc_some_data(unsigned char* inBuffer, unsigned char* outBuffer, int len, int *outLen)
Это позволит легче читать прототипы, не погружаясь в тело функции
29–Давайте переменным осмысленные имена, чтобы было удобно grep(ать) по кодовой базе
*30--Если в коде есть список чего-либо (прототипы функций, макросы, перечисления), то эти строки должны быть отсортированы по алфавиту. Если сложно сортировать вручную, то можно прибегнуть к помощи утилиты sort. Это позволит сделать визуальный бинарный поиск и найти нужную строчку. Также при сравнении 2-x отсортированных файлов отличия будут минимальные.
32–Функции CamelCase переменные snake_case.
*33–Все .h файлы снабжать защитой препроцессора от повторного включения. Это же касается *.mk файлов.
*34–Сборка из Makefile(ов) является предпочтительнее чем сборка из GUI-IDE. Makе позволяет по-полной управлять модульностью кодовой базы.
*35--Для синтаксического разбора регистров использовать объединения вкупе с битовыми полями.
/*Table 15. IB2-ADDR: I0000010*/
typedef union {
uint8_t reg_val;
struct{
uint8_t clipping_information:1;
uint8_t output_offset_information:1;
uint8_t input_offset_information:1;
uint8_t fault_information:1;
uint8_t temperature_warning_information: 3;
uint8_t res:1;
};
}Fda801RegIb2Addr_t;
Это позволит делать парсинг полей одной строчкой.
Fda801RegIb2Addr_t Reg;
Reg.reg_val = reg_val;
*36–Соблюдать программную иерархичность. Низкоуровневый модуль не должен управлять (вызывать функции) более высокоуровневого модуля. UART не должен вызывать функции LOG. И компонент LOG не должен вызывать функции CLI. Управление должно быть направлено в сторону от более высокоуровневого компонента к более низкоуровневому компоненту. Например CLI->LOG->UART. Не наоборот.
*37–Делать автоматическое форматирование отступов исходного кода. Подойдет например бесплатная утилита clang-format или GNUIndent. Это позволит делать простые выражения при поиске по коду утилитой grep. И будет минимальный diff при сравнении истории файлов. Придерживаться какого-нибудь одного стиля форматирования. Пусть будет "единообразно безобразно".
38--При сравнении переменных с константой константу ставьте слева от оператора ==.
неправильно: if (val == 10 ) doSmth=1;
правильно: if (10 == val) doSmth=1;
Когда константа на первом месте, то компилятор выдаст ошибки присвоение к константе в случае опечатки
if (10=val ) doSmth=1;
Такая конструкция
if (val = 10 ) doSmth=1;
незаметно собирается и вызовет трагедию во время исполнения.
39–В каждом if всегда обрабатывать else вариант даже если else тривиальный. Это позволит предупредить многие осечки в программе.
40–Всегда инициализировать локальные переменные в стеке. Иначе там просто будут случайные значения, которые могут что-нибудь повредить.
*41–Тесты и код разделять на разные компоненты. То есть код и тесты должны быть в разных папках. Включаться и отключаться одной строчкой в make-файле.
*42–В хорошем С-коде в принципе не должно быть комментариев. Лучший комментарий к коду - это адекватные имена функций и переменных.
*43–Собирать артефакты как минимум двумя компиляторами (CCS + IAR) или (GCC+GHS) или (Clang+GCC) и тп. Если первый компилятор пропустил ошибку, то второй компилятор может и найти ошибку.
44–Прогонять кодовую базу через статический анализатор. Хотя бы бесплатный CppCheck. Может, найдется очередная загвоздка.
45--За if, for ... всегда должны быть { }. Весьма вероятно, что условие будет пополнено операторами.
Аномалии оформления сорцов из реальной жизни (War Stories)
1–Магические циферки на каждой строчке
2–Переиспользование глобальных переменных
3--Доступ к регистрам микроконтроллера в каждом файле проекта
4–Повторяемость кода
5--Очевидные комментарии
6–"заборы" из комментариев
7--.с файлы оснащены разноименными .h файлами.
8--Макросы маленькими буквами
9--Код без модульных тестов
10–Код как в миксере перемешанный с тестами
11--Функции от 1000 до 5000 строк и даже более
12--Вставка препроцессором #include *.c файлов.
13--Вся прошивка в одном main.c файлике 75000 строк аж подвисает текстовый редактор.
14--С-функции с именами литературных персонажей.
Вывод
Общая канва такова, что при написании С-кода надо держать в уме такие понятия как простота, тесто пригодность, поддерживаемость, ремонтопригодность, модульность, согласованность (принцип наименьшего удивления), масштабируемость, иерархичность, конфигурируемость, изоляция компонентов и переносимость.
Если вы практикуете еще вспомогательные эффективные приемы написания программ на С(ях), то пишите их в комментариях
Автор:
aabzel