Lua 与 C/C++ 交互

    这节要讲Lua和C/C++的交互——Lua通过C/C++导出的dll来调用。

    LUA调用C文件中的函数方法

    • C中注册函数

    • C中提供的函数其定义要符合:

      typedef int function(lua_State *L)

    准备工作

    安装完Lua,需要在Visual Studio中配置Lua路径,使得你的编译器能搜寻到。关于VS2010的配置,见我的博文《VS2010 C++目录配置》一文。完成后新建一个Dll工程便可以了。

    我们用一个在Lua中显示Windows对话框的程序来简要介绍一下,程序虽小,但五脏俱全。程序如下:

    1. // 以便在Lua中使用
    2. extern "C"
    3. {
    4. #include <lua.h>
    5. #include <lualib.h>
    6. #include <lauxlib.h>
    7. #pragma comment(lib, "lua.lib")
    8. };
    9. #include <Windows.h>
    10. #include <iostream>
    11. using namespace std;
    12. static const char* const ERROR_ARGUMENT_COUNT = "参数数目错误!";
    13. static const char* const ERROR_ARGUMENT_TYPE = "参数类型错误!";
    14. // 发生错误,报告错误
    15. void ErrorMsg(lua_State* luaEnv, const char* const pszErrorInfo)
    16. {
    17. lua_pushstring(luaEnv, pszErrorInfo);
    18. lua_error(luaEnv);
    19. }
    20. // 检测函数调用参数个数是否正常
    21. void CheckParamCount(lua_State* luaEnv, int paramCount)
    22. {
    23. // lua_gettop获取栈中元素个数.
    24. if (lua_gettop(luaEnv) != paramCount)
    25. {
    26. ErrorMsg(luaEnv, ERROR_ARGUMENT_COUNT);
    27. }
    28. }
    29. // 显示Windows对话框.
    30. // @param [in] pszMessage string 1
    31. // @param [in] pszCaption string 2
    32. extern "C" int ShowMsgBox(lua_State* luaEnv)
    33. {
    34. const char* pszMessage = 0;
    35. const char* pszCaption = 0;
    36. // 检测参数个数是否正确.
    37. CheckParamCount(luaEnv, 2);
    38. // 提取参数.
    39. pszMessage = luaL_checkstring(luaEnv, 1);
    40. pszCaption = luaL_checkstring(luaEnv, 2);
    41. if (pszCaption && pszMessage)
    42. {
    43. ::MessageBox(
    44. NULL,
    45. pszMessage,
    46. pszCaption,
    47. MB_OK | MB_ICONINFORMATION
    48. );
    49. }
    50. else
    51. {
    52. ErrorMsg(luaEnv, ERROR_ARGUMENT_TYPE);
    53. }
    54. // 返回值个数为0个.
    55. return 0;
    56. }
    57. // 导出函数列表.
    58. static luaL_Reg luaLibs[] =
    59. {
    60. {"ShowMsgBox", ShowMsgBox},
    61. {NULL, NULL}
    62. };
    63. // Dll入口函数,Lua调用此Dll的入口函数.
    64. extern "C" __declspec(dllexport)
    65. int luaopen_WinFeature(lua_State* luaEnv)
    66. {
    67. const char* const LIBRARY_NAME = "WinFeature";
    68. luaL_register(luaEnv, LIBRARY_NAME, luaLibs);
    69. return 1;
    70. }

    程序解析

    首先我们包含Lua的头文件,并链入库文件。注意:Lua的头文件为C风格,所以用external “C”来含入。在此例中,我们最终的导出函数为“ShowMsgBox”。

    每一个导出函数的格式都为:

    1. extern Cint Export_Proc_Name(luaState* luaEnv);

    其中,luaState*所指的结构中包含了Lua调用此Dll时必备的Lua环境。那么Lua向函数传递参数该怎么办呢?实际上是用luaL_check[type]函数来完成的。如下:

    1. const char* pHelloStr = luaL_checkstring(luaEnv, 1);
    2. double value = luaL_checknumber(luaEnv, 2);
    3. int ivalue = luaL_checkint(luaEnv, 3);

    luaL_check系列函数的第二个参数是Lua调用该函数时传递参数从坐到右的顺序(从1开始)。

    然后我们看到,static的一个luaL_Reg的结构数组中包含了所有要导出的函数列表。最后通过luaopen_YourDllName的一个导出函数来完成一系列操作。YourDllName就是你最终的Dll的名字(不含扩展名)。因为你在Lua中调用此Dll时,Lua会根据此Dll名字找luaopen_YourDllName对应的函数,然后从此函数加载该Dll。

    Dll入口函数格式如下:

    1. extern "C" __declspec(dllexport)
    2. int luaopen_WinFeature(lua_State* luaEnv)
    3. {
    4. const char* const LIBRARY_NAME = "WinFeature";
    5. luaL_register(luaEnv, LIBRARY_NAME, luaLibs);
    6. return 1;
    7. }

    我们通过luaL_register将LIBRARY_NAME对应的库名,以及luaL_Reg数组对应的导出列表来注册到lua_State*对应的Lua环境中。

    Lua调用

    那么我们要如何调用该Dll呢?首先,把该Dll放到你的Lua能搜寻到的目录——当前目录、Lua安装目录下的clibs目录,然后通过require函数导入。

    因为Lua中如果你的函数调用参数只有一个,并且该参数为字符串的话,函数调用时的括号是可以省略的,所以:require(“YourLibName”)和requir“YourLibName”都是合法的。我们把刚刚生成的WinFeature.dll文件拷贝到C盘下,然后在C盘启动Lua。示例如下:

    1. > require "WinFeature"
    2. > for k, v in pairs(WinFeature) do
    3. >> print(k, v)
    4. >> end
    5. ShowMsgBox functon:0028AB90
    6. >

    可以看到,函数调用方式都是“包名.函数名”,而包名就是你的Dll的名字。我们可以用下面的方式查看一个包中的所有函数:

    1. for k, v in pairs(PackageName) do
    2. end

    然后我们调用WinFeature.ShowMsgBox函数:

    1. > WinFeature.ShowMsgBox("Hello, this is a msgBox", "Tip")

    Lua堆栈详解

    唔,那么lua_State结构如何管理Lua运行环境的呢?Lua又是如何将参数传递到C/C++函数的呢?C/C++函数又如何返回值给Lua呢?……这一切,都得从Lua堆栈讲起。

    Lua在和C/C++交互时,Lua运行环境维护着一份堆栈——不是传统意义上的堆栈,而是Lua模拟出来的。Lua与C/C++的数据传递都通过这份堆栈来完成,这份堆栈的代表就是lua_State*所指的那个结构。

    堆栈结构解析

    堆栈通过lua_push系列函数向堆栈中压入值,通过luaL_check系列从堆栈中获取值。而用luaL_check系列函数时传递的参数索引,比如我们调用WinFeature.ShowMsgBox(“Hello”, “Tip”)函数时,栈结构如下:

    栈顶
    “Tip” 2或者-1
    “Hello” 1或者-2
    栈底

    其中,参数在栈中的索引为参数从左到右的索引(从1开始),栈顶元素索引也可以从-1记起。栈中元素个数可以用lua_gettop来获得,如果lua_gettop返回0,表示此栈为空。(lua_gettop这个函数名取得不怎么样!)

    提取参数

    luaL_check系列函数在获取值的同时,检测这个值是不是符合我们所期望的类型,如果不是,则抛出异常。所有这个系列函数如下:

    1. luaL_checkany —— 检测任何值(可以为nil
    2. luaL_checkint —— 检测一个值是否为numberdouble),并转换成int
    3. luaL_checkinteger —— 检测一个值是否为numberdouble),并转换成lua_Integerprtdiff_t),在我的机子上,ptrdiff_t被定义为int
    4. luaL_checklong —— 检测一个值是否为numberdouble),并转换成long
    5. luaL_checklstring —— 检测一个值是否为string,并将字符串长度传递在[out]参数中返回
    6. luaL_checknumber —— 检测一个值是否为numberdouble
    7. luaL_checkstring —— 检测一个值是否为string并返回
    8. luaL_checkudata —— 检测自定义类型

    传递返回值

    当我们要传递返回值给Lua时,可以用lua_push系列函数来完成。每一个导出函数都要返回一个int型整数,这个整数是你的导出函数的返回值的个数。而返回值通过lua_push系列函数压入栈中。比如一个Add函数:

    可以看出,我们用lua_pushnumber把返回值压入栈,最后返回1——1代表返回值的个数。lua_push系列函数如下:

    1. lua_pushboolean —— 压入一个bool
    2. lua_pushcfunction —— 压入一个lua_CFunction类型的C函数指针
    3. lua_pushfstring —— 格式化一个string并返回,类似于sprintf
    4. lua_pushinteger —— 压入一个int
    5. lua_pushlightuserdata —— 压入自定义数据类型
    6. lua_pushliteral —— 压入一个字面值字符串
    7. lua_pushlstring —— 压入一个规定长度内的string
    8. lua_pushnil —— 压入nil
    9. lua_pushnumber —— 压入lua_Numberdouble)值
    10. lua_pushstring —— 压入一个string
    11. lua_pushthread —— 压入一个所传递lua_State所对应的线程,如果此线程是主线程,则返回1
    12. lua_pushvalue —— 将所传递索引处的值复制一份压入栈顶
    13. lua_pushvfstring —— 类似lua_pushfstring

    通过这些函数,我们可以灵活的使用C/C++的高性能特性,来导出函数供Lua调用。

    C调用LUA文件中的函数方法

    1. lua_getglobal(L, <function name>) //获取lua中的函数
    2. lua_pcall(L, <arguments nums>, <return nums>, <错误处理函数地址>)

    例如:

    1. lua_settop(m_pLua,0);
    2. lua_getglobal(m_pLua,"mainlogic");
    3. lua_pushlstring(m_pLua,(char*)msg.getBuf(),msg.size());
    4. int ret = 0;
    5. ret = lua_pcall(m_pLua,1,4,0);

    上一节介绍了如何在Lua中调用C/C++代码,本节介绍如何在C/C++中调用Lua脚本。本节介绍一个例子,通过Lua来生成一个XML格式的便签。便签格式如下:

    1. <?xml version="1.0" encoding="utf-8" ?>
    2. <note>
    3. <fromName>发送方姓名</fromName>
    4. <toName>接收方姓名</toName>
    5. <sendTime>发送时间</sendTime>
    6. <msgContent>便签内容</msgContent>
    7. </note>

    我们通过C/C++来输入这些信息,然后让Lua来生成这样一个便签文件。

    Lua代码

    1. xmlHead = '<?xml version="1.0" encoding="utf-8" ?>\n'
    2. -- Open note file to wriet.
    3. function openNoteFile(fileName)
    4. return io.open(fileName, "w")
    5. end
    6. -- Close writed note file.
    7. function closeNoteFile(noteFile)
    8. noteFile:close()
    9. end
    10. function writeNestedLabel(ioChanel, label, nestCnt)
    11. if nestCnt == 0 then
    12. ioChanel:write(label)
    13. return
    14. end
    15. for i = 1, nestCnt do
    16. ioChanel:write("\t")
    17. end
    18. ioChanel:write(label)
    19. end
    20. function generateNoteXML(fromName, toName, msgContent)
    21. local noteFile = openNoteFile(fromName .. "_" .. toName .. ".xml")
    22. if not noteFile then
    23. return false
    24. end
    25. noteFile:write(xmlHead)
    26. noteFile:write("<note>\n")
    27. local currNestCnt = 1
    28. writeNestedLabel(noteFile, "<fromName>", currNestCnt)
    29. noteFile:write(fromName)
    30. writeNestedLabel(noteFile, "</fromName>\n", 0)
    31. writeNestedLabel(noteFile, "<toName>", currNestCnt)
    32. noteFile:write(toName)
    33. writeNestedLabel(noteFile, "</toName>\n", 0)
    34. local sendTime = os.time()
    35. writeNestedLabel(noteFile, "<sendTime>", currNestCnt)
    36. noteFile:write(sendTime)
    37. writeNestedLabel(noteFile, "</sendTime>\n", 0)
    38. writeNestedLabel(noteFile, "<msgContent>", currNestCnt)
    39. noteFile:write(msgContent)
    40. writeNestedLabel(noteFile, "</msgContent>\n", 0)
    41. noteFile:write("</note>\n")
    42. closeNoteFile(noteFile)
    43. return true
    44. end

    我们通过openNoteFile和closeNoteFile来打开/关闭XML文件。generateNoteXML全局函数接受发送方姓名、接收方姓名、便签内容,生成一个XML便签文件。便签发送时间通过Lua标准库os.time()函数来获取。writeNestedLabel函数根据当前XML的缩进数目来规范XML输出格式。此文件很好理解,不再赘述。

    C++调用Lua脚本

    1. extern "C"
    2. {
    3. #include <lua.h>
    4. #include <lualib.h>
    5. #include <lauxlib.h>
    6. #pragma comment(lib, "lua.lib")
    7. };
    8. #include <iostream>
    9. #include <string>
    10. using namespace std;
    11. // 初始化Lua环境.
    12. lua_State* initLuaEnv()
    13. {
    14. lua_State* luaEnv = lua_open();
    15. luaopen_base(luaEnv);
    16. luaL_openlibs(luaEnv);
    17. return luaEnv;
    18. }
    19. // 加载Lua文件到Lua运行时环境中
    20. bool loadLuaFile(lua_State* luaEnv, const string& fileName)
    21. {
    22. int result = luaL_loadfile(luaEnv, fileName.c_str());
    23. if (result)
    24. {
    25. return false;
    26. }
    27. result = lua_pcall(luaEnv, 0, 0, 0);
    28. return result == 0;
    29. }
    30. // 获取全局函数
    31. lua_CFunction getGlobalProc(lua_State* luaEnv, const string& procName)
    32. {
    33. lua_getglobal(luaEnv, procName.c_str());
    34. if (!lua_iscfunction(luaEnv, 1))
    35. {
    36. return 0;
    37. }
    38. return lua_tocfunction(luaEnv, 1);
    39. int main()
    40. {
    41. // 初始化Lua运行时环境.
    42. lua_State* luaEnv = initLuaEnv();
    43. if (!luaEnv)
    44. {
    45. return -1;
    46. }
    47. if (!loadLuaFile(luaEnv, ".\\GenerateNoteXML.lua"))
    48. {
    49. cout<<"Load Lua File FAILED!"<<endl;
    50. return -1;
    51. }
    52. // 获取Note信息.
    53. string fromName;
    54. string toName;
    55. string msgContent;
    56. cout<<"Enter your name:"<<endl;
    57. cin>>fromName;
    58. cout<<"\nEnter destination name:"<<endl;
    59. cin>>toName;
    60. cout<<"\nEnter message content:"<<endl;
    61. getline(cin, msgContent);
    62. getline(cin, msgContent);
    63. // 将要调用的函数和函数调用参数入栈.
    64. lua_getglobal(luaEnv, "generateNoteXML");
    65. lua_pushstring(luaEnv, fromName.c_str());
    66. lua_pushstring(luaEnv, toName.c_str());
    67. lua_pushstring(luaEnv, msgContent.c_str());
    68. // 调用Lua函数(3个参数,一个返回值).
    69. lua_pcall(luaEnv, 3, 1, 0);
    70. // 获取返回值.
    71. if (lua_isboolean(luaEnv, -1))
    72. {
    73. int success = lua_toboolean(luaEnv, -1);
    74. if (success)
    75. {
    76. cout<<"\nGenerate Note File Successful!"<<endl;
    77. }
    78. }
    79. // 将返回值出栈.
    80. lua_pop(luaEnv, 1);
    81. // 释放Lua运行时环境.
    82. lua_close(luaEnv);
    83. system("pause");
    84. return 0;
    85. }

    代码解析

    初始化Lua运行时环境

    lua_State*所指向的结构中封装了Lua的运行时环境。我们用initLuaEnv这个函数来初始化。lua_open函数用来获取一个新环境,然后我们用luaopen_base打开Lua的基础库,然后用luaL_openlibs打开Lua的io、string、math、table等高级库。

    加载Lua文件

    然后我们用luaL_loadfile和lua_pcall来将一个Lua脚本加载到对应的Lua运行时环境中——注意:并不自动执行,只是加载。这两个函数如果返回非0,表示加载失败——你的Lua脚本文件可能未找到或Lua脚本有语法错误……

    加载Lua函数

    我们用lua_getglobal函数将Lua脚本中的全局函数、全局变量等入栈,放在栈顶。

    压入Lua函数调用参数

    我们用lua_push系列函数来将要调用函数所需参数全部入栈,入栈顺序为Lua函数对应参数从左到右的顺序。

    调用Lua函数

    1. int lua_pcall(lua_State* L, int nargs, int nresults, int errfunc);

    其中,L为此函数执行的Lua环境,nargs为此函数所需的参数个数,nresults为此函数的返回值个数,errfunc为发生错误时错误处理函数在堆栈中的索引。

    获取Lua函数返回值

    然后,我们可以通过检测栈顶的位置(从-1开始),来获取返回值。获取返回值后,用lua_pop将栈顶元素出栈——出栈个数为返回值个数。

    释放Lua环境

    最后,通过lua_close函数来关闭Lua环境并释放资源。

    运行结果

    我们将GenerateNoteXML.lua脚本和最终的C++生成的GenerateNoteXML.exe放在同一路径下,并运行GenerateNoteXML.exe,在此目录下会生成一个XML文件。如下:

    1. Enter your name:
    2. Jack
    3. Enter destnation name:
    4. Joe
    5. Enter message content:
    6. Hello, Can you help me?
    7. Generate Note File Successful!

    生成的arnozhang_YaFengZhang.xml文件如下:

    一定是要定义成:
    luaopen_(dll或so文件的文件名称),(dll或so文件的文件名称)必须和dll或so文件名称保持一致。

    例如(C++ windows情况):

    1. #ifdef _WIN32
    2. #define _EXPORT extern "C" __declspec(dllexport)
    3. #else //unix/linux
    4. #define _EXPORT extern "C"
    5. #endif
    6. _EXPORT int luaopen_capi_mytestlib(lua_State *L)
    7. {
    8. struct luaL_reg driver[] = {
    9. {"average", average1},
    10. {NULL, NULL},};
    11. luaL_register(L, "mylib", driver);
    12. //luaL_openlib(L, "mylib", driver, 0);
    13. return 1;
    14. }

    动态库要供LUA调用的function

    其定义要符合:

    1. typedef int function(lua_State *L)

    在动态库调用LUA注册

    将要调用的函数放到这个结构体里:

    1. struct luaL_Reg lib[] ={}

    在动态库的入口函数里调用luaL_register将这个结构体注册,在这个入口函数注册结构体时,要注册成:

    1. luaL_register(L,"XXX",lib);

    在写脚本的时候,使用require(“XXX”)

    就是入口函数的luaopen_后面的XXX,注意大小写敏感

    编译生成的动态库命令成XXX.so或XXX.dll(win)

    同入口函数的luaopen_后面的XXX一致

    示例:

    C文件如下:

    1. #include <stdio.h>
    2. #include "lua/lua.h"
    3. #include "lua/lualib.h"
    4. #include "lua/lauxlib.h"
    5. static int add(lua_State *L)
    6. {
    7. int a,b,c;
    8. a = lua_tonumber(L,1);
    9. b = lua_tonumber(L,2);
    10. c = a+b;
    11. lua_pushnumber(L,c);
    12. printf("test hello!!!\r\n");
    13. return 1;
    14. }
    15. static const struct luaL_Reg lib[] =
    16. {
    17. {"testadd",add},
    18. {NULL,NULL}
    19. };
    20. int luaopen_testlib(lua_State *L)
    21. {
    22. luaL_register(L,"testlib",lib);
    23. return 1;
    24. }

    编译: gcc test.c -fPIC -shared -o testlib.so

    lua脚本编写:

    1. require("testlib")
    2. c = testlib.testadd(15,25)
    3. print("The result is ",c);

    示例:

    1. int lua_createmeta (lua_State *L, const char *name, const luaL_reg *methods) {
    2. if (!luaL_newmetatable (L, name))
    3. return 0;
    4. luaL_openlib (L, NULL, methods, 0);
    5. lua_pushliteral (L, "__gc");
    6. lua_pushcfunction (L, methods->func);
    7. lua_settable (L, -3);
    8. lua_pushliteral (L, "__index");
    9. lua_pushvalue (L, -2);
    10. lua_settable (L, -3);
    11. lua_pushliteral (L, "__metatable");
    12. lua_pushliteral (L, "you're not allowed to get this metatable");
    13. lua_settable (L, -3);
    14. return 1;
    15. }

    示例中的luaopen_testlib函数替换为:

    1. int luaopen_testlib(lua_State *L)
    2. {
    3. lua_createmeta(L,"testlib",lib);
    4. return 1;