loop_in_codes

低调做技术__欢迎移步我的独立博客 codemaro.com 微博 kevinlynx

#

强大的bcb

早就听说bcb(borland c++ builder)是一个强大的RAD开发工具,也早就听说曾经的borland搞出的编译器堪称经典。

恰好最近在做一个GUI工具,想在界面开发上尽量快一点。每一次用上MFC都让我觉得浑身难受,总有些常用的

界面功能它就是没有。在接口实现上,MFC基本上就只是封装了WIN API而已。想想世界上还有什么强大的GUI库,

找了一下,其实不管GUI库封装的怎么样,我更多地还是需要一个工具,能够快速地堆积出界面。

 

于是,在网上下载了被国人精简了的bcb2009。然后,噩梦开始了。首先,我需要把逻辑层代码(也就是实现具体

功能的那一层)移植到BCB下。然后得到了很多和语法相关的编译错误:

 

1.
E2397: Template argument cannot have static or local linkage

这个错误发生于:

void func()
{

    struct Info

    {

        

    };

   std::queue<Info> abc;
}

它的意思是,模板参数必须是全局链接的,总之它不允许std::queue的参数是一个在函数内部临时定义

的类型(谁来告诉我这是C++标准)。

 

2.

E2357 Reference initialized with 'FileLoader::RawData', needs lvalue of type 'FileLoader::RawData'

这个错误发生于:

FileLoader::RawData FileLoader::GetRawData() const;

FileLoader::RawData &raw = loader.GetRawData(); //不能用引用

很久没看C++书,所以,谁又来告诉我C++标准里,这里到底能不能用引用?

 

3.

E2515 Cannot explicitly specialize a member of a generic template class

这个错误发生的情景更复杂些:

template <typename _Tp>

class Test
{

   template <typename _U>

   class Other;

   template <>

   class Other<void>

   {

   };
};

意思是说,我不能在一个模板类里特化成员模板类。谁又来告诉我标准规定的是什么?

 

4.

void func( Obj &a )
{
}

func( Obj() );这个也被视为错误。必须得在调用func之前自己定义个临时变量。

 

5.

我曾经留下了关于宏递归的一些代码,被用在我写的lua-binder和lua-caller中自动生成代码。这下好了,

BCB开始警告我,我的这些宏不能工作了。它和MSVC在某些事情上分歧可真是大:

#define PARAM( n ) ,typename P##n //注意这个宏包含一个逗号

#define CHR( x, y ) CHR1( x, y )

#define CHR1( x, y ) x##y

#define BCB_ERROR( a, b ) CHR( a, b )

BCB_ERROR( 1, PARAM( 1 ) ) 当这样使用宏时,基于我在GNU C上看到的关于宏的规则,会先展开

PARAM(1),于是得到BCB_ERROR( 1, ,typename P2 )。然后,BCB认为PARAM(1)展开的逗号需要参与

BCB_ERROR的展开了。于是,我的整个宏库无法工作了。

关于这个问题,我直接用MSVC写了个生成器,让MSVC替我生成各种参数的lua-binder和lua-caller,然后

写成外部头文件,最后直接在BCB里包含了这些头文件。从而使我的lua-binder和lua-caller可以继续使用。

 

然后,我的1W多行代码终于在BCB下50多个WARNINGS的提示下编译成功了。怀揣着兴奋的心情,想自己终

于可以rapid开发界面了。创建了个VCL FORM APPLICATION,噩梦又开始了:

 

1.

BCB莫名其妙地在我编译一个CPP文件时给出如下提示:

F1004 Internal compiler error at 0x59b4ea8 with base 0x5980000

看起来像是BCB的编译器给崩溃了。囧。google了一下,发现不是我人品问题,很多人遇到相同的问题。

别人给出的解决方案是:restart your bcb。从昨天晚上到现在为止,这个错误发生了好几次。

 

2.

new std::ofstream();会让程序崩溃,往不该写的地方写了东西。我就奇怪了,你BCB自己带的C++IO实现,

难道还有BUG?再次google,还真发现是BCB自己的BUG,并且在几个版本之前就存在这个BUG。那个天真

的老外还说希望在BCB2009下能被修复。修改方案如下:

1)xlocale文件里把这句话注释了:*(size_t *)&table_size = 1 << CHAR_BIT;

2)xlocale里把成员_Id_cnt访问属性改为public,然后在自己的文件里定义一次。

 

3.

程序终于可以运行了。但是BCB的IDE环境总是不那么贴心。我移动了几个窗口改成我习惯的样子,但是一重启

居然又恢复成default(难道是因为盗版)。它的智能提示似乎总是跟着鼠标指针,有时候指向某个符号,鼠标

就显示忙。为了提示某个类的成员,某个函数的原型,BCB偶尔都会卡一下。其实我不介意我的编辑器没有这

