实现一个脚本引擎(燕良译)- -

Part I:概述

序言
bl_line.jpg (1610 bytes)

OK,在你的引擎中你想有一个脚本语言.

首先,确定你需要那种脚本语言;Henry Robinson 已经写过了一个各种脚本语言区别的介绍(如果你还没读过就去读一下吧),在这个系列教程中我将讨论一个象虚幻脚本(Unreal script)那样的编译器/虚拟机系统.

下一步,你需要知道两件事:怎样实现那样一个脚本引擎,还有脚本引擎不仅仅是酷而且在实际中十分有用的一些理由.

这里是我想到的一些:

  • 有用的新语言特性象状态,隐藏代码(latent code),等等.
  • 一个沙盘环境(sandbox environment)不会导致游戏引擎的崩溃.
  • 不需要游戏内部引擎的只是或者重新编译游戏引擎就可以编写游戏的内容.
  • 完全的独立于平台的脚本代码

但是也有一些不利因素:

  • 相对较慢--脚本的运行至少比可执行代码的执行慢15倍.
  • 限制--脚本不能用来建立实际的视觉效果(部分原因是它速度的缺点).
  • 编写游戏内容的人必须学习一种新的语言.

当然我们不会因为这些就停下来,我们已经准备好实现我们的想法了.现在,从哪里开始呢?

必须阅读的东西
gl_line.jpg (1418 bytes)

