Python-C互嵌场景下如何将void指针转为Python可调用对象?
问题:如何将C中的void*回调指针转换为Python可调用对象?
我正在编写一套跨语言调用代码,流程为Python用户代码通过ctypes调用C中间库,再由C中间库通过Python嵌入机制调用Python后端函数,具体流程如下:
Python用户代码传递函数名 -> C中间库 -> Python"后端代码"
步骤分解:
- Python用户代码加载C中间库,通过ctypes传递目标函数名及参数(包含类型和值)。
- C中间库初始化Python嵌入环境,加载指定模块并调用目标函数,传入参数。
- 嵌入的Python环境执行函数并返回结果。
当前遇到的问题:Python后端函数需要接收一个回调函数参数,在C中间库中我持有这个回调的void*指针,请问如何将其转换为Python可调用对象?
原示例代码
run.py(用户代码)
import ctypes import sys if __name__ == "__main__": if sys.platform == "darwin": ext = "dylib" elif sys.platform == "linux": ext = "so" else: raise ValueError("Handle me somehow") lib = ctypes.PyDLL(f"build/libcallstuff.{ext}") initialize = lib.__getattr__("initialize") initialize() call = lib.__getattr__("call") # signature: int call(char *funcname, void * fn_p, int x) call.restype = ctypes.c_int call.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.c_int] def myfn(x): return 2 * x # Prepare all arguments to call func # fn_p signature is int f(int x) fn_t = ctypes.CFUNCTYPE(ctypes.c_int, *[ctypes.c_int]) fn_p = ctypes.cast(ctypes.pointer(fn_t(myfn)), ctypes.c_void_p) a = ctypes.c_int(21) result = call("apply".encode(), fn_p, a) print("Result is", result) finalize = lib.__getattr__("finalize") finalize()
callstuff.c(C中间库)
#define PY_SSIZE_T_CLEAN #include <Python.h> #include <stdio.h> #include <string.h> /** * 该函数通过Python嵌入机制调用dostuff.py中的指定函数 * 示例中参数被简化,实际场景会传递带类型信息的参数列表 * 省略了内存释放和错误检查逻辑 */ int call(const char *fn_name, void *fn_p, int x) { printf("I am here\n"); PyObject *pFileName = PyUnicode_FromString("dostuff"); printf("I am here 2\n"); PyObject *pModule = PyImport_Import(pFileName); printf("I am here 3\n"); PyObject *pFunc = PyObject_GetAttrString(pModule, fn_name); PyObject *pArgs = PyTuple_New(2); // 参数为:回调函数f,整数x PyObject *pValue; pValue = (PyObject *) fn_p; // ??????? 如何转换void *fn_p? PyTuple_SetItem(pArgs, 0, pValue); pValue = PyLong_FromLong(x); PyTuple_SetItem(pArgs, 1, pValue); PyObject *pResult = PyObject_CallObject(pFunc, pArgs); if (pResult != NULL) { return PyLong_AsLong(pResult); } else { return -1; } } int initialize() { Py_Initialize(); return 0; } int finalize() { Py_Finalize(); return 0; }
dostuff.py(Python后端代码)
from typing import Callable def apply(f: Callable, x): return f(x)
CMakeLists.txt(编译配置)
cmake_minimum_required(VERSION 3.18) project(PyCInterop LANGUAGES C) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) find_package(Python REQUIRED Interpreter Development) add_library(callstuff SHARED callstuff.c) target_link_libraries(callstuff PRIVATE Python::Python)
编译与运行命令
# 编译 $ cmake -S. -B build -DCMAKE_BUILD_TYPE=Debug && cmake --build build # 运行 $ python run.py
解决方案
核心问题分析
原代码的错误在于:通过ctypes.pointer(fn_t(myfn))传递的是ctypes.CFuncPtr实例的指针,而非真正的Python函数对象指针,直接强制转换为PyObject*会导致C侧访问非法内存。
最简修改方案
1. 调整Python用户代码(run.py)
直接传递Python函数对象的地址,用ctypes.py_object包装:
# 替换原代码中fn_p的定义部分 fn_p = ctypes.cast(ctypes.py_object(myfn), ctypes.c_void_p)
2. 修改C中间库代码(callstuff.c)
此时fn_p指向的是合法的Python函数对象,直接转换即可:
// 保留原赋值行,现在这行是安全的 pValue = (PyObject *) fn_p;
通用场景扩展(需C侧直接调用回调)
如果回调需要在C侧被直接调用再转发到Python,需要在C侧将C函数指针包装为自定义Python可调用对象:
// 定义回调包装函数 static PyObject* callback_wrapper(PyObject* self, PyObject* args) { int x; if (!PyArg_ParseTuple(args, "i", &x)) { return NULL; } // 从self中取出C函数指针 typedef int (*CallbackFunc)(int); CallbackFunc fn = (CallbackFunc)self; return PyLong_FromLong(fn(x)); } // 方法表 static PyMethodDef CallbackMethods[] = { {"__call__", callback_wrapper, METH_VARARGS, "调用回调函数"}, {NULL, NULL, 0, NULL} }; // 自定义类型对象 static PyTypeObject CallbackType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "Callback", .tp_basicsize = sizeof(void*), .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = CallbackMethods, }; // 初始化时注册类型 int initialize() { Py_Initialize(); if (PyType_Ready(&CallbackType) < 0) { return -1; } return 0; } // 在call函数中包装C函数指针 int call(const char *fn_name, void *fn_p, int x) { // ... 原有代码 ... // 创建自定义可调用对象并存储C函数指针 PyObject* callback_obj = PyObject_New(PyObject, &CallbackType); *(void**)callback_obj = fn_p; PyTuple_SetItem(pArgs, 0, callback_obj); // ... 原有代码 ... }
内容的提问来源于stack exchange,提问作者Dmitry Kabanov
相关产品推荐
相关产品推荐

