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.