善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。

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

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

2016-12-04

记一次恶劣的淘宝购物经历

我现在已经基本不用淘宝了,不过老婆还是喜欢在上面逛,也因此有了这次很恶劣的购物经历。
东西是一个小孩子用的书包。很便宜,29 元 RMB。我其实一直跟老婆在说「便宜没好货」,但是便宜的诱惑的确很大。买到手的过程很顺利,2016 年 11 月 05 日下单,11 月 07 日就物流签收。
东西是帮别人买的,所以一开始没有怎么放在心上。过了几天,老婆问对方书包怎么样,别人明说:「质量不好,第一天背带就开线了。」这怎么行?于是赶紧联系卖家要退货。
看到图片我其实并不奇怪。29 块钱的东西质量能好到哪里去?卖家还说去修,然而这种质量我觉得还是直接退掉麻烦比较少。在我看来其实当初就不应该买这个东西。卖家倒也爽快,因为是质量问题,图片显示得也很清楚,退就退吧。


到这里都还好,不过两个麻烦事情已经露出端倪了:
  1. 卖家要求买家「包装好」,这个本身不算不正常,但我知道会有后招。
  2. 卖家言明退货只负担 10 元运费。
这两件事情,注定会导致买家的不快。
「脏了就拒收」,这种话大概也只有淘宝卖家讲得出来。如果你这个产品有严重的质量问题,一用就碎成渣了,那是不是就不能退货了呢?
事实上,卖家想要让商品能够二次销售,对于买家而言这是个可以理解的事情。但是用这种方式讲出来,买家会有什么感受大家都很清楚。之前只是觉得你东西不好,现在就觉得你人不好了。
对于退货的时候产生的运费问题,淘宝的政策是「建议卖家和买家协商解决」。也许有的人觉得这很合理,毕竟每个个案的情况都各不相同嘛。然而,我对淘宝最大的厌恶之处,就在于这个「在淘宝上买点什么都要开『汪汪』协商」。明知退货运费这种事情卖家和买家是一定会有矛盾,还要他们「协商解决」,其实就是自己不想解决嘛。
按照淘宝的说法,以及一般人的常识理解,因为质量问题导致的退货,运费肯定是应该由卖家负担的。我也不太清楚这个卖家理直气壮的原因,也许是「业界潜规则」,也许是「小本经营」。具体原因也许都可以商量讨论,但是买家会有什么感受,大家仍然会很清楚。


然后高潮就来了,卖家开始耍流氓了。自从淘宝上的退货流程走完之后,直接不理睬买家。也不拖黑,也不对骂,就是不出声。这个时候淘宝的另一个坑跳出来了。
对不起,你不能投诉卖家,因为已经过了时间。
淘宝规定,「交易成功」15 天以后,就不能再投诉卖家了。我也搞不清楚这个 15 天到底合理不合理,但是显然,卖家只要把时间拖过这 15 天,谁都拿它没办法了。偏偏我和我太太之前都不知道这个「15 天」的事情。
而且,我太太之前刚收到货时已经手快把好评先给了。

事情呢,我大概已经说完了。淘宝既然不提供渠道,我个人要救济自然是很困难。除了去追加一个评论,上传几张图片,以及写这篇部落格,我还能做的最多是去工商局闹腾。这方面我完全不抱什么希望。
至于这个淘宝卖家呢?情感上我当然希望它倒闭关张大吉。不过,淘宝上也有好卖家,这个也是事实。林子大了什么鸟都有,不可能人人都是垃圾。我自然是不会忍气吞声。该做的事情,我已经做了,这也是为什么我不会对图片打码的原因。该付的代价,我们也已经付了。至于剩下的事情,就让它顺其自然吧。15 块钱差不多够买一盒感冒药的。

2016-09-13

自定义域名又回来啦

本来以为,自定义域名以后就跟我的 Blog 没什么关系了。但今天想起来,GoDaddy 不是 west263。共产党可以不准国内的 ISP 提供 URL 转发功能,但毕竟还奈何不了 GoDaddy。自定义域名的事情,用 URL 转发就可以解决嘛。

于是,本来的自定义域名,从 CNAME + VirtualHost 变成了 URL Forward。虽然据说在搜索引擎的打分上会有「惩罚」,但这些不是我关心的。有总比没有好。而且,URL Forward 可以把 HTTP 的流量导到 HTTPS 上。这对于目前这个全局 SSL 的时代本来就是必然的趋势。这样一来,反倒是比之前的效果还要好了。

反正都要翻墙,对吧?这年头干啥不得翻墙?

2016-08-24

与自定义域名说再见了

