Thrift Python版本中文传输问题和解决

前段时间项目内用到了Thrift封装C/S交换数据,这东西好处自然是每个平台都有相应的框架实现,客户端不用关心具体逻辑,有了.thrift定义后直接调接口(作为软饭,个人觉得用起来没有WCF舒服,这么好的东西微软不下力气推广啊)。

Java和C++写的Server都顺利调试通过,卡在Python的一个实现出现的问题上了。好吧,先不要问为什么一定要用Python做Server,这涉及到项目内的一些糟事,略过。具体问题表现是:自己定义的object,两个string field只有一个可以成功赋值。如果给其中一个赋值后,另外一个一定是空字符串。

尝试过各种情况,发现中文出现的时候复现。在Python中提到中文问题,那么自然先想到str、unicode这两个类型在搞鬼。算幸运的是,在盲目调试了一天之后,找到了好用的解决方案。文中的解决方法好用,但需要修改thrift本身代码。对于部署在实际服务器上的服务来说,可能不太适用,所以可以这样做:

?View Code PYTHON
class MyTBinaryProtocol(TBinaryProtocol.TBinaryProtocol):
    def __init__(self, trans, strictRead=False, strictWrite=True):
        TBinaryProtocol.TBinaryProtocol.__init__(self, trans, strictRead, strictWrite)
 
    def writeString(self, str):
        if type(str) is unicode:
            str = str.encode('utf-8')
        self.writeI32(len(str))
        self.trans.write(str)
 
class MyTBinaryProtocolFactory(TBinaryProtocol.TBinaryProtocolFactory):
    def __init__(self, strictRead=False, strictWrite=True):
        TBinaryProtocol.TBinaryProtocolFactory.__init__(self, strictRead, strictWrite)
 
    def getProtocol(self, trans):
        prot = MyTBinaryProtocol(trans, self.strictRead, self.strictWrite)
        return prot

然后启动Server的部分修改为:

?View Code PYTHON
pfactory = MyTBinaryProtocolFactory()

C++的类型转换重载(type casting overriding)

最近项目里面用了mysqlpp,注意到query对象在获取查询结果时候的store()方法返回了StoreQueryResult,虽然它不是指针但是可以这样用:

if (StoreQueryResult queryResult = query.store()) {...

一开始以为写错了——而且就算是运算符重载,不也只能用“!”么。查了一下相关的文档,原来这玩意是类型转换重载。

operator关键字后面的类型任意,甚至可以是void*,显而易见在这种情况下需要这么做:

private:
    bool _isValid;
public:
    operator void*() const { return _isValid ? this : nullptr; }

这样一来也可以达到上面type casting的效果。

但这几种方式或多或少都存在问题,所以The Safe Bool Idiom介绍了另外一种方式:嵌套类/安全类型转换。可以看到mysqlpp用的也是这种方式。文章最后还利用模板创建了支持多类型的版本。

这个故事告诉我们,C++的细节太多了。

p.s:想到了C#的nullable type。

在C# 2.0之前,类似int这种值类型是不能和null比较的,所以总需要提供一个getter或者其他什么method进行bool判断。有了nullable type后,就可以用int? x = 0; 这样的语法创建可和null比较的值类型对象。

那个问号其实只是语法糖,真正实现功能的是System.Nullable。嗯,到了这里就和之前看到的reusable version of type casting很像了吧。用ILSpy或者其他什么工具打开mscorlib.dll,可以看到Nullable在实现上是这样(省略一些细节):

namespace System
{
	[TypeDependency("System.Collections.Generic.NullableComparer`1"), TypeDependency("System.Collections.Generic.NullableEqualityComparer`1")]
	[Serializable]
	public struct Nullable<T> where T : struct
	{
		private bool hasValue;
		internal T value;
		public T Value
		{
			[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
			get
			{
				if (!this.HasValue)
				{
					ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_NoValue);
				}
				return this.value;
			}
		}
		[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
		public Nullable(T value)
		{
			this.value = value;
			this.hasValue = true;
		}
		[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
		public static implicit operator T?(T value)
		{
			return new T?(value);
		}
		public static explicit operator T(T? value)
		{
			return value.Value;
		}
	}
}

说白了还是privaate flag + getter + conversion method。最后两个方法,就是C#版本的type casting overriding。

用于动态layout的一个工具类

最近需要实现一个类似Google浏览器和Google搜索在Android系统上面的动态ActionBar效果,即随着拖动内容部分的动作,ActionBar部分的位置和显隐也要变化。

开始直接在一个View上实现了,结果发现又一个View要用。代码洁癖犯了,干脆直接提出来做个通用的。

支持上下左右四个方向的显隐,不过一个控件要对应一个DynamicLayouter。代码扔到gist了,懒得专门建一个repository。

用法类似于:

// 初始化,toolBar就是需要动态显隐的控件
mDynamicLayouter = new DynamicLayouter(toolBar, Orientation.BOTTOM);
 
// 在带有scrollbar的控件上设置一个touch listener用于获取拖动事件
        lstContent.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                mDynamicLayouter.feedMotionEvent(event);
                return false;
            }
        });

