Введение

В данной заметке я поделюсь своим видением идеальной библиотеки на Си. Будет предложена простая и понятная организация файлов и директорий в дистрибутиве, которую можно взять за основу, а также небольшие guidelines по общему дизайну современного API на языке Си.

В статье мы создадим простую библиотеку учёта сотрудников: people.

Мои guidelines максимально кратко

Файлы:

  1. Все файлы имплементации в src/ имеют имя вида: p_foo.c, где p есть первая буква имени бибилотеки
  2. Модули в src/ могут иметь известные друг-другу структуры и функции: всё, что используется более, чем одним модулем в src идёт в src/people_internal.h или даже в src/p_module_internal.h. Обазательно использовать слово internal для единого файла.
  3. Минимум CMake, минимум зависимостей

API:

  1. Консистентный нейминг везде: ppl_foo(), PeopleType и PPL_ERR_BAD_NAME
  2. Все функции возвращают известный enum с кодом ошибки; результаты пишутся в указатели, подаваемые с прочими аргументами в функцию
  3. Везде, где возможно в публичном API используются opaque pointers, т.е. содержание структур скрыто от пользователя и работа с ними происходит исключительно посредством библиотечных функций
  4. Квалификатор 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:

  1. Корневой
  2. Внутри общей директории с исходниками
  3. Внутри субдиректории с исходниками нашей либы people
  4. Внутри директории с тестами

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);