很长一段时间以内,我都把我托管在 Blogger 上的这个 blog,绑定上了自定义域名 blog.superliufa.com。这样做的好处很明显——可以实现免翻墙。通过非官方的 GHS,即使用 HTTP 也不会被 GFW 阻挡。
只可惜我上一个 blog「歇业」太久,所以即使绑上了自定义域名,也已经不能为我带来多少「老」的流量。并且,随着时间的推移,自定义域名的缺点越来越明显:用了自定义域名,就不能享受 Google 提供的 HTTPS 待遇。
而所谓的「免翻墙」好处,对于有一些来自 Blogger / Google 的公用资源还是无能为力。比如有的读者就反映,说页面要等好久才能打开,而且布局有问题。这些其实都是 GFW 捣的鬼。

我现在也不想去跟 GFW 玩「捉猫猫」的游戏了。反正不翻墙的人我一不指望二也不希望他们看到我的 blog,索性就扔掉自定义域名,用 blogspot.com 的域名直接来访问我的托管 blog 吧。愿意装睡的,就算你扇它耳光也不会醒,愿意当个人的,你拿枪指着他还是不会征服他的心。

那么,就让一切都回归它的本来面目吧。

2015-12-13

最近看过的书,简评几则


  • 《战争史》(基根):没有想象中的宏大,是更为学术性的书。主要针对战争本身进行研究,阅读起来略微有一些无趣。作者跟克劳塞维茨算是杠上了。
  • 《毛毛星球》(约翰.斯卡尔齐):一本小品式的「科幻」。介绍上说是硬科幻,但里面的法律内容比科学和幻想加起来还要多。就像很多读者说的那样,作者的确「很会讲故事」,是很「温情」的故事,我喜欢。只可惜有「毛毛」死去,看到最后虽然正义感得到了满足,但还是轻松不起来。
  • 《锁定目标》(汤姆.克兰西):一开篇就是火爆的动作场面,后面也是高潮不断。人基本上还是那班人,但比《虎牙》刺激多了。一本不错的克兰西作品。
  • 《悖论:破解科学史上最复杂的 9 大谜团》(吉姆.艾尔.哈利利):还算不错的入门级科普,看过之后应该能对物理学上的一些重难点有所了解,而且又不至于被吓倒。留给儿子以后看吧。
  • 《羊毛战记》(休.豪伊):不错的反乌托邦科幻,和 Fallout 系列的感觉很接近。我觉得用Fallout1 / 2 的引擎很容易就能根据这书的剧情改编出一个游戏来。144 层的 Vault,想想也很大。不过从后期透露的信息看来,虽然深度很大,但每层的面积并不是很大。作者用了一种舞台剧一样的写法,情节冲突也是一波一波地。这种写法我不是太喜欢,不过还是很期待下一部的续集《尘埃记》。
  • 《虎牙》(汤姆.克兰西):比起《锁定目标》来,这本的节奏就太过平稳了。个人认为唯一称得上动作戏的,就只有 Mall 里面那场枪战而已。不过这本书主要是为了交待「校园」的来历,大概是过渡性的作品吧。
  • 《虚无的十字架》(东野圭吾):本以为是本格派的作品,结果读完却几乎以为是松本清张写的。我对他了解还不是太深。据说是 2014 年的作品,难道东野圭吾在转型成社会派?作为日式推理小说并不是太好看,看起来被亚马逊放进特价书目里面并不是毫无理由。线索太散乱,布局感又太明显,立意很高,但显然对这个话题又无力下定论。何况是个很沉重的故事,看起来实在是太不轻松了。

2015-12-02

完了完了,今年又要完了

数了数,从去年的年末座谈会到现在,今年(2015 年)一共只写了八篇 Blog。平均一个月还不到一篇,我真的是太过于懒惰了。

其实,因为不见得全是懒惰。一方面,今年公司上马新项目,我是主程,工作时间特别忙,已经很难再抽出空来写 Blog 了。另外一方面,今年家里出的状况也特别的多,私人时间也很难抽得出来。曾经寄希望于搬家之后每天上下班时间能够在公共交通上完成这个任务,但现在看起来也不太适合。总之吧,客观原因还是挺多的。

争取这个月多写写吧。眼下貌似没有那么忙了。不然月底竜堂家四兄弟又要来找我的麻烦了。

2015-08-27

也谈 Shadowsocks 的「关闭」风波

之前已经在 Google+ 上零星地发表过一些看法。本文总的来说可以看作是对这些零星看法的一个汇总和系统化。已经看过的朋友可以无视。
另外,标题之所以对「关闭」二字加引号,是因为这种开源项目从严格意义上来说没有什么关闭不关闭的。项目已经被许多人 fork 过,Release 也都还在,就算最新源代码被删了又怎么样呢?也就只是做给衙门看看吧。

