API condivisa minima libreria condivisa Linux vs esempio ABI
Questa risposta è stata estratta dall'altra mia risposta: che cos'è un'applicazione binaria di interfaccia (ABI)?ma ho sentito che risponde direttamente anche a questo, e che le domande non sono duplicate.
Nel contesto delle librerie condivise, l'implicazione più importante di "avere un'ABI stabile" è che non è necessario ricompilare i programmi dopo che la libreria è cambiata.
Come vedremo nell'esempio seguente, è possibile modificare l'ABI, rompendo i programmi, anche se l'API è invariata.
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 funziona bene con:
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
Supponiamo ora che per v2 della libreria, vogliamo aggiungere un nuovo campo da mylib_mystrict
chiamare new_field
.
Se abbiamo aggiunto il campo prima old_field
come in:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
e ricostruito la libreria ma non main.out
, quindi l'asserzione fallisce!
Questo perché la linea:
myobject->old_field == 1
aveva generato un assembly che sta tentando di accedere al primo int
della struttura, che ora è new_field
invece previsto old_field
.
Pertanto questo cambiamento ha rotto l'ABI.
Se, tuttavia, aggiungiamo new_field
dopo old_field
:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
quindi il vecchio assembly generato accede ancora al primo int
della struttura e il programma funziona ancora, perché abbiamo mantenuto l'ABI stabile.
Ecco una versione completamente automatizzata di questo esempio su GitHub .
Un altro modo per mantenere stabile questo ABI sarebbe stato quello di trattare mylib_mystruct
come una struttura opaca , e accedere ai suoi campi solo attraverso i metodi di supporto. Ciò rende più semplice mantenere stabile l'ABI, ma comporterebbe un sovraccarico di prestazioni poiché faremmo più chiamate di funzione.
API vs ABI
Nell'esempio precedente, è interessante notare che l'aggiunta di new_field
prima ha old_field
solo rotto l'ABI, ma non l'API.
Ciò significa che se avessimo ricompilato il nostro main.c
programma con la libreria, avrebbe funzionato a prescindere.
Avremmo anche rotto l'API se avessimo cambiato ad esempio la firma della funzione:
mylib_mystruct* mylib_init(int old_field, int new_field);
poiché in quel caso, main.c
smetterebbe di compilare del tutto.
API semantica vs API di programmazione vs ABI
Possiamo anche classificare le modifiche API in un terzo tipo: modifiche semantiche.
Ad esempio, se avessimo modificato
myobject->old_field = old_field;
per:
myobject->old_field = old_field + 1;
allora questo non avrebbe rotto né API né ABI, ma main.c
avrebbe comunque rotto!
Questo perché abbiamo cambiato la "descrizione umana" di ciò che la funzione dovrebbe fare piuttosto che un aspetto evidente a livello di programmazione.
Ho appena avuto la visione filosofica che la verifica formale del software in un certo senso sposta di più l '"API semantica" in un "API verificabile a livello di programmazione".
API semantica vs API di programmazione
Possiamo anche classificare le modifiche API in un terzo tipo: modifiche semantiche.
L'API semantica, di solito è una descrizione in linguaggio naturale di ciò che l'API dovrebbe fare, di solito inclusa nella documentazione dell'API.
È quindi possibile interrompere l'API semantica senza interrompere la creazione del programma stesso.
Ad esempio, se avessimo modificato
myobject->old_field = old_field;
per:
myobject->old_field = old_field + 1;
allora questo non avrebbe rotto né l'API di programmazione, né l'ABI, ma main.c
l'API semantica si sarebbe rotta.
Esistono due modi per controllare a livello di programmazione l'API del contratto:
- prova un mucchio di valigie angolari. Facile da fare, ma potresti sempre perderne uno.
- verifica formale . Più difficile da fare, ma produce prove matematiche di correttezza, essenzialmente unificando la documentazione e i test in modo "umano" / verificabile dalla macchina! Fintanto che non c'è un bug nella tua descrizione formale ovviamente ;-)
Testato in Ubuntu 18.10, GCC 8.2.0.