Diferencia entre API y ABI
Soy nuevo en la programación de sistemas Linux y me encontré con API y ABI mientras leía Programación de sistemas Linux .
Definición de API:
Una API define las interfaces mediante las cuales una pieza de software se comunica con otra en el nivel de origen.
Definición de ITB:
Mientras que una API define una interfaz de origen, una ABI define la interfaz binaria de bajo nivel entre dos o más piezas de software en una arquitectura particular. Define cómo interactúa una aplicación consigo misma, cómo interactúa una aplicación con el kernel y cómo interactúa una aplicación con las bibliotecas.
¿Cómo puede un programa comunicarse a nivel fuente? ¿Qué es un nivel de fuente? ¿Está relacionado con el código fuente de alguna manera? ¿O la fuente de la biblioteca se incluye en el programa principal?
La única diferencia que conozco es que la API la utilizan principalmente los programadores y la ABI la utiliza principalmente un compilador.
API: interfaz del programa de aplicación
Este es el conjunto de tipos/variables/funciones públicas que expone desde su aplicación/biblioteca.
En C/C++ esto es lo que se expone en los archivos de encabezado que se envían con la aplicación.
ABI: interfaz binaria de aplicaciones
Así es como el compilador crea una aplicación.
Define cosas (pero no se limita a):
- Cómo se pasan los parámetros a las funciones (registros/pila).
- Quién limpia los parámetros de la pila (persona que llama/destinatario de la llamada).
- Donde se coloca el valor de retorno para la devolución.
- Cómo se propagan las excepciones.
Principalmente me encuentro con estos términos en el sentido de un cambio incompatible con API o un cambio incompatible con ABI.
Un cambio de API es esencialmente donde el código que se habría compilado con la versión anterior ya no funcionará. Esto puede suceder porque agregaste un argumento a una función o cambiaste el nombre de algo accesible fuera de tu código local. Cada vez que cambias un encabezado y te obliga a cambiar algo en un archivo .c/.cpp, has realizado un cambio de API.
Un cambio de ABI ocurre cuando el código que ya se ha compilado con la versión 1 ya no funcionará con la versión 2 de un código base (generalmente una biblioteca). Generalmente es más complicado realizar un seguimiento de esto que un cambio incompatible con API, ya que algo tan simple como agregar un método virtual a una clase puede ser incompatible con ABI.
Encontré dos recursos extremadamente útiles para descubrir qué es la compatibilidad ABI y cómo preservarla:
- La lista de lo que se debe y no se debe hacer con C++ para el proyecto KDE
- Cómo escribir bibliotecas compartidas de Ulrich Drepper.pdf (autor principal de glibc)
Ejemplo de API ejecutable mínima de biblioteca compartida de Linux frente a 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 están duplicadas.
En el contexto de las bibliotecas compartidas, la implicación más importante de "tener una ABI estable" es que no es necesario volver a compilar los programas después de que cambia la biblioteca.
Como veremos en el ejemplo siguiente, es posible modificar la ABI, rompiendo programas, aunque la API no se modifique.
C Principal
#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;
}
milib.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;
}
milib.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 la versión 2 de la biblioteca, queremos agregar un nuevo campo al mylib_mystruct
archivo llamado new_field
.
Si agregamos el campo antes old_field
como en:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
y reconstruí 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 ensamblaje que intenta acceder al primero int
de la estructura, que ahora es new_field
en lugar del esperado old_field
.
Por lo tanto, este cambio rompió el ABI.
Sin embargo, si añadimos new_field
después old_field
:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
entonces el antiguo ensamblado generado aún accede a la primera int
estructura y el programa aún funciona, porque mantuvimos la ABI estable.
Aquí hay una versión totalmente automatizada de este ejemplo en GitHub .
Otra forma de mantener estable esta ABI habría sido tratarla mylib_mystruct
como una estructura opaca y acceder solo a sus campos a través de métodos auxiliares. Esto hace que sea más fácil mantener estable la ABI, pero generaría una sobrecarga de rendimiento ya que haríamos más llamadas a funciones.
API frente a ABI
En el ejemplo anterior, es interesante notar que agregar new_field
before old_field
solo rompió la ABI, pero no la API.
Lo que esto significa es que si hubiéramos recompilado nuestro main.c
programa con 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 compilar por completo.
API semántica versus API de programación
También podemos clasificar los cambios de API en un tercer tipo: cambios semánticos.
La API semántica suele ser 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 construcción del programa.
Por ejemplo, si hubiésemos 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 sí se rompería.
Hay dos formas de comprobar mediante programación la API del contrato:
- Pruebe un montón de casos de esquina. Es fácil de hacer, pero es posible que siempre se te escape alguno.
- 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 de una manera verificable "humana"/máquina. Siempre y cuando no haya un error en tu descripción formal, por supuesto ;-)
Probado en Ubuntu 18.10, GCC 8.2.0.
Estas son mis explicaciones simples:
- API: piense en
include
archivos. Proporcionan interfaces de programación. - ABI: piense en el módulo del kernel. Cuando lo ejecuta en algún kernel, tiene que acordar cómo comunicarse sin archivos incluidos, es decir, como una interfaz binaria de bajo nivel.