反射和泛型delegate引起的Windows Phone App奇怪异常

昨天因为一个bug拖到晚上1点才睡——还没解决。今天又逐行debug才发现问题所在,这个因为反射的引起的crash实在是太熬人了,记录一下或许能帮到其他人。

先说一下场景和上下文。之前Windows 8项目中需要用到一个解析数据到同一个基类派生出来类型的聚合类,其中有大量的解析函数,根据期望类型调用不同的parser。如果用if else写会很挫,于是我用了dictionary将type和function对应起来:

?View Code CSHARP
private Dictionary<Type, Func<string, BaseResult>> _parseFunctions;

增加方法时候,由于懒得一个个typeof硬编码类型,于是用反射这样映射进去:

?View Code CSHARP
        protected void AddParseFunction<T>(Func<string, T> func) where T : BaseResult {
            _parseFunctions.Add(func.GetType().GetMethod("Invoke").ReturnType, func);
        }

除了GetMethod硬编码之外,用的时候十分轻松愉快。现在想在Windows Phone平台上实现相同的效果,结果上来就发现Func不好用:.net 4.5版本支持参数协变和返回值反变:

?View Code CSHARP
public delegate TResult Func<in T, out TResult>(
	T arg
)

但3.5版本不支持。所以需要改写成delegate:

?View Code CSHARP
public delegate T ParserFunctionDelegate<out T>( string inputString ) where T : BaseResult;

代码虽然是WP7.1的,但在WP8上跑得顺畅——注意问题来了,这段代码可以在WP7.1上编译通过——之前因为想到反正要做两个平台兼容的App,所以一直用WP8模拟器/设备测试。昨晚在WP7平台上调试发现坏菜,启动后就各种异常出现,而且直接跑到UnhandledException,却没有任何详细异常信息和调用栈,最后出错地点只能看到是调用封装类的xaml.cs或者.cs里面。碰到的错误都是“Cannot load type xxx from assembly yyy”,具体异常出现过:

  • InvalidOperationException
  • MethodNotFoundException
  • TargetInvocationException
  • ……

根据具体调用地点出现了乱七八糟各种平时没用过的异常。但基本上一眼看去就可以确定是反射相关问题,可惜我没顺着相关调用代码一点点debug,而是傻逼呵呵地继续怀疑是UI层或者工程重组时候GAC缓存出错,这个上面浪费了大量时间,也是为什么到半夜才睡的原因。

那么问题在哪呢?首先不是泛型delegate本身的错,因为我直接用上面GetMethod代码去拿ReturnType没有引发异常。那么肯定是协变的问题,去掉了delegate的out关键字后……先是提示类型错误,因为dictionary里面存储的是基类类型delegate,但我们传进去的是where限定的子类类型delegate,去掉了协变支持后,返回值便没法存入dictionary中。由此导致AddParseFunction不能接受泛型delegate,添加进去的parser functions也必须返回BaseResult类型而非子类型了,由此函数只有增加一个type参数(这也是最开始要避免的)手动传进来,没法再用反射拿到类型。

这样修改完成后终于搞定了棘手的crash,但代码变得异常寒碜。要说代码必须写得烂一些还能忍,那么这个完全无法从crash调用栈上获取任何信息的问题就不能忍了。如果是因为协变参数出问题,你好歹定位到相应错误行上呢,要么干脆就编译不过,省得个大圈子修改错误。

又一个python围棋工具库

