Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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. 詳細對比

基本對比表

比較項目APIABI
全名Application Programming InterfaceApplication 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

關鍵差異

項目一般函數呼叫系統呼叫
指令callsyscall (x86-64) 或 int 0x80 (x86)
目標User Space 函數Kernel Space
系統呼叫號不需要需要(放在 rax)
參數傳遞rdi, rsi, rdx, rcx, r8, r9rdi, 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):
  新程式 → 舊環境 ❌ 通常不行

破壞後向相容 = 生態系統災難
保持後向相容 = 生態系統健康

總結表

核心差異

方面APIABI
定義原始碼介面二進制介面
對象程式設計師編譯器/機器
內容函數名、參數暫存器、記憶體佈局
變動影響改原始碼重新編譯
文件API 文件ABI 規格書
可讀性人類可讀機器可讀
例子add(a, b)mov rdi, a; mov rsi, b; call add

相容性矩陣

狀況APIABI結果
理想狀況穩定穩定✅ 完全相容
只有 ABI 穩定改變穩定❌ 不相容
只有 API 穩定穩定改變❌ 需重新編譯
都不穩定改變改變❌ 完全不相容

關鍵要點

  1. API 是給人看的,ABI 是給機器看的
  2. 系統呼叫是 ABI 的一個重要組成部分
  3. 完整的向後相容需要 API 和 ABI 同時穩定
  4. 改變 API 參數永遠是破壞性變更,ABI 穩定也救不了
  5. 後向相容(舊→新)通常保證,前向相容(新→舊)通常不保證
  6. 破壞後向相容的代價極大:生態系統崩潰、使用者流失
  7. Zig 1.0 後會同時保證 API 和 ABI 的後向相容性
  8. 不同作業系統有不同的系統呼叫 ABI
  9. Linux 的系統呼叫 ABI 超級穩定,這是為什麼舊程式能在新核心運行
  10. 只增不減原則:可以新增功能,不能改變或刪除現有的

快速參考卡

相容性方向

後向相容(Backward)← 重要!通常保證
  過去 ──────→ 未來
  舊程式      新環境
    └────✅────┘

前向相容(Forward)← 通常不保證
  未來 ──────→ 過去
  新程式      舊環境
    └────❌────┘

穩定性層次

完整向後相容 = API 穩定 + ABI 穩定

API 穩定:
✓ 函數簽名不變
✓ 不刪除現有 API
✓ 只新增新功能

ABI 穩定:
✓ 呼叫約定不變
✓ 記憶體佈局不變
✓ 系統呼叫號碼不變
✓ 符號名稱不變

常見錯誤

❌ "ABI 穩定就可以改 API"
   → 錯!改 API 參數還是會破壞相容性

❌ "後向相容就是前向相容"
   → 錯!方向完全相反

❌ "只要重新編譯就沒事"
   → 只對 API 問題有效,ABI 問題需要相同版本

✅ "後向相容 = 舊的在新環境能用"
   → 對!這是最重要的保證

最後更新: 2025-10-20