些提示功能,在MSVC下我也从不用VA来帮我写代码。我甚至不厌其烦地在VIM下敲代码切窗口去看函数原型,

但是,你他妈作为一个IDE就得像个IDE的样子,要不,你干脆关掉所有功能,别给我卡就行了。

 

这个时候我开始怀疑选择BCB会不会是一个错误的开始,或者说在使用某个东西时,总会带着使用其他同类东西

的感觉甚至偏见去看待这个新事物。但是,在我想坚持继续使用BCB时,我一compile,它又提示我:

F1004 Internal compiler error at 0x59b4ea8 with base 0x5980000

posted @ 2009-08-15 11:17 Kevin Lynx 阅读(5744) | 评论 (17)编辑 收藏

指针和模块健壮

仔细想想能导致一个C++程序崩溃的几乎90%原因都是跟指针有关。空指针野指针,一不小心
程序就崩了。写C++程序的人基本上都知道这个问题。在我们周围避免这些问题的常规方法
也很多,诸如auto_ptr(及其他基于template的原始指针wrapper)、SAFE_DELETE。当然也
会有很多人在实现一个函数时会很勤劳地对每一个parameter进行合法判断。

其实,我们都知道,auto_ptr这些东西始终是无法避免野指针和空指针带来的灾难。
SAFE_DELETE也不能阻止别人使用这个空指针。

在我看过的一些开源项目的代码中,这些代码给人的感觉就是别人总能详细地掌控各种资源
(包括指针及其他变量)的使用情况。相比之下,公司隔壁组的老大则显得保守很多。他要
求我们几乎要对所有指针的使用进行空值判断(野指针也判断不了),当然,各种成员变量
也要进行即使现在看上去没多大用的初始化。

也许,这样做后程序是不会挂掉了。但是,就我们的观点来看,这样反而会隐藏一些BUG。
为什么我们不能详尽地去管理一个指针?一个指针变为空了,总是因为在这之前发生了错误
。当然,野指针本身就是愚蠢代码产生的东西,这里没必要讨论。空指针之所以为空,也是
因为在很多时候我们把空作为失败/错误/无效的标志。

恰好上周我的一些代码就真的在空指针上出现了问题。外网的服务器随时会因为玩家的一些
临界操作行为而崩溃掉。虽然我通过修改脚本来屏蔽这个问题(因为不能说停机维护就停机
维护),但是总感觉程序是不安全的。人不吃点教训绝对不学乖。

后来我对这个问题彻底思考了一下。很多程序员都自认聪明。在写C++程序时,我从来不提
供没用的public接口,尤其是set/get。我也从来不对没必要的成员变量进行初始化。我给
的理由是对于这些东西我都有很清晰的把握,我为什么要做stupid的事情?

但是,我几乎从来没有界定,指针在哪些情况下需要去判断为空?函数的参数绝对不需要。
假如函数的参数就是个空指针,那是client程序员的责任。仅供模块内使用的指针(包含其
他资源)在内部使用时也不需要去判断。如果去判断了,那说明你对你自己写的模块都缺乏
精确的把握,证明你的设计思维不够清晰。

什么时候需要判断?当指针依赖于外部环境时,例如读配置文件、载入资源,因为外部因素
不确定不在自己控制范围内,那么进行判断。同样,当使用了其他模块返回的指针值时,也
需要判断。这个其实和“外部环境”属于同一种情况。因为我们对其他模块也不清楚,更为
隐蔽的是(随着其他模块的改变,将来会在你的模块里爆发崩溃错误),其他模块由别人维
护,其变化更不受自己控制。之前我对这一点界定不是很清楚,这也是我犯错的原因。

现在想想,像游戏服务器这种程序,里面塞着各种各样的游戏功能。无论是哪一个模块出现
个空指针访问出错的问题,都会直接让服务器崩掉。关键是这个结果经常伴随着玩家的损失
。所以理想状态下,把每一个模块都放置在单独的进程里,确实是很有好处的。

posted @ 2009-06-28 19:35 Kevin Lynx 阅读(2309) | 评论 (5)编辑 收藏

GDI+中从内存读取图片/保存图片到内存

要给项目中增加一个新的模块,需要先在服务器端做一些图片处理相关的工作。本来,对图片

做一些诸如ALPHA混合旋转缩放的操作,在游戏客户端应该是很容易的事。但是这事要在服务

器做,就不得不引入一些第三方库。反正我们的服务器运行于WINDOWS下,这里又需要处理

JPG图片的加载,我就考虑到了GDI+。

 

在这之前对GDI+没有过任何接触。直接翻了MSDN,还好居然有个一系列的usage。GDI+的Image

本身支持JPG的直接载入。但是并没有我理想中的CreateFromMemory( const void *buf )接口。

