本章介紹如何使用 ctypes 和 cffi 動態載入和呼叫 C 函式庫。

本章目標

學完本章後,你將能夠:

  1. 理解 FFI(Foreign Function Interface)的概念
  2. 使用 ctypes 呼叫系統函式庫
  3. 使用 cffi 的 ABI 和 API 模式

【原理層】什麼是 FFI?

動態連結庫

現代作業系統使用動態連結庫(Shared Library)來共享程式碼:

1不同平台的動態連結庫:
2├── Linux:   .so  (Shared Object)
3├── macOS:   .dylib (Dynamic Library)
4└── Windows: .dll (Dynamic Link Library)
5
6優點:
7- 節省記憶體(多個程式共享同一份)
8- 更新方便(不需重新編譯主程式)
9- 模組化設計

FFI 的概念

FFI(Foreign Function Interface)是一種讓程式語言呼叫其他語言函式的機制:

1Python 程式
23    ↓ FFI
4┌───────────────┐
5│  C 函式庫     │
6│  - libc.so    │
7│  - libm.so    │
8│  - 自訂 .so   │
9└───────────────┘

ctypes vs cffi

特性ctypescffi
來源標準庫第三方
設計物件導向 APIC 語法描述
效能較慢較快(API 模式)
學習曲線較平緩需要 C 語法知識
PyPy 支援有限完整

【設計層】ctypes 基礎

載入動態連結庫

 1import ctypes
 2import ctypes.util
 3
 4# 方法 1:直接載入
 5# Linux/macOS
 6libc = ctypes.CDLL("libc.so.6")  # Linux
 7libc = ctypes.CDLL("libc.dylib")  # macOS
 8
 9# Windows
10msvcrt = ctypes.CDLL("msvcrt")
11
12# 方法 2:使用 find_library(推薦,跨平台)
13from ctypes.util import find_library
14
15libc_path = find_library("c")
16print(f"libc 路徑: {libc_path}")
17libc = ctypes.CDLL(libc_path)
18
19# 方法 3:載入自訂函式庫
20mylib = ctypes.CDLL("./mylib.so")

C 型別對應

 1import ctypes
 2
 3# 基本型別對應
 4"""
 5C 型別           ctypes 型別        Python 型別
 6─────────────────────────────────────────────────
 7char             c_char            bytes (長度 1)
 8wchar_t          c_wchar           str (長度 1)
 9char *           c_char_p          bytes 或 None
10wchar_t *        c_wchar_p         str 或 None
11int              c_int             int
12unsigned int     c_uint            int
13long             c_long            int
14unsigned long    c_ulong           int
15long long        c_longlong        int
16float            c_float           float
17double           c_double          float
18void *           c_void_p          int 或 None
19"""
20
21# 範例:設定函式的參數和回傳型別
22libc = ctypes.CDLL(ctypes.util.find_library("c"))
23
24# strlen 函式:size_t strlen(const char *s)
25libc.strlen.argtypes = [ctypes.c_char_p]
26libc.strlen.restype = ctypes.c_size_t
27
28result = libc.strlen(b"Hello, World!")
29print(f"字串長度: {result}")  # 13

指標操作

 1import ctypes
 2
 3# 建立指標
 4x = ctypes.c_int(42)
 5ptr = ctypes.pointer(x)  # 指向 x 的指標
 6
 7print(f"值: {ptr.contents.value}")  # 42
 8
 9# 修改值
10ptr.contents.value = 100
11print(f"新值: {x.value}")  # 100
12
13# 指標型別
14IntPtr = ctypes.POINTER(ctypes.c_int)
15
16# 空指標
17null_ptr = IntPtr()
18print(f"是否為空: {not null_ptr}")  # True
19
20# byref:輕量級的指標傳遞(不建立完整指標物件)
21def example_with_byref():
22    value = ctypes.c_int(0)
23    # 假設某函式需要 int* 參數
24    # some_func(ctypes.byref(value))
25    return value.value

結構體與聯合

 1import ctypes
 2
 3# 定義結構體
 4class Point(ctypes.Structure):
 5    _fields_ = [
 6        ("x", ctypes.c_double),
 7        ("y", ctypes.c_double),
 8    ]
 9
