本篇全面总结了 C++ 与 Lua 脚本交互的相关内容。内容目录:
编译并配置 Lua 库
Lua 与 C++ 交互原理
C++ 调用 Lua 脚本
Lua 调用 C++ 函数
Lua 调用 C++ 函数模块
Lua 以模块形式使用 C++ 类
Lua 以面向对象形式使用 C++ 类
Lua 与 C++ 全局数组交互
常用 API 总结
1 编译并配置 Lua 库 想要 Build 源码并直接使用 Lua 环境可以查看官网的详细步骤:lua-users wiki: Building Lua In Windows For Newbies
为了与 C++ 程序交互,这里我们使用 VS 来将 Lua 编译成动态库,供 C++ 程序调用。
首先在 VS 中新建一个 DLL 项目:
切换到 Release 模式,然后将 Lua 源码的 src 文件夹复制到工程目录下,并将其中除 lua.c 和 luac.c 之外的所有 .c 和 .h 文件添加到工程中(因为这两个文件中有 main 函数,编译时会出错,也可以把 main 改个名字)。
修改工程属性,C/C++ -> 预编译头 -> 不使用预编译头:
再修改:C/C++ -> 预处理器 -> 预处理器定义,加上 LUA_BUILD_AS_DLL
宏定义:
之后进行生成,就可以得到编译后的库文件了:
接下来为了方便后续使用,我们在合适的位置新建一个文件夹来放置 Lua 库和头文件:
然后将 Lua 源码中的 src 文件夹下的文件复制到 include 文件夹下,将生成的 .lib 文件复制到 lib 文件夹下。
接下来新建一个 C++ 工程,然后在配置项中加入包含目录:
在链接器配置中添加 Lua 库目录和库文件:
之后生成项目的 Debug/Release 目录,并将 Lua 动态库 .dll 文件放到该文件夹下:
然后写一个测试程序测试是否配置成功:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <iostream> #include <string.h> using namespace std; extern "C" { #include "lua.h" #include "lauxlib.h" #include "lualib.h" } void main () { lua_State *L = luaL_newstate (); lua_pushstring (L, "I am so cool~" ); lua_pushnumber (L, 20 ); if (lua_isstring (L, 1 )) { cout << lua_tostring (L,1 ) << endl; } if (lua_isnumber (L, 2 )) { cout << lua_tonumber (L, 2 ) << endl; } lua_close (L); return ; }
输出:
就表明配置完成了。
2 Lua 与 C++ 交互原理 Lua 与 C/C++ 交互是通过一个虚拟栈来实现的,这个虚拟栈实际上就是一个 struct,具体实现可以从 Lua 源码中 lstate.c 文件中的 static void stack_init (lua_State *L1, lua_State *L)
函数看起。
与常规栈不同的是 Lua 虚拟栈不仅有正向索引(正数),还有反向索引(负数),其中正数索引 1 表示栈底,负数索引 -1 表示栈顶:
于是 Lua 和 C++ 程序交互时,如果要传递值,基本原理就是:
而如果要发生函数交互,过程会稍微复杂一点:
当 C++ 要调用 Lua 函数时,Lua 先将 Lua 函数压入栈中,C++ 再将数据(作为参数)继续压入栈中,然后用 API 调用栈上的 Lua 函数 + 参数,调用完后,Lua 函数和参数都会出栈,而函数计算后的值会压入栈中:
当 Lua 要调用 C++ 函数时,需要通过 API 注册符合 Lua 规范的 C++ 函数,来让 Lua 知道该 C++ 函数的定义。
存入栈中的数据可以是 Lua 中的任何数据类型,包括数值, 字符串, 指针, talbe, 闭包等等,因为 Lua 的虚拟栈在源码中是这样定义的:
1 2 3 4 5 6 7 8 typedef union StackValue { TValue val; struct { TValuefields; unsigned short delta; } tbclist; } StackValue;
其中的数据都是用一个名为 TValue
的结构体来存储的,这个结构体中就包含了 Lua 中的所有数据类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef union Value { struct GCObject *gc; void *p; lua_CFunction f; lua_Integer i; lua_Number n; } Value; #define TValuefields Value value_; lu_byte tt_ typedef struct TValue {TValuefields; } TValue;
更多的实现可以查看源码中的 lobject.h 文件。
Lua 和 C++ 通信时有一个符合设计原则的约定:所有的 Lua 中的值由 Lua 来管理,C/C++ 中产生的值 Lua 不知道, 类似表达了这样一种意思:”如果你(C/C++)想要什么,你告诉我(Lua),我来产生,然后放到栈上,你只能通过 api 来操作这个值,而我负责管好我的世界 “。这样的解耦很有必要,因为这可以保证只要是 Lua 中的变量,Lua 就要负责这些变量的生命周期和垃圾回收,而 C/C++ 程序就无需多虑。所以,必须由 Lua 来创建这些值。
3 C++ 调用 Lua 脚本 根据以上原理,C++ 调用 Lua 脚本时一般步骤是:
使用 luaL_newstate()
创建 Lua 状态机,返回指向虚拟栈的指针
使用 luaL_loadfile()
加载 Lua 脚本文件
使用 lua_pcall()
运行 Lua 脚本文件
需要获取 Lua 中的值时:
使用 lua_getglobal
获取值
使用 lua_toXXX
将值转化为对应的 C++ 数据类型
如果获取的值为 table 的话,可以使用 lua_getfield
和 lua_setfield
来获取和修改 table 中的元素
需要获取 Lua 中的函数时:
使用 lua_getglobal
获取函数
如果函数有参数的话,使用 lua_pushXXX
将参数依次入栈
使用 lua_pcall()
调用函数,然后从栈顶取出函数返回值即可
接下来我们创建一个简单的 Lua 脚本:
1 2 3 4 5 str = "Test Lua script." tbl = {name = "LYC" , id = 2020262914 } function add (a,b) return a + b end
然后使用 C++ 程序来调用脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 #include <iostream> #include <string.h> #include "lua.hpp" using namespace std;int main () { lua_State* L = luaL_newstate (); if (L == NULL ) { return -1 ; } int bRet = luaL_loadfile (L, "test.lua" ); if (bRet) { cout << "load file error" << endl; return -1 ; } bRet = lua_pcall (L, 0 , 0 , 0 ); if (bRet) { cout << "pcall error" << endl; return -1 ; } lua_getglobal (L, "str" ); string str = lua_tostring (L, -1 ); cout << "str = " << str.c_str () << endl; lua_getglobal (L, "tbl" ); lua_getfield (L, -1 , "name" ); str = lua_tostring (L, -1 ); lua_getfield (L, -2 , "id" ); int id = lua_tonumber (L, -1 ); cout << "tbl:name = " << str.c_str () << endl; cout << "tbl:id = " << id << endl; lua_getglobal (L, "add" ); lua_pushnumber (L, 10 ); lua_pushnumber (L, 20 ); int iRet = lua_pcall (L, 2 , 1 , 0 ); if (iRet) { const char * pErrorMsg = lua_tostring (L, -1 ); cout << pErrorMsg << endl; lua_close (L); return -1 ; } if (lua_isnumber (L, -1 )) { int fValue = lua_tonumber (L, -1 ); cout << "Result is " << fValue << endl; } lua_pushstring (L, "Master" ); lua_setfield (L, 2 , "name" ); lua_getfield (L, 2 , "name" ); str = lua_tostring (L, -1 ); cout << "tbl:name = " << str.c_str () << endl; lua_newtable (L); lua_pushstring (L, "A New Table" ); lua_setfield (L, -2 , "name" ); lua_getfield (L, -1 , "name" ); str = lua_tostring (L, -1 ); cout << "newtbl:name = " << str.c_str () << endl; lua_close (L); return 0 ; }
输出:
当我们修改 Lua 脚本的内容:
1 2 3 4 5 6 7 8 str = "A new Lua script." tbl = {name = "New" , id = 6666666 } function add (a, b) for i = 10 , 1 , -1 do a = a + b end return a end
重新运行的结果:
4 Lua 调用 C++ 函数 Lua 中使用 C++ 函数的一般步骤是:
将 C++ 函数包装成 Lua 环境认可的 Lua_CFunction 格式
将包装好的函数注册到 Lua 环境中
像使用普通 Lua 函数那样使用注册函数
Lua_CFunction 的基本格式如下:
1 2 3 4 static int xxxxx (lua_State *L) { return 一个数字; }
函数的返回值代表函数返回结果的个数,而要获取参数则是通过将栈中某个位置的元素转换成 C++ 数据类型来实现,比如一个 3 个输入参数,2 个返回值的函数实现为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static int average (lua_State *L) { int n = lua_gettop (L); double sum = 0 ; int i; for (i = 1 ; i <= n; i++) { sum += lua_tonumber (L, i); } lua_pushnumber (L, sum / n); lua_pushnumber (L, sum); return 2 ; }
然后将其注册到 Lua 环境中:
1 2 lua_register (L, "func" , average);
之后我们编写一个 Lua 脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 print ("age" , age)for k,v in pairs (newTable) do print ("k = " ,k," v = " ,v) end print ("name" , newTable.name)avg, sum = func(10 , 20 , 30 , 40 , 50 ) print ("The average is " , avg)print ("The sum is " , sum)
接下来是 C++ 部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <iostream> #include <string.h> #include "lua.hpp" using namespace std;static int average (lua_State* L) { int n = lua_gettop (L); double sum = 0 ; int i; for (i = 1 ; i <= n; i++) { sum += lua_tonumber (L, i); } lua_pushnumber (L, sum / n); lua_pushnumber (L, sum); return 2 ; } int main () { lua_State* L = luaL_newstate (); luaL_openlibs (L); lua_register (L, "func" , average); lua_pushinteger (L, 18 ); lua_setglobal (L, "age" ); lua_newtable (L); lua_pushstring (L, "lili" ); lua_setfield (L, -2 , "name" ); lua_setglobal (L, "newTable" ); luaL_dofile (L, "test.lua" ); lua_close (L); return 0 ; }
运行结果:
5 Lua 调用 C++ 函数模块 当 Lua 中需要调用多个 C++ 函数时,可以将这些函数封装成一个模块,模块实际上是一个 table,然后使用 luaL_newlib
就可以将该模块压入栈中,之后将自定义的模块注册到 Lua 环境中就可以使用了。
依然以上面的函数为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include <iostream> #include <string.h> #include "lua.hpp" using namespace std;static int average (lua_State* L) { int n = lua_gettop (L); double sum = 0 ; int i; for (i = 1 ; i <= n; i++) { sum += lua_tonumber (L, i); } lua_pushnumber (L, sum / n); lua_pushnumber (L, sum); return 2 ; } static const luaL_Reg mylibs_funcs[] = { { "func" , average }, { NULL , NULL } }; int lua_openmylib (lua_State* L) { luaL_newlib (L, mylibs_funcs); return 1 ; } static const luaL_Reg lua_reg_libs[] = { { "base" , luaopen_base }, { "mylib" , lua_openmylib }, { NULL , NULL } }; int main (int argc, char * argv[]) { lua_State* L = luaL_newstate (); luaL_openlibs (L); const luaL_Reg* lua_reg = lua_reg_libs; for (; lua_reg->func; ++lua_reg) { luaL_requiref (L, lua_reg->name, lua_reg->func, 1 ); lua_pop (L, 1 ); } luaL_dofile (L, "test.lua" ); lua_close (L); return 0 ; }
在 Lua 中使用自定义模块中的函数时需要加上模块名字:
1 2 3 4 avg, sum = mylib.func(10 , 20 , 30 , 40 , 50 ) print ("The average is " , avg)print ("The sum is " , sum)
运行结果:
6 Lua 以模块形式使用 C++ 类 Lua 中还可以调用 C++ 中的类。我们可以将 C++ 中的类封装成函数模块,供 Lua 使用。
例如一个 Student 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #pragma once #include <iostream> #include <string> using namespace std;class Student { public : Student (); ~Student (); string get_name () ; void set_name (string name) ; unsigned int get_age () ; void set_age (unsigned int age) ; void print () ; private : string _name; unsigned int _age; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include "Student.h" using namespace std;Student::Student () :_name("Empty" ), _age(0 ) { cout << "Student Constructor" << endl; } Student::~Student () { cout << "Student Destructor" << endl; } string Student::get_name () { return _name; } void Student::set_name (string name) { _name = name; } unsigned int Student::get_age () { return _age; } void Student::set_age (unsigned int age) { _age = age; } void Student::print () { cout << "name :" << _name << " age : " << _age << endl; }
然后我们需要将这个类的各种方法以及创建这个类对象的方法注册成为 Lua 的全局函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #pragma once #include "Student.h" #include "lua.hpp" int lua_create_new_student (lua_State* L) ;int lua_get_name (lua_State* L) ;int lua_set_name (lua_State* L) ;int lua_get_age (lua_State* L) ;int lua_set_age (lua_State* L) ;int lua_print (lua_State* L) ;static const luaL_Reg lua_reg_student_funcs[] = { { "create" , lua_create_new_student }, { "get_name" , lua_get_name }, { "set_name" , lua_set_name }, { "get_age" , lua_get_age }, { "set_age" , lua_set_age }, { "print" , lua_print }, { NULL , NULL }, }; int luaopen_student_libs (lua_State* L) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include "StudentRegFunc.h" int lua_create_new_student (lua_State* L) { Student** s = (Student**)lua_newuserdata (L, sizeof (Student*)); *s = new Student (); return 1 ; } int lua_get_name (lua_State* L) { Student** s = (Student**)lua_touserdata (L, 1 ); luaL_argcheck (L, s != NULL , 1 , "invalid user data" ); lua_settop (L, 0 ); lua_pushstring (L, (*s)->get_name ().c_str ()); return 1 ; } int lua_set_name (lua_State* L) { Student** s = (Student**)lua_touserdata (L, 1 ); luaL_argcheck (L, s != NULL , 1 , "invalid user data" ); luaL_checktype (L, -1 , LUA_TSTRING); std::string name = lua_tostring (L, -1 ); (*s)->set_name (name); return 0 ; } int lua_get_age (lua_State* L) { Student** s = (Student**)lua_touserdata (L, 1 ); luaL_argcheck (L, s != NULL , 1 , "invalid user data" ); lua_pushinteger (L, (*s)->get_age ()); return 1 ; } int lua_set_age (lua_State* L) { Student** s = (Student**)lua_touserdata (L, 1 ); luaL_argcheck (L, s != NULL , 1 , "invalid user data" ); luaL_checktype (L, -1 , LUA_TNUMBER); (*s)->set_age ((unsigned int )lua_tointeger (L, -1 )); return 0 ; } int lua_print (lua_State* L) { Student** s = (Student**)lua_touserdata (L, 1 ); luaL_argcheck (L, s != NULL , 1 , "invalid user data" ); (*s)->print (); return 0 ; } int luaopen_student_libs (lua_State* L) { luaL_newlib (L, lua_reg_student_funcs); return 1 ; }
然后来编写脚本:
1 2 3 4 5 6 7 8 local student_obj = Student.create ()Student.print (student_obj) Student.set_name(student_obj,"Jack" ) Student.set_age(student_obj,25 ) Student.print (student_obj)
最后是主函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> #include <string.h> #include "Student.h" #include "StudentRegFunc.h" #include "lua.hpp" using namespace std;static const luaL_Reg lua_reg_libs[] = { { "base" , luaopen_base }, { "Student" , luaopen_student_libs}, { NULL , NULL } }; int main (int argc, char * argv[]) { if (lua_State* L = luaL_newstate ()) { const luaL_Reg* lua_reg = lua_reg_libs; for (; lua_reg->func; ++lua_reg) { luaL_requiref (L, lua_reg->name, lua_reg->func, 1 ); lua_pop (L, 1 ); } if (luaL_dofile (L, "test.lua" )) { cout << lua_tostring (L, -1 ) << endl; } lua_close (L); } else { cout << "luaL_newstate error !" << endl; } return 0 ; }
运行结果:
7 Lua 以面向对象方式使用 C++ 类 上述将 C++ 类注册为函数模块的形式有一个严重的问题,那就是我们无法保证 userdata 的合法性。因为在 C++ 中,我们只是简单的判断了一下传进来的 userdata 是否为 NULL,并没有办法判断传进来的 userdata 参数是通过 Student.create 函数得到的,如果我传一个错误的 userdata 进去,程序也会继续运行,但有可能使内存遭到破坏。
一种可行的方案是,为每种类型创建一个唯一的元表。每当创建了一个 userdata 后,就用相应的元表来标记它。而每当得到一个 userdata 后,就检查它是否拥有正确的元表。由于 Lua 代码不能改变 userdata 的元表,因此也就无法传一个错误的 userdata 进去。
为每个 userdata 都创建一个元表,那就需要有个地方来存储这个新的元表。在 Lua 中,通常习惯是将所有新的 C++ 类型注册到注册表中,以一个类型名作为 key,元表作为 value。由于注册表中还有其它的内容,所以必须小心地选择类型名,以避免与 key 冲突。
具体的实现可以查看:Lua和C++交互:在Lua中以面向对象的方式使用C++注册的类
8 Lua 与 C++ 全局数组交互 具体查看:Lua和C++交互:全局数组交互
9 常用 API 总结 9.1 虚拟栈基本操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int lua_gettop (lua_State *L) ; void lua_settop (lua_State *L, int idx) ; void lua_pushvalue (lua_State *L, int idx) ; void lua_remove (lua_State *L, int idx) ; void lua_insert (lua_State *L, int idx) ; void lua_replace (lua_State *L, int idx) ; int lua_checkstack (lua_State *L, int n) ;
9.2 数据压栈 lua_pushXXX:
1 2 3 4 5 6 7 8 9 10 11 12 LUA_API void (lua_pushnil) (lua_State *L) ;LUA_API void (lua_pushnumber) (lua_State *L, lua_Number n) ;LUA_API void (lua_pushinteger) (lua_State *L, lua_Integer n) ;LUA_API const char *(lua_pushlstring) (lua_State *L, const char *s, size_t len); LUA_API const char *(lua_pushstring) (lua_State *L, const char *s); LUA_API const char *(lua_pushvfstring) (lua_State *L, const char *fmt, va_list argp); LUA_API const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...); LUA_API void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n) ;LUA_API void (lua_pushboolean) (lua_State *L, int b) ;LUA_API void (lua_pushlightuserdata) (lua_State *L, void *p) ;LUA_API int (lua_pushthread) (lua_State *L) ;
9.3 判断栈中数据类型 lua_isXXX:
1 2 3 4 5 6 LUA_API int (lua_isnumber) (lua_State *L, int idx) ;LUA_API int (lua_isstring) (lua_State *L, int idx) ;LUA_API int (lua_iscfunction) (lua_State *L, int idx) ;LUA_API int (lua_isinteger) (lua_State *L, int idx) ;LUA_API int (lua_isuserdata) (lua_State *L, int idx) ;LUA_API int (lua_type) (lua_State *L, int idx) ;
9.4 获取栈中数据 lua_toXXX:
1 2 3 4 5 6 7 8 9 LUA_API lua_Number (lua_tonumberx) (lua_State *L, int idx, int *isnum) ;LUA_API lua_Integer (lua_tointegerx) (lua_State *L, int idx, int *isnum) ;LUA_API int (lua_toboolean) (lua_State *L, int idx) ;LUA_API const char *(lua_tolstring) (lua_State *L, int idx, size_t *len); LUA_API size_t (lua_rawlen) (lua_State *L, int idx) ;LUA_API lua_CFunction (lua_tocfunction) (lua_State *L, int idx) ;LUA_API void *(lua_touserdata) (lua_State *L, int idx); LUA_API lua_State *(lua_tothread) (lua_State *L, int idx); LUA_API const void *(lua_topointer) (lua_State *L, int idx);
9.5 获取栈中数据 lua_getXXX:
1 2 3 4 5 6 7 8 9 10 11 12 LUA_API int (lua_getglobal) (lua_State *L, const char *name) ;LUA_API int (lua_gettable) (lua_State *L, int idx) ;LUA_API int (lua_getfield) (lua_State *L, int idx, const char *k) ;LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n) ;LUA_API int (lua_rawget) (lua_State *L, int idx) ;LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n) ;LUA_API int (lua_rawgetp) (lua_State *L, int idx, const void *p) ; LUA_API void (lua_createtable) (lua_State *L, int narr, int nrec) ;LUA_API void *(lua_newuserdata) (lua_State *L, size_t sz); LUA_API int (lua_getmetatable) (lua_State *L, int objindex) ;LUA_API int (lua_getuservalue) (lua_State *L, int idx) ;
9.6 向栈中写入数据 lua_setXXX:
1 2 3 4 5 6 7 8 9 LUA_API void (lua_setglobal) (lua_State *L, const char *name) ;LUA_API void (lua_settable) (lua_State *L, int idx) ;LUA_API void (lua_setfield) (lua_State *L, int idx, const char *k) ;LUA_API void (lua_seti) (lua_State *L, int idx, lua_Integer n) ;LUA_API void (lua_rawset) (lua_State *L, int idx) ;LUA_API void (lua_rawseti) (lua_State *L, int idx, lua_Integer n) ;LUA_API void (lua_rawsetp) (lua_State *L, int idx, const void *p) ;LUA_API int (lua_setmetatable) (lua_State *L, int objindex) ;LUA_API void (lua_setuservalue) (lua_State *L, int idx) ;
10 参考资料