API-интерфейс совместно используемой библиотеки Linux с примером ABI
Этот ответ был извлечен из моего другого ответа: Что такое двоичный интерфейс приложения (ABI)? , но я чувствовал, что он также отвечает и на этот, и что вопросы не являются дубликатами.
В контексте разделяемых библиотек наиболее важным следствием «наличия стабильного ABI» является то, что вам не нужно перекомпилировать свои программы после изменения библиотеки.
Как мы увидим в примере ниже, можно изменить ABI, нарушая работу программ, даже если API не изменился.
main.c
#include <assert.h>
#include <stdlib.h>
#include "mylib.h"
int main(void) {
mylib_mystrict *myobject = mylib_init(1);
assert(myobject->old_field == 1);
free(myobject);
return EXIT_SUCCESS;
}
mylib.c
#include <stdlib.h>
#include "mylib.h"
mylib_mystruct* mylib_init(int old_field) {
mylib_mystruct *myobject;
myobject = malloc(sizeof(mylib_mystruct));
myobject->old_field = old_field;
return myobject;
}
mylib.h
#ifndef MYLIB_H
#define MYLIB_H
typedef struct {
int old_field;
} mylib_mystruct;
mylib_mystruct* mylib_init(int old_field);
#endif
Компилируется и работает нормально с:
cc='gcc -pedantic-errors -std=c89 -Wall -Wextra'
$cc -fPIC -c -o mylib.o mylib.c
$cc -L . -shared -o libmylib.so mylib.o
$cc -L . -o main.out main.c -lmylib
LD_LIBRARY_PATH=. ./main.out
Теперь предположим, что для v2 библиотеки мы хотим добавить в mylib_mystrict
новое поле с именем new_field
.
Если мы добавили поле до old_field
как в:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
и перестроить библиотеку, но не main.out
, тогда утверждение не выполняется!
Это потому, что строка:
myobject->old_field == 1
сгенерировал сборку, которая пытается получить доступ к самой первой int
структуры, которая теперь new_field
вместо ожидаемого old_field
.
Поэтому это изменение сломало ABI.
Если, однако, мы добавим new_field
после old_field
:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
тогда старая сгенерированная сборка все еще обращается к первой int
структуры, и программа все еще работает, потому что мы сохранили ABI стабильным.
Вот полностью автоматизированная версия этого примера на GitHub .
Другим способом сохранения стабильности ABI было бы обрабатывать mylib_mystruct
как непрозрачную структуру и обращаться к ее полям только через помощники методов. Это упростит поддержание стабильности ABI, но приведет к снижению производительности, поскольку мы будем выполнять больше вызовов функций.
API против ABI
В предыдущем примере интересно отметить, что добавление new_field
перед old_field
только нарушило ABI, но не API.
Это означает, что если бы мы перекомпилировали нашу main.c
программу с библиотекой, она бы работала независимо от этого.
Однако мы бы также нарушили API, если бы изменили, например, сигнатуру функции:
mylib_mystruct* mylib_init(int old_field, int new_field);
, поскольку в этом случае main.c
вообще перестанет компилироваться.
Семантический API против API программирования против ABI
Мы также можем классифицировать изменения API по третьему типу: семантические изменения.
Например, если мы изменили
myobject->old_field = old_field;
до:
myobject->old_field = old_field + 1;
тогда это не сломало бы ни API, ни ABI, но main.c
все равно сломалось бы!
Это потому, что мы изменили «человеческое описание» того, что должна делать функция, а не программно заметный аспект.
У меня только что было философское понимание того, что формальная проверка программного обеспечения в некотором смысле перемещает больше "семантического API" в более "программно проверяемый API".
Семантический API против API программирования
Мы также можем классифицировать изменения API по третьему типу: семантические изменения.
Семантический API, как правило, представляет собой описание на естественном языке того, что должен делать API, обычно включается в документацию API.
Поэтому возможно нарушить семантический API, не нарушая саму сборку программы.
Например, если мы изменили
myobject->old_field = old_field;
до:
myobject->old_field = old_field + 1;
тогда это не нарушило бы ни API программирования, ни ABI, но main.c
семантический API сломался бы.
Существует два способа программной проверки API контракта:
- проверить несколько угловых случаев. Это легко сделать, но вы всегда можете пропустить один.
- формальная проверка .Сложнее сделать, но производит математическое доказательство правильности, по существу объединяя документацию и тесты в «человеческий» / машинно проверяемый способ!Пока в вашем формальном описании нет ошибки, конечно; -)
Протестировано в Ubuntu 18.10, GCC 8.2.0.