言归正传……

这次 Shadowsocks 作者被盖世太保警告,然后 GitHub 上的项目代码被删,在 IT 界造成了不小的震动。除了愤怒与哀叹,还有以下几个问题 / 观点也被抛出:
  • 认为没什么人对 Shadowsocks 作贡献。
  • 把 GPL 开源转闭源违反协议。
说起来,这两个问题,其实是开源项目的问题,与 GFW 无关。原作者在删代码之前,也曾言辞激烈地作过批评。现在的这两个看法,基本上也延续了原作者的思路。不过,我倒是有一些不同的看法。

首先,要说说对开源项目的贡献这种事情。
在程序员(开发人员)看来,对开源项目作贡献,通常意义上意味着需要修改 / 提交代码。至少,也得明确地指出 Bug 的所在,可能还得需要有对应的解决方案或手段。这样,才能算是对项目作出了贡献。
但是,在我看来,那些只是默默地使用着 Shadowsocks,用来配合自己的 VPS 完成翻墙的人,他们都对这个项目有贡献。如果测试出了错误,当然应该指出。但是还有更多没有测试出错误,没有遇到错误的使用者,他们同样可以被视为是测试人员之一。一个庞大的用户基数,是保障一个软件变得越来越成熟的基础。如果没有什么人用,即使自己或一个小团队测试并修正了很多 Bug,也不能确定它是不是真的足够稳定耐用。如果有很多人使用,但是没有什么人报告 Bug,那只是说明了作者水平不错,并不能说明别人不贡献。
那么,如果一个开源项目做得不好,差劲,问题多多。别人用了之后拔腿就跑,还是不肯修改或报告错误,这是不是就能说明「天朝那么多牛 B 哄哄的程序员不肯作贡献」了呢?
答案还是「否」。开源项目,是有自己的生态系统的。如果一个项目大家都跑,那它就应该消亡。用脚投票,也是一种贡献,不能因此责怪别人「不作贡献」。只有完全不用,这就真的是「不作贡献」,但你难道把这种事情怪到卖菜小贩头上?
在我看来,之所以 Shadowsocks 原作者会觉得没什么人帮他改代码,恰恰在于他把这个项目做得太好用了。大家用起来都很舒服,自然不会有什么人想要去改。会想去改的人,就更可能是那些「爱折腾」却又不具备相关能力的人,所以作者才会觉得有压力。
换句话说,对于真正有能力改动代码的人,以及要求不高的人而言。Shadowsocks 一开始就已经足够好了。这就过滤掉了一大群人。「劣币驱逐良币」这个比喻用在这里可能不是很恰当,不过我相信大家懂我的意思。对于一个用户数够大的软件而言,沉默的人占大多数,这是一个好事。原作者可能还没意识到这个情况。

GPL 转闭源这个的确违反了许可协议,这没有什么话好说。我是在想另外一件事情:GPL 到底保护了什么?
GPL 要求后继项目也必须公开源代码,并且也必须采用 GPL。这保证了项目代码会永远开源下去。但问题是,如果你采用 GPL 许可证,你是想保护你的思想还是你的代码?
由于要求比较苛刻,可能会有更多的人不愿意从 GPL 项目中 fork,特别是那些想商业化的公司、团队或个人。这样一来,你的代码还是原来的代码,没有人来夺走,但你的代码其实是你思想的结晶,否则它们只是一堆敲击键盘而成的无意义的文字而已。如果没有人愿意 fork,那你的思想也就无法被继承并发扬。
如果我想让自己的思想发扬光大的话,我会选择让自己的代码进入 Public Domain。并且,只提供思路及核心代码,包括一个最简实现。至于项目最后包装成啥样,有兴趣的就自己动手。改动代码也好,只是加个壳也好,做成商业应用去卖钱也好,免费分发也罢,都可以。
本来推墙这种事情,就是需要五花八门,三教九流一齐上。你管它是志愿军还是雇佣兵。反倒是如果仅此一种工具,就很容易被 GFW 对付。提供一种可行的思路,比做出一个完美的实用产品,可能更合适一点。
这样的好处就是,原作者的压力小了,甚至可以不需要去维护。不需要去考虑产品化,不需要在 UI 上费脑筋,不需要为了不同用户的一些无关大局的小需求改来改去。这些事情,如果需要,就自然有人会去做。作为原作者,可以把精力放在更有意义的事情上——如果你有那个能力的话。

