PAYATHON 2026

Python 能否定义供 shared library 直接调用的 C function?

技术老齐

结论

Python 可以用 ctypes.CFUNCTYPE 创建采用普通 C calling convention 的 callback,C 代码再通过函数指针调用对应的 Python 函数。

普通 Python 函数无法直接成为名为 bar 的 ELF、Mach-O 或 PE 导出符号。所以下面这段未经修改的 C 代码不能直接找到 Python 中定义的 bar:

int foo(float f) {
    return f + bar();
}

如果 mod.so 通过动态链接符号 bar 调用函数,就需要 native adapter,或者通过 Cython、CFFI embedding 等方式生成带有该导出符号的 native library。如果可以修改 mod.so,让它接收函数指针,那么只用 ctypes 即可,C 代码中也不再需要 PyObject_CallObject()。

向 shared library 传入 callback

可以把 bar 作为函数指针传给 foo:

typedef int (*bar_fn)(void);

int foo(float f, bar_fn bar) {
    return (int)(f + bar());
}

Python 端通过 CFUNCTYPE 创建 callback:

import ctypes

BAR_CALLBACK = ctypes.CFUNCTYPE(ctypes.c_int)

@BAR_CALLBACK
def bar():
    return 1337

dll = ctypes.CDLL("./mod.so")

dll.foo.argtypes = [ctypes.c_float, BAR_CALLBACK]
dll.foo.restype = ctypes.c_int

result = dll.foo(42.0, bar)
assert result == 42 + 1337

CFUNCTYPE 会创建一个 native callback 入口。对 C 代码来说,它就是一个普通函数指针,因此不需要包含 Python.h,也不用调用 PyObject_CallObject()。

callback 执行时仍然依赖 Python runtime 和 ctypes 来处理参数与返回值转换以及 GIL 管理,只是 mod.so 的 C 代码不需要处理这些细节。

不想在每次调用时传入函数指针

可以增加一个只调用一次的注册接口:

typedef int (*bar_fn)(void);

static bar_fn registered_bar = NULL;

void set_bar_callback(bar_fn callback) {
    registered_bar = callback;
}

int foo(float f) {
    if (registered_bar == NULL) {
        return -1;
    }

    return (int)(f + registered_bar());
}

对应的 Python 代码是:

import ctypes

BAR_CALLBACK = ctypes.CFUNCTYPE(ctypes.c_int)

@BAR_CALLBACK
def bar():
    return 1337

dll = ctypes.CDLL("./mod.so")

dll.set_bar_callback.argtypes = [BAR_CALLBACK]
dll.set_bar_callback.restype = None

dll.foo.argtypes = [ctypes.c_float]
dll.foo.restype = ctypes.c_int

dll.set_bar_callback(bar)

assert dll.foo(42.0) == 1379

注册后,调用 foo() 时就不用再传 callback 参数。系统中有多项 API 时,还可以注册一个由函数指针组成的结构体:

struct mod_api {
    int (*bar)(void);
    void (*log_message)(const char *);
};

void set_mod_api(const struct mod_api *api);

这种设计比依赖 main executable 导出多个全局符号更容易移植,也便于根据 mod_api.json 自动生成绑定代码。

为什么不能直接解析 Python 中的 bar

C 编译器看到下面的声明:

int bar(void);

会生成对 native symbol bar 的引用。加载 mod.so 时,dynamic linker 必须在 executable 或其他已加载的 shared library 中找到这个符号。

而 Python 中的定义:

def bar():
    return 1337

创建的是 Python object。它既不是 native object file 中的机器码函数,也不会被登记到 dynamic linker 的符号表中。

CFUNCTYPE 可以创建供 C 调用的入口地址,但这个地址只是函数指针,不会自动注册为名为 bar 的全局动态符号。需要通过参数、注册函数或 API table,把这个地址交给 mod.so。

必须保持 mod.so 完全不变时

假设 mod.so 已经固定为:

extern int bar(void);

int foo(float f) {
    return (int)(f + bar());
}

加载它之前,进程中必须有一个真正导出 bar 的 native module,例如:

  • 手写或自动生成的 adapter.so;
  • 由 Cython 生成的扩展;
  • 使用 CFFI embedding 生成的 shared library;
  • 其他能够生成并导出 native symbol 的编译工具。

这些方式都需要某种 native bridge。纯 Python 和 ctypes 没有通用接口可以向当前进程的动态符号表中追加具名函数符号。修改 GOT、重写 call instruction 或使用特定平台的 loader 技巧有可能达到类似效果,但这些做法缺乏可移植性,也容易受链接方式和安全机制影响。

注意事项

使用 ctypes callback 时需要注意:

  • 必须保留 callback object 的强引用。callback 一旦被垃圾回收,C 代码保存的函数指针就会失效。
callbacks = {
    "bar": BAR_CALLBACK(bar_impl),
}
  • CFUNCTYPE 适用于常见的 C calling convention。如果 Windows 上的目标函数使用 stdcall,应按照 ABI 改用 WINFUNCTYPE。
  • argtypes 和 restype 必须与 C 声明完全一致,包括整数宽度、指针类型、结构体布局和 calling convention。
  • Python exception 不能直接跨越 C ABI 边界。callback 应自行捕获异常,并返回双方约定的错误码:
@BAR_CALLBACK
def bar():
    try:
        return calculate_value()
    except Exception:
        return -1
  • 如果 C 从其他线程调用 callback,ctypes 会处理进入 Python 所需的线程状态。不过,thread-local 状态、性能和回调频率仍需分别评估。
  • callback 依赖存活的 Python interpreter。程序退出或 interpreter finalization 期间,C 代码不能继续调用已经注册的 callback。

如果可以调整 mod.so 的 ABI,直接传入函数指针或注册 API table 最简单。如果 mod.so 必须继续直接调用名为 bar 的符号,那么仍需使用现有的 adapter 方案,并可根据 mod_api.json 自动生成 adapter。

备注:内容仅供参考。