news 2026/6/26 2:24:42

python的函数怎么转换为pybind11中的std::function(底层原理)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
python的函数怎么转换为pybind11中的std::function(底层原理)

事情是这样的,我有一个c++接口,接收一个类似void(*CB)(Data&)的函数指针,当我尝试将此接口暴露给python的时候,AI一步到位给我写好了转换代码如下:

#include <cstdio> #include <pybind11/pybind11.h> #include <pybind11/stl.h> #include <pybind11/functional.h> namespace py = pybind11; typedef void(*CB)(int&); class Test{ public: void test(CB cb) { printf("printf"); } }; PYBIND11_MODULE(testpy, m) { m.doc() = "pybind11 wrapper for testpy class"; py::class_<Test>(m, "Test") .def(py::init<>(), "Constructor for the Test class") .def("test", [](Test& self, std::function<void(int&)> callback) { //... ... auto cb = [callback](int&d){ callback(d); }; // 下面这行报错,错误信息类似: error: cannot convert ‘pybind11_init_testpy(pybind11::module_&)::<lambda(Test&, std::function<void(int&)>)>::<lambda(int&)>’ to ‘CB’ {aka ‘void (*)(int&)’} return self.test(cb); } }
import testpy def test_cb(i): print(f'test_cb i = {i}') t = testpy.Test() t.test()

首先,这里有个结论,AI给出的代码是错误的(带捕获参数的lambda不能直接转换为裸指针,可看注释部分),但是形式上面给出了转换代码的大致框架,这里精简后,整个问题的核心就是:pybind11的std::function怎么转换为函数指针。

下面我们看看怎么解决上面3个问题,并解决最终问题。




问题1:python的函数怎么转换为pybind11中的std::function(底层原理)


要解决这个问题,我们得了解python的函数传递过来,这里的callback到底是什么东西?由于pybind11是大量的宏来生成代码,为了快速得到我们要的内容,我们使用gdb来对堆栈进行分析。