2015-08-19

蹊跷的 LNK1104 错误

最近在给公司开发下一代产品,所以比较忙,也没有太多的事情写 Blog。不过昨天在技术上遇到一件不大不小的事情,也许值得拿出来说一下。

这个下一代产品是从头全新开发的,为此我提供了一些基础库的代码,给到项目组的其它 Project 使用。其中有一个是用的动态链接库的形式。为了方便其它 Project 使用,我把如下的预编译宏写在了头文件中:
#pragma comment(lib, "XEngine")
这样,所有需要包含这个头文件的代码,就不需要自己去搞清楚到底要链接哪个 lib 文件了。

但是,同事在拿到代码做全新编译的时候,出现了 LNK1104 错误。Cannot Open File XEngine.lib。同事当然一头雾水,Baidu 上搜了一下之后,一头埋进去检查硬盘坏道去了。

我这边一开始没有问题,当把发布目录中的 XEngine.lib 删除了之后也出现了同样的问题。源代码对编译结果产生了依赖,这当然是不对的。问题当然出在那句预编译宏上,只不过代码的编写时间过去有点久了,我一时没想起来。
正好这个 DLL 具有导出函数,因此 VC 自动生成了相关的代码,只需要把前面的预编译宏放在这里面就可以了:
#ifdef XENGINE_EXPORTS
#define XENGINE_API __declspec(dllexport)
#else
#define XENGINE_API __declspec(dllimport)
#pragma comment(lib, "XEngine")
#endif
这样就解决这个问题了。如果没有这些自动生成的代码可以利用,也可以参考着自己写一个。

2015-06-04

按更新时间同步单个文件的简易方案

我曾经抱怨过,有个很简单的单个文件同步需求,却居然难以找到有软件能够解决。也许是需求太过于简单,反而没人去考虑。连 Allway Sync 这么好的东西,也做不到。于是一直都是人肉解决。

最近有点厌倦了每次 Copy 来 Copy 去的事情。而且中间也不是没有出过错。一出错就会丢一次的数据。索性简单地写了个 bat 批处理脚本来解决这种事情:
@echo off
xcopy D:\仓库\文件.txt .\ /D /Y >nul
xcopy .\文件.txt D:\仓库\ /D /Y >nul
运行这个 bat 脚本后,更新时间比较早的那个副本,会被另外一个覆盖掉。接下来只要每次编辑前或编辑后都记得运行一下这个脚本,就可以使两边文件版本一致,不需要再考虑复制方向对不对的问题了。

2015-03-11

在 Windows 上用 x64 的 Python 运行 GoAgent

首先声明:本文适用于至少懂编程,并且爱折腾的人。不一定要很熟悉 Python,但至少应该入过门了。

GoAgent 自带 Python27 运行环境,不过因为本机上也安装了 Python 2.7 x64,所以希望能直接使用我安装的 64 位版本。总结下来,要点有:
  • 无论你是否准备使用 x64 版 Python。GoAgent 与 Python 2.7.9 不兼容,应该安装 Python 2.7.8 版本。
  • 要运行 GoAgent 还需要 gevent 和 pyOpenSSL。可以手动安装,也可以通过 pip 来安装。推荐后者因为省事。
  • Python 2.7.8(无论是否 x64)及更早的版本,没有自带 pip。所以要去下载 get-pip.py,然后用 python 运行这个脚本安装 pip。
    ps: 用 get-pip.py 下载到的 pip.exe 的版本,比 Python 2.7.9 里面带的要新一些。所以就算有自带我也会换掉它。

另一个重点来了:
有些版本的 GoAgent 里面对于 crypt32.dll 的加载代码在 x64 的 Python 中有问题,需要修改。改成像这样就可以了:
crypt32 = ctypes.WinDLL(u'crypt32.dll')
crypt32_handle = crypt32._handle
释放的时候要这样:
ctypes.windll.kernel32.FreeLibrary.argtypes = [HMODULE]
ctypes.windll.kernel32.FreeLibrary(crypt32_handle)
在前面还要:
from ctypes.wintypes import *
否则 x64 下会报错,因为 HMODULE 和 long 的长度在 x64 下不一样了。

我用的 2.1.17 的 GoAgent 中,对于 crypt32.dll 的加载代码还位于 proxy.py 中。最新的 GoAgent 已经移到 proxylib.py 里面了。什么时候移的我没有去考察过。代码改法可能略有不同,不过思路应该是一样的。

折腾好以后。把本机上的 Python.exe 复制一份改名成 Python27.exe。接下来就可以把 GoAgent 自带的 Python27.* 给删掉啦。