看起来唯一可以从内存创建Gdiplus::Image对象的方法是从一个叫IStream*的COM东西。我揣摩

微软为什么没有提供我理想中的那个接口,或者说要把GDI+设计成这样,可能还是考虑到对多语

言的支持。于是问题转换为如何将一个C语言的const void*转换为IStream*。我甚至在开始的时候

感觉到是不是要自己实现个Stream。后来在google上找到了一个似乎是标准的方法:首先创建个

HGLOBAL对象,然后通过GlobalLock就可以将一个C的const void*直接memcpy到这个HGLOBAL

里,最后,通过CreateStreamOnHGlobal这样的接口就可以得到一个IStream。

 

恶心的是,基于之前对服务器内存使用的优化,我现在对于内存的使用非常敏感(谁说现在内存

大了就可以任意malloc了??)。上面那个过程对于资源的管理在MSDN文档中似乎显得有点

模糊。CreateStreamOnHGlobal函数的第二个参数指定当IStream->Release的时候,是否会自动

删除这个HGLOBAL对象。我虽然对COM不懂,但也知道它的对象是基于一种引用计数的管理方式。

逐字看了下文档,发现一个final单词,原来是IStream->Release最后一次释放时,会同时释放掉

这个HGLOBAL对象。更让人发指的是,我猜测Image( IStream * )来创建Image时,Image又

会对这个IStream进行一次AddRef。我发觉MSDN对于Gdiplus::Image::FromStream函数的说明

也有点模糊。我揣摩使用FromStream获得的Image*,是否需要手动去delete?这个地方的内存

资源管理,一定得搞个水落石出。结果是,FromStream的实现就是简单地new了个Image。而

Image内部肯定会对IStream进行AddRef,并且,如果在Image销毁前销毁这个HGLOBAL,这个

Image基本也就废了。

 

也就是说,Image本身不对HGLOBAL中的图片数据进行复制。囧。别想让我再写个wrap class把

HGLOBAL和Image纠结在一起,简单考虑,将CreateStreamOnHGlobal第二个参数设为TRUE。

 

要将一个Image保存为一段内存,也比较麻烦。我的方法和google上的相同。当然,微软的库依

然让我在很多细节上栽跟斗(如前所说,可能这是基于多语言支持的考虑)。首先需要创建个空

的IStream,即CreateStreamOnHGlobal第一个参数为NULL。然后将Image Save到这个IStream。

再根据该IStream::Seek获取其大小,自己再分配段内存,最后IStream::Read读取进来。同样,

需要注意相关内存资源的管理。

 

下午简单把以上两个过程简单封装了下。

下载代码。

posted @ 2009-05-28 20:23 Kevin Lynx 阅读(13122) | 评论 (4)编辑 收藏

DNF游戏声音资源提取

    上午公司断网,晚上失眠头痛没精神,于是随便打开了DNF游戏目录下的资源文件。以
前一直对提取游戏资源存在好奇,需要对一些关键字节猜测其加密方式。
    DNF游戏目录下soundpacks下的npk文件看起来似乎比较简单,这里直接给出文件格式,
懒得写分析思路了。
    文件开头的十六个字节是一个固定字符串:NeoplePack_Bill\0。
    接下来四个字节表示本npk文件里打包了多少个WAV文件。npk文件是一个包含了很多声
音或者图片的打包文件。类似这种打包文件,一般文件头都会保存一个文件列表。而这个列
表里又会附加上偏移量和大小等信息。
    接下来的数据就是这里所说的列表。每一个列表项包含三个数据域:偏移、大小、文件
名。如下示意:

    NeoplPack_Bill\0 (16 bytes)
    file_count( 4 bytes)
    item1:offset(4 bytes), size(4 bytes)
    item2:offset(4 bytes), size(4 bytes)
    ...
    itemn:offset(4 bytes), size(4 bytes)
    ...

    文件列表之后,就是具体的每个文件的内容。开始我还在担心npk会为每一个声音文件
加密。或者只保存声音文件的具体数据,而声音文件文件头则只保存一份(因为所有文件的
文件头很有可能全部是一样的)。后来稍微搜索了下WAV的格式,只需要比对下npk中某一个
文件内容的头部是否和WAV格式的头部相同,就可以基本断定其是否加密。
    结果是,npk对包内的每一个WAV文件没做加密。
    然后立即写了个程序,根据文件列表中的偏移值和大小值,将每一个WAV单独取出来,就
OK了。
    完整的格式为:

    NeoplPack_Bill\0 (16 bytes)
    file_count( 4 bytes)
    item1:offset(4 bytes), size(4 bytes)
    item2:offset(4 bytes), size(4 bytes)
    ...
    itemn:offset(4 bytes), size(4 bytes)
    file1
    file2
    ...
    filen

    我想图片资源也应该差不多,不过图片资源肯定要复杂些。下午公司网络好了,网上搜
