4.1 ctypes 與 cffi:動態綁定
4.1 ctypes 與 cffi:動態綁定
本章介紹如何使用 ctypes 和 cffi 動態載入和呼叫 C 函式庫。
本章目標
學完本章後,你將能夠:
- 理解 FFI(Foreign Function Interface)的概念
- 使用 ctypes 呼叫系統函式庫
- 使用 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 程式
2 │
3 ↓ FFI
4┌───────────────┐
5│ C 函式庫 │
6│ - libc.so │
7│ - libm.so │
8│ - 自訂 .so │
9└───────────────┘ctypes vs cffi
| 特性 | ctypes | cffi |
|---|---|---|
| 來源 | 標準庫 | 第三方 |
| 設計 | 物件導向 API | C 語法描述 |
| 效能 | 較慢 | 較快(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 原生支援)
11│
12└── 效能要求高
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思考題
- 為什麼 cffi 的 API 模式比 ABI 模式快?這與 Python 的執行模型有什麼關係?
- 在什麼情況下,使用 ctypes/cffi 會比重新用 Python 實現更好?
- 如何安全地處理 C 函式庫中的記憶體分配?
實作練習
- 使用 ctypes 包裝
time.h中的time()和localtime()函式 - 使用 cffi 包裝一個簡單的數學函式庫,提供矩陣乘法功能
- 比較 ctypes、cffi ABI 模式、cffi API 模式在大量函式呼叫時的效能差異
延伸閱讀
下一章:Cython