在虚幻(Unreal)发布前很久我就开始了.我浏览他们的技术站点,并且发现了虚幻脚本参考文档(

一个编译器基本上包括一下这些组成部分:

  • 一个符号表,其中存储所有的符号及其信息,例如类型,范围,等等.
  • 一个词法分析器,他的功能是将字符流(例如源文件)转换为记号(例如关键词,操作符等等).
  • 一个语法分析器(parser),他的功能是读取记号流,并建立语法树.
  • 一个语义检查器,用来检查语法树的语义错误.
  • 一个中间代码生成器,用来把语法树转换为中间代码
  • 一个优化器,用来优化中间代码
  • 一个代码生成器,用来从中间代码生成字节码.
  • 最后但不是最少,字节码将要在其上执行的虚拟机

layout.jpg (34875 bytes)

如果你编写完了所有这些组件,组合到一起,它们将成为一个完整的脚本语言系统.

这是全部吗?
gl_line.jpg (1418 bytes)

受到了一点点打击吗?毕竟决定使用脚本不是那么酷,DLL真的是唯一的路吗?没关系,我将很快讨论每一组件的细节,它们的绝大多数并不是那么困难.建立一个完整的脚本引擎是一个巨大的工作,但是,本质上是构造你自己的代码.

在这个教程的剩余部分我们将开发一个简单的编译器/虚拟机系统.尽管他没有地方象是一个完整的脚本语言,但是他实现了上面提到的所有组件.我在回想一个操作字符串的简单语言.

现在检查上面的连接,了解他们.顺便提一下,感谢所有的评论.

Quote!
gl_line.jpg (1418 bytes)

"But the plans were on display ..."
"On display? I eventually had to go down to the cellar to find them."
"That's the display department."
"With a torch."
"Ah, well the lights had probably gone."
"So had the stairs."
"But look, you found the notice didn't you?"
"Yes," said Arthur, "yes I did. It was on display in the bottom of a locked filing cabinet stuck in a disused lavatory with a sign on the door saying Beware of the Leopard."

HHG 1:1


Part II:词法分析器

序言
gl_line.jpg (1418 bytes)

我总是说在学一个东西的时候例子总是不能足够简单.这就是为什么当我想要设计一个包含所有完整编译器应该有的特性的简单的编译器时感到很累.我拼凑了一个字符串处理语言,它使用象C那样的语法,有BASIC那样的功能.下面是用我们的语言的正确编写的一个程序.

print "Please enter your name > ";
input name;
if (name == "Jan") { // string comparison
name = "my creator"; // string assignment
happy = "yes";
}
print "Thank you, " + name + "!/n" + // string concatenation
"You've just made a simple program very happy!";

就象你看到的,他没有构造象函数,类等等那样的功能,它甚至没有数值类型.这就是最终的东西,但是,他是很容易扩展的.

但是在接触那个之前我们还一很长的路要走--记得上次的组件列表吗?今天我们将实现第一个:词法分析器或称短语分析器.这是一个很好的热身,因为它不是编译器中真正难的部分.

OK,准备好了吗?

是什么,为什么和怎么作
gl_line.jpg (1418 bytes)

首先我猜你想要知道词法分析器是什么和为什么我们要用它?词法分析器的任务是把源文件的字符流转换成记号流.本质上它查看连续的字符然后把它们识别为"单词".

我们当然可以写一个函数用来把源文件当前位置取得的字符串与我们的所有关键字比较,但是这将是不可忍受的慢.所以我们使用有限自动机来识别单词(燕良注:就是DFA了,设计过程是正则式==>NFA==>DFA==>最小化).如果你不知道它是什么,好吧,你不需要知道(燕良注:如果你想知道,请看本文最后的附注).

关于词法分析器的一个基本情况是我们不需要作实际的艰苦的工作,我们使用一个叫作"LEX"的程序生成词法分析器.这是一个标准的UNIX程序,他也有几个win 32的版本(燕良注:我有一个FLEX.exe).

这里有完整的LEX手册的HTML版.

好的,现在你知道了词法分析器作什么和我们将如何制作它.现在你可以下载 *tut2.zip*并且看一眼那些代码.这部分的源程序是string.l(燕良注:LEX源程序)和main.cpp以及几个头文件.请注意ZIP文件中含有目录结构,flex.exe在主目录,这部分的代码在tut2/目录.

LEX规则
bl_line.jpg (1610 bytes)

LEX需要一些简单的规则来生成我们的词法分析器.在介绍规则之前,先让我们看一下LEX源程序的分段.

说明部分
%%
规则部分
%%
辅助程序部分

<说明部分>包含一些正则式(regular expression )的宏(正则式在LEX手册中有解释,想彻底了解它请看这里).这些告诉LEX我们使用的LETTER,DIGIT, IDENT(标识符,通常定义为字母开头的字母数字串)和STR(字符串常量,通常定义为双引号括起来的一串字符)是什么意思.(燕良注:呵呵,多熟悉呀.)

这部分也可以包含一些初始化代码.例如用#include来使用标准的头文件和前向说明(forward references).这些代码应该再标记"%{"和"%}"之间,你马上将看到我include了一个lexsymb.h 文件.

<规则部分>可以包括任何你想用来分析的代码;我们这里包括了忽略所有注释中字符的功能,传送ID名称和字符串常量内容到主调函数和main函数的功能.

lexsymb.h 文件声明了词法分析器函数将要返回的记号的符号.它还声明了一个'yylval' 共用体(union),用来传送额外的信息(例如标识符的名字)到主调函数;这里我们使用这个特殊的共用体可以使下一部分更清晰些.

现在让我们看一下实际的规则.我使用/* */作注释;LEX是一个相当老的程序,所以它着支持//引导的注释.顺便提一下,我们将使用LEX生成C程序,C++版的LEX程序也有,但是标准的UNIX LEX产生C代码.我们想要使这东西便携,不是吗? (燕良注: LEX源文件 .L--->FLEX--->C源文件,默认文件名是lex.yy.c)


"if" {return IF;}
"=" {return ASSIGN;}
";" {return END_STMT;}
{IDENT} {Identifier (); /* identifier: copy name */
return ID;}
{STR} {StringConstant (); /* string constant: copy contents */
return STRING;}
"//" {EatComment();} /* comment: skip */
/n {lineno++;} /* newline: count lines */
{WSPACE} {} /* whitespace: (do nothing) */
. {return ERROR_TOKEN;} /* other char: error, illegal token */

我删去了一些非常简单的规则.就象你看到的那样,每一条规则开始部分是LEX将要识别的样式,接下来是一些代码告诉LEX当规则匹配后作什么(这部分代码可以包含C++代码,因为LEX只是简单把它们的拷贝到输出文件中).记住最顶端的规则被最优先评估,这通常很重要.

头3条规则十分的简单,它们只是识别一个字符串,然后返回相对应记号的符号.你可以改变这些字符串,例如你想使用":="来作赋值操作符.

第4行是第一条有趣的规则:它使用了IDENT宏,它识别不满足前面的条件的字母/数字串.如果匹配,它将调用Identifier()函数,此函数把yytext(保存当前记号的文本)的内容赋复制到一个新字符数组.词法分析器返回ID记号,主调函数可以使用'yylval->str'指针来访问标识符非名称.STR对于字符常量实现同样的功能.

下一行规则处理注释,换行和空白.注意行号将被计数,将来我们在出错信息中将使用它.最后一行告诉LEX如果输入不能满足上面所有的规则(表达式"."的意思是:除了'/n'以外的所有字符),我们应该返回一个错误记号,然后让主调函数决定作什么.

LEX的源程序可以使用下面的命令行来编译成LEX.CPP:
        flex -olex.cpp string.l

ZIP中还包含一个MSVC 6.0 (string.dsp)的Project文件,我相信它在5.0中也能工作,但是我不确定.Project为string.l设置了一个自定义命令行,所以它可以被自动编译.

不幸的是LEX使用一个非标准的头文件,unistd.h,它不能在windows中使用.在主目录中有一个空的unistd.h文件,请添加主目录到include路径中(in MSVC:Tools->Options->Directories->Include).

lex.cpp包含一个满足我们规则的完整的词法分析器.它是那么简单!主程序只是使用词法分析函数读取一个记号,然后显示记号的名字和值(它是ID还是STR).你可以试着加入一些测试数据,然后观察词法分析器如何处理它们;随机的字符序列通常被识别为ID,我们不使用的字符,例如'$'引发一个ERROR_TOKEN.你也可以试试example.str (在主目录).

情况会变好的
bl_line.jpg (1610 bytes)

好吧,我们现在有了一个可以"读"的程序.遗憾的是它对它读的是什么和这些是否符合我们的标准依然没有概念.它只是接受它知道的一些记号.

看来它需要知道语法,惊人的巧合,语法正是我们下一部分将要讨论的.下一个组件是语法分析器,它的功能是找出程序的结构并且检查语法.

这样就变得真正有趣了.我们将能使程序成为一个编译器,它将接受一些东西,并不只是因为它可以接受几乎所有东西,而是因为它知道这个程序的语法是正确的.我知道你肯定和我一样激动,但是我不得不等到下一部分...

Quote!
gl_line.jpg (1418 bytes)

"And so it was only with the advent of pocket computers that the startling truth became finally apparent, and it was this:

Numbers written on restaurant bills within the confines of restaurants do not follow the same mathematical laws as numbers written on any other pieces of paper in any other parts of the Universe.

This single fact took the scientific world by storm. It completely revolutionized it. So many mathematical conferences got held in such good restaurants that many of the finest minds of a generation died of obesity and heart failure and the science of maths was put back by years."

HHG 2:7

燕良的附注:词法分析的手工设计举例
gl_line.jpg (1418 bytes)

程序的功能是把下面这些实常数转换成相应的科学计数表示:

  • "Pi"转换成0.314159265359E1
  • "E"转换成0.271828182846E1
  • "Degree"转换成0.174532E-1
  • 一般的实常数按值转换,例如456==>0.3456e4,0.0098==>0.98E-2

设计思路:

Pi,E,Degree可以当作关键字来处理,不是本程序的主要部分,本程序的主要功能是识别一下各种形式的实常数:

  • a.
  • b.
  • a.b
  • a.E[+/-]c
  • .bE[+/-]c
  • a.bE[+/-]c
  • aE[+/-]c

识别上述形式实常数的DFA为:
DFA.jpg (34762 bytes)

参见程序:CONSTANT.zip

附送另一个程序,识别C语言源程序的LEX源程序.

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值