索了下,发现居然已经有了DNF资源提取工具了,唉。

    提供下源代码和MingW编译好的可执行文件,另声明:本文及相关工具代码只作学习研究
用,任何后果与作者无关。

posted @ 2009-05-12 22:50 Kevin Lynx 阅读(5214) | 评论 (2)编辑 收藏

kl中的错误处理

kl中的错误处理

    之前我一直说错误处理是kl里的软肋,由于一直在关注一些具体功能的改进,也没有对
这方面进行改善。

    我这里所说的错误处理,包括语言本身和作为库本身两方面。
    语言本身指的是对于脚本代码里的各种语法错误、运行时错误等的处理。好的处理应该
不仅仅可以报告错误,而且还能忽视错误让处理过程继续。
    而把kl解释器作为一个库使用时,库本身也应该对一些错误情况进行报告。

    整体上,kl简单地通过回调函数指针来把错误信息传给库的应用层。而因为我希望整个
kl实现的几层(词法分析、语法分析、符号表、解释器等)可以尽可能地独立。例如虽然语
法分析依赖于词法分析(依赖于词法分析提供的接口),但是因为词法分析并不对语法分析
依赖,所以完全可以把词法分析模块拿出来单独使用。所以,在日志方面,我几乎为每一层
都附加了个error_log函数指针。
    而用户层在通过kllib层使用整个库时,传入的回调函数会被间接地传到词法分析层。
实际上,当kl作为一个库时,kllib正是用于桥接库本身和用户层的bridge。

    另一方面,语言本身在处理错误的脚本代码时,错误分为几大类型层次:
    1.词法错误 lex error,如扫描字符串出错
    2.语法错误 syntax error,整理语法树时出错
    3.运行时错误 runtime error,在解释执行代码时出错
    4.库错误 lib error,发生在kllib这个bridge层的错误
    kl在报告错误信息时,会首先附加该错误是什么类型的错误。

    这里最麻烦的是语法错误的处理。因为语法分析时发生错误的可能性最大,错误类型也
有很多。例如你少写了分号,少写了括号,都会导致错误。这个阶段发生错误不仅要求能准
确报告错误,还需要忽略错误让整个过程尽量正确地下去。

    语法分析阶段最根本的就是符号推导(单就kl的实现而言),所谓的符号推导是这样一
个过程,例如有赋值语句:a = 1;语法分析时,语法分析器希望(所谓的推导)等号后面会
是一个表达式,当分析完了表达式后,又希望接下来的符号(token)是分号作为该语句的结
束。
    所以,klparser.c中的syn_match正是完成这个过程。每次你传入你希望的符号,例如
分号,该函数就检查词法分析中当前符号(token)是否是分号。当然,对于正确的脚本代码,
它是一个分号,但是如果是错误的代码,syn_match就会打印诸如:
    >>syntax error->unexpected token-> ....
    即当前的符号是不被期望的。

    上面完成了错误的检测。对于错误的忽略,或者更高级点地对错误的校正,kl中处理得
比较简单,即:直接消耗掉这个不是期望中的符号。例如:
    a = 1 /* 忘加了分号 */
    b = 1;
    上面两句代码被处理时,在处理完a=1后,发现当前的符号(token)b(是一个ID token)不
是期望(expect)中的分号,首先报告b不是期望的符号,然后kl直接掠过b,获取下个符号=。
然后处理a=1这个过程结束。当然,下次处理其他语句时,发现=符号,又会继续发生错误。

    错误信息中比较重要的还有行号信息。之前kl这方面一直存在BUG,我在写贪食蛇例子
的时候每次新加代码都不敢加太多。因为解释器报告的错误行号总是错误的,我只能靠有没
有错误来找错误,而不能通过错误信息找错误。
    行号信息被保存在词法分析状态中(lexState:lineno),语法分析中获取token时,会取
出当前的行号,保存到语法树树节点中。因为包括解释模块都是基于树节点的,所以词法分
析语法分析解释器三层都可以准确报告行号。

    但是之前解释器报告的行号始终很诡异。症结在于我在载入脚本代码文件时,以rb方式
载入,即二进制形式。于是,在windows下,每行文本尾都会有\r\n两个字符。而在词法分
析阶段对于行号的增加是:
    case '\n':
    case '\r':
        ls->lineno ++;
    不同OS对于文本文件的换行所添加的字符都不一样,例如windows用\r\n,unix系用\n
,貌似Mac用\r。所以,词法分析这里写应该可以准确地处理行号。

    但是对于windows,这里就直接将行号增加了两次,所以也就导致了行号出错的问题。查