#0 test_cb (i=@0x7fffffffd714: 9999) at testpy.cpp:19 #1 0x00007ffff73bfded in Test::test (this=0xc49820, cb=0x7ffff73ab120 <test_cb(int&)>) at testpy.cpp:13 #2 0x00007ffff73ab3f7 in operator()(Test &, std::function<void(int&)>) const (__closure=0xc49678, self=..., callback=...) at testpy.cpp:33 #3 0x00007ffff73ac08c in pybind11::detail::argument_loader<Test&, std::function<void(int&)> >::call_impl<void, pybind11_init_testpy(pybind11::module_&)::<lambda(Test&, std::function<void(int&)>)>&, 0, 1, pybind11::detail::void_type>(struct {...} &, std::index_sequence, pybind11::detail::void_type &&) (this=0x7fffffffd8a0, f=...) at /usr/include/pybind11/cast.h:1480 #4 0x00007ffff73abd00 in pybind11::detail::argument_loader<Test&, std::function<void(int&)> >::call<void, pybind11::detail::void_type, pybind11_init_testpy(pybind11::module_&)::<lambda(Test&, std::function<void(int&)>)>&>(struct {...} &) (this=0x7fffffffd8a0, f=...) at /usr/include/pybind11/cast.h:1454 #5 0x00007ffff73ab9b7 in operator() (__closure=0x0, call=...) at /usr/include/pybind11/pybind11.h:254 #6 0x00007ffff73aba6c in _FUN () at /usr/include/pybind11/pybind11.h:224 #7 0x00007ffff73bd52c in pybind11::cpp_function::dispatcher (self=0x7ffff75fedc0, args_in=0x7ffff7421f80, kwargs_in=0x0) at /usr/include/pybind11/pybind11.h:946 #8 0x0000000000581ecf in cfunction_call (func=0x7ffff740ba10, args=<optimized out>, kwargs=<optimized out>) at ../Objects/methodobject.c:537 #9 0x0000000000549205 in _PyObject_MakeTpCall (tstate=0xba5748 <_PyRuntime+459656>, callable=0x7ffff740ba10, args=<optimized out>, nargs=2, keywords=0x0) at ../Objects/call.c:240 #10 0x0000000000549c3d in _PyObject_VectorcallTstate (kwnames=<optimized out>, nargsf=<optimized out>, args=<optimized out>, callable=<optimized out>, tstate=<optimized out>) at ../Include/internal/pycore_call.h:90 #11 0x00000000005d7109 in _PyEval_EvalFrameDefault (tstate=tstate@entry=0xba5748 <_PyRuntime+459656>, frame=<optimized out>, frame@entry=0x7ffff7fb2020, throwflag=throwflag@entry=0) at Python/bytecodes.c:2706 #12 0x00000000005d564b in _PyEval_EvalFrame (throwflag=0, frame=0x7ffff7fb2020, tstate=0xba5748 <_PyRuntime+459656>) at ../Include/internal/pycore_ceval.h:89 #13 _PyEval_Vector (kwnames=0x0, argcount=0, args=0x0, locals=0x7ffff75f9a80, func=0x7ffff75da160, tstate=0xba5748 <_PyRuntime+459656>) at ../Python/ceval.c:1683 #14 PyEval_EvalCode (co=co@entry=0x7ffff75604b0, globals=globals@entry=0x7ffff75f9a80, locals=locals@entry=0x7ffff75f9a80) at ../Python/ceval.c:578 #15 0x00000000006087b2 in run_eval_code_obj (locals=0x7ffff75f9a80, globals=0x7ffff75f9a80, co=0x7ffff75604b0, tstate=0xba5748 <_PyRuntime+459656>) at ../Python/pythonrun.c:1722 #16 run_mod (mod=<optimized out>, filename=<optimized out>, globals=0x7ffff75f9a80, locals=0x7ffff75f9a80, flags=<optimized out>, arena=<optimized out>) at ../Python/pythonrun.c:1743 #17 0x00000000006b4853 in pyrun_file (fp=fp@entry=0xbf6480, filename=filename@entry=0x7ffff7409ca0, start=start@entry=257, globals=globals@entry=0x7ffff75f9a80, locals=locals@entry=0x7ffff75f9a80, closeit=closeit@entry=1, flags=0x7fffffffe0a8) at ../Python/pythonrun.c:1643 #18 0x00000000006b45ba in _PyRun_SimpleFileObject (fp=fp@entry=0xbf6480, filename=filename@entry=0x7ffff7409ca0, closeit=closeit@entry=1, flags=flags@entry=0x7fffffffe0a8) at ../Python/pythonrun.c:433 #19 0x00000000006b43ef in _PyRun_AnyFileObject (fp=0xbf6480, filename=filename@entry=0x7ffff7409ca0, closeit=closeit@entry=1, flags=flags@entry=0x7fffffffe0a8) at ../Python/pythonrun.c:78 #20 0x00000000006bc455 in pymain_run_file_obj (skip_source_first_line=0, filename=0x7ffff7409ca0, program_name=0x7ffff75f9bf0) at ../Modules/main.c:360 #21 pymain_run_file (config=0xb48328 <_PyRuntime+77672>) at ../Modules/main.c:379 #22 pymain_run_python (exitcode=0x7fffffffe09c) at ../Modules/main.c:629 #23 Py_RunMain () at ../Modules/main.c:709 #24 0x00000000006bbf3d in Py_BytesMain (argc=<optimized out>, argv=<optimized out>) at ../Modules/main.c:763 #25 0x00007ffff7c2a1ca in __libc_start_call_main (main=main@entry=0x518ac0 <main>, argc=argc@entry=2, argv=argv@entry=0x7fffffffe2e8) at ../sysdeps/nptl/libc_start_call_main.h:58 #26 0x00007ffff7c2a28b in __libc_start_main_impl (main=0x518ac0 <main>, argc=2, argv=0x7fffffffe2e8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe2d8) at ../csu/libc-start.c:360 #27 0x00000000006574f5 in _start ()

