Linux shared library ตัวอย่าง API ที่ทำงานได้น้อยที่สุดเทียบกับ ABI
คำตอบนี้ถูกดึงออกมาจากคำตอบอื่น ๆ ของฉัน: application binary interface (ABI) คืออะไร? แต่ฉันรู้สึกว่ามันตอบคำถามนี้โดยตรงเช่นกันและคำถามนั้นไม่ซ้ำซ้อน
ในบริบทของไลบรารีที่ใช้ร่วมกันความหมายที่สำคัญที่สุดของ "การมี ABI ที่เสถียร" คือคุณไม่จำเป็นต้องคอมไพล์โปรแกรมของคุณอีกครั้งหลังจากที่ไลบรารีมีการเปลี่ยนแปลง
ดังที่เราจะเห็นในตัวอย่างด้านล่างมันเป็นไปได้ที่จะแก้ไข ABI, การแบ่งโปรแกรมแม้ว่า API จะไม่เปลี่ยนแปลง
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
ก่อนหน้าold_field
นี้ ABI ที่แตกออกมาเท่านั้น แต่ไม่ใช่ API
สิ่งนี้หมายความว่าถ้าเราคอมไพล์main.c
โปรแกรมของเราซ้ำกับไลบรารี่
เราจะใช้ API แตก แต่ถ้าเราเปลี่ยนตัวอย่างเช่นฟังก์ชันของลายเซ็น:
mylib_mystruct* mylib_init(int old_field, int new_field);
เนื่องจากในกรณีนั้นmain.c
จะหยุดรวบรวมทั้งหมด
Semantic API กับการเขียนโปรแกรม API เทียบกับ ABI
นอกจากนี้เรายังสามารถจัดประเภทการเปลี่ยนแปลง API ในประเภทที่สาม: การเปลี่ยนแปลงทางความหมาย
ตัวอย่างเช่นถ้าเรามีการปรับเปลี่ยน
myobject->old_field = old_field;
ถึง:
myobject->old_field = old_field + 1;
จากนั้นสิ่งนี้จะไม่แตก API หรือ ABI แต่main.c
จะยังพัง!
นี่เป็นเพราะเราเปลี่ยน "คำอธิบายของมนุษย์" ในสิ่งที่ฟังก์ชั่นควรจะทำแทนที่จะเป็นแง่มุมที่เห็นได้ชัดโดยทางโปรแกรม
ฉันเพิ่งเข้าใจปรัชญาว่าการตรวจสอบอย่างเป็นทางการของซอฟต์แวร์ในแง่หนึ่งย้าย "semantic API" ไปสู่ "API ที่ตรวจสอบได้ทางโปรแกรม" มากกว่า
Semantic API กับ Programming 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