了下文档,发现以文本方式打开文件("r"),调用fread函数读入文件内容时,就会自动把
\r\n替换为\n。

    代码改后,又出问题。这个时候,通过fseek和ftell获取到的文件尺寸,貌似包括了
\r\n,而fread出来的内容却因为替换\r\n为\n而没有这么多。
    不过文件载入不属于kl库本身,kl只接收以字符串形式表示的脚本代码,所以也算不了
核心问题。

    同样,最新代码可以从google SVN获取。当然,我也在考虑是否换一个新的项目地址。

posted @ 2009-03-26 17:17 Kevin Lynx 阅读(3145) | 评论 (0)编辑 收藏

kl sample:贪食蛇

 

    貌似最近CPPBLOG写一门脚本语言比较流行,连我这种山寨程序员都搞出一个像C又像
BASIC的所谓脚本语言,可见其流行程度。


    这个kl脚本例子,是一个具有基本功能的贪食蛇游戏。这个例子中使用了两个插件:
HGE引擎、以及一个撇脚的二维数组插件。因为kl对于数组的实现不是那么漂亮,而我实在
不想因为加入二维数组的支持而让代码看起来更乱,所以直接不支持这个特性。考虑到二维
数组的应用在一些小游戏中还是比较重要(例如这个贪食蛇,总需要个容器去保存游戏区域
的属性),所以撇脚地加了个支持number的二维数组插件。

    HGE插件我只port了部分接口,也就是注册了一部分函数到脚本里,提供基本的贴图功
能。(port--我实在找不到一个合适的词语来形容这种行为---HGE到一门脚本语言里,我似
乎做过几次)

    不知道有没必要提供贪食蛇的实现算法,这似乎说出来有点弱智。- - 不过为了方便别
人阅读kl脚本代码,我还是稍微讲一下。游戏中使用一个二维数组保存整个游戏区域,所谓
的游戏区域就是蛇可以活动到的地方。每一个二维数组元素对应游戏区域中的一个格子,姑
且称为tile。每个tile有一个整数值表示其属性,如BODY、WALL、FOOD、NONE。蛇体的移动
归根结底就是蛇头和蛇尾的移动。蛇头和蛇尾属性一样,但是蛇头负责把所经过的tile设置
为BODY,而蛇尾则把经过的tile设置为NONE。蛇头的移动方向靠玩家控制,每次蛇头转弯时
,都会记录一个转弯点到一个队列。转弯点包括转弯XY坐标以及转向的方向。蛇尾每次移动
时都会检查是否到达了一个转弯点,是的话就设置自己的移动方向为该转弯点记录的方向。

    虽然我写了kl这个脚本语言,但是语言特性并不是我设计的。我只是取了C语言的一些
特性。所以在写这个sample的时候,我对于kl这个脚本语言的感觉,就是一个像basic的C。
因为它太单一,就像BASIC一样只拥有语言的一些基本功能,不能定义复杂的结构,没有天
生的对各种数据结构的支持(例如某些语言直接有list, tuple之类)。

    以前中学的时候在电子词典上用GVBASIC写小游戏,当时除了BASIC什么也不知道。今天
写这个贪食蛇例子,感觉就像以前用BASIC。

    回头说说一些kl脚本里的特性。从这个例子里(见下载包里的snake.kl),诸如while,
for,if...else if...被支持(之前发布的版本里还不支持for和else if)。全局变量支持
赋初值(上个版本不支持)。当然,还演示了如何使用插件函数。

    但是,仍有一些特性在我的懒惰之下被置之不理。例如return后必须跟一个表达式,这
意味着单纯的return;将被视为语法错误。对于if( a && b ),kl会计算所有的表达式,而
别的语言也许会在a会false后不计算b,这也许不算个问题,但起码我还没修正。还有,kl
内部对于错误的报告依然没被修复,少打一个分号你会得到一系列错误的报告,但是却没有
准确的行号。甚至,你会看到解释器崩掉。不要紧,在我心里,它作为当年电子词典上那个
GVBASIC而言,已经很强大的了。:DD

    最近接触了很多UNIX和GNU之类的东西,发觉没有提供版权说明的‘开源’,原来都是伪
开源。虽然我也想按照GNU编码标准里所说为kl的发布包里附加Changelog之类的说明,但是
出于懒惰,还是以后再说吧。同样,这次提供的下载里包含了一些编译好的东西,所以我不
保证它在你的机器上依然可以运行。我使用了MingW来编译这些,并且提供有点丑陋的Makefile。
HGE使用了1.81版本。
    贴张图给懒得下载的人:

snake_screenshot

    下载例子,包含脚本代码。

    如果要获取kl实现代码,建议从我在google的SVN获取:
http://code.google.com/p/klcommon/