10# 使用結構體
11p = Point(3.0, 4.0)
12print(f"Point: ({p.x}, {p.y})")
13
14# 巢狀結構體
15class Rectangle(ctypes.Structure):
16    _fields_ = [
17        ("top_left", Point),
18        ("bottom_right", Point),
19    ]
20
21rect = Rectangle(Point(0, 0), Point(10, 10))
22print(f"矩形: ({rect.top_left.x}, {rect.top_left.y}) -> "
23      f"({rect.bottom_right.x}, {rect.bottom_right.y})")
24
25# 陣列
26IntArray5 = ctypes.c_int * 5
27arr = IntArray5(1, 2, 3, 4, 5)
28print(f"陣列: {list(arr)}")
29
30# 聯合(Union)
31class IntOrFloat(ctypes.Union):
32    _fields_ = [
33        ("i", ctypes.c_int),
34        ("f", ctypes.c_float),
35    ]
36
37u = IntOrFloat()
38u.f = 3.14
39print(f"作為 float: {u.f}")
40print(f"作為 int: {u.i}")  # 記憶體的整數解釋

【實作層】ctypes 實戰

呼叫系統 API

 1import ctypes
 2import ctypes.util
 3import os
 4
 5# 取得 process ID(跨平台範例)
 6if os.name == 'posix':
 7    libc = ctypes.CDLL(ctypes.util.find_library("c"))
 8
 9    # pid_t getpid(void)
10    libc.getpid.restype = ctypes.c_int
11    pid = libc.getpid()
12    print(f"PID (ctypes): {pid}")
13    print(f"PID (os): {os.getpid()}")
14
15# 呼叫數學函式
16libm = ctypes.CDLL(ctypes.util.find_library("m"))
17
18# double sqrt(double x)
19libm.sqrt.argtypes = [ctypes.c_double]
20libm.sqrt.restype = ctypes.c_double
21
22result = libm.sqrt(2.0)
23print(f"sqrt(2) = {result}")

回呼函式

 1import ctypes
 2
 3# 定義回呼函式型別
 4# qsort 的比較函式:int (*compar)(const void *, const void *)
 5CMPFUNC = ctypes.CFUNCTYPE(
 6    ctypes.c_int,      # 回傳型別
 7    ctypes.c_void_p,   # 參數 1
 8    ctypes.c_void_p    # 參數 2
 9)
10
11def py_compare(a, b):
12    """Python 比較函式"""
13    # 將 void* 轉換為 int*
14    a_val = ctypes.cast(a, ctypes.POINTER(ctypes.c_int)).contents.value
15    b_val = ctypes.cast(b, ctypes.POINTER(ctypes.c_int)).contents.value
16    return a_val - b_val
17
18# 包裝為 C 回呼
19c_compare = CMPFUNC(py_compare)
20
21# 使用 qsort
22libc = ctypes.CDLL(ctypes.util.find_library("c"))
23
24# void qsort(void *base, size_t nmemb, size_t size,
25#            int (*compar)(const void *, const void *))
26IntArray = ctypes.c_int * 5
27arr = IntArray(5, 2, 8, 1, 9)
28
29print(f"排序前: {list(arr)}")
30
31libc.qsort(
32    arr,                          # base
33    len(arr),                     # nmemb
34    ctypes.sizeof(ctypes.c_int),  # size
35    c_compare                     # compar
36)
37
38print(f"排序後: {list(arr)}")

處理字串

 1import ctypes
 2
 3libc = ctypes.CDLL(ctypes.util.find_library("c"))
 4
 5# 注意:Python 3 的字串是 unicode
 6# ctypes 的 c_char_p 需要 bytes
 7
 8# 錯誤示範
 9# libc.strlen("Hello")  # TypeError
10
11# 正確做法
12result = libc.strlen(b"Hello")
13print(f"長度: {result}")
14
15# 建立可修改的字串緩衝區
16buffer = ctypes.create_string_buffer(b"Hello", 20)
17print(f"原始: {buffer.value}")
18
19# strcpy:複製字串
20libc.strcpy(buffer, b"World")
21print(f"複製後: {buffer.value}")
22
23# 處理 wchar_t(寬字元)
24# wchar_t *wcscat(wchar_t *dest, const wchar_t *src)
25if hasattr(libc, 'wcscat'):
26    wbuffer = ctypes.create_unicode_buffer("Hello, ", 50)
27    libc.wcscat(wbuffer, "World!")
28    print(f"寬字串: {wbuffer.value}")

【設計層】cffi 基礎

安裝與基本使用

1pip install cffi
 1from cffi import FFI
 2
 3ffi = FFI()
 4
 5# ABI 模式:動態載入,不需編譯
 6ffi.cdef("""
 7    int strlen(const char *s);
 8    double sqrt(double x);
 9""")
10
11# 載入函式庫
12libc = ffi.dlopen(None)  # None = 載入預設 C 函式庫
13
14# 呼叫函式
15result = libc.strlen(b"Hello, cffi!")
16print(f"strlen: {result}")