从上面看,核心就是从python解释器,到了pybind11::cpp_function::dispatcher,然后到了我们的test函数。如果我们对pybind11不熟悉的话,我们还是需要深入去看pybind11才能回答我们上面的问题,但是我这里想到了另外一个办法。

我们都知道,pybind11底层是由cpython实现的,因此我们通过cpython来实现上面的同样的功能是什么样子的呢?直接让AI生成示例如下:

#define PY_SSIZE_T_CLEAN #include <Python.h> #include <cstdio> #include <functional> // --- C++ 逻辑模拟部分 --- typedef void(*CB)(int&); class Test{ public: void test(CB cb) { int i = 9999; cb(i); } }; void test_cb(int& i) { printf("test_cb from cxx i = %d\n", i); } // --- CPython 包装部分 --- // 定义 Python 中的 Test 对象结构 typedef struct { PyObject_HEAD Test* cpp_obj; // 指向实际的 C++ 对象 } PyTestObject; void pybind11_like_func(Test& self, std::function<void(int&)> callback) { //... ... auto cb = [callback](int&d){ callback(d); }; int i = 8888; cb(i); // return self.test(cb); return self.test(test_cb); } // Test.test(callback) 的实现 static PyObject* PyTest_test(PyTestObject* self, PyObject* args) { PyObject* pycallback = NULL; // 1. 解析参数,期望得到一个可调用对象 if (!PyArg_ParseTuple(args, "O", &pycallback)) { return NULL; } if (!PyCallable_Check(pycallback)) { PyErr_SetString(PyExc_TypeError, "Parameter must be callable"); return NULL; } // 2. 核心:模拟 std::function<void(int&)> 的构造 // 我们在这里捕获 py_callback 指针。注意:实际生产中需要处理引用计数 std::function<void(int&)> cpp_callback = [pycallback](int& d) { // A. 必须获取 GIL,因为回调可能由 C++ 触发 PyGILState_STATE gstate = PyGILState_Ensure(); // B. 参数转换:C++ int& -> Python Long PyObject* arg = PyLong_FromLong((long)d); PyObject* arg_tuple = PyTuple_Pack(1, arg); // C. 调用 Python 函数 PyObject* result = PyObject_CallObject(pycallback, arg_tuple); // D. 错误处理与清理 if (!result) { PyErr_Print(); } Py_XDECREF(result); Py_DECREF(arg_tuple); Py_DECREF(arg); // E. 释放 GIL PyGILState_Release(gstate); }; pybind11_like_func(*self->cpp_obj, cpp_callback); Py_RETURN_NONE; } // --- 类型与模块定义 --- static PyMethodDef PyTest_methods[] = { {"test", (PyCFunction)PyTest_test, METH_VARARGS, "Execute test with callback"}, {NULL, NULL, 0, NULL} }; static PyTypeObject PyTestType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "testcpy.Test", .tp_basicsize = sizeof(PyTestObject), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_methods = PyTest_methods, .tp_new = PyType_GenericNew, }; static struct PyModuleDef testcpymodule = { PyModuleDef_HEAD_INIT, "testcpy", "CPython version of testcpy", -1, NULL }; PyMODINIT_FUNC PyInit_testcpy(void) { PyObject* m; if (PyType_Ready(&PyTestType) < 0) return NULL; m = PyModule_Create(&testcpymodule); if (m == NULL) return NULL; Py_INCREF(&PyTestType); PyModule_AddObject(m, "Test", (PyObject*)&PyTestType); return m; }

其实我们已经看到了,我们用cpython来实现的话,调用的std::function一定是一个带状态的callable obj。至此,我们已经解决了问题1。




问题2:pybind11中的std::function 怎么转换为c++层的裸函数


实际的方法就是在pybind11代码层,添加一个全局静态变量进行转换,参考如下代码(重点查看PyCBWrapper相关的内容):