posted @ 2009-03-25 21:17 Kevin Lynx 阅读(4348) | 评论 (1)编辑 收藏

实现一种解释性脚本语言(七)

author: Kevin Lynx email: zmhn320#163.com date: 3.12.2009

脚本与C语言交互

    这其实是这一系列的最后一篇,因为我觉得没什么其他需要写的了。
    一般而言,脚本语言同C语言交互,包括在C语言中注册C函数到脚本,从而扩展脚本的
功能,以及在C语言中调用脚本函数。
    为了扩展脚本的功能,这里引入插件的概念。kl在这方面大致上实现得和lua相似。kl
支持静态插件和动态插件。
    在C语言中调用脚本函数,kl中提供了一些简单的接口用于满足需求。

静态插件

    静态插件其意思是在C代码中注册函数到脚本中,并随脚本库一起编译链接成最终执行
程序。因为其绑定是在开发一个程序的过程中,所以被称为静态的。
    一个插件函数,指的是可以被注册进脚本的C函数。这种函数必须原型一样,在kl中这
个函数的原型为:typedef struct TValue (*kl_func)( ArgType arg_list );   
    当你定义了一个这样的原型的函数时,可以通过kl库提供的:
    int kl_register( struct klState *kl, kl_func f, const char *name )来注册该
函数到kl脚本中。该函数参数很简单,第三个参数指定注册进脚本中时的名字。

    原理比较简单:在解释器中保存着一个插件符号表,该符号表的符号名就是这个函数提
供的名字,符号对应的值就是第二个参数,也就是插件函数的函数地址。
    解释器解释到函数调用时,先从插件符号表中查找,如果找到符号,就将符号的值转换
为插件函数,并调用之。

    插件函数的参数其实是一个参数链表。脚本里调用插件函数时,所传递的参数将被解释
器整理成参数链表并传递给插件函数。kl库中(集中在kllib.h中)提供了一些方便的接口用
于获取每个参数。
    插件函数的返回值也将被解释器转换为脚本内部识别的格式,并在必要的时候参与运算

动态插件

    动态插件同静态插件的运作方式相同,所不同的是动态插件的插件函数被放在动态运行
时库里,例如windows下的dll。
    kl插件编写标准里要求每个动态插件必须提供一个lib_open函数。kl解释器(或者kl库
--当被用作库时)载入一个动态插件时,会直接调用lib_open函数。lib_open函数的主要目
的就是把该插件中的所有函数都注册进脚本里。

    因为动态插件在设计之初没有被考虑,所以我并没有为kl加入一些原生的关键字用于导
入动态插件,例如import、require之类。我在静态插件层次提供了这个功能。即我提供了
一个libloader静态插件,链接进kl解释器程序。该静态插件提供脚本一个名为import的函
数。该函数负责动态载入dll之类的动态库,并调用里面的lib_open函数完成动态插件的注
册。

C程序里调用脚本函数

    这个比较简单,通常C语言想调用一个脚本函数时,会传入脚本函数名。因为脚本函数名
都保存在全局符号表里,kl库从全局符号表找到该函数符号,并转换其值为语法树节点指针
,然后传入解释器模块解释执行。
    kl库提供struct TValue kl_call( struct klState *kl, const char *name, ArgType args );
用于在C里调用脚本函数。

代码导读

    kllib.h/kllib.c作为一个桥接层,用于封装其他模块可以提供给外部模块使用的接口,
如果将kl作为一个库使用,用户代码大部分时候只需要使用kllib.h中提供出来的接口。
    源码目录plugin下的kllibbase.c中提供了静态插件的例子,kllibloader.c提供了装载
动态插件的功能。
    源码目录plugin/hge目录下是一个封装2D游戏引擎HGE部分接口到kl脚本中的动态插件
例子。
    源码目录test/kl.c是一个简单的kl解释程序,它用于执行一段kl代码。这个程序同之前
说的解释器不是同一回事。当我说到解释器时,它通常指的是klinterpret.c中实现的解释
模块,而解释器程序则指的是一个使用了kl库的独立解释器可执行程序。

posted @ 2009-03-12 09:35 Kevin Lynx 阅读(4908) | 评论 (4)编辑 收藏

实现一种解释性脚本语言(六)

author: Kevin Lynx email: zmhn320#163.com date: 3.11.2009

解释器

    整理出语法树后,我们就可以根据语法树,并配合符号表开始解释执行脚本代码。这就
是接下来要涉及到的解释器。

工作原理

    在第四节中讲语法树时,其实就已经提到解释器的大致工作原理。
    一个kl的hello world例子代码大致为:
    function main()
    {
        print( "hello world\n" );
    }
    在第二节中我描述了kl代码整体上的结构,是以函数为单位的。因此,对于一个完整的
