65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Lua 5.2 扩展 C++ 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (14投票s)

2013 年 1 月 1 日

CPOL

13分钟阅读

viewsIcon

59440

downloadIcon

1552

使用 Lua 5.2 C 接口的子集来扩展 C++ 应用程序。

引言

如果最终用户能够通过某种脚本语言修改应用程序行为,那么许多应用程序会更具适应性。一些商业应用程序提供了这种功能。例如,Microsoft Office 中的 Visual Basic 脚本,或者像《魔兽世界》这样的视频游戏中使用的 Lua。脚本语言以应用程序为平台,提供一套服务,最终用户可以通过脚本语言进行访问和操作。

嵌入式语言有很多选择,有各种开源和闭源引擎可用,也可以选择从头开始创建。其中最知名的有 JavaScript、Lua 和 Python,但还有许多其他选择。将脚本引擎嵌入 Microsoft Windows 的一般方法是使用包含脚本引擎的动态链接库 (DLL),并通过一组函数调用来访问引擎中的服务。

在本文中,我们将探讨将 Lua 5.2 脚本引擎嵌入用 C++ 编写并在 Visual Studio 2005 中开发的测试平台。Lua 所需的组件可在 lua.org 网站上找到。出于我们的目的,我们下载了 Lua 5.2 的编译版 Windows DLL,其中包含头文件、需要链接的库文件以及必须随应用程序可执行文件一起提供的实际 DLL。虽然本示例使用了 Visual Studio 2005,但它也应该与更高版本的 Visual Studio 兼容。

本文概述了使用 Lua 5.2 脚本引擎的特定实现。lua.org 网站以及其他网站上提供了更多信息。但是,这里提供的测试平台是您进行自行实验的一个起点。

背景

开发应用程序时,将其作为领域特定平台而非单体应用程序是一种不同的产品开发理念。这样的领域特定平台是一种工具包,提供各种软件服务,供最终用户实现其领域特定应用程序的自有版本。插件或加载项的概念已用于各种软件中,包括 Microsoft Office 和 Microsoft Visual Studio,以及 Eclipse 等其他集成开发环境,以及 Adobe Photoshop 等图像处理应用程序。《魔兽世界》MMORPG 有大量加载项,其他类似的游戏也有。一些游戏提供工具,允许人们添加额外内容,从而创建一个模组社区。

一款名为 GenPOS 的商业销售点应用程序具有一些有助于工具包理念的特性。

  • 布局管理器实用程序,用于设计屏幕布局,以定位窗口、按钮和装饰。
  • 控件字符串功能,用于自动化某些工作流程。
  • 各种参数,用于修改软件行为。
  • 助记符数据库,允许以多种语言显示消息。
  • 一个用于通过远程访问接口动态更改参数和助记符的接口。
  • 一个用于从终端提取财务和运营数据的接口。

然而,有些更改需要通过实际源代码来处理,并且需要开发机构修改源代码才能修改行为。例如,其中一个领域是打印收据更改(一些替代方案通过参数和助记符提供)。其他限制的一个简短列表包括:

  • 现有的控件字符串功能缺乏状态测试和跳转。
  • 无法根据当前状态动态修改屏幕行为和显示信息。

与其试图增强和改进当前非常简单的控件字符串功能以提供额外的脚本编写能力,不如我们决定研究其他可能的增强功能,这些功能将提供更大的产品可变性,而无需大量的开发工作。正如我们通过引入布局管理器实用程序让客户设计自己的屏幕布局和工作流程一样,我们也希望有一个相当灵活的机制,允许销售点经销商通过脚本为客户提供增值服务。我们还打算提供足够的应用程序服务访问权限,使经销商和最终客户能够自行修改应用程序行为。最后,我们希望使用一种具有一定用户群、能够为使用者提供资源的编程语言。

我们对在销售点源代码中使用 Lua 5.2 脚本引擎进行了一些简单的实验,以了解将此功能插入应用程序的难度。从实验来看,我们希望暴露的大部分应用程序服务已经作为各种函数可用,并且可以通过一些适配器轻松地提供给 Lua 脚本引擎。

