API vs ABI 完整指南
目錄
1. 核心概念
一句話定義
- API (Application Programming Interface):原始碼層面的介面(給程式設計師看的)
- ABI (Application Binary Interface):二進制層面的介面(給編譯器/機器看的)
形象比喻
API = 餐廳菜單
菜單上寫:
- 番茄炒蛋 $100
- 宮保雞丁 $150
- 麻婆豆腐 $120
客人(程式設計師)看菜單(API):
✓ 知道有什麼菜(函數)
✓ 知道價格(參數)
✓ 知道怎麼點(呼叫方式)
→ 菜單是給人類看的介面
ABI = 廚房 SOP
廚房內部規定:
- 番茄炒蛋用 2 號爐
- 油溫 180°C
- 先炒蛋再炒番茄
- 用特定的盤子裝
廚師(編譯器)遵循 SOP(ABI):
✓ 怎麼傳遞訂單(參數傳遞)
✓ 怎麼排列食材(記憶體佈局)
✓ 怎麼出菜(返回值)
→ SOP 是給廚房內部用的規則
2. 詳細對比
基本對比表
| 比較項目 | API | ABI |
|---|---|---|
| 全名 | Application Programming Interface | Application Binary Interface |
| 層級 | 原始碼層級 | 二進制/機器碼層級 |
| 誰使用 | 程式設計師 | 編譯器、作業系統、CPU |
| 定義什麼 | 函數名稱、參數、返回值 | 記憶體佈局、呼叫約定、暫存器使用 |
| 可讀性 | 人類可讀 | 機器可讀 |
| 文件形式 | API 文件、註解 | 技術規格書 |
| 改變影響 | 需要修改原始碼 | 需要重新編譯 |
| 例子 | add(a, b) | mov rdi, a; mov rsi, b; call add |
流程圖
程式設計師
↓
【寫原始碼】 ← 使用 API
↓
編譯
↓
【產生二進制】 ← 遵循 ABI
↓
執行
↓
CPU
3. 具體例子
例子 1:簡單的加法函數
API 視角(原始碼)
// 這是 API:你看到的原始碼介面
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
// 如何使用(API)
const result = add(5, 3);
// API 告訴你:
// ✓ 函數叫 "add"
// ✓ 需要兩個 i32 參數
// ✓ 回傳一個 i32
// ✓ 在原始碼中這樣呼叫
ABI 視角(編譯後的二進制)
; 這是 ABI:編譯後的機器碼
; x86-64 System V ABI 規定:
; - 第一個整數參數放 rdi 暫存器
; - 第二個整數參數放 rsi 暫存器
; - 返回值放 rax 暫存器
add:
mov eax, edi ; 取第一個參數
add eax, esi ; 加上第二個參數
ret ; 返回(結果在 rax)
; 呼叫時
mov edi, 5 ; 第一個參數 = 5
mov esi, 3 ; 第二個參數 = 3
call add ; 呼叫函數
; 返回後 eax = 8
// ABI 規定:
// ✓ 參數怎麼傳(哪個暫存器)
// ✓ 返回值放哪裡(哪個暫存器)
// ✓ 堆疊如何使用
// ✓ 誰負責清理堆疊
例子 2:結構體(Struct)
API 視角
// API:定義資料結構
const Point = struct {
x: i32,
y: i32,
};
// 使用 API
var p = Point{ .x = 10, .y = 20 };
const x_value = p.x; // 存取欄位
// API 告訴你:
// ✓ 有 x 和 y 兩個欄位
// ✓ 都是 i32 型別
// ✓ 如何建立和存取
ABI 視角
記憶體佈局(ABI 規定):
地址 內容
0x1000: 0A 00 00 00 (x = 10, 4 bytes)
0x1004: 14 00 00 00 (y = 20, 4 bytes)
ABI 規定:
✓ x 在偏移 0
✓ y 在偏移 4
✓ 總大小 8 bytes
✓ 對齊 4 bytes
✓ 小端序(little-endian)
如果 ABI 改變,例如:
- 改成大端序
- x 和 y 順序對調
- 對齊改成 8 bytes
→ 舊的二進制無法使用
例子 3:API 改變 vs ABI 改變
情境 1:API 改變(函數改名)
// 舊版本
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
// 新版本:改名
pub fn sum(a: i32, b: i32) i32 { // ← 名字改了
return a + b;
}
// 影響:
❌ 原始碼層級:需要修改程式碼
const result = add(5, 3); // ❌ 編譯錯誤:找不到 add
const result = sum(5, 3); // ✅ 需要改成這樣
✅ 二進制層級:編譯後的機器碼相同
// 因為底層呼叫約定沒變
// 只是函數名稱變了而已
情境 2:ABI 改變(呼叫約定變)
// 舊版本
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
// 編譯器用 System V ABI:
// - 參數用 rdi, rsi
// - 返回值用 rax
// 新版本:編譯器改用新的 ABI
pub fn add(a: i32, b: i32) i32 { // ← 原始碼一樣
return a + b;
}
// 新編譯器改用不同 ABI:
// - 參數用 rcx, rdx (← 不同了!)
// - 返回值用 rax
// 影響:
✅ 原始碼層級:不需要改程式碼
const result = add(5, 3); // ✅ 原始碼完全相同
❌ 二進制層級:舊的編譯好的庫無法使用
// 舊庫期望參數在 rdi, rsi
// 新程式把參數放在 rcx, rdx
// → 參數傳錯位置 → 結果錯誤或崩潰
例子 4:使用第三方庫
有原始碼(只需要 API 相容)
// 第三方庫 v1.0
pub fn process(data: []u8) !void { ... }
// 你的程式
const lib = @import("lib");
try lib.process(my_data); // ✅ 使用 API
// 升級到 v2.0
// 只要 API 不變(函數名、參數相同)
// 重新編譯就能用
// → 不需要修改你的程式碼
只有編譯好的二進制(需要 ABI 相容)
# 第三方提供編譯好的 .so 或 .dll
libawesome.so (用 Zig 0.13 編譯)
# 你用 Zig 0.16
zig build-exe myapp.zig -lawesome
# 如果 ABI 不相容:
❌ 連結失敗
❌ 或執行時崩潰
❌ 因為二進制層面不相容
# 解決方法:
1. 用相同版本的 Zig 重新編譯庫(需要原始碼)
2. 或使用相同版本的 Zig
4. 系統呼叫與 ABI
系統呼叫是什麼?
使用者程式(User Space)
↓
[系統呼叫] ← 這是一扇門
↓
作業系統核心(Kernel Space)
系統呼叫 = 使用者程式請求作業系統幫忙做事的方式
常見的系統呼叫
// 檔案操作
open() // 開啟檔案
read() // 讀取檔案
write() // 寫入檔案
close() // 關閉檔案
// 行程管理
fork() // 建立子行程
exec() // 執行程式
exit() // 結束程式
// 記憶體管理
mmap() // 記憶體映射
brk() // 調整堆積大小
ABI 如何規範系統呼叫
一般函數呼叫(User Space → User Space)
; 呼叫一般函數 add(5, 3)
; 使用 System V ABI (x86-64 Linux)
mov edi, 5 ; 第一個參數放 rdi
mov esi, 3 ; 第二個參數放 rsi
call add ; 直接呼叫函數
; 返回值在 rax
系統呼叫(User Space → Kernel Space)
; 呼叫系統呼叫 write(1, "Hello", 5)
; 使用 Linux x86-64 系統呼叫 ABI
mov rax, 1 ; 系統呼叫號碼:1 = write
mov rdi, 1 ; 第一個參數:fd = 1 (stdout)
mov rsi, msg ; 第二個參數:buffer 位址
mov rdx, 5 ; 第三個參數:長度 = 5
syscall ; 觸發系統呼叫(不是 call)
; 返回值在 rax
關鍵差異
| 項目 | 一般函數呼叫 | 系統呼叫 |
|---|---|---|
| 指令 | call | syscall (x86-64) 或 int 0x80 (x86) |
| 目標 | User Space 函數 | Kernel Space |
| 系統呼叫號 | 不需要 | 需要(放在 rax) |
| 參數傳遞 | rdi, rsi, rdx, rcx, r8, r9 | rdi, rsi, rdx, r10, r8, r9 |
| 特權級別 | 使用者模式 | 切換到核心模式 |
注意:系統呼叫用 r10 而不是 rcx(第四個參數)
ABI 的完整層次
ABI(應用程式二進制介面)
│
├── 一般函數呼叫約定 (User Space ↔ User Space)
│ ├── 參數傳遞規則
│ ├── 返回值規則
│ └── 堆疊使用規則
│
└── 系統呼叫約定 (User Space ↔ Kernel Space)
├── 系統呼叫號碼
├── 參數傳遞規則(略有不同)
├── syscall 指令的使用
└── 錯誤處理機制
系統呼叫號碼表(Linux x86-64)
// 這些號碼是 ABI 的一部分
// 改變這些號碼 = 破壞 ABI
#define SYS_read 0
#define SYS_write 1
#define SYS_open 2
#define SYS_close 3
#define SYS_stat 4
#define SYS_fstat 5
// ...
#define SYS_exit 60
#define SYS_fork 57
// ...
這些號碼必須穩定,否則舊程式無法在新核心上執行!
不同平臺的系統呼叫 ABI
Linux x86-64
; 參數順序:rdi, rsi, rdx, r10, r8, r9
; 系統呼叫號:rax
; 指令:syscall
mov rax, 1 ; write
mov rdi, 1 ; fd
mov rsi, buffer ; buf
mov rdx, len ; count
syscall
Linux x86 (32-bit)
; 參數順序:ebx, ecx, edx, esi, edi, ebp
; 系統呼叫號:eax
; 指令:int 0x80
mov eax, 4 ; write (32-bit 的號碼不同!)
mov ebx, 1 ; fd
mov ecx, buffer ; buf
mov edx, len ; count
int 0x80
macOS (Darwin) x86-64
; 類似 Linux 但有差異
; 系統呼叫號需要加上 0x2000000
mov rax, 0x2000004 ; write (BSD-style)
mov rdi, 1
mov rsi, buffer
mov rdx, len
syscall
完整的呼叫層次
┌─────────────────────────────────────┐
│ 高階語言(Zig, C, Rust) │
│ std.io.print(), printf() │
└──────────────┬──────────────────────┘
│ 編譯 (遵循一般函數 ABI)
↓
┌─────────────────────────────────────┐
│ 標準庫函數(libc) │
│ write(), read(), open() │
└──────────────┬──────────────────────┘
│ 包裝系統呼叫
↓
┌─────────────────────────────────────┐
│ 系統呼叫介面 │ ← ABI 規範這一層
│ syscall(SYS_write, ...) │
└──────────────┬──────────────────────┘
│ syscall 指令 (遵循系統呼叫 ABI)
↓
┌─────────────────────────────────────┐
│ 作業系統核心 │
│ 實際執行 I/O 操作 │
└─────────────────────────────────────┘
5. 穩定性與相容性
後向相容 vs 前向相容
後向相容(Backward Compatibility)
定義:舊的程式可以在新環境中執行
舊版本編譯的程式 → 可以在新版本環境運行
例如:
- 2020 年用 Zig 1.0 編譯的程式
- 可以在 2025 年的 Zig 1.5 環境執行
時間軸視覺化:
2024 ───────────→ 2025 ───────────→ 2026
Zig 1.0 Zig 1.5 Zig 2.0
↓ ↓ ↓
編譯程式A 編譯程式B 編譯程式C
│ │ │
│ 後向相容 │ │
└──────✅───────→│ │
│ │ 後向相容 │
└──────✅────────┴──────✅───────→│
可以運行 可以運行
例子:
# 1995 年編譯的 Linux 程式
gcc -o oldprog oldprog.c # 在 Linux 2.0 編譯
# 2025 年執行
./oldprog # 在 Linux 6.x 執行 ✅ 可以運行!
# 為什麼?
# Linux 承諾系統呼叫 ABI 永遠後向相容
# - write() 的系統呼叫號碼 (1) 沒變
# - 參數傳遞方式沒變
# - 呼叫約定沒變
前向相容(Forward Compatibility)
定義:新的程式可以在舊環境中執行
新版本編譯的程式 → 可以在舊版本環境運行
時間線:
未來編譯 ──────→ 過去執行
❓ 通常不保證
例子:
# 2025 年編譯的程式
gcc -o newprog newprog.c # 在 Linux 6.x 編譯
# 使用了新的系統呼叫 xyz
# 試圖在 1995 年的環境執行
./newprog # 在 Linux 2.0 執行 ❌ 失敗!
# 為什麼失敗?
# 舊核心沒有 xyz 系統呼叫
# 舊環境不認識新功能
對比總結
| 項目 | 後向相容 | 前向相容 |
|---|---|---|
| 方向 | 舊 → 新 | 新 → 舊 |
| 定義 | 舊程式在新環境可用 | 新程式在舊環境可用 |
| 保證 | 通常保證 ✅ | 通常不保證 ❌ |
| 例子 | Windows XP 程式在 Win11 執行 | Win11 程式在 XP 執行(不行) |
| 重要性 | 非常重要 | 較少考慮 |
記憶方式
後向相容 = 向後看 = 照顧舊的東西
Backward = 舊程式在新環境能用
前向相容 = 向前看 = 新的在舊環境用
Forward = 新程式在舊環境能用(通常不行)
時間軸:
過去 ───────→ 現在 ───────→ 未來
(舊) (中) (新)
│ ↑ │
└──後向相容───┘ │
└──前向相容───┘
✅ 保證 ❌ 通常不保證
實際案例
Linux 的承諾:
// Linus Torvalds 的著名聲明:
// "We do not break userspace"
// "我們不會破壞使用者空間程式"
// 後向相容:✅ 保證
// 1990 年代的程式可以在 2025 年執行
// 前向相容:❌ 不保證
// 2025 年的程式無法在 1990 年代執行
// (使用了當時不存在的系統呼叫)
Windows 的例子:
後向相容 ✅:
- Windows XP 程式可以在 Windows 11 執行
- Microsoft 投入大量資源維護相容性
- 甚至有「相容模式」幫助執行更老的程式
前向相容 ❌:
- Windows 11 程式無法在 Windows XP 執行
- 新程式使用了新的 API/系統呼叫
- 舊系統無法理解
API 穩定 vs ABI 穩定
API 穩定(原始碼相容性)
// 版本 1.0
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
// 版本 2.0 - API 穩定
pub fn add(a: i32, b: i32) i32 { // ← 簽名相同
return a + b; // 實作可能優化
}
// 影響:
✅ 原始碼不需要修改
✅ 重新編譯就能用新版本
ABI 穩定(二進制相容性)
// 版本 1.0
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
// 編譯:參數用 rdi, rsi;返回值用 rax
// 版本 2.0 - ABI 穩定
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
// 編譯:參數用 rdi, rsi;返回值用 rax(相同)
// 影響:
✅ 不需要重新編譯
✅ 1.0 編譯的程式可以用 2.0 的庫
向後相容需要兩者兼具
完整的向後相容 = API 穩定 + ABI 穩定
✅ API 穩定:
- 不改變現有函數的簽名
- 不移除現有函數
- 只新增新的 API
✅ ABI 穩定:
- 對相同的 API,調用方式不變
- 資料結構佈局不變
- 呼叫約定不變
兩者缺一不可!
常見誤解
❌ 誤解:ABI 穩定 = API 可以改變參數
// Zig 1.0
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
// Zig 1.5 - 改變 API
pub fn add(a: i32, b: i32, c: i32) i32 { // ← 加了參數
return a + b + c;
}
// 結果:❌ 不相容
// 即使 ABI 穩定,參數數量變了還是不行
// 舊程式碼只傳 2 個參數
// 新函數期望 3 個參數
// → 編譯錯誤或執行錯誤
✅ 正確做法:保留舊 API,新增新 API
// Zig 1.0
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
// Zig 1.5 - 保持向後相容
pub fn add(a: i32, b: i32) i32 { // ← 保留不變
return a + b;
}
pub fn addThree(a: i32, b: i32, c: i32) i32 { // ← 新增函數
return a + b + c;
}
// 結果:✅ 完全相容
// - 舊程式碼繼續用 add()
// - 新程式碼可以選擇用 addThree()
如何確保後向相容
設計原則
1. 凍結核心 ABI
✓ 參數傳遞規則不變
✓ 返回值規則不變
✓ 記憶體佈局不變
✓ 呼叫約定不變
2. 只增不減原則
✓ 可以新增函數
✓ 可以新增功能
✗ 不能改變現有的
✗ 不能刪除現有的
3. 版本號規則
- 大版本號:可以破壞相容性 (1.x → 2.0)
- 小版本號:必須後向相容 (1.0 → 1.5)
4. 持續測試
- 舊程式必須能在新環境執行
- 自動化測試確保相容性
- 回歸測試防止破壞
實現方式
1. 永不改變現有約定
; Zig 1.0 的 ABI
; 函數 add(a: i32, b: i32) -> i32
; 參數傳遞:
mov edi, [a] ; 第一個參數用 rdi
mov esi, [b] ; 第二個參數用 rsi
call add
; 返回值在 rax
; Zig 1.5 的 ABI
; 相同!不能改變
mov edi, [a] ; 第一個參數還是用 rdi
mov esi, [b] ; 第二個參數還是用 rsi
call add
; 返回值還是在 rax
✅ 後向相容保持
2. 只增加,不修改或刪除
// Zig 1.0
pub fn calculate(x: i32) i32 { ... }
// Zig 1.5 - 後向相容的做法 ✅
pub fn calculate(x: i32) i32 { ... } // ← 保留舊的
pub fn calculateEx(x: i32, y: i32) i32 { ... } // ← 新增新的
// ❌ 不能這樣做(破壞後向相容)
pub fn calculate(x: i32, y: i32) i32 { ... } // ← 改變簽名
3. 保持資料結構佈局
// Zig 1.0
const Point = struct {
x: i32, // offset 0
y: i32, // offset 4
};
// 記憶體大小:8 bytes
// Zig 1.5 - 後向相容 ✅
const Point = struct {
x: i32, // offset 0,不變
y: i32, // offset 4,不變
};
// 記憶體大小:8 bytes,不變
✅ 舊程式讀取 Point 時:
x 在 offset 0 ← 正確
y 在 offset 4 ← 正確
4. 保持符號名稱
# Zig 1.0 編譯產生
libmath.so
- add (符號名稱)
- mul (符號名稱)
# Zig 1.5 編譯產生
libmath.so
- add (符號名稱不變) ✅
- mul (符號名稱不變) ✅
- div (新增的符號) ✅
# 舊程式尋找 "add" 符號
# 在新的 libmath.so 找到 ✅
# 調用方式相同 ✅
# → 可以執行
破壞後向相容的後果
// 假設 Zig 1.5 破壞了 ABI
// 使用者在 Zig 1.0 編譯程式
zig build-exe myapp.zig # 產生 myapp
// Zig 1.5 改變了 ABI(假設)
// - 參數傳遞改用 rcx, rdx 而不是 rdi, rsi
// 使用者升級到 Zig 1.5
./myapp # ❌ 崩潰或錯誤結果
// 災難性後果:
❌ 所有舊程式都不能用
❌ 使用者必須重新編譯所有東西
❌ 預編譯的庫全部失效
❌ 生態系統崩潰
❌ 使用者失去信任
💥 社群分裂
語言的穩定性承諾
C 語言
// API:幾十年相對穩定
int printf(const char *format, ...); // 1970s 至今
// ABI:超級穩定
// System V ABI (Linux):1980s 至今基本不變
// Windows x64 ABI:穩定超過 20 年
後向相容性:✅✅✅ 極佳
- 1990 年代編譯的程式今天仍能運行
- 這是 C 生態系統健康的關鍵
→ C 的生態系統極其健康且成熟
Go 語言(1.0 後)
// Go 1 Compatibility Promise
// Go 1.0 發佈後的保證
✅ API 穩定:
- 現有函數不改變簽名
- 只新增新功能
- 不刪除現有 API
✅ ABI 穩定:
- 二進制相容(同架構內)
✅ 後向相容:
- Go 1.0 的程式可以用 Go 1.21 編譯
- Go 1.15 編譯的程式可以在 Go 1.21 環境執行
❌ 前向相容:
- Go 1.21 的程式無法用 Go 1.0 編譯
- (新語法特性、新標準庫功能)
→ Go 生態系統非常健康
Python 的教訓
# Python 2 → Python 3:破壞後向相容
# Python 2 程式
print "Hello" # Python 2 語法
# Python 3 執行
print "Hello" # ❌ SyntaxError
# 結果:
❌ 社群分裂長達 10+ 年
❌ 大量程式庫需要同時維護兩版本
❌ 遷移痛苦且緩慢
❌ 直到 2020 年才停止支援 Python 2
教訓:破壞後向相容的代價極大
6. Zig 的情況
Zig 1.0 之前(現狀)
// ⚠️ API 不穩定
// 0.11: std.heap.page_allocator
// 0.13: std.heap.GeneralPurposeAllocator
// ⚠️ ABI 不穩定
// 每個版本可能改變呼叫約定
// ❌ 後向相容:不保證
// 0.13 編譯的程式可能無法在 0.16 環境運行
→ 導致生態系統碎片化
→ 不同版本的庫無法互用
→ 這是為了在 1.0 前尋找最佳設計
問題:版本不匹配
# 使用 Zig 0.16 編譯
# 但第三方庫是為 0.13 寫的
zig build
# 結果:
error: root source file struct 'std' has no member named 'io'
# 原因:
# - 0.13 有 std.io.getStdOut()
# - 0.16 改變了 API 結構
# → 編譯失敗
# → 無後向相容性
Zig 1.0 之後(承諾)
// ✅ API 穩定
// - 現有函數簽名不變
// - 不移除現有 API
// - 只新增新功能
// ✅ ABI 穩定
// - 相同 API 的調用方式不變
// - 資料結構佈局不變
// ✅ 後向相容保證
// - Zig 1.0 的程式碼可以用 1.x 編譯
// - Zig 1.0 編譯的庫可以給 1.x 使用
// - Zig 1.3 編譯的程式可以在 1.9 環境執行
// ❌ 不保證前向相容
// - Zig 1.5 的程式無法用 1.0 編譯
// - (可能使用了新的語法或標準庫功能)
// ❌ 跨大版本不保證相容
// - Zig 1.x 程式可能無法在 2.0 環境執行
// - Zig 2.0 可以有破壞性變更
→ 生態系統將會健康發展
→ 與 Go、Rust 類似的穩定性保證
實際影響例子
現在(1.0 前)
# 2024 年用 Zig 0.13 編譯
zig-0.13 build-exe myapp.zig
# 產生 myapp
# 2025 年升級到 Zig 0.16
zig-0.16 version # 0.16.0
# 執行舊程式
./myapp # ❌ 可能失敗或行為異常
# 因為:
# - ABI 可能改變
# - 動態連結的標準庫不相容
# - 系統呼叫包裝可能不同
# 解決:必須重新編譯
zig-0.16 build-exe myapp.zig
未來(1.0 後)
# 2026 年用 Zig 1.0 編譯
zig-1.0 build-exe myapp.zig
# 產生 myapp
# 2028 年,Zig 已是 1.8 版
zig-1.8 version # 1.8.0
# 執行舊程式
./myapp # ✅ 完美運行!
# 因為:
# - ABI 保持穩定
# - 1.0 的程式保證在 1.x 環境執行
# - 後向相容承諾
# 不需要重新編譯!
Zig 如何處理跨平臺差異
// Zig 的標準庫自動處理不同平臺的 ABI 差異
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello\n", .{});
}
// Zig 內部會根據目標平臺:
// - Linux x86-64: 使用 syscall 指令,參數用 rdi, rsi, rdx...
// - Linux x86: 使用 int 0x80,參數用 ebx, ecx, edx...
// - Windows: 使用 Windows API,完全不同的 ABI
// - macOS: 使用 Darwin 系統呼叫,號碼 + 0x2000000
// 這些都由 Zig 的標準庫自動處理
Zig 與 C 的互操作
// C 有穩定的 ABI
// Zig 可以透過 C ABI 與 C 庫溝通
const c = @cImport({
@cInclude("math.h");
});
pub fn main() void {
const result = c.sqrt(16.0); // ✅ 可以用
// 因為:
// 1. Zig 知道 C 的 API(函數簽名)
// 2. Zig 遵循 C 的 ABI(呼叫約定)
}
7. 實用建議
檢查清單:當你遇到相容性問題
❓ 是 API 問題還是 ABI 問題?
編譯時出錯:
├─ "找不到函數" → API 問題
├─ "參數型別不符" → API 問題
└─ "語法錯誤" → API 問題
→ 解決:修改原始碼以匹配新 API
連結或執行時出錯:
├─ "undefined symbol" → ABI 問題
├─ "segmentation fault" → ABI 問題
└─ "結果不正確" → 可能是 ABI 問題
→ 解決:重新編譯所有東西,使用相同版本
測試後向相容性
# 測試流程
# 1. 用舊版本編譯
zig-1.0 build-exe myapp.zig
# 產生 myapp (1.0 版本的二進制)
# 2. 用新版本環境執行
zig-1.5 # 切換環境
./myapp # 直接執行舊的二進制
# 3. 檢查結果
# ✅ 正常執行 → 後向相容成功
# ❌ 崩潰或錯誤 → 破壞後向相容
# 4. 自動化測試
# 建立測試套件確保每個版本都能執行舊程式
對於 Zig 開發者
現階段(1.0 之前)
# 1. 固定使用穩定版本
zigup 0.13.0
zigup default 0.13.0
# 2. 在專案中記錄版本
echo "0.13.0" > .zigversion
echo "# Requires Zig 0.13.0" >> README.md
# 3. 選擇活躍維護的第三方庫
# 檢查最近 commit 時間
# 4. 準備好手動修改依賴
# 或等待庫更新
未來(1.0 之後)
# 1. 可以安心升級小版本
zigup 1.5.0 # 從 1.0.0 升級
# 不需要修改程式碼
# 2. 可以使用預編譯的庫
# 不需要每次都重新編譯
# 3. 生態系統會更健康
# 庫之間相容性更好
記憶口訣
API = 看得懂的介面(What to call)
ABI = 底層的規則(How to call)
API 變 → 改程式碼
ABI 變 → 重編譯全部
API = 菜單(給客人看)
ABI = SOP(給廚房用)
系統呼叫 = ABI 的特殊部分
向後相容 = API + ABI 都穩定
後向相容(Backward):
舊程式 → 新環境 ✅ 保證
前向相容(Forward):
新程式 → 舊環境 ❌ 通常不行
破壞後向相容 = 生態系統災難
保持後向相容 = 生態系統健康
總結表
核心差異
| 方面 | API | ABI |
|---|---|---|
| 定義 | 原始碼介面 | 二進制介面 |
| 對象 | 程式設計師 | 編譯器/機器 |
| 內容 | 函數名、參數 | 暫存器、記憶體佈局 |
| 變動影響 | 改原始碼 | 重新編譯 |
| 文件 | API 文件 | ABI 規格書 |
| 可讀性 | 人類可讀 | 機器可讀 |
| 例子 | add(a, b) | mov rdi, a; mov rsi, b; call add |
相容性矩陣
| 狀況 | API | ABI | 結果 |
|---|---|---|---|
| 理想狀況 | 穩定 | 穩定 | ✅ 完全相容 |
| 只有 ABI 穩定 | 改變 | 穩定 | ❌ 不相容 |
| 只有 API 穩定 | 穩定 | 改變 | ❌ 需重新編譯 |
| 都不穩定 | 改變 | 改變 | ❌ 完全不相容 |
關鍵要點
- API 是給人看的,ABI 是給機器看的
- 系統呼叫是 ABI 的一個重要組成部分
- 完整的向後相容需要 API 和 ABI 同時穩定
- 改變 API 參數永遠是破壞性變更,ABI 穩定也救不了
- 後向相容(舊→新)通常保證,前向相容(新→舊)通常不保證
- 破壞後向相容的代價極大:生態系統崩潰、使用者流失
- Zig 1.0 後會同時保證 API 和 ABI 的後向相容性
- 不同作業系統有不同的系統呼叫 ABI
- Linux 的系統呼叫 ABI 超級穩定,這是為什麼舊程式能在新核心運行
- 只增不減原則:可以新增功能,不能改變或刪除現有的
快速參考卡
相容性方向
後向相容(Backward)← 重要!通常保證
過去 ──────→ 未來
舊程式 新環境
└────✅────┘
前向相容(Forward)← 通常不保證
未來 ──────→ 過去
新程式 舊環境
└────❌────┘
穩定性層次
完整向後相容 = API 穩定 + ABI 穩定
API 穩定:
✓ 函數簽名不變
✓ 不刪除現有 API
✓ 只新增新功能
ABI 穩定:
✓ 呼叫約定不變
✓ 記憶體佈局不變
✓ 系統呼叫號碼不變
✓ 符號名稱不變
常見錯誤
❌ "ABI 穩定就可以改 API"
→ 錯!改 API 參數還是會破壞相容性
❌ "後向相容就是前向相容"
→ 錯!方向完全相反
❌ "只要重新編譯就沒事"
→ 只對 API 問題有效,ABI 問題需要相同版本
✅ "後向相容 = 舊的在新環境能用"
→ 對!這是最重要的保證
最後更新: 2025-10-20