ctypes: Python 的标准库,无需额外安装,适合快速调用简单的 C 函数。- CFFI: 一个功能更强大、更现代的外部函数接口库,支持 CPython, PyPy, Cython 等,性能更好,语法也更灵活。
下面我将详细介绍这两种方法,并提供完整的代码示例。
核心概念:Python 字符串与 C 字符串的区别
在深入代码之前,必须理解一个关键点:
- Python 字符串 (
str): 在 Python 3 中,str对象是 Unicode 编码的,是抽象的、不可变的序列,它本身不能直接传递给 C 代码。 - *C 字符串 (`char
)**: 在 C 中,char*` 通常是一个指向字符数组的指针,它没有内置的长度信息,并且是特定编码的(通常是 ASCII 或 UTF-8)。
当 Python 调用 C 函数时,必须进行一个转换过程:
Python str -> 编码 -> 字节串 (bytes) -> 传递给 C -> C 函数接收 char*。
反之,当 C 函数返回一个 char* 时:
C 函数返回 char* -> Python 接收 -> 解码 -> Python str。
使用 ctypes (Python 标准库)
ctypes 是最直接的方法,但需要手动处理很多底层细节,如函数原型定义、参数类型、内存管理等。
场景:调用一个简单的 C 函数,该函数接收一个字符串并打印其长度。
步骤 1: 编写 C 代码并编译成动态链接库
我们创建一个 C 文件 mylib.c。
// mylib.c
#include <stdio.h>
#include <string.h>
// 定义一个函数,接收一个 C 风格的字符串 (const char*)
// const char* 表示 C 函数不会修改这个字符串
void print_string_info(const char* str) {
if (str == NULL) {
printf("Received a NULL string.\n");
return;
}
size_t length = strlen(str);
printf("C Function Received: \"%s\"\n", str);
printf("C Function Calculated Length: %zu\n", length);
}
// 定义一个函数,接收一个字符串并返回一个新的字符串
// 调用者负责返回的内存!这里我们为了简单,让调用者释放。
char* get_greeting(const char* name) {
// 在 C 中,我们需要为返回的字符串分配内存
char* greeting = (char*)malloc(50 * sizeof(char));
if (greeting == NULL) {
return NULL;
}
sprintf(greeting, "Hello, %s!", name);
return greeting;
}
编译它成一个共享库(在 Linux/macOS 上是 .so 文件,在 Windows 上是 .dll 文件)。
Linux / macOS:
# -fPIC 表示生成位置无关的代码,用于共享库 # -shared 表示生成共享库 # -o mylib.so 指定输出文件名 gcc -fPIC -shared -o mylib.so mylib.c
Windows (使用 MinGW 或 Visual Studio 命令行):
# -LD 表示生成 DLL # -o mylib.dll 指定输出文件名 gcc -LD -o mylib.dll mylib.c
步骤 2: 在 Python 中使用 ctypes 调用
我们创建一个 Python 脚本 main.py 来调用这个库。
# main.py
import ctypes
import os
# --- 1. 加载共享库 ---
# 根据你的操作系统,使用不同的文件名和加载方式
if os.name == 'nt': # Windows
lib = ctypes.CDLL('./mylib.dll')
else: # Linux or macOS
lib = ctypes.CDLL('./mylib.so')
# --- 2. 定义函数原型 (非常重要!) ---
# 告诉 Python print_string_info 函数接收一个 const char* 参数
# ctypes.c_char_p 用于表示 C 的 char* 或 const char*
lib.print_string_info.argtypes = [ctypes.c_char_p]
# 告诉 Python get_greeting 函数接收一个 const char* 并返回一个 char*
lib.get_greeting.argtypes = [ctypes.c_char_p]
lib.get_greeting.restype = ctypes.c_char_p # 设置返回类型
# --- 3. 准备参数并调用 ---
# 创建一个 Python 字符串
python_string = "你好,世界!Hello World!"
# Python 3 的 str 不能直接传递,必须先编码成 bytes
# .encode() 默认使用 UTF-8 编码,这是现代 C 应用程序最常用的编码
c_string = python_string.encode('utf-8')
print("--- 调用 print_string_info ---")
# 调用 C 函数,传递编码后的字节串
lib.print_string_info(c_string)
print("\n--- 调用 get_greeting ---")
# 调用返回字符串的 C 函数
name_to_pass = "Alice"
name_bytes = name_to_pass.encode('utf-8')
# 调用 C 函数
# c_char_p 返回的是 ctypes 对象,我们需要访问其 value 属性来获取 bytes
# 或者直接解码
result_ptr = lib.get_greeting(name_bytes)
# C 函数返回的是 char*,它是一个指向内存的指针
# 我们需要将其内容解码回 Python 字符串
# result_ptr.value 是一个 bytes 对象
if result_ptr:
python_result = result_ptr.value.decode('utf-8')
print(f"Python received from C: {python_result}")
# **非常重要!** C 函数使用 malloc 分配了内存,Python 的垃圾回收器不会自动释放它
# 我们必须手动调用 C 的 free 函数来释放内存,否则会造成内存泄漏
free_func = lib.free # 假设我们在 C 库中也导出了 free 函数
free_func.argtypes = [ctypes.c_void_p] # free 接收一个 void* 指针
free_func(result_ptr)
print("Memory freed.")
else:
print("C function returned NULL.")
ctypes 关键点总结
ctypes.CDLL()/ctypes.WinDLL(): 用于加载共享库。argtypes: 一个列表,定义 C 函数每个参数的类型。ctypes.c_char_p是char*的标准表示。restype: 定义 C 函数的返回值类型。.encode('utf-8'): 必须的步骤,将 Pythonstr转换为 C 可以理解的bytes。.decode('utf-8'): 必须的步骤,将 C 返回的bytes转换回 Pythonstr。- 内存管理: C 函数使用
malloc,calloc,strdup等分配内存,Python 必须手动调用对应的free或ctypes.pythonapi.PyMem_Free来释放,否则会导致内存泄漏。
使用 CFFI (推荐)
CFFI 提供了更高级的抽象,可以自动处理很多 ctypes 需要手动管理的细节,比如字符串的编解码,它有两种主要用法:cdef (用于编译时) 和 api (用于运行时),这里我们展示更灵活的 api 用法。
步骤 1: 安装 CFFI
pip install cffi
步骤 2: 编写 C 代码 (和 ctypes 一样)
mylib.c 文件与之前完全相同。
步骤 3: 在 Python 中使用 CFFI 调用
创建 main_cffi.py 脚本。
# main_cffi.py
from cffi import FFI
# --- 1. 定义 FFI 对象并声明 C 函数 ---
ffi = FFI()
# 在一个字符串中声明 C 函数的原型
# cffi 会根据这个声明自动处理类型转换
ffi.cdef("""
// const char* 告诉 cffi 这个函数不会修改字符串
void print_string_info(const char *str);
char* get_greeting(const char *name);
""")
# --- 2. 加载共享库 ---
# 这一步和 ctypes 类似
if os.name == 'nt': # Windows
lib = ffi.dlopen('./mylib.dll')
else: # Linux or macOS
lib = ffi.dlopen('./mylib.so')
# --- 3. 调用 C 函数 ---
# 创建一个 Python 字符串
python_string = "你好,世界!Hello World (from CFFI)!"
# **CFFI 的魔法之处**:
# 你可以直接将 Python 字符串传递给期望 char* 的 C 函数
# CFFI 会在后台自动将其编码成 bytes 并传递给 C
print("--- 调用 print_string_info ---")
lib.print_string_info(python_string)
print("\n--- 调用 get_greeting ---")
name_to_pass = "Bob"
# 同样,可以直接传递 Python 字符串
result_ptr = lib.get_greeting(name_to_pass)
# CFFI 的另一个魔法之处:
# 当 C 函数返回 char* 时,CFFI 会自动将其包装成一个 cffi 对象
# 你可以使用 .decode() 方法直接将其转换为 Python 字符串
# 这比 ctypes 的 .value.decode() 更直观
if result_ptr != ffi.NULL: # 检查是否为 NULL
python_result = ffi.string(result_ptr).decode('utf-8')
print(f"Python received from C: {python_result}")
# **内存管理仍然重要!**
# 我们仍然需要手动释放 C 分配的内存
# cffi 提供了方便的方式来调用 C 标准库函数
ffi.release(result_ptr) # 这是一个便捷函数,等同于调用 free
print("Memory freed.")
else:
print("C function returned NULL.")
CFFI 关键点总结
FFI(): 创建一个 FFI 对象。ffi.cdef(): 声明 C 函数的接口,CFFI 会解析这些声明。ffi.dlopen(): 加载共享库,功能等同于ctypes.CDLL。- 自动编解码: 最大的优点,可以直接在 Python 和 C 之间传递
str对象,CFFI 会自动处理编码(默认为 UTF-8)和解码,大大简化了代码。 ffi.string(): 将 C 返回的char*指针转换为一个bytes对象,然后可以方便地调用.decode()。ffi.NULL: 用于检查 C 指针是否为空。- 内存管理: 仍然需要手动管理。
ffi.release()是一个方便的工具,用于释放由 Cmalloc分配的内存。
ctypes vs CFFI 对比
| 特性 | ctypes |
CFFI |
|---|---|---|
| 依赖 | Python 标准库,无需安装 | 需要额外安装 pip install cffi |
| 易用性 | 较低,需要手动处理编码、解码、内存管理 | 非常高,自动处理字符串转换,语法更清晰 |
| 性能 | 对于简单调用足够快 | 性能通常更好,尤其是在复杂交互中 |
| 灵活性 | 可以直接调用内存地址,非常底层 | API 设计更现代化,专注于函数调用 |
| 推荐场景 | 快速原型、简单的库调用、无法安装新包的环境 | 新项目、复杂的 C 库集成、追求代码清晰度和可维护性 |
对于绝大多数新项目,强烈推荐使用 CFFI,它极大地简化了 Python 与 C 之间的字符串交互,减少了出错的可能性,代码也更易读。
只有在你不能安装第三方库,或者只需要进行一次性的、非常简单的 C 函数调用时,才考虑使用 ctypes。
