杰瑞科技汇

Python调用C函数时,字符串参数如何正确传递?

  1. ctypes: Python 的标准库,无需额外安装,适合快速调用简单的 C 函数。
  2. 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_pchar* 的标准表示。
  • restype: 定义 C 函数的返回值类型。
  • .encode('utf-8'): 必须的步骤,将 Python str 转换为 C 可以理解的 bytes
  • .decode('utf-8'): 必须的步骤,将 C 返回的 bytes 转换回 Python str
  • 内存管理: C 函数使用 malloc, calloc, strdup 等分配内存,Python 必须手动调用对应的 freectypes.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() 是一个方便的工具,用于释放由 C malloc 分配的内存。

ctypes vs CFFI 对比

特性 ctypes CFFI
依赖 Python 标准库,无需安装 需要额外安装 pip install cffi
易用性 较低,需要手动处理编码、解码、内存管理 非常高,自动处理字符串转换,语法更清晰
性能 对于简单调用足够快 性能通常更好,尤其是在复杂交互中
灵活性 可以直接调用内存地址,非常底层 API 设计更现代化,专注于函数调用
推荐场景 快速原型、简单的库调用、无法安装新包的环境 新项目、复杂的 C 库集成、追求代码清晰度和可维护性

对于绝大多数新项目,强烈推荐使用 CFFI,它极大地简化了 Python 与 C 之间的字符串交互,减少了出错的可能性,代码也更易读。

只有在你不能安装第三方库,或者只需要进行一次性的、非常简单的 C 函数调用时,才考虑使用 ctypes

分享:
扫描分享到社交APP
上一篇
下一篇