Ejemplo de API mínima ejecutable de biblioteca compartida de Linux vs ABI
Esta respuesta se ha extraído de mi otra respuesta: ¿Qué es una interfaz binaria de aplicación (ABI)? pero sentí que también responde directamente a esta, y que las preguntas no son duplicados.
En el contexto de las bibliotecas compartidas, la implicación más importante de "tener un ABI estable" es que no necesita volver a compilar sus programas después de que la biblioteca cambie.
Como veremos en el ejemplo a continuación, es posible modificar la ABI, interrumpiendo los programas, aunque la API no haya cambiado.
C Principal
#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 y funciona bien 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
Ahora, supongamos que para v2 de la biblioteca, queremos agregar un nuevo campo al mylib_mystrict
llamado new_field
.
Si agregamos el campo antes old_field
como en:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
y reconstruyó la biblioteca pero no main.out
, ¡entonces la afirmación falla!
Esto se debe a que la línea:
myobject->old_field == 1
había generado un ensamblado que intenta acceder al primero int
de la estructura, que ahora es en new_field
lugar del esperado old_field
.
Por lo tanto, este cambio rompió el ABI.
Sin embargo, si agregamos new_field
después old_field
:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
entonces el antiguo ensamblado generado aún accede al primero int
de la estructura, y el programa aún funciona, porque mantuvimos el ABI estable.
Aquí hay una versión completamente automatizada de este ejemplo en GitHub .
Otra forma de mantener esta ABI estable habría sido tratarla mylib_mystruct
como una estructura opaca , y solo acceder a sus campos a través de métodos auxiliares. Esto hace que sea más fácil mantener estable la ABI, pero incurriría en una sobrecarga de rendimiento ya que haríamos más llamadas a funciones.
API vs ABI
En el ejemplo anterior, es interesante notar que agregar el new_field
anterior old_field
solo rompió la ABI, pero no la API.
Lo que esto significa es que si hubiéramos compilado nuestro main.c
programa contra la biblioteca, habría funcionado de todos modos.
Sin embargo, también habríamos roto la API si hubiéramos cambiado, por ejemplo, la firma de la función:
mylib_mystruct* mylib_init(int old_field, int new_field);
ya que en ese caso, main.c
dejaría de compilarse por completo.
API semántica vs API de programación vs ABI
También podemos clasificar los cambios de API en un tercer tipo: cambios semánticos.
Por ejemplo, si hubiéramos modificado
myobject->old_field = old_field;
a:
myobject->old_field = old_field + 1;
entonces esto no habría roto ni API ni ABI, ¡pero main.c
aún así se rompería!
Esto se debe a que cambiamos la "descripción humana" de lo que se supone que debe hacer la función en lugar de un aspecto programáticamente notable.
Acabo de tener la idea filosófica de que la verificación formal del software en cierto sentido mueve más de la "API semántica" a una "API verificable programáticamente".
API semántica vs API de programación
También podemos clasificar los cambios de API en un tercer tipo: cambios semánticos.
La API semántica, por lo general, es una descripción en lenguaje natural de lo que se supone que debe hacer la API, generalmente incluida en la documentación de la API.
Por lo tanto, es posible romper la API semántica sin romper la compilación del programa.
Por ejemplo, si hubiéramos modificado
myobject->old_field = old_field;
a:
myobject->old_field = old_field + 1;
entonces esto no habría roto ni la API de programación ni la ABI, pero main.c
la API semántica se rompería.
Hay dos formas de verificar mediante programación la API del contrato:
- prueba un montón de casos de esquina. Fácil de hacer, pero siempre puedes perderte uno.
- la verificación formal . Es más difícil de hacer, pero produce una prueba matemática de corrección, esencialmente unificando la documentación y las pruebas en una forma "humana" / máquina verificable. Siempre que no haya un error en su descripción formal, por supuesto ;-)
Probado en Ubuntu 18.10, GCC 8.2.0.