Linux共享库最小可运行API与ABI示例
该答案已从我的另一个答案中提取:什么是应用程序二进制接口(ABI)?但是我觉得它也可以直接回答这个问题,而且这些问题不是重复的。
在共享库的上下文中,“具有稳定的ABI”最重要的含义是,在更改库后,您无需重新编译程序。
正如我们将在下面的示例中看到的那样,即使API不变,也可以修改ABI,破坏程序。
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
可以使用以下命令进行编译和正常运行:
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
现在,假设对于库的v2,我们要向mylib_mystrict
调用添加一个新字段new_field
。
如果我们old_field
像之前那样添加字段:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
并重建了库,但没有重建main.out
,则断言失败!
这是因为行:
myobject->old_field == 1
生成了试图访问int
结构的第一个结构的程序集,该程序集现在new_field
不是预期的old_field
。
因此,此更改中断了ABI。
但是,如果new_field
在old_field
以下位置添加:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
然后int
,由于我们保持了ABI的稳定,因此旧生成的程序集仍然可以访问结构的第一个结构,并且程序仍然可以运行。
这是GitHub上此示例的全自动版本。
保持此ABI稳定的另一种方法是将其mylib_mystruct
视为不透明的结构,并且只能通过方法助手来访问其字段。这样可以更轻松地保持ABI稳定,但是会增加性能开销,因为我们需要执行更多的函数调用。
API与ABI
在前面的示例中,有趣的是,添加new_field
before old_field
只会破坏ABI,而不会破坏API。
这意味着,如果我们重新编译了我们的 main.c
针对库程序,则无论如何它都可以工作。
但是,如果我们更改了函数签名,我们也会破坏API,例如:
mylib_mystruct* mylib_init(int old_field, int new_field);
因为在那种情况下 main.c
将完全停止编译。
语义API与编程API与ABI
我们还可以将API更改分类为第三种类型:语义更改。
例如,如果我们修改了
myobject->old_field = old_field;
至:
myobject->old_field = old_field + 1;
那么这将不会破坏API或ABI,但main.c
仍然会破坏!
这是因为我们更改了该功能应该执行的“人工描述”,而不是程序上引人注目的方面。
我只是有一种哲学上的见解,即某种意义上的软件形式验证将更多的“语义API”转移到了一个“可程序验证的API”中。
语义API与编程API
我们还可以将API更改分类为第三种类型:语义更改。
语义API通常是API应该执行的自然语言描述,通常包含在API文档中。
因此,可以在不破坏程序构建本身的情况下破坏语义API。
例如,如果我们修改了
myobject->old_field = old_field;
至:
myobject->old_field = old_field + 1;
那么这将不会破坏编程API或ABI,但是 main.c
语义API会破坏。
有两种方法可以通过编程方式检查合同API:
- 测试一堆极端情况。容易做到,但您可能总是会错过一个。
- 正式验证。难度较大,但会产生数学上正确性的证明,实质上是将文档和测试统一为“人为” /机器可验证的方式!只要您对课程的正式描述中没有错误;-)
已在Ubuntu 18.10,GCC 8.2.0中测试。