Thư viện chia sẻ Linux có thể chạy API tối thiểu so với ví dụ ABI
Câu trả lời này đã được trích xuất từ câu trả lời khác của tôi: Giao diện nhị phân ứng dụng (ABI) là gì? nhưng tôi cảm thấy rằng nó cũng trả lời trực tiếp câu hỏi này và các câu hỏi không trùng lặp.
Trong ngữ cảnh của các thư viện dùng chung, ý nghĩa quan trọng nhất của việc "có ABI ổn định" là bạn không cần phải biên dịch lại các chương trình của mình sau khi thư viện thay đổi.
Như chúng ta sẽ thấy trong ví dụ dưới đây, có thể sửa đổi ABI, phá vỡ các chương trình, mặc dù API không thay đổi.
C chính
#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
Biên dịch và chạy tốt với:
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
Bây giờ, giả sử rằng đối với v2 của thư viện, chúng tôi muốn thêm một trường mới để mylib_mystrict
gọi new_field
.
Nếu chúng ta đã thêm trường trước old_field
như trong:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
và xây dựng lại thư viện nhưng không main.out
, sau đó khẳng định thất bại!
Điều này là do dòng:
myobject->old_field == 1
đã tạo ra hội đồng đang cố gắng truy cập vào đầu tiên int
của cấu trúc, mà bây giờ new_field
thay vì dự kiến old_field
.
Do đó, sự thay đổi này đã phá vỡ ABI.
Tuy nhiên, nếu chúng tôi thêm new_field
sau old_field
:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
sau đó, hội đồng được tạo cũ vẫn truy cập vào int
cấu trúc đầu tiên và chương trình vẫn hoạt động, bởi vì chúng tôi giữ cho ABI ổn định.
Đây là phiên bản hoàn toàn tự động của ví dụ này trên GitHub .
Một cách khác để giữ ABI ổn định này sẽ là xử lý mylib_mystruct
như một cấu trúc mờ và chỉ truy cập vào các trường của nó thông qua các trình trợ giúp phương thức. Điều này giúp cho việc giữ ABI ổn định dễ dàng hơn, nhưng sẽ phải chịu chi phí hoạt động khi chúng tôi thực hiện nhiều cuộc gọi chức năng hơn.
API so với ABI
Trong ví dụ trước, thật thú vị khi lưu ý rằng việc thêm new_field
trước đó old_field
, chỉ phá vỡ ABI chứ không phá vỡ API.
Điều này có nghĩa là, nếu chúng tôi đã biên dịch lại main.c
chương trình của chúng tôi chống lại thư viện, nó sẽ hoạt động bất kể.
Tuy nhiên, chúng tôi cũng đã phá vỡ API nếu chúng tôi đã thay đổi ví dụ chữ ký hàm:
mylib_mystruct* mylib_init(int old_field, int new_field);
vì trong trường hợp đó, main.c
sẽ ngừng biên dịch hoàn toàn.
API ngữ nghĩa và API lập trình so với ABI
Chúng tôi cũng có thể phân loại các thay đổi API theo loại thứ ba: thay đổi ngữ nghĩa.
Ví dụ: nếu chúng tôi đã sửa đổi
myobject->old_field = old_field;
đến:
myobject->old_field = old_field + 1;
sau đó, điều này sẽ phá vỡ cả API và ABI, nhưng main.c
vẫn sẽ phá vỡ!
Điều này là do chúng tôi đã thay đổi "mô tả con người" về những gì chức năng được cho là làm thay vì khía cạnh đáng chú ý về mặt lập trình.
Tôi chỉ có cái nhìn sâu sắc về mặt triết học rằng việc xác minh chính thức phần mềm theo nghĩa chuyển nhiều hơn "API ngữ nghĩa" thành một "API có thể kiểm chứng bằng lập trình" hơn.
API ngữ nghĩa và API lập trình
Chúng tôi cũng có thể phân loại các thay đổi API theo loại thứ ba: thay đổi ngữ nghĩa.
API ngữ nghĩa, thường là một mô tả ngôn ngữ tự nhiên về những gì API phải làm, thường được bao gồm trong tài liệu API.
Do đó, có thể phá vỡ API ngữ nghĩa mà không phá vỡ bản dựng chương trình.
Ví dụ: nếu chúng tôi đã sửa đổi
myobject->old_field = old_field;
đến:
myobject->old_field = old_field + 1;
sau đó, điều này sẽ không phá vỡ API lập trình, cũng không phải ABI, nhưng main.c
API ngữ nghĩa sẽ phá vỡ.
Có hai cách để lập trình kiểm tra API hợp đồng:
- kiểm tra một loạt các trường hợp góc. Dễ làm, nhưng bạn luôn có thể bỏ lỡ một.
- xác minh chính thức . Khó hơn để làm, nhưng tạo ra bằng chứng toán học về tính chính xác, về cơ bản thống nhất tài liệu và kiểm tra thành một cách có thể kiểm chứng "con người" / máy! Miễn là không có lỗi trong mô tả chính thức của bạn ;-)
Đã thử nghiệm trong Ubuntu 18.10, GCC 8.2.0.