Exemple d'API minimale exécutable de la bibliothèque partagée Linux par rapport à l'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 estimé qu'elle répondait aussi directement à celle-ci, et que les questions ne sont pas redondantes.
Dans le contexte des bibliothèques partagées, l'implication la plus importante de "avoir une ABI stable" est que vous n'avez pas besoin de recompiler vos programmes après un changement de bibliothèque.
Comme nous allons le voir dans l'exemple ci-dessous, il est possible de modifier l'ABI, en cassant des programmes, même si l'API reste inchangée.
main.c
#include <assert.h>
#include <stdlib.h>
#include "mylib.h"
int main(void) {
mylib_mystruct *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 bien 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 version 2 de la bibliothèque, nous voulons ajouter un nouveau champ à mylib_mystruct
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'assert échoue !
C'est parce que la ligne :
myobject->old_field == 1
a généré un assemblage qui essaye d'accéder au tout premier int
de la structure, qui est maintenant new_field
au lieu de l'attendu old_field
.
Ce changement a donc 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 assemblage généré accède toujours au premier int
de la structure, et le programme fonctionne toujours, parce que nous avons gardé l'ABI stable.
Voici un version entièrement automatisée de cet exemple sur GitHub .
Une autre façon de garder cette ABI stable aurait été de traiter mylib_mystruct
comme un structure opaque et n'accède à ses champs qu'au moyen d'aides de méthode. Il est ainsi plus facile de maintenir la stabilité de l'ABI, mais cela entraînerait un surcoût en termes de performances, car il y aurait davantage d'appels de fonctions.
API et ABI
Dans l'exemple précédent, il est intéressant de noter que l'ajout de l'élément new_field
avant old_field
a seulement cassé l'ABI, mais pas l'API.
Ce que cela signifie, c'est que si nous avions recompilé notre main.c
contre la bibliothèque, ça aurait quand même fonctionné.
Mais nous aurions également brisé l'API si nous avions modifié, par exemple, la signature de la fonction :
mylib_mystruct* mylib_init(int old_field, int new_field);
puisque dans ce cas, main.c
cesserait complètement de compiler.
API sémantique, API de programmation et 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 brisé ni l'API ni l'ABI, mais main.c
se briserait quand même !
Cela est dû au fait que nous avons modifié la "description humaine" de ce que la fonction est censée faire plutôt qu'un aspect perceptible par le programme.
J'ai juste eu l'intuition philosophique que vérification formelle des logiciels déplace en quelque sorte l'"API sémantique" vers une "API vérifiable par programme".
API sémantique et 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 briser l'API sémantique sans briser la construction du programme elle-même.
Par exemple, si nous avions modifié
myobject->old_field = old_field;
à :
myobject->old_field = old_field + 1;
alors cela n'aurait brisé ni l'API de programmation, ni l'ABI, mais main.c
l'API sémantique se briserait.
Il existe deux façons de vérifier par programme l'API du contrat :
- tester un certain nombre de cas particuliers. C'est facile à faire, mais vous risquez toujours d'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 "humaine" / vérifiable par la 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.
3 votes
Par niveau de source, ils veulent dire quelque chose comme un fichier d'inclusion pour exposer les définitions de fonctions
1 votes
Voir aussi stackoverflow.com/q/2171177/632951