2017-02-03

折腾boost::python的一些收获

最近从事了一些通过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")搞定的。

三、清空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作数据处理,看起来人畜无害的,都得加锁,无论读/写,否则你就等着爆吧,特别是服务端程序。

这也就是我一再警告避免在多线程下使用boost::python的原因。当然,我这边是不得不用,只能小心翼翼,如履薄冰,然后尽量控制不要让菜鸟程序员去写这种代码。由此可见,隐藏了实现细节并非全是好事,也并非全是对新手有益。