ABI 模式 vs API 模式

 1from cffi import FFI
 2
 3# ========== ABI 模式 ==========
 4# 優點:簡單,不需編譯器
 5# 缺點:效能較差,型別檢查較弱
 6
 7ffi_abi = FFI()
 8ffi_abi.cdef("""
 9    double sin(double x);
10    double cos(double x);
11""")
12libm = ffi_abi.dlopen("m")  # 或 None 使用預設
13
14import math
15print(f"sin(π/2) = {libm.sin(math.pi / 2)}")
16
17# ========== API 模式 ==========
18# 優點:效能好,完整型別檢查
19# 缺點:需要編譯器
20
21ffi_api = FFI()
22ffi_api.cdef("""
23    double compute_something(double x, double y);
24""")
25
26# 設定原始碼(會編譯成擴展模組)
27ffi_api.set_source("_example",
28    """
29    double compute_something(double x, double y) {
30        return x * x + y * y;
31    }
32    """,
33)
34
35# 編譯(通常在 setup.py 中執行)
36# ffi_api.compile()

cffi 的型別系統

 1from cffi import FFI
 2
 3ffi = FFI()
 4
 5# 定義結構體
 6ffi.cdef("""
 7    typedef struct {
 8        double x;
 9        double y;
10    } Point;
11
12    typedef struct {
13        Point center;
14        double radius;
15    } Circle;
16""")
17
18# 建立結構體實例
19point = ffi.new("Point *")
20point.x = 3.0
21point.y = 4.0
22
23# 或者一次初始化
24point2 = ffi.new("Point *", {'x': 1.0, 'y': 2.0})
25
26# 巢狀結構體
27circle = ffi.new("Circle *", {
28    'center': {'x': 0.0, 'y': 0.0},
29    'radius': 5.0
30})
31
32print(f"圓心: ({circle.center.x}, {circle.center.y})")
33print(f"半徑: {circle.radius}")
34
35# 陣列
36arr = ffi.new("int[5]", [1, 2, 3, 4, 5])
37print(f"陣列: {list(arr)}")
38
39# 動態大小陣列
40n = 10
41dynamic_arr = ffi.new(f"double[{n}]")
42for i in range(n):
43    dynamic_arr[i] = i * 0.5

【實作層】cffi 實戰

包裝簡單的 C 函式庫

假設我們有一個簡單的 C 函式庫 mathutil.c

 1// mathutil.c
 2#include <math.h>
 3
 4double vector_length(double x, double y, double z) {
 5    return sqrt(x*x + y*y + z*z);
 6}
 7
 8int fibonacci(int n) {
 9    if (n <= 1) return n;
10    int a = 0, b = 1;
11    for (int i = 2; i <= n; i++) {
12        int tmp = a + b;
13        a = b;
14        b = tmp;
15    }
16    return b;
17}

使用 cffi 包裝:

 1# build_mathutil.py
 2from cffi import FFI
 3
 4ffi = FFI()
 5
 6# 宣告要使用的函式
 7ffi.cdef("""
 8    double vector_length(double x, double y, double z);
 9    int fibonacci(int n);
10""")
11
12# 設定原始碼
13ffi.set_source(
14    "_mathutil",  # 模組名稱
15    """
16    #include <math.h>
17
18    double vector_length(double x, double y, double z) {
19        return sqrt(x*x + y*y + z*z);
20    }
21
22    int fibonacci(int n) {
23        if (n <= 1) return n;
24        int a = 0, b = 1;
25        for (int i = 2; i <= n; i++) {
26            int tmp = a + b;
27            a = b;
28            b = tmp;
29        }
30        return b;
31    }
32    """,
33    libraries=['m'],  # 連結數學函式庫
34)
35
36if __name__ == "__main__":
37    ffi.compile(verbose=True)
1# 使用編譯後的模組
2from _mathutil import ffi, lib
3
4length = lib.vector_length(1.0, 2.0, 2.0)
5print(f"向量長度: {length}")  # 3.0
6
7fib_10 = lib.fibonacci(10)
8print(f"fibonacci(10): {fib_10}")  # 55

回呼函式

 1from cffi import FFI
 2
 3ffi = FFI()
 4
 5ffi.cdef("""
 6    typedef int (*compare_func)(int, int);
 7    void custom_sort(int *arr, int size, compare_func cmp);
 8""")
 9
10ffi.set_source("_sort_example",
11    """
12    typedef int (*compare_func)(int, int);
13
14    void custom_sort(int *arr, int size, compare_func cmp) {
15        // 簡單的冒泡排序
16        for (int i = 0; i < size - 1; i++) {
17            for (int j = 0; j < size - i - 1; j++) {
18                if (cmp(arr[j], arr[j+1]) > 0) {
19                    int tmp = arr[j];
20                    arr[j] = arr[j+1];
21                    arr[j+1] = tmp;
22                }
23            }
24        }
25    }
26    """
27)
28
29# 編譯後使用
30# from _sort_example import ffi, lib
31
32# @ffi.callback("int(int, int)")
33# def py_compare(a, b):
34#     return a - b
35
36# arr = ffi.new("int[5]", [5, 2, 8, 1, 9])
37# lib.custom_sort(arr, 5, py_compare)
38# print(list(arr))  # [1, 2, 5, 8, 9]