kl脚本代码,其经过语法处理后,将建立一棵大的语法树,该语法树大致结构为:
    fn1_node
        stmt_node1
        stmt_node2
        ...
    fn2_node
        stmt_node1
        stmt_node2
        ...

    fn1_node和fn2_node同属于同一个作用域,fn1_node的sibling指针指向fn2_node,即在
整个树结构中,每一个node通过child[3]成员连接其子节点,通过sibling指针连接其相邻
的节点。   
    解释器解释执行时,就是从main函数所对应的节点开始递归执行的。对于每个节点,都
可以知道该节点对应了哪种程序逻辑:是加法运算、比较运算、还是一些控制语句等等。
    以这样的控制语句举例:
    if( 1 ) print( "true" );
    对if语句而言,其语法树结构为:
          if_node
         /   |    \
        /    |     \
    con_exp    then_stmt else_stmt

    即,if语句有最多有三个子节点(child[3]),child[0]指向if的条件表达式,child[1]
指向条件表达式为真时执行的语句序列,如果if有else部分,那么child[2]就指向else部分
的语句序列。
    那么,在发现某个节点是if节点时,就首先计算其条件表达式节点。这个节点的计算方
式同脚本中其他所有表达式的计算方式相同,当然,它也是一个递归操作。计算完后判断该
表达式的值是否为真,为真则递归执行if节点的child[1]节点,否则检查是否有else节点,
有的话就执行child[2]节点。

    其他所有节点的解释方式都是相同的。


解释器环境

    解释器环境指的是解释器在解释执行脚本代码时,所需要的运行时环境。kl中主要是符
号表信息。一个解释器环境会有三个符号表:全局符号表,主要保存全局变量以及脚本函数
符号;函数局部符号表,在解释调用一个脚本函数时,会建立临时的符号表;插件符号表,
用于保存插件注册的函数。

如何解释执行函数

    函数主要有两大类型:脚本内定义的函数以及插件注册进符号表的函数。无论是哪种函
数,都会在符号表中建立对应的符号。对于前者,符号被保存于全局符号表,其保存的内容
是该函数节点的节点指针;而对于后者,则保存的插件函数的函数地址值。

    每一次解释器解释到一个函数调用节点时,会优先在插件符号表中查找该函数符号。如
果找到,就将其值转换为约定的插件函数类型(如同lua里注册的C函数一样),然后整理参
数调用之。这个时候代码执行权转接到插件函数里。如果没找到,就在全局符号表里查找,
找到后就强转为语法树节点指针,并解释执行该节点下的语句。

代码导读

    解释器的代码位于klinterpret.h/klinterpret.c中。整体上而言没什么特别的地方,
主要是利用语法树的特点。
    完成了这一节后,kl就已经可以解释执行所有的脚本语句。当然,因为没有输出功能,
只能在调试器里看看计算结果。下一节里会讲到将脚本结合进C语言,从而可以让C语言注册
所谓的插件函数到脚本里,也就可以让脚本具有print这样的输出函数。

posted @ 2009-03-11 09:12 Kevin Lynx 阅读(3602) | 评论 (0)编辑 收藏

实现一种解释性脚本语言(五)

author: Kevin Lynx email: zmhn320#163.com date: 3.10.2009

符号表

    在上一节中,当我们的解释器解释执行age=age+1这个语法树时,会涉及到变量age的值
。实际上我们还需要个保存脚本中相关变量的模块,当我们的解释器获取到一个ID树节点时
,需要从这个模块中获取出该变量的值,并参与运算。
    这个我称之为符号表。我想到这里,我所说的概念很可能和教科书有点不一样了。

什么是符号表?

    符号表(symbol table)就如同其字面意思一样,是一个表,更宽泛地说是一个保存符号
的容器。
    脚本中诸如变量函数之类的东西都算作符号,例如age。符号表就是保存这些符号的容
器。
    在kl中,符号表保存着某一个作用域里的变量。其全局符号表还保存着函数符号,对于
函数符号而言,其值为语法树树节点的指针值。当调用一个函数时,将该值转换为树节点,
然后执行。当然,这应该算做解释执行一节的细节,不多说。

    再明确下符号表的作用,举例,在上一节中,涉及到这么一个例子函数:
    value factor( TreeNode *node )
    {
        switch( node->type )
        {
            case ID:
                /* 在这里,发现一个树节点类型为ID,就需要根据ID对应的名字,也就
                 是age,在符号表中查找age的值 */
                return age;   
        /* ... */
        }
    }
    以上注释阐述了符号表的作用。

符号表的实现

    其实不管符号表如何实现,对于其他模块而言,对符号表的唯一要求就是提供几个类似
这样的接口:
    value sym_lookup( const char *name );
    void sym_insert( const char *name, value val );
    也就是说,提供查找符号值,以及插入新符号的接口。

    在kl中,使用了<编译原理与实践>中相同的符号表数据结构实现。即使用了hash表,
