Exemple d'API exécutable minimale de bibliothèque partagée Linux vs ABI
Cette réponse a été extraite de mon autre réponse: Qu'est-ce qu'une interface binaire d'application (ABI)? mais j'ai senti que cela répond directement à celle-ci aussi, et que les questions ne sont pas des doublons.
Dans le contexte des bibliothèques partagées, l'implication la plus importante d '"avoir un ABI stable" est que vous n'avez pas besoin de recompiler vos programmes après les changements de bibliothèque.
Comme nous le verrons dans l'exemple ci-dessous, il est possible de modifier l'ABI, interrompant les programmes, même si l'API est inchangée.
principal 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
Compile et fonctionne correctement avec:
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
Maintenant, supposons que pour la v2 de la bibliothèque, nous souhaitons ajouter un nouveau champ à mylib_mystrict
appelé new_field
.
Si nous avons ajouté le champ avant old_field
comme dans:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
et reconstruit la bibliothèque mais pas main.out
, alors l'assertion échoue!
C'est parce que la ligne:
myobject->old_field == 1
avait généré l'assembly qui tente d'accéder au tout premier int
de la structure, qui est maintenant new_field
au lieu de l'attendu old_field
.
Par conséquent, ce changement a cassé l'ABI.
Si, cependant, nous ajoutons new_field
après old_field
:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
alors l'ancien assembly généré accède toujours au premier int
de la structure, et le programme fonctionne toujours, car nous avons maintenu l'ABI stable.
Voici une version entièrement automatisée de cet exemple sur GitHub .
Une autre façon de maintenir cette ABI stable aurait été de la traiter mylib_mystruct
comme une structure opaque et d'accéder uniquement à ses champs via des assistants de méthode. Cela facilite le maintien de la stabilité de l'ABI, mais entraînerait une surcharge de performance car nous ferions plus d'appels de fonctions.
API vs ABI
Dans l'exemple précédent, il est intéressant de noter que l'ajout de l' new_field
avant old_field
, n'a cassé que l'ABI, mais pas l'API.
Ce que cela signifie, c'est que si nous avions recompilé notre main.c
programme contre la bibliothèque, cela aurait fonctionné de toute façon.
Nous aurions également cassé l'API si nous avions changé par exemple la signature de la fonction:
mylib_mystruct* mylib_init(int old_field, int new_field);
car dans ce cas, main.c
arrêterait complètement la compilation.
API sémantique vs API de programmation vs ABI
Nous pouvons également classer les changements d'API dans un troisième type: les changements sémantiques.
Par exemple, si nous avions modifié
myobject->old_field = old_field;
à:
myobject->old_field = old_field + 1;
alors cela n'aurait cassé ni l'API ni l'ABI, mais main.c
le casserait quand même!
Ceci est dû au fait que nous avons changé la "description humaine" de ce que la fonction est censée faire plutôt qu'un aspect perceptible par programme.
J'ai juste eu la perspicacité philosophique que la vérification formelle du logiciel dans un sens déplace plus de «l'API sémantique» vers une «API plus vérifiable par programme».
API sémantique vs API de programmation
Nous pouvons également classer les changements d'API dans un troisième type: les changements sémantiques.
L'API sémantique est généralement une description en langage naturel de ce que l'API est censée faire, généralement incluse dans la documentation de l'API.
Il est donc possible de casser l'API sémantique sans casser la construction du programme lui-même.
Par exemple, si nous avions modifié
myobject->old_field = old_field;
à:
myobject->old_field = old_field + 1;
alors cela n'aurait cassé ni l'API de programmation, ni l'ABI, mais main.c
l'API sémantique serait cassée .
Il existe deux façons de vérifier par programme l'API du contrat:
- tester un tas de boîtiers d'angle. Facile à faire, mais vous pourriez toujours en manquer un.
- vérification formelle . Plus difficile à faire, mais produit une preuve mathématique de l'exactitude, unifiant essentiellement la documentation et les tests d'une manière vérifiable «humaine» / machine! Tant qu'il n'y a pas de bogue dans votre description formelle bien sûr ;-)
Testé dans Ubuntu 18.10, GCC 8.2.0.