« December 2008 | Main | February 2009 »

January 15, 2009

出在 recv 上的一个 bug

今天 卡牌对决 新版本发布,几乎都已经更新包放出来了,突然发现了 bug 。那个紧张哦,好在临危不乱 :) 用了半小时仔细阅读代码,找出了 bug 。

由于这次需要部署多个网络接入点,解决网通、电信、教育网互联的问题。我申请了各个网络的多台机器。新申请的机器是跑的 debian/linux ,而不是我们开发时用的 freebsd 。一个以前在 freebsd 上跑的正常的程序,放在 linux 出了问题,一开始让我很不解。

仔细查看后,发现 bug 出在一个 recv 调用上。用 gdb attach 到锁死的进程中,确认果然是一处 recv 调用没有返回。

原来,一旦用阻塞方式去 recv 0 字节长度。比如:

recv(fd,buf,0,MSG_WAITALL);

在 linux 上,系统调用就会一直阻塞而不返回了。

经同事验证,freebsd 上这样做,调用会返回 0 ,并设置一个奇怪的 errno 。但这种行为不会导致程序的行为错误。

当然,这是一个程序实现的 bug ,recv 传长度 0 一种结果未定义的行为。只是我觉得 freebsd 的行为要人性一点,呵呵。


btw, 这两天发现,debian 下默认的 syslogd 不太好用。居然不能像 freebsd 下的那样,配置成按 ident 字符串过滤。怪不得在 linux 下开发的同事没有使用 syslog 输出 log 的习惯。

或者,是我不会用?

January 12, 2009

在不安全的网络环境下安全上网

偶尔,我会在公众场合上网。但是不敢以自己的身份登陆任何网站。这年头,自己家的机器都不安全了(使用非 Windows 平台的除外),哪敢信任不知底细的机器啊。

话说,我是没买过没用过笔记本的,对这个东西比较抵触。之前已经极大的挖掘了 Palm 手机的功能,比如可以用 ssh 登陆远程自己的机器。可以用浏览器访问一些简单的控制界面等等。只要在自己放在公网上的服务器配置好了,问题都不大。记得有一天,我想看一个网站上的图片,但是手机浏览器对那个网站的页面罢工。我是先 ssh 登陆到自己服务器,用 wget 下载下页面,再用 grep 分析 html ,然后 wget 下图片。并用 ImageMagick 缩小,压缩转换格式。最后放到自己的 web server 上,让手机浏览器可以顺利观看的。(顺便节约了许多 GRPS 流量)

这套方案用起来过于繁琐,也有很大的局限性。

昨天睡觉的时候胡思乱想。如果能弄一套通用的东西,或许就可以让我放心的去用别人的机器了。比如出差的时候突然需要上网,光靠手机又不够的时候,可以冲到网吧里暂时用一下。

当然一切技术方案都不是完美的,只能尽可能的做到安全了。另外需要使用方便,不需要安装额外的软件。在大多数可以连接互联网的桌面系统上,最低软件配置应该至少有个浏览器吧,只能打这个的主意了。

为了防止被人在本机之外截走网络包(例如网吧的网关被动了手脚),应该做一个 https 连接的 proxy 。我想会有现成的软件的。

通过 https 的 proxy 访问自己想访问的网站,大多数线路上的安全问题可以放心了。这个 proxy 应该可以帮我保存所有的 cookie 在 proxy server 端,保证不泄露隐私。

第二步,我不能在非自己的机器上输入任何的用户名、密码。因为无法确认键盘是否被监听。不光是键盘输入,其实,无论以任何方式的输入,都不应该让密码存在于陌生的机器内存中。使用 usb key 或许是一个好办法,但是不是所有的机器都有 usb 接口,有的话也不一定允许插入。

所以我想,最好的办法是,把自己的各组密码全部事先在安全的环境放在 proxy server 上。通过 proxy server 转发提交就可以了。如果需要管理的密码太多(比如我,每个地方用的密码全部不一样),就可以为每组密码设置一个自己明白的字符串标识,下拉选择。

