Exemplo de API mínima executável da biblioteca compartilhada Linux vs ABI
Esta resposta foi extraída da minha outra resposta: O que é uma interface binária de aplicativo (ABI)? mas senti que ele responde diretamente a este também, e que as perguntas não são duplicadas.
No contexto de bibliotecas compartilhadas, a implicação mais importante de "ter uma ABI estável" é que você não precisa recompilar seus programas após a alteração da biblioteca.
Como veremos no exemplo abaixo, é possível modificar a ABI, interrompendo os programas, mesmo que a API não seja alterada.
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
Compila e executa bem com:
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
Agora, suponha que, para a v2 da biblioteca, desejemos adicionar um novo campo ao mylib_mystrict
chamado new_field
.
Se adicionamos o campo antes old_field
como em:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
e reconstruiu a biblioteca, mas não main.out
, a declaração falha!
Isso ocorre porque a linha:
myobject->old_field == 1
gerou um assembly que está tentando acessar o primeiro int
da estrutura, que agora está em new_field
vez do esperado old_field
.
Portanto, essa mudança quebrou a ABI.
Se, no entanto, adicionarmos new_field
depois old_field
:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
então, o antigo assembly gerado ainda acessa o primeiro int
da estrutura e o programa ainda funciona, porque mantivemos a ABI estável.
Aqui está um versão totalmente automatizada deste exemplo no GitHub .
Outra maneira de manter essa ABI estável seria tratar mylib_mystruct
como uma estrutura opaca e acessar apenas seus campos por meio de auxiliares de método. Isso facilita a manutenção da ABI estável, mas acarretaria uma sobrecarga de desempenho, pois faríamos mais chamadas de função.
API vs ABI
No exemplo anterior, é interessante notar que adicionar o new_field
antesold_field
quebrou apenas a ABI, mas não a API.
O que isso significa é que, se tivéssemos recompilado nosso main.c
programa contra a biblioteca, ele teria funcionado independentemente.
No entanto, também teríamos quebrado a API, se tivéssemos alterado, por exemplo, a assinatura da função:
mylib_mystruct* mylib_init(int old_field, int new_field);
pois nesse caso, main.c
pararia de compilar completamente.
API semântica vs API de programação vs ABI
Também podemos classificar as alterações da API em um terceiro tipo: alterações semânticas.
Por exemplo, se tivéssemos modificado
myobject->old_field = old_field;
para:
myobject->old_field = old_field + 1;
então isso não main.c
quebraria a API nem a ABI, mas ainda quebraria!
Isso ocorre porque alteramos a "descrição humana" do que a função deve fazer, em vez de um aspecto perceptível programaticamente.
Acabei de ter a visão filosófica de que a verificação formal de software , de certo modo, move mais a "API semântica" para uma "API programaticamente verificável".
API semântica vs API de programação
Também podemos classificar as alterações da API em um terceiro tipo: alterações semânticas.
A API semântica, geralmente é uma descrição da linguagem natural do que a API deve fazer, geralmente incluída na documentação da API.
Portanto, é possível quebrar a API semântica sem interromper a construção do programa.
Por exemplo, se tivéssemos modificado
myobject->old_field = old_field;
para:
myobject->old_field = old_field + 1;
então isso não main.c
quebraria nem a API de programação nem a ABI, mas a API semântica seria interrompida.
Há duas maneiras de verificar programaticamente a API do contrato:
- teste vários casos de canto. Fácil de fazer, mas você sempre pode perder uma.
- verificação formal . Mais difícil de fazer, mas produz provas matemáticas de correção, essencialmente unificando documentação e testes de uma maneira "humana" / verificável por máquina! Contanto que não exista um bug em sua descrição formal, é claro ;-)
Testado no Ubuntu 18.10, GCC 8.2.0.