使用 Lua 5.2 脚本引擎

Lua 脚本引擎是用 C 编程语言编写的,在 C 或 C++ 应用程序中使用它非常简单。网上有很多资源提供了在应用程序中使用 Lua 5.2 脚本引擎的片段。5.2 版 Lua 的函数接口与早期版本相比确实有一些变化,主要是初始化和启动接口。

基本初始化如下:

  1. 使用 lua_newstate() 函数创建一个新的 Lua 状态。
  2. 如果需要,加载标准 Lua 库 luaL_openlibs()

初始化完成后,您可以通过以下方式运行 Lua 脚本:

  1. 使用加载函数之一(如 luaL_loadfile())将 Lua 块或 Lua 脚本加载到引擎中。
  2. 使用调用函数之一(如 lua_pcall())执行 Lua 块。

如果您想加载一个 Lua 脚本文件,然后从应用程序中执行 Lua 脚本中的函数,您必须执行已加载的 Lua 块。当 Lua 块被加载时,它会被编译并加载到 Lua 脚本引擎中,但尚未执行。Lua 块将在运行时创建任何全局变量或函数,但直到 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 脚本引擎的任何函数都应该是完全可重入的,或者应该使用某种监视器、信号量或临界区方法来强制访问不可共享的服务,以提供线程安全访问。

Using the Code

此 Visual Studio 2005 项目中的源代码由三个 C++ 源文件以及一个用于测试一些基本功能的示例 Lua 源文件组成。主文件是 Parser01.cpp,它加载指定的 Lua 源文件,然后调用几个不同的函数。UtilityFunctions.cpp 文件包含 LuaSimpleWrapper 方法的源代码。InitEnviron.cpp 文件包含我们希望应用程序提供的附加函数到 Lua 环境中。

我们正在考虑使用的方法是,在销售点应用程序启动时指定一个包含 Lua 脚本的文件。作为启动的一部分,销售点将在一个线程中启动,在该线程中将初始化 Lua 脚本引擎,并加载和执行指定的 Lua 源文件。Lua 块将保留在内存中,当销售点应用程序中发生事件时,其中一些事件将被转移到 Lua 脚本进行处理。本示例程序中的测试平台是我们正在考虑的方法的探索。

在下面提供的 Lua 源文件中的函数中,我们使用了 InitEnviron.cpp 文件中提供的一些函数,以便操作宽字符字符串,这些字符串不是标准的 Lua 字符串。标准的 Lua 文本字符串是 char 字符串(C 风格的单字节字符字符串)。这些附加函数提供了一种方法,供 Lua 脚本执行字符串操作,如宽字符字符串的连接或比较。

下面的示例是一个小的 Lua 函数,它接收三个参数并执行一系列操作。函数名 xxfunc 是一个全局名称,可以通过使用 lua_getglobal() 函数从 Lua 全局字典中检索它,并将函数句柄推送到 Lua 虚拟机堆栈上,从而使应用程序能够访问它。然后使用其中一个 push 函数(如 lua_pushstring())将函数的参数推送到 Lua 虚拟机堆栈上,然后使用 lua_pcall() Lua 脚本引擎函数执行该函数。

-- 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 类中的一个辅助函数 TriggerGlobalCall(),我们可以通过以下 C++ 源代码行调用 Lua 函数。TriggerGlobalCall() 方法接受一个描述性字符串,该字符串指示要调用的全局 Lua 函数(函数或分配给变量的函数或表项),以及参数类型的描述。参数紧随描述性字符串。这种变量函数调用可能是一个运行时错误源,因为有几个独立的代码部分必须一致:(1)TriggerGlobalCall() 调用中的描述性 string;(2)TriggerGlobalCall() 调用中提供的实际参数;(3)正在调用的 Lua 函数。