第三,这个 https 的 web proxy 系统如何安全登陆呢?

最简单的方法是事先生成一组一次性密码(OTP),用掉一个就销毁一个。出门前抄在本子上,或放在手机里。

btw, OTP 其实是个很廉价且安全的方案,建议游戏里用 :D 比如允许用户在安全环境下索取一组 OTP 自己打印出来放着。然后在自己不放心的环境下使用。这里面会有些设计上的细节,不展开分析了。


嗯,不知道做上面这样一套东西麻不麻烦。是不是有开源的东西改吧改吧,拼拼凑凑就能做出来。 :D

The New C Standard

周末开始读一本书,《The New C Standard》。尚未出版,但是电子版可以自由下载。

牛人写的计算机方面的书,我想有两个极端。要么像高爷爷的 TAOCP ,厚厚的几大本,还在待续;要么如 K&R 的 TCPL ,薄薄一本,躺在床上轻松读完。

之前,我从没想过,以 C 语言为主题的书,可以砖头样的一大本,1600+ 页。用 80 克纸双面打印,会厚达 9.2 厘米(作者语)。 不过看过副标题,稍微可以理解:An Economic and Cultural Commentary 。感情上升到经济和文化了。敢用这个词的人绝非泛泛之辈。

一不小心读到凌晨五点,居然 Introduction 的一半还没读完 :( ,也就是说,前 100 页都还没正式谈 C 语言呢。

由于是英文版,阅读速度受到了极大的限制。而行文中大量我不认识的单词和长句,迫使我不断的查字典。就这样还能坚持读下来,只能说,这是一本好书,有趣的书。

好吧,简单说,这是一本对 C 的标准及相关层面的一切注条评论的书。

对于每一个点,都在几个方面展开:将其和 C++ 、其它语言对比;描述一般的实现方法;对其做出评论;给出 Coding Guidelines (编码指引)。

下面,我试着将前面介绍部分选译几段,鉴于原文的行文与我来说有许多艰深之处,恐怕很难译的准确,姑且看之吧。


在谈及 C 的新标准时:

C++

许多开发者把 C++ 看作 C 的一个超集,并期待能把 C 代码移到 C++ 里。虽然这本书无意卷入可能是为了让其在 C++ 中更为有效的那些(译注:C 语言标准)重大重新设计的争论中,但本书也尽力驱散 C 是 C++ 的一个子集的神话。对双方而言,它们相互之间平等不分彼此,但是这些章节倾向于关注用 C++ 的翻译器(译注:原文用的 translator 指代把代码翻译成机器可以执行的指令的东西,并不特指编译器或解释器)去翻译 C 代码时需要考虑的问题。

关于 C++ 标准,ISO/IEC 14882:1998(E),对 C99 标准中构造概念能说些什么呢?

  • 措词相同。什么也不用说。
  • 措词相近。在英文语法上有细微的不同,术语上的区别,或是微小的问题。这些通常会被指出来。
  • 措词不同但意思相同。因为词句的排列迥异,很难说它们是一致的。但是含义相同。如果不从 C++ 世界的视野强调的话,很难指出它们从 C 的视野来看有什么不同。
  • 措词不同且含义不同。在这里会引用 C++ 的措词,并对其不同点做一番讨论。
  • 没有和 C99 标准里的句子对应的 C++ 标准里的句子。这经常会出现,因为 C99 标准里构造的概念,有些在 C++ 标准里并不存在,而这一点在之前的句子中已经指出来了。

这里使用一个特别的形式注释源码,即混用了 C /* 风格 */ 和 C++ // 风格 。

C++ 的先驱被称为 C with Classes 。在开发 C++ 的时期,C++ 存在于一个有大量 C 专家和 C 源码的环境下。Stroustrup 的一些引入与 C 不兼容特性的企图遭到了来至 C++ 用户们的抗议。[1310]

C 和 C++ 盘根错节的关系,存在于开发者的心态中,在于厂商只提供单一的翻译器而仅用一个选项来区分语言,也在于构建一个程序时同时使用两种语言翻译生成的模块。这些意味着有必要阐述两者之间任何的差异。

在 1989 年四月的 WG14 会议上,ISO 问了两个问题:(1) C++ 语言是否应该被标准化?(2) WG14 是否是一个应该去做这件事的委员会?关于 (1) 的决定非常接近,一些人争吵说 C++ 还不够成熟到有理由开始标准化,而另一些认为向标准化迈进将使语言稳固下来(对于规范和实现的不断变化会导致用 C++ 做关键任务的应用软件的开发者和头痛)。最终认可的是,应该有一份 C++ 标准,但一致同意不应该由 WG14 来创建这份标准。在 1991 年四月的 WG21 ,ISO C++ 标准委员会成立;两个月之后委员会第一次碰头。

一些 C++ 中附加的背景信息被拿出来。尤其是那些不同的概念,或是术语,用来描述本质上相同的行为。

在一些地方,描述了一些只存在于 C++ 而不见于 C 的概念。这样做的理论基础是基于有些只用 C++ 翻译器的 C 开发者,有可能偶尔用到一些 C++ 里的概念。许多 C++ 翻译器提供了一个 C 兼容模式,这个模式往往提供了更多一点的 C++ 里的概念。这份描述也提供了一些关于,为什么东西到了 C++ 里就变的不一样了的背景。每个人都有一个视点,即使是 C++ 的创造者,Bjarne Stroustrup 。但是最终的说法属于监督语言标准开发的标准主体,SC22 。以下,是最开始的状态。

Resolutions Prepared at the Plenary Meeting of ISO/IEC JTC 1/SC22 Vienna, Austria September 23–29, 1991

Resolution AK Differences between C and C++

Notwithstanding that C and C++ are separate languages, ISO/IEC JTC1/SC22 directs WG21 to document differences in accordance with ISO/IEC TR 10176.

Resolution AL WG14 (C) and WG21 (C++) Coordination

While recognizing the need to preserve the respective and different goals of C and C++, ISO/IEC JTC1/SC22 directs WG14 and WG21 to ensure, in current and future development of their respective languages, that differences between C and C++ are kept to the minimum. The word "differences" is taken to refer to strictly conforming programs of C which either are invalid programs in C++ or have different semantics in C++.

这个状态在第一份 C++ 标准完成后被更新,但是对于 C 标准的修订来说,很难产生重要的影响了。

Resolutions Prepared at the Eleventh Plenary Meeting of ISO/IEC JTC 1/SC22 Snekkersten, Denmark August 24–27, 1998

Resolution 98-6: Relationship Between the Work of WG21 and that of WG14

Recognizing that the user communities of the C and C++ languages are becoming increasingly divergent, ISO/IEC JTC 1/SC22 authorizes WG21 to carry out future revisions of ISO/IEC 14882:1998 (Programming Language C++) without necessarily adopting new C language features contained in the current revision to ISO/IEC 9899:1990 (Programming Language C) or any future revisions thereof.

ISO/IEC JTC 1/SC22 encourages WG14 and WG21 to continue their close cooperation in the future.


其它语言

为什么这本书要讨论其它语言?因为开发者不喜欢把他的整个工作生命耗在一门语言上(或许有些 Cobol 和 Fortran 程序员将这样做)。

C 不是这个世界上唯一的编程语言(虽然有些开发者曾经把它当成唯一语言)。其它语言的特性能够使得开发者对于 C 的精神(设计,滋味,世界观)的理解更加尖锐。一些 C 的概念可能从不同的方式被替换,其它的方式之间也互相关连。

C 中存在的函数性会影响到对算法编码的方式(不要忘记个人差异[1117, 1118] )。因为 C 里怎样去做这件事决定了源码中的段落只能按那种方式写出来;但它们在其它语言中,可以以不同方式来写,并且有不同的运行时的特性 [1119] 。对于开发者来说,赏识那些他们写出来的代码中由 C 语言的特性带来的结果,很难做到。这就像让鱼试图理解在水里和在陆地上的区别一样。

有些概念通行于所有的编程语言,而另一些仅仅存在于 C (通常也会存在于 C++)。有些概念对于特定类型的语言很普通—— 算法、功能、命令式、形式化的,等等。用 C 去做一件事情的方式通常不是达到目的或是得到算法效果的唯一路径。有时 C 有其特有的方式。有时 C 类似于别的语言的做法。有时有些语言以与 C 迥然不同的方式去做事,无论是实现同一个想法,还是用于另一个不同的世界观下。

这里无意声称 C 或其它语言更好或更差,因为它们有自己独特的设计哲学,或是包含了一个特定的概念。这个分类不会刻意的去考察其它语言做了些什么,也不会试图讨论除了这个东西很像或不像 C 之外的关于其它语言的东西。只是从 C 的视点去看其它语言而已。

当开发者从 C 转移到其它语言时,在一年左右的时间里(或者更长时间,这取决于花在新语言上的具体时间),倾向于以 C-like 的风格去使用新语言(这非常像人们在学习英语时,一开始倾向于使用他们母语中的语法和句法;可以说的很流利,听起来也没有障碍)。

笔者针对许多 C 开发者的体会是,他们倾向于认为只有 C 才是值得了解其细节的语言。这个章节不想改变这种看法,也没有涉及这个方面。了解一些其它语言怎么运作的知识没有什么坏处。

有一些经过时间考验的语言,比如说 Cobol 和 Fortran 。虽然 Pascal 和 Ada 可能在怎样编写可维护的、健壮的代码方面有很强的影响力,不过它们只存在于相对较短的时代,出现并消逝。在写作这部作品的时间,有六个 Ada 95 的实现。1995 年的一份语言使用纵览[590] 发生在 DoD 武器系统中有 49.5 M 行 Ada 83 代码( C89 有 32.5 M 行,其它语言是 66.18 M 行)。由于对 Pascal 标准缺乏兴趣,导致人们去问是否应该来提取一份公认的标准(ISO 规则需要一份标准每五年被评审一次)。Java 语言正在侵入嵌入式系统市场(它成为互联网的通用语的期望看起来没有发生)。它很新潮,一直保持在公众的眼球内。Lisp 在它被创造出来的 40 年里,一直都拥有专注的用户。有一篇论文对它的赞誉超过了 C 。[411]

这本书提及的语言的参考资料有:Ada,[645] Algol 68,[1407] APL,[654] BCPL,[1162] CHILL,[656] Cobol,[634] Fortran,[640] Lisp[650] (Scheme[719]), Modula-2,[646] Pascal,[639] Perl,[1438] PL/1,[633] Snobol 4,[521] and SQL.[641]

对于一些实现和 C 完全不同的语言包括:APL,[178] 函数式语言,[1082] 以及 ML.[50]


在论及文化时,书中这样写道:

每一种语言都有一个与之相关联的文化。文化导致思考特定事情,做特定事情时,都有一种特定的方式[1015]。这些最初的选择怎样发生、为何发生,引出了一些有趣的历史背景,而这些在这本书的其它章节讨论。不过这些内容与编码指引的章节无关。

文化这个词用在 C 开发者的一些普通惯例上,或许过于华丽。对于开发者们,即使不给出一些形而上学的大概念,就已经够偏狭和过于自信的了。使用 "惯例" 这个名词既表达出其功能性,又能减少它们被夸大的可能。

"惯例" 可以被想象成一组事情怎样(用 C)去完成的设想及期望的集合。"C 风格" 这个名词有时用来形容这些设想及期望。然而,这个词对于不同的开发者、不同的环境,有许多不同的含义,非常容易导致误解与争论。因此在这本书里极力远离关于风格的概念。

在很多方面,惯例是一个模因机器[125]。(译注:meme machine ,这个词来至于《自私的基因》。meme 指代人类文化进化的基因,一种模仿单位。)开发者阅读已经存在的代码,学习里面包含的思想,潜意识的使用这些思想编写新的代码。许多特定的编写代码的方式不需要对包含它们的程序有用处。他们仅仅需要表现得对编写这些代码的开发者有用就行,或是适合开发者最喜爱的做事情的方式。有时候,开发者不去彻底的分析要写些什么,他们只是跟从他人。软件开发有它自己的时尚和潮流,这一点和其它任何由信息驱动的活动一样[124]。

在观察编码指引中的惯例的效果之前,我们应当先问一下是些什么东西构成了惯例。至于书中提到的指引中给出的推荐,构成惯例的东西被记录在惯用法的章节。开发者不喜欢用科学的方法来处理这个问题。他们工作在一个或多个应用领域,接触各式各样源代码,和其他各种各样的开发者讨论 C 。虽然一些公司可以选择调节他们的指引到适用于指定应用领域与工作环境下的实际情况,但这本书中的指引建议还是试图适用于一般的应用领域。

惯例并不会全部记录下来,有时,开发者甚至不能表述出它们是什么。有经验的开发者有时用 C 方式的表达式去做事情,或者只是我自己感觉是这样。一旦问起这些表达式的含义的时候,他们不能给出一个确切的答案。这种人类行为(知道一些事情但不能表述出它是什么)在实验室里经常出现。

……


在谈及编码指引( coding guidelines )时,关于效率问题,书中这样写:

经验显示,许多开发者相信代码的效率是代码质量的一个重要指标。这个信念不仅仅存在于 C 的文化中,且有着悠久的历史[772]。效率在一些应用领域保留着一些争议,这些编码指引通常以效率为理由来解释一些不良习惯,而这些不良习惯(从操作结果的视野来看)是需要慎重考虑的。

经验显示,一些开发者把源代码视觉上的致密性和翻译后程序运行时的性能同等看待。虽然有一些语言存在这样的相关性(例如,一些 Basic 的实现:几乎是在早期所有玩具电脑上的基于解释运行的 Basic ,都对源代码做实时的翻译),但这并不存在于 C 中。这是一个应该在开发者的教育期被覆盖的译题。(译注:这段让我想起了儿时的 Basic 时代,整天想着把 Basic 程序写的更精巧,以便让它跑的更快)

经验还显示开发者在做一个选择时,很大因素取决于他估计需要做多大的键盘输入量。减少键盘输入的行为包括选择更简短的标识符名字,使用剪切粘贴来复制代码段,使用键盘快捷键,以及创建一些编辑器宏(有时需要的比他们节省下来的更多的时间)。


在写到开发人员对翻译器的期望时,对 C 语言有一段精辟的论述:

  1. C 拥有在效率方面的名声。它能写出程序尽可能是使用处理器的资源。写出这样的代码取决于对处理器以及翻译器如何把 C 语言中的概念映射为机器码这两方面的知识的了解。没有几个开发人员对这些课题有足够的了解以便能始终写出非常有效率的程序。笔者有时预测用自己写的编译器生成的代码都有困难。笔者发现一个普适法则:如果有任何开发人员声称在语句级上有一种使用方法效率最高,都可以很安全的说他错了。语句级的性能开销不需要去争论,交给翻译器去操心好了。

  2. C 拥有在致密性上的名声。每条 C 语句对应的机器码指令的体积比相对于别的语言来说是一个很小的数字。可以说,C 是一种所见即所得的语言,从 C 语言映射到机器码简洁、显而易见。(暂时把优化器在后面做的工作先放在一边)。这个期望曾经被 WG14 的一些成员用作反对等于操作符用于结构类型的论据;那样的话,一条操作就将潜在的生成大量的代码,用于逐条比较每个结构成员。引入 inline 函数声明从一些角度破坏了这一点(这取决于 inline 是被想象成类函数宏的替代品,还是在当前嵌入一个不能被实现成宏的函数)。

  3. C 有着是一种表里如一的语言的名声。开发者通常能预测到他们所写代码的行为。没有什么阴暗角落藏着一些偶然一遇的用法会导致一些语言概念以意想不到的方式运作。虽然 C 委员会不能绝对保证永远不会出现另人惊奇的行为,但他们一直致力于最小意外原则。


翻译这个东西真累,翻的不好,可能还有错误的理解。不过我尽力不在里面夹带私货了 :D 有兴趣的同学去读原版吧。

January 06, 2009

一个 C 接口设计的问题

C 语言在本质上,参数传递都是值传递。不像 Pascal 和 C++ 可以传引用。这一点,使得 C 语言可以保持简单的设计,但另一方面也颇为人诟病。

因为性能问题,指针不得不被引入。可以说,用 C 语言实现的软件,其实现的 Bug 90% 以上都来至于指针,应该是没有夸大了。当然设计引起的问题或许更为关键一些,那些于指针无关。

纠结于性能问题上,层次比较低。可 C 语言就是一个活跃在较低层次的语言,一旦你选择用它,就不得不关心性能问题。反过来,把 C 模仿成更高级的语言,倒是有点画蛇添足了。好了,让我们来看个实际的涉及参数传递的相关问题,用 C 语言该如何设计。

最近同事在做一个类似 Protocol Buffers 的东西。这个东西做好并不容易,设计上尤为困难。其中的设计难点:设计一个合适的 DSL (领域专用语言) 我们讨论过很久,也分析了好几天,但今天不打算谈了。拣个小东西说:当我们把一个二进制结构化数据块解析出来,传递到 C 语言中,让 C 语言可以方便的访问数据结构时,接口如何设计?


这个问题在目标语言不是 C 而是更高级的语言(尤其是有 gc 机制的语言)时,都不是问题。可 C 语言本身是没有对象概念的。

C 语言有结构,但是不具备描述动态长度的能力;没有字符串,只有定长的字符数组;甚至没有多维数组,只有一维数组的数组。

C 函数的参数及返回值可以是结构,但在接口设计中,或许是因为值传递,以及考虑 ABI 的简洁性的关系,常常使用结构指针。返回结构指针往往有生命期管理的苦恼。即使到了 C++ 里,允许返回结果/对象了,可所谓返回值优化也是件相当让人困扰的事情(如果你打算完全放弃了解语言的细节,无视细微处的性能问题。那么,为什么不考虑使用 Java 或是 Python ,无论什么都比选择 C++ 强)。

对于返回一组复杂数据,通常的办法有些什么?

最常用的方法是,调用者分配空间,传递给处理函数。由处理函数反向填写结构内容。这样的好处是,调用者可以选择把空间分配在栈上还是堆上。一点小提示:在语法上,C 语言允许你把一个数组当成指针来传递。所以你可以定义一个长度为 1 的结构数组类型。用起来好看一些。具体见标准库中的 setjmp 的定义。不过作为我个人的理念来说,不太主张在 C 语言设计的软件中,为了减少几次键盘输入,而使用过多的语言特性。

这个方式的缺点是,你很难让调用者定义不定的数据结构。尤其是在结构里还有对别的结构的引用。

跟这个相似的是接收字符串。最典型的例子是标准库中的 fgets ,提供一个接收缓冲区的地址指针,和一个缓冲区大小。(注:gets 则是一个失败的设计)同样在 Windows 的 API 中,也随处可见这样的例子。

第二,就是由函数自己分配内存,交给调用者去释放。大家只需要约定内存管理的接口即可。标准库中的 strdup 就是这样做的,同样的还有 readline 库中的 readline 。C 语言统一使用 malloc 管理内存,不像 C++ 提供了更灵活(更难控制,更容易出问题?)的 new 操作符重载。所以,给出这个约定并不会增添太多的麻烦。btw, 由于微软 VC 的 CRT 对 malloc 等实现的过于糟糕,导致很多 Windows 的软件自行实现内存管理器。或者在库中开放自定义内存管理器注入的接口。这其实有点越俎代庖了。gcc 提供的 CRT 里, malloc 性能就相当不错了。

缺点呢?内存只能从堆上分配;而且增加了内存泄露的隐患;设计角度上讲,也不太干净。对于复杂数据结构,这个方法也无能为力。C 语言里并没有所谓析构函数的说法。

作为对第二点的一种补充方案,用的人就凤毛麟角了。那就是给你的系统加入 gc 。实际上,就是约定另一种内存管理方法。我们的项目部分模块在用,效果还不错。gc 库已经开源,请参考这里。如果信不过这套东西,可以考虑 COM 的机制:增减引用。COM 旨在建立一种对象模型,可惜 C 语言中没有对象的概念,在 C 的层面使用 COM ,痛苦了一些。对于粒度比较小的东西,性能也将是问题。

第三种,用的人也比较多。就是在函数内部开一块静态空间,用于数据返回。返回的指针指向的数据的生命期可以保证到下次调用同一函数之前。静态空间可以声明在数据段里,也可以在程序初始化时从堆上分配出来,这样利于在空间不够的时候扩展。至于这块静态空间什么时候释放的问题,不用太操心。即使不去释放它们也不用内疚。操作系统会帮你回收的,还会比你干的更出色。C 是为了实现 UNIX 而诞生,而 UNIX 的哲学就是,编写简单的程序专心干好自己的事,让更高层次的程序(通常是 shell 或动态语言)去组合它们,让操作系统去管理它们。在 Windows 上,Unix 编程哲学未必有用,但大原则没错的。

这个方案有另外一个问题,就是函数不可重入,且有线程安全问题。重入问题可以想办法避免。线程安全可以用 TLS 解决。老实说,我个人不看好在 C 语言中使用多线程解决问题。多线程也是违背 Unix 哲学的。如果你有几件事情需要协调起来做,使用多进程;如果你有几百件事情需要同时来做,考虑换个思路,玩玩 Erlang 啥的。


回到今天我们面临的问题。用一种 DSL 来描述一个数据结构(比 C 的结构表达能力更强的),然后生成对应语言的解析库。如果目标语言是 C 的话,我们生成的代码如何返回对 C 程序员友好的结构化数据呢?

这让我想到了 MySQL 的 C 语言接口。很多初学 C++ 的程序员,很喜欢把那些 C 接口“封装”成“漂亮”的 C++ 接口。直接返回 vector 套 map 的多层模板实例。不知道有多少人干过?前几年我带实习生的时候反正见过不少。如果同学你现在醒悟了,明白这是件巨傻X 的事情,那么握握手,我们有共同理念;否则(C++ 封装以后不是很“酷”吗?),我们暂时没有共同语言了。

我不是想说 MySQL 的 C 接口设计的很好,不过是中规中矩。只是 C++ 不是 C ,C 也不是 C++ 。(话说,上面提到的 C++ 封装,我也不认为是正确的使用 C++ )反复提及 C++ ,是因为,我发现今天很大比例的 C 程序员其实是从 C++ 开始启蒙的,而不是相反。把 C++ 当成 C 用的危害其实比不上把 C 当成 C++ 用。前者不过是把汽车开到自行车的速度,至少不怕摔跤了,跑起来还能安全点;而后者,非要把自行车踩到高速公路中间,迟早非撞死不可。

最方便 C 程序使用的莫过于传入一个结构指针,让库去解析数据,填写这个结构了。

但是,如果结构里有字符串、不定长数组(通常会根据前面解析出来的数据决定后面的长度,对于 C 的编程技巧来说,允许把结构体的最后一个数组的长度设为 0 ,假设成不定长的,从而减少一次间接的指针引用。但是对于结构中有多个不定长数组则无法使用这个技巧。)等等的话,就很难避免指针了。

数据中一旦出现指针(间接引用别的数据),就有内存管理问题。

最开始,考虑过一个很 C++ 的方案,传入一个内存管理器。这种设计在 STL 里就有。所有 STL 的容器都可以指定一个 allocator ,供灵活的管理内存。前几年我倒是认为这是个相当巧妙的东西。没有细想,自定义分配器最终有多大的意义?自定义内存管理器,很大程度上是因为效率因素引起的。但性能问题永远不是根本问题。制作软件是为了达到特定的目的,而软件开发的问题更多的是是解决复杂度问题。往往复杂度带来的性能问题更加严重。然后为了解决复杂度带来的性能问题去引入更高的复杂度,出现恶性循环的可能性非常之大。

即使我们传入的内存管理器(或是直接使用 CRT 里的 malloc,但这样就没可能利用堆栈分配空间了),还会面临新问题,如何回收结构中间接引用的数据。引入析构函数指针?OMG 。

后来,我们设想使用一个内部静态空间,所有的解析结果都分配在内部,自我管理。这些空间还可以复用。大部分解析结果也就是临时用用,这样做很方便。而且调用者不用太关心数据的生命期。

但是,一旦调用者需要把结果(一个复杂结构)保存一段时间的话,他就遇到困难。

当然,也可能不是困难。当我们面对这个设计难点时,都应该向上考虑一层,究竟这是一个问题吗?我们需要这么用吗?

调用者可以自己遍历这个数据结构,把他需要的数据,以自己的方式复制出来,组织起来。他们需要的是数据,而不是对数据结构完全的拷贝。

仔细考虑过以后,我们还是发现,保留完整的数据结构是有意义的。不像 C++ ,C 没有对象赋值操作符重载这种语法糖,我也不喜欢用宏去模拟一个出来。增加一个拷贝函数指针其实和增添一个析构函数一样,对 C 来说,不那么漂亮。(当然,同时增加了开发量,我们需要编写更多的代码自动生成器)

最终,我们采用了由调用者传入缓冲区指针的方案。要求解析器生成的数据结构放在一块连续的内存空间上。这样,调用者就可以把指针直接定义成最终方便访问的结构或联合。但是提供更充裕的内存空间,存放那些内部引用的数据(比如字符串)。

因为结果数据区是由调用者提供,就不存在数据复制移动引起的指针调整问题(调用者可以自己先分配好)。

最后一个问题是,如何让调用者估算数据接收区的大小呢?

很多 Windows API 可以通过两次调用来完成,第一次空调用计算需要的缓冲区大小,第二次真的去填写数据。根据实际需求分析过之后,我认为在我们这个模块的应用上,这样做是多余的。我们尽可以让用户随便给一个估算大小去处理数据,一旦空间不够,返回错误信息。让用户自己扩大缓冲区,重新调用一次即可。

btw, 不断重试是我们最终认可的最 KISS 的方案。一开始,我们认为让处理程序自己分配内存,并自己使用 realloc 更好。后来发现,完全是多余的设计。因为,解析二进制流是 O(1) 的操作,不比估算长度慢;而往往调用者都能正确估算接收区应有的长度,即使简单的每次两倍的方法扩展接收区大小,也不会浪费多少处理时间。即使他们需要精确分配结果需要的内存块,尽可以用一个足够大的公用缓冲区接收,然后得到长度信息,重新在特定内存上重来一次即可。


写累了。想表达的也表达完了。今天到此为止。 :D

ps. 前几天写了一篇关于 一种对汉字更环保的 Unicode 编码方案 ,我昨晚花了两小时写了个简单的 C 实现。可以把 UTF-8 或 UTF-16 转换到我自己定义的暂且命名为 UTF-C 的编码上,也可以转回来。代码用的行数比预想的要多一些,因为我低估了 UTF-8 的处理复杂度(其实也不复杂啦)。

有兴趣的同学可以看这里