#include <cstdio> #include <pybind11/pybind11.h> #include <pybind11/stl.h> #include <pybind11/functional.h> #include <functional> namespace py = pybind11; typedef void(*CB)(int&); class Test{ public: void test(CB cb) { int i = 9999; cb(i); } }; struct PyCBWrapper { static std::function<void(int&)> py_cb; static void trampoline(int& i) { if (nullptr != py_cb) PyCBWrapper::py_cb(i); } }; std::function<void(int&)> PyCBWrapper::py_cb = nullptr; void test_cb(int& i) { printf("test_cb from cxx i = %d\n", i); } PYBIND11_MODULE(testpy, m) { m.doc() = "pybind11 wrapper for testpy class"; py::class_<Test>(m, "Test") .def(py::init<>(), "Constructor for the Test class") .def("test", [](Test& self, std::function<void(int&)> callback) { //... ... auto cb = [callback](int&d){ callback(d); }; int i = 8888; cb(i); PyCBWrapper::py_cb = callback; // return self.test(cb); return self.test(PyCBWrapper::trampoline); }); }




问题3:pybind11中的std::function对象复制的注意事项


问题2的这段代码会直接运行报错,堆栈如下

#0 __pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44 #1 __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78 #2 __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:89 #3 0x00007ffff7c4527e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26 #4 0x00007ffff7c288ff in __GI_abort () at ./stdlib/abort.c:79 #5 0x00000000004b1252 in ?? () #6 0x00000000004b2908 in _Py_FatalErrorFunc () #7 0x00000000004b2cd2 in ?? () #8 0x000000000060861e in ?? () #9 0x00000000006a6e93 in PyEval_AcquireThread () #10 0x00007ffff73b9d20 in pybind11::gil_scoped_acquire::gil_scoped_acquire (this=0x7fffffffd760) at /usr/include/pybind11/gil.h:82 #11 0x00007ffff73dd1b1 in pybind11::detail::type_caster<std::function<void (int&)>, void>::load(pybind11::handle, bool)::func_handle::~func_handle() (this=0xc49870, __in_chrg=<optimized out>) at /usr/include/pybind11/functional.h:97 #12 0x00007ffff73dd324 in pybind11::detail::type_caster<std::function<void (int&)>, void>::load(pybind11::handle, bool)::func_wrapper::~func_wrapper() (this=0xc49870, __in_chrg=<optimized out>) at /usr/include/pybind11/functional.h:103 #13 0x00007ffff73e0ff2 in std::_Function_base::_Base_manager<pybind11::detail::type_caster<std::function<void(int&)>, void>::load(pybind11::handle, bool)::func_wrapper>::_M_destroy (__victim=...) at /usr/include/c++/13/bits/std_function.h:175 --Type <RET> for more, q to quit, c to continue without paging-- #14 0x00007ffff73e0ce4 in std::_Function_base::_Base_manager<pybind11::detail::type_caster<std::function<void(int&)>, void>::load(pybind11::handle, bool)::func_wrapper>::_M_manager (__dest=..., __source=..., __op=std::__destroy_functor) at /usr/include/c++/13/bits/std_function.h:203 #15 0x00007ffff73e02aa in std::_Function_handler<void(int&), pybind11::detail::type_caster<std::function<void(int&)>, void>::load(pybind11::handle, bool)::func_wrapper>::_M_manager (__dest=..., __source=..., __op=std::__destroy_functor) at /usr/include/c++/13/bits/std_function.h:282 #16 0x00007ffff73b62c1 in std::_Function_base::~_Function_base (this=0x7ffff73ff440 <PyCBWrapper::py_cb>, __in_chrg=<optimized out>) at /usr/include/c++/13/bits/std_function.h:244 #17 0x00007ffff73bff78 in std::function<void(int&)>::~function (this=0x7ffff73ff440 <PyCBWrapper::py_cb>, __in_chrg=<optimized out>) at /usr/include/c++/13/bits/std_function.h:334 #18 0x00007ffff7c47a76 in __run_exit_handlers (status=0, listp=<optimized out>, run_list_atexit=run_list_atexit@entry=true, run_dtors=run_dtors@entry=true) at ./stdlib/exit.c:108 #19 0x00007ffff7c47bbe in __GI_exit (status=<optimized out>) at ./stdlib/exit.c:138 #20 0x00007ffff7c2a1d1 in __libc_start_call_main (main=main@entry=0x518c60, argc=argc@entry=2, argv=argv@entry=0x7fffffffda28) at ../sysdeps/nptl/libc_start_call_main.h:74 #21 0x00007ffff7c2a28b in __libc_start_main_impl (main=0x518c60, argc=2, argv=0x7fffffffda28, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffda18) at ../csu/libc-start.c:360 #22 0x0000000000657b05 in _start ()

