使用Lua来扩展C++程序的方法
介绍
如果用户能够通过一些脚本语言来修改应用本身的行为,那么许多应用可以变得更适合用户使用。一些商业应用就提供了此类便利。例如 Microsoft Office 的 VBA 脚本编程或在视频游戏 World of Warcraft 中使用 Lua 。脚本语言把应用作为一个平台提供一系列终端用户可以获得并操控的服务。
做为嵌入到程序中的语言,我们有很多可用的选择:开源和不开源的脚本引擎,或者可以从头开始创建一个。现在,最为熟知的脚本语言是JavaScript,Lua和Python,还有很多其它的语言等等。在Microsoft Windows平台上 嵌入脚本引擎的一般方法是使用一个 包含脚本引擎动态链接库(DLL)中,然后使用一系列的函数调用访问引擎中的服务。
在这篇文章里面我们将看到如何使用lua5.2脚本引擎嵌入到C++的代码中,我们的测试用例使用的开发工具是Visual Studio 2005.这开始讲解之前我们要从lua官方网站下载一些必要的组件,lua5.2中包含有已经编译好的dll以及头文件和链接用的导入库。需要说明的是当你使用lua嵌入到C++代码的时候,编译出来的可执行文件必须包含有lua的DLL即动态链接库,否则会提示运行出错,缺少必要的dll。这些例子都是用Visual Studio 2005编写,当然对于VS的后续版本是兼容的。
这篇文章向您描述了一个使用lua5.2作为脚本引擎的特定实现,在lua的官方文档(lua.org)以及一些网站中有许多关于这方面的信息,这里仅仅是为你提供一些测试例子,作为你开始lua旅程之前的引导
对于产品开发而言,开发一个完整专业应用程序跟开发单块应用程序有不同的策略和方法。譬如说:开发完整专业应用平台需要对终端用户提供大量丰富的插件工具和服务去实现他们自己特定的领域需求。插件程序的策略和方法被应用在许多的软件中,例如微软的Office,Visual Studio以及集成开发环境Eclipse,甚至是图片处理软件Adobe Photoshop. 网游魔兽世界就提供大量的add-on(插件)给用户,一些其他的游戏提供类似的工具,让玩家可以增加额外的内容从而创造一个与众不同的社区。
作为商业应用程序,GenPOS,有一些特性也是极具工具集的策略的:
- 布局管理器工具然该用户可以通过调整窗口,按钮的位置,文字,内容等属性来调整界面的布局。
- 字符控制功能可以让一些工作流自动化
- 丰富的参数化设计可以更改软件的功能
- 数据库的助记符可以使得显示不同的语言,使得多语言更容易
- 通过远程接口可以动态地改变参数和助记符
- 提供接口从终端获取财物和运行的状态数据
不管怎样,有些模块是被源代码实际控制的,这部分模块的更改需要通过开发部门的协作才能完成软件行为的改变。例如说,打印收据(一些显示内容是从参数或者助记符的输入中获取的)。简短列举这些限制可能包含:
- 现有的字符控制功能缺少状态测试和跳转
- 不能沟通动态地根据现有的状态修改和显示信息
与其试图加强和改进当前的非常简单的控制字符串功能,以提供包含额外脚本功能的销售终端,我们最终决定寻找到其它可能的方案,不需要大量开发到改进,且提供更多的产品改进能力。就像我们引入的布局管理器使客户能够设计自己的屏幕布置和工作流程一样,我们希望有一个相当灵活的机制让经销商的销售终端通过脚本为他们的客户提供增值服务。我们还打算提供足够的应用服务访问权限,让经销商和最终客户将能够修改自己的应用程序的行为。最后,我们要使用有一定程度到用户社区的语言,感兴趣到人们可以使用社区的资源。
我们已经用5.2版本的Lua脚本引擎在POS源代码上做了一些简单的实验来观察向程序中添加功能的难度。从实验结果来看,我们想通过程序服务来展现的各种功能已经是可用的了,并且可以通过对Lua脚本引擎做一些修改就可以使用起来。
使用 Lua 5.2 脚本引擎
Lua 脚本引擎本身是由 C 语言写成的,在 C 或 C++ 中使用 Lua 脚本也相当简单。你在网上也可以找到很多集成了 Lua 5.2 脚本引擎的的程序或程序片段,并且 Lua 5.2 的程序接口和先前 Lua 版本只有在初始化和启动等接口上存在少量变化,所以旧的 Lua 程序可以不做修改或只做很小的修改就可以移植到 Lua 5.2环境下。
基本的初始化步骤如下:
- 使用 lua_newstate() 创建一个新的 Lua 状态机。
- 若有必要,调用 luaL_openlibs() 函数加载 Lua 的标准库。
一旦初始化了 Lua 脚本引擎,你可以通过如下步骤执行一段 Lua 脚本:
- 使用 luaL_loadfile 加载一段 Lua 程序或脚本到 Lua 执行引擎中;
- 调用 lua_pcall 函数执行已加载的脚本。
如果想在应用程序中加载Lua脚本并执行其中的函数,你必须执行被加载的Lua程序块(chunk)。刚刚加载的程序块只是编译后存放于Lua的脚本引擎中,并没有被执行。只有在程序块被执行后,Lua中的全局变量和函数才会被创建,在这之前这些任何全局变量和函数对于应用程序来说都不可用。作为Lua引擎的环境由应用程序提供给Lua脚本引擎的任何全局变量和函数也不可用。应用程序必须首先创建变量和函数,并使用函数lua_setglobal()让它们可用。在文件 UtilityFunctions.cpp 中的函数int LuaSimpleWrapper::TriggerGlobalCall()定义了一个例子,它在Lua虚拟栈上动态创建一个Lua函数调用,并用Lua脚本引擎中的lua_pcall()函数,以给定的参数来执行此函数。
所有Lua脚本引擎函数或服务,都用到一个包含Lua脚本引擎状态信息的句柄或数据结构指针。这个句柄被指定为lua_State*或者指向lua_State变量的指针。每次成功的lua_newstate()的调用都会返回一个lua_State指针,失败时返回NULL。这个数据结构是用来指示特定会话的变量,它允许Lua脚本引擎同时有多个会话并行。当应用程序用到Lua脚本引擎中的函数,或应用程序提供给Lua脚本引擎的服务被调用时,lua_State结构提供的会话环境唯一指示了某个Lua会话状态。这意味着,应用程序提供给Lua脚本引擎的函数应该是完全可重入的,或者提供某种监视,比如信号量和临界区使得非共享服务能够提供线程安全的访问。
使用代码
在Visual Studio 2005工程目录中包含三个C++文件和一个测试基本功能的Lua例子。主体部分在Parser01.cpp中。在这里,加载了特定的Lua文件,然后又调用了一些别的函数。UtilityFunctions.cpp包含了LuaSimpleWrapper方法的源代码。InitEnviron则包含了我们希望提供给Lua环境的的函数。
我们正考虑使用的这个方法是, 在销售点应用程序启动一个包含Lua脚本的文件被指定。作为启动,销售点将启动一个初始化Lua脚本引擎并且加载和执行指定的Lua源文件的线程。Lua块将一直保留在内存中,并且在销售点应用程序中,随着事件的进行一些事件会被转移到Lua脚本进行处理。这个示例程序中的测试工具是一个对我们正在考虑的这种方法的探索。
在下面提供的lua源码的函数中,我们使用了几个在文件InitEnviron.cpp中提供的几个函数,处理非标准Lua字符串的‘宽字符串'。标准的Lua字符串是char字符串(C风格的单字节字符串)。这些附加函数为Lua处理这些宽字符串提供了方法,例如字符串的连接,比较。
下面展示一个含有三个参数并执行一系列操作的Lua函数。函数名xxfunc是一个全局名字,应用程序可以通过调用函数lua_getglobal()从Lua全局字典中检索出来并在Lua虚拟堆栈上把句柄传给lua_getglobal。然后函数的参数可以通过调用像一个lua_pushstring()的函数推送到Lua虚拟堆栈上,接着调用Lua脚本引擎函数lua_pcall()执行xxfunc函数。
代码如下:
-- a sample Lua global function that can be invoked from the application or from Lua function xxfunc (myMessage, wide1, wide2) trace("## xxfunc() called.") trace(" "..myMessage) trace (" myFrame index "..myFrame.FrameIndex) trace (" myFrame2 index "..myFrame2.FrameIndex) trace (" myFrame3 index "..myFrame3.FrameIndex) -- compare two wide char strings that were passed in as arguments trace (" compare wide "..wcscmp(wide1, wide2)) -- generate a wide char string from a Lua string local widestring = wcscre("WIDE1") trace (" compare with generated "..wcscmp (wide1, widestring)) -- try out the wcscat and the wcscre functions to generate a string traceW (wcscat (wcscre(" concat two "), wcscat(wide1, wide2))) traceW (wcscat (wcscre(" concat multi "), wide1, wcscre(" "), wide2)) -- tryout wcscat with a non-string argument which should be skipped. traceW (wcscat (wcscre(" concat multi "), 2, wcscre(" "), wide2)) local myMemEntry = GetMnemonic (15) if (myMessage) then if (myMessage.Type == "FRAMEWORK") then local myNem = GetMnemonic (20) end end end
以上在Lua脚本中已经被加载的Lua函数可以被应用程序调用。使用来自LuaSimpleWrapper类中的一个助手函数,我们可以调用带有以下C++代码行的Lua函数。方法TriggerGlobalCall()需要一个标识全局Lua函数(函数或赋给一个变量或表实例的函数)的描述性字符串来调用带有描述的参数类型。这些参数遵循描述性字符串。这种类型的变量函数调用会成为一个运行时错误的根源,因为它包含几个单独的必须匹配的源:
1. TriggerGlobalCall()中的描述性字符串;
2. theTriggerGlobalCall()提供的实际参数;
3. 被调用的Lua函数。
代码如下:
if (myLua.TriggerGlobalCall ("xxfunc:s,w,w", "TriggerGlobalCall", L"WIDE1", L"WIDE2") < 0) { cout << "%% " << myLua.GetLastErrorString() << endl ; }
富有表达力的字符串使用了由逗号分隔的列表来区分参数的类型,列表中每个字母代表着各自的类型,如一个ANSI字符串,一个长字符串,一个浮点型,一个整型,一个函数的地址。在 TriggerGlobalCall() 方法的代码中,字母中间的逗号被忽略了,实际上他们存在的作用只是为了使这个富有表达力的字符串更易识别和理解。
上面的方法TriggerGlobalCall()调用了Lua的xxfunc()函数会产生如下的结果输出。这输出由Lua函数使用TriggerGlobalCall()里定义的参数列表的参数生成。
代码如下:
## xxfunc() called. TriggerGlobalCall myFrame index 0 myFrame2 index 1 myFrame3 index 2 compare wide -1 compare with generated 0 concat two WIDE1WIDE2 concat multi WIDE1 WIDE2 concat multi WIDE2 getTransactionMnemonic() 15
我们在LuaSimpleWrapper 类中提供的TriggerGlobalCall()方法允许指定一个可访问一个函数的表值,这个函数已经在Lua表中分配了一个键值。描述性字符串的格式是:tablename.key,这里“tablename”是一个Lua表的全局名而“key”是访问函数所需的键值。
代码如下:
// specify a function to be invoked by the OnEvent() handler // specify a different event type which is not in the Lua script if (myLua.TriggerGlobalCall ("myFrame2.OnEvent:s,f", "EVENT_TYPE_J2", SimpleFunc) < 0) { cout << "%% " << myLua.GetLastErrorString() << endl; }
将C/C++函数导出到Lua引擎中
为了创建一个要在Lua脚本中使用的C或者C++辅助函数,C/C++应用程序就必需提供该函数体(the function body)并使用适当的Lua引擎函数让该新函数变为在Lua引擎中可用。在应用程序里将一个函数提供给在Lua引擎中进行使用所需的函数调用要将若干值压入Lua的虚拟堆栈之中,然后调用lua_setglobal()函数,就可以把应用程序中的函数作为全局函数提供给在Lua脚本引擎中使用。
代码如下:
lua_pushcclosure (lua, concatMultiWideStrings, 0); lua_setglobal (lua, "wcscat");
Lua为函数提供闭包概念。当一个Lua脚本引擎调用应用函数时,一个闭包允许一个应用指定一个或多个提供给应用函数的值。这些值可以被应用函数更新,这个例子使用的一个特性是通过一个与应用函数关联的计数器增量提供一个惟一值。C++关于这方面的源代码的一个例子可以在方法int LuaSimpleWrapper::InitLuaEnvironment()中找到,这个方法为Lua脚本引擎提供CreateFrame()函数。
代码如下:
// CreateFrame() function that will create a frame with an index // This function uses two variables which are used to store the // frame data allowing it to be used by the application in order to // send events to a specific frame object in the Lua code. // we access the array of the list of objects, m_ListOfObjects[], with objectindex // and we access the specific frame for the object with frameindex. lua_pushnumber(m_luaState, 0); // frameindex, count of frames for this Lua state object, init to zero lua_pushnumber(m_luaState, m_MyObjectCount); // objectindex, which Lua state object am I? // create the C closure with the above two arguments, lua_pushcclosure (m_luaState, ParserLuaCreateGlobalFrame, 2); lua_setglobal (m_luaState, "CreateFrame");
要导出的C++函数的源代码应该具有如下所示的形式。函数concatMultiWideStrings ()使用了一系列的Lua引擎里的函数处理LUa虚拟堆栈之中的一些数值并将处理结果返回给Lua引擎。以下所示函数说明了要使用lua_State *这个参数提供给该函数相关的session环境。这个函数用以将多个字符串拼接到一起。Lua脚本引擎提供了位于Lua虚拟堆栈之中的参数的个数信息。我们还可以使用Lua引擎提供的lua_type()函数判断出参数的数据类型,从而可以跳过那些不是正确类型的参数。
然而,实际上在我们只想使用LUA_STRING这种类型的情况下,Lua脚本引擎会执行相应的类型转换,但Lua所做的从其它类型到字符串类型的转换的结果是C风格的单字节字符所组成的字符串串而不是我们所预期的双字节宽度的字符组成的字符串。
代码如下:
// concatenate multiple wide strings // const wchar_t *wcscat(wchar_t *wcharSt1, const wchar_t *wcharSt2, const wchar_t *wcharSt3, ...) static int concatMultiWideStrings (lua_State *lua) { int nPushCount = 0; int nArgIndex = 1; int argc = lua_gettop(lua); wchar_t tempBuffer[2048]; if (argc > 0) { wchar_t *pWideString = &tempBuffer[0]; size_t iLen = 1; while (nArgIndex <= argc) { if (lua_type(lua, nArgIndex) == LUA_TSTRING) { const wchar_t *msgX = (wchar_t *) lua_tostring (lua, nArgIndex); while (*msgX) {*pWideString++ = *msgX++; iLen++; } } nArgIndex++; } *pWideString = 0; // final zero terminator lua_pushlstring (lua, (char *)(&tempBuffer), iLen * sizeof(wchar_t)); nPushCount++; } return nPushCount; }
关注点
该测试工具(test harness)的第一个版本只是个非常简单的开头,只是简单的装载一个简单Lua脚本,在运行后也只是使用Lua的输出函数在控制台(console)中输出一个“Hello World”。随着在调查嵌入式Lua所具有的潜在能力时,脚本和测试工具会变得越来越复杂,很快就会发现明显需要有某种方式将Lua虚拟堆栈中所有内容打印出来,才能理解Lua引擎同应用程序之间到底是如何进行通信的。 要多次在运行突然中断,使用调试器单步进入C++源代码时使用堆栈内容输出函数查看堆栈中内容,只有这样才能理解到底发生了什么问题并找出问题的修复办法。
Lua语言的动态特性,像JavaScript这类的松散类型语言一样,会鼓励有冒险精神的程序员写出一些非常有趣的代码,但是也会产生一些非常难以调试和测试的代码。这个难度主要来自于使用了两种语言,而在Visual Studio中又缺乏对Lua调试的支持。
在实现方法int LuaSimpleWrapper::TriggerGlobalCall ()时,在Visual Studio debugger里运行的情况下我们遇到了导致Lua的脚步引擎(script engine)在Windows错误对话框中要执行应用程序关闭或退出操作的测试案例(test case)。我们认为,之所以出现这个问题,是由于在出现了错误的情况下,有些特定的值被压入了Lua的虚拟堆栈之中,从而未被恰当处理所导致的。为了解决该问题,在发现错误的情况下,我们要用下面所示的C++代码对Lua的虚拟堆栈进行清理。在此测试套件(test harness)中,我们有两个不同的测试,一个用来对指定的全局变量是否存在进行测试,另一个测试的是,如果通过使用“global.key”语法指定了一个关键值(key value),那么其中的全局变量必定为一个表,否则便为错误。
代码如下:
lua_getglobal (m_luaState, globalName); if (lua_type(m_luaState, lua_gettop(m_luaState)) == LUA_TNIL) { // if the global variable does not exist then we will bail out with an error. strcpy_s (m_lastLuaError, sizeof(m_lastLuaError), "Global variable not found: "); strcat_s (m_lastLuaError, sizeof(m_lastLuaError), globalName); m_lastState = LuaDescripParse; // error so we will just clear the Lua virtual stack and then return // if we do not clear the Lua stack, we leave garbage that will cause // problems with later function calls from the application. // we do this rather than use lua_error() because this function is called // from the application and not through Lua. lua_settop (m_luaState, 0); return -1; }