記憶體管理

 1from cffi import FFI
 2
 3ffi = FFI()
 4
 5# ffi.new() 分配的記憶體會自動管理
 6data = ffi.new("int[100]")  # 自動回收
 7
 8# 需要手動管理的情況
 9ffi.cdef("""
10    void *malloc(size_t size);
11    void free(void *ptr);
12""")
13
14libc = ffi.dlopen(None)
15
16# 手動分配
17ptr = libc.malloc(100)
18if ptr != ffi.NULL:
19    # 使用記憶體...
20
21    # 必須手動釋放
22    libc.free(ptr)
23
24# 使用 ffi.gc() 自動管理外部分配的記憶體
25def auto_managed_alloc(size):
26    ptr = libc.malloc(size)
27    if ptr == ffi.NULL:
28        raise MemoryError()
29    # 設定 finalizer,當 Python 物件被回收時自動呼叫
30    return ffi.gc(ptr, libc.free)
31
32managed_ptr = auto_managed_alloc(100)
33# 不需要手動 free,Python GC 會處理

【選擇指南】ctypes vs cffi

決策流程

 1需要呼叫 C 函式庫?
 2 3├── 只需要簡單呼叫系統 API
 4│   └── ctypes(標準庫,不需額外安裝)
 5 6├── 需要包裝複雜的 C 函式庫
 7│   └── cffi API 模式(更好的型別檢查)
 8 9├── 在 PyPy 上執行
10│   └── cffi(PyPy 原生支援)
1112└── 效能要求高
13    └── cffi API 模式 或 考慮 Cython/pybind11

效能比較

 1import timeit
 2
 3# 測試函式呼叫開銷
 4setup_ctypes = """
 5import ctypes
 6import ctypes.util
 7libm = ctypes.CDLL(ctypes.util.find_library("m"))
 8libm.sqrt.argtypes = [ctypes.c_double]
 9libm.sqrt.restype = ctypes.c_double
10"""
11
12setup_cffi = """
13from cffi import FFI
14ffi = FFI()
15ffi.cdef("double sqrt(double x);")
16libm = ffi.dlopen("m")
17"""
18
19# 結果(僅供參考,實際數據取決於環境)
20# ctypes: ~0.3 μs per call
21# cffi ABI: ~0.2 μs per call
22# cffi API: ~0.05 μs per call
23# 原生 Python math.sqrt: ~0.02 μs per call

常見錯誤與除錯

 1import ctypes
 2
 3# 錯誤 1:忘記設定 argtypes/restype
 4libc = ctypes.CDLL(ctypes.util.find_library("c"))
 5# result = libc.strlen("hello")  # 可能 crash 或回傳錯誤值
 6
 7# 正確做法
 8libc.strlen.argtypes = [ctypes.c_char_p]
 9libc.strlen.restype = ctypes.c_size_t
10result = libc.strlen(b"hello")
11
12# 錯誤 2:傳遞 Python str 而非 bytes
13# libc.strlen("hello")  # TypeError
14
15# 錯誤 3:回呼函式被垃圾回收
16CALLBACK = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)
17
18def bad_example():
19    def callback(x):
20        return x * 2
21
22    c_callback = CALLBACK(callback)
23    # 如果這裡把 c_callback 傳給 C,然後函式結束
24    # c_callback 可能被回收,C 呼叫時會 crash
25    return c_callback  # 必須保持參考
26
27# 正確做法:保持回呼的參考
28_callbacks = []  # 全域列表保持參考
29
30def safe_example():
31    def callback(x):
32        return x * 2
33
34    c_callback = CALLBACK(callback)
35    _callbacks.append(c_callback)  # 保持參考
36    return c_callback

思考題

  1. 為什麼 cffi 的 API 模式比 ABI 模式快?這與 Python 的執行模型有什麼關係?
  2. 在什麼情況下,使用 ctypes/cffi 會比重新用 Python 實現更好?
  3. 如何安全地處理 C 函式庫中的記憶體分配?

實作練習

  1. 使用 ctypes 包裝 time.h 中的 time()localtime() 函式
  2. 使用 cffi 包裝一個簡單的數學函式庫,提供矩陣乘法功能
  3. 比較 ctypes、cffi ABI 模式、cffi API 模式在大量函式呼叫時的效能差異

延伸閱讀


下一章:Cython