最近从事了一些通过 boost 在 C++ 中调用 Python 脚本的工作。折腾下来有一些收获,记录一下,也许也可以帮到有些人。
对于习惯了 Python 的 C++ 程序员而言,boost::python::object 这个东西是一个巨大的诱惑。它让你几乎可以像在 Python 里面那样使用弱类型的变量,同时还支持数组和字典之类的复杂变量类型,并且还支持嵌套。这简直就是一个万能变量类型,有了它,常见的需求几乎都可以满足了。
而且它还快,还容易用。它其实是 PyObject* 的一个封装,也就是说 PyObject* 其实功能也一样,但是没它容易使用。JsonCPP 里面的 Json::Value 也可以「万能」,但性能与 boost::python::object 相差颇多。这真的是一个巨大的诱惑。
但是在这里,我要给大多数 C++ 程序员泼一盆冷水。在单线程下,这个梦想可能真的是事实:但是在多线程下,boost::python::object 就是一个坑!
boost::python::object 为什么快?因为它基于 PyObject*,具有引用计数,所以赋值才飞快,浅拷贝嘛。但是引用计数的问题就是线程不安全。
当然,光是引用计数本身不会导致线程安全性问题,导致问题的是引用计数带来的临界区对象引用问题,而归根结底,是 Python C API 的「并发问题不归我管」的思路。boost::python 的封装机制使得对象引用不好控制,也不太可见。想法是好的:使用者不需要关心这些。但现实就很无奈了。
所以,尽管很不甘心,关于万能变量类型的实现,我还是老老实实地用回了 Json::Value。
要从 boost::python::object 转换成 C/C++ 原生变量类型,一般用 boost::python::extract<T>。转出来的对象调 check() 就可以确定是不是正确的类型。转 boost::python::list 或 boost::python::dict 也是一样。
有一点需要特别注意的是:用 boost::python::extract<bool> 的时候,很可能得不到你预想的结果。你会发现,int 也被转成 bool 了(check() 返回 true),反过来也一样。这个不是 Bug,是 boost::python 故意这样搞的。BOOST_PYTHON_BOOL_INT_STRICT 宏可以解决这个问题(必须改动 boost::python 源代码并重新编译,因为相关代码在 cpp 里面不在头文件里面),它使得 int 和 bool 将被严格区分。
但是 boost::python 之所以把 int 和 bool 混起来用也不是没有道理的。这样使得 MFC 里面的那些 BOOL(不是 bool)类型的函数形参(和返回值)可以直通 Python,封装起控件来尤其方便。如果你设了 BOOST_PYTHON_BOOL_INT_STRICT,就不能这样干了,必须显式转换。左是一刀,右也是一刀。你们自己掂量吧。
反正我是没有去加 BOOST_PYTHON_BOOL_INT_STRICT,而是自己通过 _stricmp(value.ptr()->ob_type->tp_name, "bool") 搞定的。
一、关于万能变量类型
对于习惯了 Python 的 C++ 程序员而言,boost::python::object 这个东西是一个巨大的诱惑。它让你几乎可以像在 Python 里面那样使用弱类型的变量,同时还支持数组和字典之类的复杂变量类型,并且还支持嵌套。这简直就是一个万能变量类型,有了它,常见的需求几乎都可以满足了。
而且它还快,还容易用。它其实是 PyObject* 的一个封装,也就是说 PyObject* 其实功能也一样,但是没它容易使用。JsonCPP 里面的 Json::Value 也可以「万能」,但性能与 boost::python::object 相差颇多。这真的是一个巨大的诱惑。
但是在这里,我要给大多数 C++ 程序员泼一盆冷水。在单线程下,这个梦想可能真的是事实:但是在多线程下,boost::python::object 就是一个坑!
boost::python::object 为什么快?因为它基于 PyObject*,具有引用计数,所以赋值才飞快,浅拷贝嘛。但是引用计数的问题就是线程不安全。
当然,光是引用计数本身不会导致线程安全性问题,导致问题的是引用计数带来的临界区对象引用问题,而归根结底,是 Python C API 的「并发问题不归我管」的思路。boost::python 的封装机制使得对象引用不好控制,也不太可见。想法是好的:使用者不需要关心这些。但现实就很无奈了。
所以,尽管很不甘心,关于万能变量类型的实现,我还是老老实实地用回了 Json::Value。
二、类型转换与判断
要从 boost::python::object 转换成 C/C++ 原生变量类型,一般用 boost::python::extract<T>。转出来的对象调 check() 就可以确定是不是正确的类型。转 boost::python::list 或 boost::python::dict 也是一样。
有一点需要特别注意的是:用 boost::python::extract<bool> 的时候,很可能得不到你预想的结果。你会发现,int 也被转成 bool 了(check() 返回 true),反过来也一样。这个不是 Bug,是 boost::python 故意这样搞的。BOOST_PYTHON_BOOL_INT_STRICT 宏可以解决这个问题(必须改动 boost::python 源代码并重新编译,因为相关代码在 cpp 里面不在头文件里面),它使得 int 和 bool 将被严格区分。
但是 boost::python 之所以把 int 和 bool 混起来用也不是没有道理的。这样使得 MFC 里面的那些 BOOL(不是 bool)类型的函数形参(和返回值)可以直通 Python,封装起控件来尤其方便。如果你设了 BOOST_PYTHON_BOOL_INT_STRICT,就不能这样干了,必须显式转换。左是一刀,右也是一刀。你们自己掂量吧。
反正我是没有去加 BOOST_PYTHON_BOOL_INT_STRICT,而是自己通过 _stricmp(value.ptr()->ob_type->tp_name, "bool") 搞定的。
三、清空 boost::python::list
用惯 STL 刚开始用 boost::python 的人可能会头大——怎么 dict 有 clear() 但 list 没有?
答案很简单,因为 Python C API 没提供,所以 boost::python 就没有。
说到底,boost::python 就是对 Python C API 的一个封装而已。你如果去看 boost::python 的源代码,甚至会发现里面不少的「成员函数」其实是跑去执行了一句 Python 脚本。不了解细节的 C++ 程序员很容易在这种地方栽跟头,所以我觉得 boost::python 绝对不会是一个终极形态。
要想清空数组,并不是完全没有办法。我找到的一个办法是 boost::python::delitem(list, boost::python::slice())。并发调用时记得用本文最后一节的办法加锁。但是我是真的不建议在多线程环境下用 boost::python,太多坑了。如果实在要用,在完成了 Python 脚本调用,把数据转换好之后,就赶紧离开 boost::python 区域吧。
四、boost::python::object 深拷贝
boost::python::object 默认的赋值操作都是作浅拷贝,极快。然而,有的时候需要深拷贝怎么办呢?
简单数据类型直接 extract 出来就是深拷贝(传值)了。会有问题的仅仅是复杂数据结构,具体地说,list 和 dict。
dict 又是有提供 copy(),理由还是——Python 里面的 dict 有 copy(),而 list 就没有。我尝试过最简单的办法,就是把一个 list 放到一个 dict 里面去 copy() 完再拿出来用。这样肯定可以达到目的。至于有没有更好的办法呢?我反正是自那之后就弃坑了,你们去研究吧。
五、多线程并发加锁
C++ 调 Python 的时候的加锁是一个不小的话题。不过这方面中文资料也算不少,有兴趣可以去搜来研究。我这里就简单地说一下跟 boost::python 相关的部分。
加锁是用 PyGILState_STATE,像这样:
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();
…… // 调用Python脚本
PyGILState_Release(gstate);
这种例子网上很多,就不细说了。
关键在于,我前面也提到了,boost::python 里面很多看起来是 object 的成员函数的东西,其实都是对 Python 脚本的调用,所以都得加锁。
事实上,只要你用到了 boost::python 的地方,都得加锁。不管是 module 和 call 这种看起来就跟「调用」二字相关的,还是在对 boost::python::object 作数据处理,看起来人畜无害的,都得加锁,无论读 / 写,否则你就等着 Crash 吧,特别是服务端程序。
这也就是我一再警告避免在多线程下使用 boost::python 的原因。当然,我这边是不得不用,只能小心翼翼,如履薄冰,然后尽量控制不要让菜鸟程序员去写这种代码。由此可见,隐藏了实现细节并非全是好事,也并非全是对新手有益。