hash数组中每个元素保存的是一个链表头节点。每一个符号字符串通过散列函数得到hash数
组索引,然后在该索引里进行一次线性查找。很典型的hash结构。

    另一方面,因为kl支持全局和函数局部两个作用域。所以kl中有一个全局符号表,用于
保存全局变量以及所有的函数符号;同时每一次进入一个函数时,就会创建一个临时的局部
符号表,用于存储局部变量;后来,为了支持插件,插件函数被特定地保存在另一个全局符
号表里。

代码导读

    kl中的符号表实现代码在klsymtab.h/klsymtab.c中,实现比较简单,无需多言。

posted @ 2009-03-10 08:58 Kevin Lynx 阅读(3190) | 评论 (0)编辑 收藏

实现一种解释性脚本语言(四)

author: Kevin Lynx email: zmhn320#163.com date: 3.9.2009

语法分析

    语法分析接收词法分析阶段的token集合为输入,将这些没有关系的tokens整理为相互
之间有关系的结构。书面点的说法叫语法树。
    每一次让我写这些文绉绉的概念真让我受不了:D。

语法树

    语法树简单来说就是一个以token作为每个节点的树型结构。例如我们有表达式age =
age + 1;,在词法阶段它被整理为token集合:age, =, age, +, 1。那么在经过语法分析后
,这些tokens将被整理为大致如下的树形结构:
        =
      /   \
    age    +
         /   \
       age     1

    整理成这样的结构有什么好处?就kl解释器而言,最直接的好处就是我可以递归地解释
这棵树执行。例如:

    value compute( TreeNode *root )
    {
        /* child[0]保存结果值age,child[1]是那个+表达式 */
        return op_exp( root->child[1] );
    }

    value op_exp( TreeNode *node )
    {
        switch( node->op )
        {
            case '+':
            {
                /* + 表达式必然有左右操作数 */
                value left = factor( node->child[0] );
                value right = factor( node->child[1] );
                return left + right;
            }
        }
    }
    value factor( TreeNode *node )
    {
        switch( node->type )
        {
            case ID:
                /* 查找age的值 */
                return age;

            case CONST:
                /* 1 是常量 */
                return node->cvalue;
        }
    }

    如你所见,当我们完成了语法分析阶段,我们就可以完成我们的解释器了。后面我会单
独讲解下整个解释过程,包括每个模块是如何协作的。我不知道其他解释器是怎么做的,但
是我这样做,起码结果是对的。

如何整理出语法树?

    这里不得不提到所谓的BNF文法,很明显你还是无法从我这里获取编译原理里某个概念
的讲解。我这里提这个概念完全是方便我提到这个东西。
    每一种语言都有其自己的BNF文法,因为万恶的先知告诉我们,每一门语言都需要建立
其语法树。- -!
    就像词法分析一样,因为大部分语言的结构都差不多,所以我觉得词法分析和语法分析
基本上都没有任何特别之处。也就是说,别的语言的BNF你可以直接拿来改改用。
    抄个BNF如下:
    exp -> exp adop term | term
    addop -> + | -
    term -> term mulop factor | factor
    mulop -> *
    factor -> (exp) | number
    这个BNF用来描述一般的算数表达式(+-*/)。简单来说,一门语言的BNF就是用于描述该
语言所有语句的东西,包括if、while、函数定义之类。建议你google一下C语言的BNF,并
改造之用于你自己的语言。

    那么有了BNF之后,该如何整理出语法树呢?
    通常,我们的代码里都会直接有对应exp、term、addop之类的函数。按照我这句话的意
思,上面抄的BNF被翻译为程序代码后,就可能为:
    exp()
    {
        if( ... ) left = exp()
        right = term();
        left addop right;
    }
    term()
    {
        if( ... ) left = term()
        right = factor();
        left mulop right;
    }
    factor()
    {
        if( ... ) return exp();
        else return number;
    }

    (可能还会涉及到EBNF,用于处理重复和选择的一些情况---不用管这句话)

    每一个函数基本上都会返回一个树节点,当然,该节点下可能会有很多子节点。   

总结

    语法分析基本上就是以上信息。它将词法分析输出的token集合整理成一颗语法树。为
了整理出这棵语法树,你需要找一份用于描述你语言的BNF,然后根据BNF翻译成处理代码。

代码导读

    kl中的整个语法分析代码位于klparser.c/klparser.h中,其BNF基本上取自<编译原理与
实践>附录中的C_语言。

posted @ 2009-03-09 11:12 Kevin Lynx 阅读(3622) | 评论 (3)编辑 收藏

仅列出标题
共12页: First 4 5 6 7 8 9 10 11 12