从堆栈分析可知,问题出在程序退出的时候,PyCBWrapper::py_cb(全局静态变量)析构的时候,这个时候其实python解释器已经退出了,再使用python解释器相关的资源,就会报错。这个其实就是问题3。由于从上面的例子可以知道,这个时候的PyCBWrapper::py_cb是一个捕获了python函数的PyObject的std::function,那么解决方案也很简单,那就是在上面的pybind11代码层中添加如下核心代码:

PYBIND11_MODULE(testpy, m) { m.doc() = "pybind11 wrapper for testpy class"; py::class_<Test>(m, "Test") .def(py::init<>(), "Constructor for the Test class") .def("test", [](Test& self, std::function<void(int&)> callback) { //... ... auto cb = [callback](int&d){ callback(d); }; int i = 8888; cb(i); PyCBWrapper::py_cb = callback; // return self.test(cb); self.test(PyCBWrapper::trampoline);
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 2:23:51

FRSM V6 Dense MoE vs Transformer — 全维度技术报告

核心结论 FRSM V6 Dense MoE 训练速度慢于 Transformer(同结构下 3.6x),但推理 O(1)、长序列显存恒定、总成本在推理部署场景下更优。它不是 Transformer 的替代品,而是特定场景下的更好选择。一、FRSM 架构概述 FRSM(Fast Recurrent State Machine)是一个多尺度内容门控状态机…

作者头像 李华
网站建设 2026/6/26 2:22:23

MuleSoft+LangChain企业级AI编排实战:构建安全可控的AI调度层

1. 项目概述&#xff1a;当企业级集成遇上大模型&#xff0c;谁在真正调度AI&#xff1f;我在做企业级AI落地咨询的这十年里&#xff0c;见过太多团队把LLM当成万能胶——往CRM里塞一个API密钥&#xff0c;调个OpenAI接口&#xff0c;就号称“上线了智能销售助手”。结果呢&…

作者头像 李华
网站建设 2026/6/26 2:17:37

实战Demo——Hello Mcp

文章目录Exercise : Hello MCP基本信息学习目标前置准备需求规格步骤指引期望输出提示扩展挑战My AnswerExercise : Hello MCP 基本信息 项目内容难度⭐预估时间1-2 小时核心技能JSON-RPC 2.0, stdin/stdout 传输, nlohmann/json产出物一个可运行的最简 MCP 服务器 (约 100 行…

作者头像 李华
网站建设 2026/6/26 2:15:23

深圳设备机箱机柜生产厂家:支持非标定制加工

在深圳&#xff0c;众多行业对设备机箱机柜的需求日益增长&#xff0c;而且越来越多的企业需要非标定制加工服务。在这样的市场背景下&#xff0c;选择一家合适的生产厂家至关重要。深圳钣金加工厂家找机汇五金&#xff0c;它就是一家在机箱机柜定制等领域表现出色的企业。行业…

作者头像 李华
网站建设 2026/6/26 2:10:43

ROS C++回调机制与Spinning原理深度解析

1. 项目概述&#xff1a;为什么Callbacks和Spinning是ROS C开发的“呼吸节奏” 刚接触ROS C编程的人&#xff0c;十有八九会在 ros::spin() 这行代码前卡住两小时——程序跑起来没反应&#xff0c;话题不回调&#xff0c;日志不打印&#xff0c; rqt_graph 里节点连着线却…

作者头像 李华