You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python-C互嵌场景下如何将void指针转为Python可调用对象?

问题:如何将C中的void*回调指针转换为Python可调用对象?

我正在编写一套跨语言调用代码,流程为Python用户代码通过ctypes调用C中间库,再由C中间库通过Python嵌入机制调用Python后端函数,具体流程如下:

Python用户代码传递函数名 -> C中间库 -> Python"后端代码"

步骤分解:

  1. Python用户代码加载C中间库,通过ctypes传递目标函数名及参数(包含类型和值)。
  2. C中间库初始化Python嵌入环境,加载指定模块并调用目标函数,传入参数。
  3. 嵌入的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 03:34:59