if (myLua.TriggerGlobalCall ("xxfunc:s,w,w", 
          "TriggerGlobalCall", L"WIDE1", L"WIDE2") < 0) {
    cout << "%% " << myLua.GetLastErrorString() << endl ;
}

描述性 string 使用逗号分隔的单字母参数类型列表,其中字母 s 表示 char string,字母 w 表示宽字符 string,字母 d 表示 double,字母 i 表示整数,字母 f 表示函数地址。在 TriggerGlobalCall() 方法的代码中,字母之间的逗号被忽略,它们实际上只是使描述性字符串更容易识别的方式。

上面调用 Lua 函数 xxfunc()TriggerGlobalCall() 方法将产生以下输出。此输出是由 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 表中键的函数。描述性 string 的格式为 "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++ 应用程序必须提供函数体,然后使用适当的 Lua 引擎函数使新函数可供 Lua 引擎使用。应用程序中用于向 Lua 引擎提供新辅助函数的函数调用会将值推送到 Lua 虚拟机堆栈上,然后调用 lua_setglobal() 函数,将应用程序函数作为全局函数提供给 Lua 脚本引擎。

lua_pushcclosure (lua, concatMultiWideStrings, 0);
lua_setglobal (lua, "wcscat");

Lua 支持函数闭包的概念。闭包允许应用程序指定一个或多个值,当 Lua 脚本引擎调用该应用程序函数时,这些值将可用。这些值可以被应用程序函数更新,在本例中,通过递增与应用程序函数关联的计数器来提供唯一值的功能。C++ 源代码的示例可以在 int LuaSimpleWrapper::InitLuaEnvironment() 方法中找到,该方法将 CreateFrame() 函数提供给 Lua 脚本引擎。

// 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 * 参数的使用,该参数为函数提供会话环境。此特定函数允许连接多个字符串。Lua 脚本引擎提供 Lua 虚拟机堆栈上可用参数的数量。我们还可以使用提供的 lua_type() 函数来确定参数的类型,从而允许我们跳过类型不正确的参数。

实际上,Lua 脚本引擎会执行变量类型转换,但在我们的例子中,我们只想使用 LUA_TSTRING 类型,因为 Lua 对其他类型进行的任何到 string 类型的转换都会生成 C 风格的单字节字符 string,而不是我们期望的双字节宽字符 string

// 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;
}

关注点

这个测试平台的第一版本非常简单,最初只有一个简单的 Lua 脚本,它会加载并运行,使用 Lua 输出函数向控制台输出 "Hello World"。随着脚本和测试平台变得越来越复杂,在研究嵌入 Lua 的潜力时,很快就发现需要一种方法来转储 Lua 虚拟机堆栈,以便理解 Lua 引擎和应用程序之间的通信是如何工作的。几次突然的行为中断需要在使用调试器逐步调试 C++ 源代码时使用堆栈转储函数,以了解发生了什么并确定行为的修复方法。

Lua 的动态特性,就像 JavaScript 等松散类型语言一样,可以鼓励富有冒险精神的程序员编写一些真正有趣的源代码,但它也可能导致难以调试和难以测试的源代码。使用两种不同的语言以及 Visual Studio 中缺乏 Lua 的调试功能加剧了这种困难。

在实现 int LuaSimpleWrapper::TriggerGlobalCall() 方法时,我们遇到了导致 Lua 脚本引擎执行应用程序关闭或退出的测试用例,导致在 Visual Studio 调试器中运行时出现 Windows 错误对话框。我们认为问题是某些值被推送到 Lua 虚拟机堆栈上,并且由于错误条件,这些值没有被正确处理。为了解决这个问题,当检测到错误时,我们使用以下 C++ 源代码序列清空了 Lua 虚拟机堆栈。在测试平台中,我们有两个不同的测试:一个测试指定的全局名称是否存在;第二个测试是如果使用 "global.key" 语法指定了一个键值,那么该全局变量必须是一个表,否则就是一个错误。

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;
}
© . All rights reserved.