既然已经有了pytools和其他成熟的python实现的围棋工具类库,为什么还要写?看起来是犯了重复发明轮子(reinventing the wheel)的大忌,但其实这里面存在软件工程(你看,又要婊软件工程这个概念了,有兴趣看此方面文章的请参考《软件工艺》一书)理解中的一个迷思:重新实现一个功能库就是重复发明轮子。在很多情况下,这两者的概念并不等同。另外,“重复发明轮子”在我看来本身就是好习惯,试想如果没有持续发明新的轮子,原始的木造车轮如何适应各种路面,我们也用不上成本持续降低但各方面性能都在提高的车轮。扩展开来说,正是由于不断有新车轮出现,人类社会才能一直进步不是吗。

“重复发明方形轮子”才是问题所在,杜绝无用的创造,不管它有多华丽。pongba老师的《锤子和钉子》一文中提到过埋头苦干,陶醉在自我世界中的框架设计者,看完后觉得浑身中枪。这便是方轮子的例子,“JOJO,我不重复发明方轮子了啊!”

大牛vczh曾经提到过他心目中学习开发知识的一个最佳流程,深以为然:

  1. 明确需求后,不管最好做法是什么先用自己的想法实现一个能用的
  2. 学习相关理论,反思自己想法中的问题
  3. 参考别人成熟、高效的实现进行修改
  4. 重写/重构自己的版本

等等,这篇blog似乎跑题严重,继续说这个工具库,地址在https://github.com/arakuma/py_golibs。考虑中分为三部分:

  • 棋谱文件(sgf)解析
  • 围棋相关的模型、规则定义
  • 网络对弈支持(gtp

这个列表也是整个库自底向上的层次。目前进度为sgf部分搞定,支持所有SGF FF[4]的定义。围棋实体部分完成了基本模型定义、对于sgf信息到上层事件的转换(将所有sgf node转换成Action类)、写了一个基本的演示用棋盘类;待完成的则是规则定义和gtp实现。

另外事件转换部分还有些问题,我之前认为以“添加棋子(AB/AW)”、“走一手棋(B/W)”为基本单元比较合适,但写着写着发现很多重要属性实际上不在这几个属性内,而是作为单独的属性出现,例如盘面价值(V)、图示(FG)等等,在不想将Action直接返回给上层使用的情况下,这些信息应该怎么通知还得想想。

对了,演示用的棋盘,本来计划用extended ascii的字符,期待中是这样:

 a b c d e f g h i
a┌─┬─┬─┬─┬─┬─┬─┬─┐a
b├─┼─┼─┼─┼─┼─┼─┼─┤b 
c├─┼─•─┼─┼─┼─☻─┼─┤c
d├─┼─☺─┼─┼─☻─☺─☻─┤d
e├─┼─┼─┼─☻─┼─☺─☺─┤e
f├─┼─☻─┼─┼─┼─┼─┼─┤f
g├─┼─•─┼─┼─☺─•─┼─┤g
h├─┼─┼─┼─┼─┼─┼─┼─┤h
i└─┴─┴─┴─┴─┴─┴─┴─┘i
 a b c d e f g h i

但这些字符在python中输出后基本都变成乱码了,不得已暂时改为下面这样。不过修改过程中顺手把之前懒得做的棋盘margin/padding/zooming都实现了。

SGF recorded with SGFC 1.16 by Arno Hollosi
Event 21st Meijin @ Nihon Ki-in on 1996-10-18,19
Black: Takemiya Masaki (9 dan), White Cho Chikun (9 dan)

   a b c d e f g h i j k l m n o p q r s   

a  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  a
b  +-+-+-+-+-+-+-+-+-+-+-+-+-+-B-W-W-+-+  b
c  +-+-+-+-+-+-+-+-+-+-+-+-+-W-+-B-W-+-+  c
d  +-+-+-W-+-+-+-+-+-*-+-+-+-+-+-B-W-+-+  d
e  +-+-+-+-+-+-+-+-+-+-+-+-+-+-B-+-B-W-+  e
f  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-B-W-+  f
g  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-B-+-+  g
h  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  h
i  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  i
j  +-+-+-*-+-+-+-+-+-*-+-+-+-+-+-B-+-+-+  j
k  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  k
l  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  l
m  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  m
n  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  n
o  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  o
p  +-+-+-W-+-+-+-+-+-*-+-+-+-+-+-B-+-+-+  p
q  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  q
r  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  r
s  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  s

   a b c d e f g h i j k l m n o p q r s