开发代码产生器的经验

 

开发代码产生器的经验

 

转载时请注明出处:http://blog.csdn.net/absurd/

 

提到代码产生器,很容易让人联想到Rrose之类的工具,它们根据UML图产生相应的代码,同时还可以从代码反向生成UML图。Rrose是一个庞大的系统,自然让人觉得代码产生器也是一个复杂的东西,所以不少人对代码产生器抱着敬而远之的态度。其实,代码产生器可大可小,小则几行代码,大则数万行代码。大小本身不是问题,重要的是它能提高我们的工作效率。

 

产生式编程已经不是什么新概念了,早在好几年前,国外一些专家都曾预言,产生式编程将在软件开发中占据重要地位。事实也是如些,产生式编程已经是软件开发中不可或缺的方法之一了。根据《产生式编程方法、工具与应用》一书中的描述,泛型编程、面向方面的编程、面向意图的编程和代码产生器都属于产生式编程。

 

本文重点在代码产生器上,当然我们的目标是研究代码产生器本身的开发,而不是某个特定代码产生器的使用方法。说来很惭愧,我从来没有去研究过某些经典的代码产生器,它们的实现方法是非常值得学习的。本文讲述的仅仅一些个人经验,但我相信这些的经验仍然有些实用价值的。

 

代码产生器的基本模型为:

 codegen

代码产生器要根据一些规则才能产生代码,即使这个规则蕴涵在代码产生器之中,从逻辑上讲,它仍然是存在的,我们把这些规则称为元代码。元数据就是用来描述数据的数据,比如DTD是用来描述XML文件的,XML文件本身也是数据,所以可以把DTD称为元数据。元代码有类似的含义,它是对将要产生的代码描述,从某种意义上讲,它本身也是一段代码。

 

代码产生器的目标是要产生代码,我们把产生出来的代码称为目标代码。所谓目标代码是针对代码产生器而言的,这里的目标代码通常会成为编译器的源代码。至于这些目标代码的具体语言,要视的情况而定,实际上可以为任何语言,甚至是自然语言。

 

代码产生器有点像编译器,把一种语言转换成另外一种语言。从更广泛的角度来看,很多应用程序本身就是一个代码产生器,比如用CGI/ASP/PHP等开发的WEB程序,接收用户输入的参数,输出HTML文件,把这样的应用程序当作代码产生器也是可以的。这里不讨论广义的代码产生器,而专注于程序员所用的代码产生器。这类代码产生器有一定特殊之处,下面我们看看开发代码产生器所要考虑的问题。

 

1.         是否有必要开发代码产生器。

这是我们要考虑的第一问题,有人说代码产生器是个好东西,为什么不要呢?问题在于任何好东西都是有代价的,开发代码产生器也是一样,至少要耗费我们的时间和精力。如果开发代码产生器本身的代价远大于直接编写代码本身,开发这样代码产生器意义就不大了。可以参考下面的公式:

 

(编写代码的工作量 - 编写规则的工作量 ) * 重用次数 大于 开发代码产生器的工作量

 

从功利上讲,只有上述公式成立时才考虑使用代码产生器。当然有例外,比如出于兴趣或者研究的目的去开发也是可以的。

 

比如在决定开发gclassfactory时所考虑的,如果只是编写一个gobject子类,开发代码产生器要花数几倍的时间,那我会选择手工去编写那些宏、安装属性、创建信号和重载函数,忍一忍也就过去了。但是我们整个平台是基于glib/gtk+的,以后有很多这样的子类要写,若利用代码产生器,累积起来的工作量非常可观了。同时我也希望练习一下用glib写程序,所以最终决定glib来开发gclassfactory了。

 

2.         是否能够使用代码产生器。

代码产生器的适用范围是有限的,它不是万能的,否则程序员这个职业就消失了。工具是愚蠢的,好的工匠应该了解工具的长处和短处,充分利用它们的特长。代码产生器只适用于那些规则性比较强的情况。因为规则是抽象的,以少量的规则可以产生大量的代码,也就是说代码产生器的输出一定要大于输入(根据工作量来衡量),否则花了很长时间开发工具,结果只是让事情复杂化了。

 

比如对于gclassfactory来说,输入可以用标准idl或者xml格式描述gobject子类,这些描述几乎没有什么冗余信息,输出gobject子类代码则有很多类似又有差异的代码,比如函数原型、函数外包装、实现函数的原型、实现函数的框架、大量的宏、安装属性和创建信号等等。输出的代码量是输入的代码量的三倍以上,而且编写输入规则时根本不用思考。

 

3.         选择开发语言。

