Как я подхожу к дизайну API библиотеки на Си
Содержание
Введение⌗
В данной заметке я поделюсь своим видением идеальной библиотеки на Си. Будет предложена простая и понятная организация файлов и директорий в дистрибутиве, которую можно взять за основу, а также небольшие guidelines по общему дизайну современного API на языке Си.
В статье мы создадим простую библиотеку учёта сотрудников: people.
Мои guidelines максимально кратко⌗
Файлы:
- Все файлы имплементации в
src/
имеют имя вида:p_foo.c
, где p есть первая буква имени бибилотеки - Модули в
src/
могут иметь известные друг-другу структуры и функции: всё, что используется более, чем одним модулем вsrc
идёт вsrc/people_internal.h
или даже вsrc/p_module_internal.h
. Обазательно использовать слово internal для единого файла. - Минимум CMake, минимум зависимостей
API:
- Консистентный нейминг везде:
ppl_foo()
,PeopleType
иPPL_ERR_BAD_NAME
- Все функции возвращают известный enum с кодом ошибки; результаты пишутся в указатели, подаваемые с прочими аргументами в функцию
- Везде, где возможно в публичном API используются opaque pointers, т.е. содержание структур скрыто от пользователя и работа с ними происходит исключительно посредством библиотечных функций
- Квалификатор
extern
на функциях в хедерах
Библиотека people⌗
Определяемся с именем бибилотеки⌗
Самый важный шаг – это выбор имени. В Си отсутствуют namespaces, поэтому дизайнеры библиотек обычно, во избежании коллизии символов,
предваряют все функции определённым коротким именем. Наша либа называется people
, поэтому используем что-нибудь типа ppl
для
названия функций: ppl_inint()
, ppl_close()
, и т.п. Для названия же типов данных можно использовать полное имя: PeopleDepartment
, PeoplePerson
и т.п.
Итак, функции называем методом snake_case строго по шаблону: ppl_department_create()
. Типы данных называем методом CamelCase через typedef: PeopleDepartment
и т.п.
Организация файлов⌗
Предлагается следующая иерархия для любой бибилотеки:
people/
├── CMakeLists.txt
├── lib
│ ├── CMakeLists.txt
│ └── people
│ ├── CMakeLists.txt
│ ├── include
│ │ └── people
│ │ └── people.h
│ └── src
│ └── p_foo.c
└── test
├── CMakeLists.txt
└── test1.c
6 directories, 7 files
Файлы CMakeLists.txt⌗
Всего имеем четыре файла CMakeLists.txt:
- Корневой
- Внутри общей директории с исходниками
- Внутри субдиректории с исходниками нашей либы people
- Внутри директории с тестами
cmake_minimum_required(VERSION 3.5)
set(CMAKE_C_STANDARD 90)
set(CMAKE_C_STANDARD_REQUIRED ON)
project(people LANGUAGES C)
add_subdirectory(lib)
add_subdirectory(test)
add_subdirectory(people)
set(SRCS
include/people/people.h
src/p_foo.c
)
set(NAME people)
add_library(${NAME} SHARED ${SRCS})
target_include_directories(${NAME} PUBLIC include/)
target_link_libraries(${NAME} PUBLIC m)
set(NAME test1)
add_executable(test1 test1.c )
target_link_libraries(test1 PRIVATE people)
Файл публичного интерфейса библиотеки: people.h⌗
Это бубличный API нашей либы. Он может быть создан строго на экспорт, или же на него можно ссылаться и из исходников нашей либы.
Данный файл при установке на хосте должен будет иметь следующий путь: /usr/local/include/people/people.h
. При наличии нескольких файлов
или будущих API модулей нашей либы, все они будут аккуратно помещены в свою директорию: $PREFIX/include/people
.
Вот так может выглядеть наша либа.
#pragma once
/*
* People library. Public API.
*/
struct PeopleDepartment;
typedef struct PeopleDepartment PeopleDepartment;
struct PeoplePerson;
typedef struct PeoplePerson PeoplePerson;
enum PeopleError {
PPL_OK = 0,
PPL_ERR_INPUT_NULL,
PPL_ERR_OUTPUT_NULL,
PPL_ERR_BAD_SALARY,
PPL_ERR_BAD_NAME,
PPL_ERR_BAD_SIZE,
PPL_ERR_DEPARTMENT_EMPTY,
PPL_ERR_DEPARTMENT_FULL,
};
typedef enum PeopleError PeopleError;
extern PeopleError ppl_department_create(const char* name,
PeopleDepartment** out);
extern PeopleError ppl_department_add_person(PeopleDepartment* d,
const char* name,
int salary);
extern PeopleError ppl_department_list(PeopleDepartment* d,
PeoplePerson** outv,
int size,
int* actual_size);
extern PeopleError ppl_person_get_name(PeoplePerson* p, const char** out);
extern PeopleError ppl_person_get_salary(PeoplePerson* p, int* out);
extern PeopleError ppl_person_set_salary(PeoplePerson* p, int salary);
Все функции возвращают известные error-коды, структуры скрыты от пользователя (opaque pointer/typedef void), выходные параметры передаются по указателям.
Клиентский код, тест нашей либы⌗
Вот так клиент мог бы юзать наш API.
#include <people/people.h>
#include <stdio.h>
int main(int argc, char** argv) {
PeopleDepartment* d;
// create department
(void)ppl_department_create("Game department", &d);
// add persons
PeopleError retv[10] = {0};
retv[0] = ppl_department_add_person(d, "N. Burkov", 50000);
retv[1] = ppl_department_add_person(d, "J. Doe", 20000);
retv[2] = ppl_department_add_person(d, "M. Nobody", 10000);
retv[3] = ppl_department_add_person(d, "B. Gates", 990);
retv[4] = ppl_department_add_person(d, "D. Knuth", 40000);
int i;
for (i = 0; i < 5; ++i) {
if (retv[i] != PPL_OK) {
printf("Couldn't add person %d: error code %d\n", i + 1, retv[i]);
}
}
printf("All or some Persons are added to the department\n");
// list persons
PeoplePerson* pv[10];
int num_persons;
PeopleError ret = ppl_department_list(d, pv, 10, &num_persons);
if (ret != PPL_OK) {
printf("Couldn't get department list of persons: error code: %d\n", ret);
return -1;
}
printf("There are %d total persons in the department\n", num_persons);
for (i = 0; i < num_persons && i < 10; ++i) {
const char* name;
int salary;
(void)ppl_person_get_name(pv[i], &name);
(void)ppl_person_get_salary(pv[i], &salary);
printf("Person: %s, current salary %d\n", name, salary);
}
return 0;
}
Не получилось добавить B. Gates, т.к. salary слишком низка (о чём можно узнать из возвращённого значения).
Couldn't add person 4: error code 3
All or some Persons are added to the department
There are 4 total persons in the department
Person: N. Burkov, current salary 50000
Person: J. Doe, current salary 20000
Person: M. Nobody, current salary 10000
Person: D. Knuth, current salary 40000
Внутренняя имплементация либы people⌗
Унифицированная сигнализация ошибок через возвращаемое значение делает код даже самых примитивных функций довольно педантичным.
Весь конечный автомат ошибочных состояний полностью локализован и заранее раскрыт в публичном API посредством типа PeopleError
.
#pragma once
#include <stdlib.h>
#include <people/people.h>
#include "utlist.h"
#include "people_internal.h"
struct PeopleDepartment {
const char* name;
PeoplePerson* persons;
};
struct PeoplePerson {
const char* name;
int salary;
PeoplePerson* next;
};
PeopleError ppl_department_create(const char* name, PeopleDepartment** out) {
if (name == NULL) {
return PPL_ERR_BAD_NAME;
}
if (out == NULL) {
return PPL_ERR_OUTPUT_NULL;
}
PeopleDepartment* d = malloc(sizeof *d);
d->name = name;
d->persons = NULL;
*out = d;
return PPL_OK;
}
PeopleError ppl_department_add_person(PeopleDepartment* d,
const char* name,
int salary) {
if (d == NULL) {
return PPL_ERR_INPUT_NULL;
}
if (name == NULL) {
return PPL_ERR_BAD_NAME;
}
if (salary < 1000) {
return PPL_ERR_BAD_SALARY;
}
PeoplePerson* p;
int count;
LL_COUNT(d->persons, p, count);
if (count >= 5) {
return PPL_ERR_DEPARTMENT_FULL;
}
p = malloc(sizeof *p);
p->name = name;
p->salary = salary;
p->next = NULL;
LL_APPEND(d->persons, p);
return PPL_OK;
}
PeopleError ppl_department_list(PeopleDepartment* d,
PeoplePerson** outv,
int size,
int* actual_size) {
if (d == NULL) {
return PPL_ERR_INPUT_NULL;
}
if (outv == NULL) {
return PPL_ERR_OUTPUT_NULL;
}
if (size < 1) {
return PPL_ERR_BAD_SIZE;
}
PeoplePerson* p;
int count;
LL_COUNT(d->persons, p, count);
if (count < 1) {
return PPL_ERR_DEPARTMENT_EMPTY;
}
int num_wrote = 0;
for (p = d->persons; p != NULL; p = p->next) {
if (num_wrote < size) {
outv[num_wrote] = p;
num_wrote += 1;
}
}
if (actual_size != NULL) {
*actual_size = count;
}
return PPL_OK;
}
PeopleError ppl_person_get_name(PeoplePerson* p, const char** out) {
if (p == NULL) {
return PPL_ERR_INPUT_NULL;
}
if (out == NULL) {
return PPL_ERR_OUTPUT_NULL;
}
*out = p->name;
return PPL_OK;
}
PeopleError ppl_person_get_salary(PeoplePerson* p, int* out) {
if (p == NULL) {
return PPL_ERR_INPUT_NULL;
}
if (out == NULL) {
return PPL_ERR_OUTPUT_NULL;
}
*out = p->salary;
return PPL_OK;
}
PeopleError ppl_person_set_salary(PeoplePerson* p, int salary);