9.3 构建脚本

在3.3.5节中我们已经接触过了,我认为编程语言的处理器应该尽可能让程序通过一个可执行文件就可以运行。任何一种语言,在其制作的初期都是个不成熟的语言,因此不可能在一开始安装的时候就让人有所期待。另外,在别人试用语言的时候,要花费很多时间进行高门槛的安装过程可不是一个好主意。

在代码清单9-7中例举的异常类程序,在crowbar中编写的话就非常简单了,但是要在C语言中编写的话我想可能要花不少的功夫。因此,我们需要一种用crowbar编写源代码,将crowbar本身打包为可执行文件的方法。

另外,虽然Diksam中以外部文件的形式保存着现在的 diksam.lang包的源代码,但我还是想要把它打包到可执行文件中去。

在这里让我们研究一下这个方法。

9.3.1 基本思路

首先先来研究一下crowbar。想法很简单,比如下面这段crowbar代码builtin.crb:

function create_exception_class(parent) {

this = new_object();

this.parent = parent;

this.create = closure(message) {

(之后省略)

基于上面这段代码,生成了下面的C代码。也就是说,crowbar的代码被作为C语言的字符串保存起来。

include <stdio.h>

include "CRB.h"

static char *st_builtin_src[] = {

"function create_exception_class(parent) {\n",

" this = new_object();\n",

" this.parent = parent;\n",

" this.create = closure(message) {\n",

(之后省略)

之后再进行编译、链接、执行的时候,在加载使用者指定的crowbar源代码之前,先编译这段字符串就可以了。

另外,将builtin.crb转换成为C语言代码的程序是在crowbar中编写的。

当然,这段程序必须要在编译crowbar的过程中。为了在编译crowbar的过程中能让crowbar运行起来,我又制作了没有组建构建脚本状态的“minicrowbar”可执行文件,并且利用minicrowbar来完成转换工作。

也不是特意要做这样细致的工作,而是我觉得用C语言写这种字符串转换程序不太好。在我的意识中,crowbar作为一门以Perl为目标的语言,确实还是应该在crowbar中进行字符串处理。这也是为什么我要尝试着去这么做的原因。

9.3.2 YY_INPUT

保存在C语言字符串常量中的crowbar代码,会在使用者编写的程序之前进行编译。

制作crowbar的时候,编译采用了yacc和lex协同作业的方式。因此,最先加载源代码的是lex(生成的程序)。lex(生成的程序)在默认状态下将从全局变量 yyin的文件指针中加载源代码(如代码清单2-2)。

但是,这次不是从文件中加载,而是加载内存中的字符串。在这种情况下, flex会使用宏 YY_INPUT(但是,这个宏的移植性未必很高)。

像下面这样,通过替换 YY_INPUT的定义,可将fle的标准的输入例程替换为单独的输入例程。

/ crowbar.l的开头 /

undef YY_INPUT

define YY_INPUT(buf, result, max_size)(result = my_yyinput(buf, max_size))

输入例程要接收缓冲和缓冲大小(fgets() 流)两个参数,缓冲中存入字符串并返回该字符串的字数。缓冲必须以“\0”结尾。

crowbar 的 my_yyinput() 引用了当前解释器的“输入模式”,在有 CRB_FILE_INPUT_MODE 的情况下从文件中输入,在有 CRB_STRING_INPUT_MODE的情况下从字符串输入。这个“输入模式”保存在 CRB_Interpreter中。

构建脚本将在创建 CRB_Interpreter 的时候进行编译。在这之后,将使用同一个解释器编译用户程序,此时为了能够让用户程序发生错误时显示正确的行号,解释器中保存的行号会被重置为1。

9.3.3 Diksam的构建脚本

在Diksam 的book_ver.0.3 中, diksam.lang 是以外部文件的形式存在的,单独的可执行文件没办法运行。这对于构建脚本来说的确不太方便。

虽然Diksam的程序可以由多个文件组成,但是基本思路还是和crowbar一样。

构建脚本并没有在文件系统中保存源文件的实体,因此,会在源文件的搜索路径 DKM_REQUIRE_SEARCH_PATH、 DKM_LOAD_SEARCH_PATH的开头默认添加上。

另外,和这个修改一并完成的,还有使用者即使不显式 require 标准包 diksam.lang,它也会被默认 require进来。

9.3.4 三次加载/链接

在之前的Diksam中, push_function、 new等指令的操作数是函数和类的索引值,它们通过以下方法取值 [18](在8.1.5节中已经介绍过)。

在编译的时候,将每个 DVM_Executable中固有的索引值作为操作数。

在向DVM中加载的时候,将字节码中的对应位置替换为 DVM_VirtualMachine中固有的索引值。

在这种方法中,一个 DVM_Executable被特化到一个DVM 中,因此在多个DVM中不能共享 DVM_Executable。

可能有人会问了:“当初为什么要创建多个DVM呢?”例如,在Web应用中,一台服务器上很可能运行着多个应用程序,在这种情况下,不是应该为每个应用程序分别分配不同的DVM吗?再举一个其他的例子,在用Diksam编写Diksam的集成开发环境(IDE)的情况下,驱动IDE的DVM和在IDE的菜单上选择“执行”而启动起来的DVM,也应该是两个不同的DVM。

因此,这次我们引入了间接引用对照表,通过它就可以不用再替换字节码中函数和类的索引值了。间接引用对照表保存在 ExecutableEntry中,在加载的时候创建。

struct ExecutableEntry_tag {

DVM_Executable *executable;

int  *function_table; ←函数的间接引用对照表

int  *class_table; ←类的间接引用对照表

int  *enum_table; ←枚举的间接引用对照表

int  *constant_table; ←常量的间接引用对照表

Static  static_v;

struct ExecutableEntry_tag *next;

};

并且,按说在加载时局部变量的偏移量也要进行字节码的替换,但是这次并没有修改。这里说的替换通常在同样的机器上运行的DVM中,也会出现相同的结果。