个人认为每个程序员至少熟练掌握一门脚本语言和一门编译语言,我用过不少语言,比如汇编(ARM/Z80/x86)PBVBAutolispC/C++等,但真正熟练的只有bash+awk系列和C/C++。脚本语言的好处在于方便,不需要编译; 也更高级,用少量的代码可以实现很强大的功能。但脚本在规模变大时,极难维护,变得很复杂,也就失去了优势。

 

通常情况下,如果代码产生器的规则简单,用脚本来写比较有利。当规则比较复杂,这些规则本身就是一种微型语言或者用XML之类的语言来描述时,使用编译语言更方便一点。当然像pythonruby这样功能强大的语言,集脚本语言和编译语言两者之所长,因为我不懂,就不讨论它们了。

 

前几天遇到一个问题,要定义一组宏,它的格式是这样的:

 

KEYMAP(GDK_Op_Left, GDK_F12, DIKS_F12)

KEYMAP(GDK_Op_Right, GDK_F13, DIKS_F13)

 

大约有30多行,第一列的Op_Left之类是自定义的按键,是我们讨论的结果,放在一个表格中,手工把这份表格转换成以上的宏,不难也要不了多少时间,但这样单调的事很容易出错,特别对于我这样粗心大意的人来说。于是决定用awk来做:

 

awk 'BEGIN{i = 4} {print "KEYMAP(GDK_" $1 ", GDK_F" i ", DIKS_F" i ")"; i++}' keys.txt

 

这就是代码产生器!就一行代码。简单吧,它却产生了30多行代码。其实我经常在用这样的代码产生器,给我节省了不少时间,减少了出错的可能。所以能用脚本就用脚本,脚本实现困难时才考虑用C/C++等编译语言。

 

4.         分离规则读入与代码产生两个部分。

如果代码产生器比较复杂,应该规划一下代码产生器的架构。代码产生器架构是一个典型的管道--过滤器模型,所以应该把读取规则的部分和代码产生的部分独立开来,两者互不影响。如果与编译器对应起来,读取规则的部分就对应于编译器的前端,代码产生的部分对应于编译器的后端。

 

为什么要这样做呢?原因在于隔离变化和代码重用。

 

规则的格式是可以变化的,只要语义不变,它的变化不会影响到代码产生器,这为支持多种规则格式提供了方便。比如在开发gclassfactroy的时候,想到描述接口的通常采用IDL,为了简单我采用了XML,但我把规则读取部分抽象成一个loader,不同的loader动态的加载进来,要支持IDL只需要编写一个IDLloader即可,不需要对gclassfactory做一点改动。

 

同样的,把代码产生部分抽象成一个coder,也是动态加载的。现在支持产生C语言代码,若要支持产生C++代码,就编写一个产生C++代码的coder,也不需要对gclassfactory做任何改动。甚至用它来产生测试程序,来产生RPC的程序,理想情况下也不需要对gclassfactory做任何修改,只要编写新的coder即可。

 

5.         选择中间语言。

中间语言是个好东西,它本身就是一种抽象,即对规则的抽象。抽象后的规则不再依赖于特定的loader,比如就接口描述而言,不管你用IDL描述的还是用XML描述的,到了中间语言这里,这些差异已经泯灭了,都表示成同样的东西。

 

分离规则读入和代码产生两个部分,中间语言起了非常重要的作用,把两者联系起来了,但耦合很松散,两者仍然可以独立变化。中间语言几乎在任何与格式转换相关的程序中,都广泛的使用着,特别是在编译器和文档格式转换等系统中。

 

中间语言可以是一种实际的语言,有自己特定的格式,也可以只是一种内存中的数据结构。因此到中间语言的转换也有两种方式,对于前者,可以先把原始输入转换成中间语言,保存在文件中,再由中间语言的loader加载进来。对于后者,每种loader都是独立,直接转换成中间语言。

 

比如,在gclassfactory(参考[open source] gclassfactory)中,它的中间语言是一个内存中的数据结构,每个loader是独立的,直接转换成中间语言,然后交给代码产生组件处理。而在cilc (参考[open source] cilc) 中,它的中间语言是XML格式的,要先把其它格式转换成XML后,才能用cilc处理。

 

一个典型的代码产生器的结构如下:

codegen_impl

有的代码产生器还带有图形用户界面,但是对于良好的设计,图形用户界面是可以独立存在的,其核心部分应该与上图大同小异。比如拿Rrose来说,它一定有一个中间语言,先把UML的图形表示转换成中间语言,再根据中间语言来产生代码,而不会直接通过界面元素产生代码。

 

~~end~~

 

 

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页