让 Lua 支持中文变量名

在做策划表格解析的时候,我们希望可以在表格里直接填写一些脚本代码。我们的脚本语言使用的 Lua ,所以,直接填写 Lua 代码最为简单。但是,策划同学强烈需要在脚本中直接使用中文。而 Lua 原生并不支持使用中文作为变量名。一开始我们使用了一些变通的方案:比如建立一个字典,把中文词通过程序替换成相应的拼音。倒也能工作。

昨天在午饭途中的电梯里,我想到了另一个方案,用了一个下午实现出来验证可用。

修改 Lua 的语法解析代码,让其支持汉字并非难事。但我不太想通过给 Lua 打补丁,修改 Lua 语言的方式来做这件事情。即,我不想因为这个项目为 Lua 创造一门方言。但是,我们却可以把策划表格中填写的代码当成一种 DSL ,正如之前我实现的公式解析 那样。把这部分用 Lua 的方言来实现,把修改的影响减少到最小,而不蔓延到整个系统的实现语言中去,或许是个更好的方法。

因为 Lua 是否支持中文变量名,只是一个语法解析层面的问题。到了虚拟机解析 bytecode 层面就不存在了。即,我们修改 Lua 的实现,让它支持中文变量名,它解析源代码生成的 bytecode ,是完全可以直接在未修改过的 Lua 环境中运行的,甚至连调试信息都完全兼容。

我们可以在系统的 Lua 环境中以 Lua 库的形式再嵌入一个修改过的 Lua 解析器,用这个支持汉字变量名的解析器来解析从策划表格中读出来的脚本,生成 bytecode ,然后再在母体中运行它们。正好之前制作了一个多 State 的 共享数据库 用来储存表格数据,完全可以在初始化数据库时使用修改版的 Lua 解析器。

在未修改的 Lua 环境中嵌入另一个版本修改过的 Lua 虚拟机在链接设置上需要特别小心。因为这是两个版本的 Lua 却有相同的 API 。我的做法是先定义一组 C 接口,仅用来编译 Lua 代码:

struct code_state * code_open(void *buffer, size_t sz);
const char * code_load(struct code_state *L, const char * source, size_t *dump_sz);
void code_close(struct code_state *);

这里,struct code_state 其实就是 lua_State 但换个名字以示区分。我在定义接口时,考虑到希望更好的控制内存,在初始化的时候,由外部传入解析中需要用到的内存块。利用 lua 可直接定制 Alloc 的特性(实现一个简单的 bump allocator),让这个独立的 Lua 虚拟机资源使用高效可控。

我把这组 API 实现在一个独立的动态库中,静态链接修改过的 Lua lib ,并不导出任何 Lua 相关的 api 。这样就和母体的 Lua 环境绝缘了。

第二步,实现一个标准的 Lua 扩展库,动态链接前面这组 C 接口,就可以方便的在母体的 Lua 环境中加载支持汉字变量名的 Lua 代码。


如何修改 Lua 源代码支持汉字变量名呢?

Lua 的源代码结构非常清晰,做到这一点相当简单。以 Lua 5.2 为例,语法解析代码在 llex.c 中,但阅读一下就可以发现我们并不需要修改这个文件。Lua 是通过自己定义的 lislalpha lislalnum 两个函数来判断变量名的。

这两个函数定义在 lctype.h 中,我们只需要修改这个文件即可。

下面,我希望让 Lua 认为 UTF-8 中汉字字符也通过 lislalpha 的检查。关于 UTF-8 汉字的编码规则,可以参考之前我写过的一篇 blog 。虽然不太严谨,我直接把 0x80-0xbf 0xe0-0xef 段的字符全部认为是汉字。

根据配置,Lua 定义了两个版本的 lislalpha 。当系统使用标准 ASCII 字符集时,Lua 使用自己优化过的查表版本;否则则调用系统的 isalpha 函数。对于后者,Lua 还检查了下划线,我们只需要追加汉字集的检测即可。

对于前一种情况,可以修改 lctype.c 中 Lua 自己的查表实现,也就是一张表。

在这张表里,Lua 定义了单个字节每个编码的属性,用位域表示的。支持判断一个字符是否是 ALPHA ,是否为数字,或是 16 进制数字,是否可以打印(这在 Lua 标准库中输出字符串有用到)等等。

我们只需要修改这张表就可以了。把第 8 到 B 行,以及 E 行全部修改为 0x01 或是 0x05 (0x05 可以让 Lua 认为汉字是可打印的)。