Open iChienWei opened 6 years ago
本文原写于 2014-10-25
了解脚本语言和编译语言的话,都知道脚本语言轻量和易修改维护的特点,Lua 本身又是由 C 编写而成,作为 C/C++ 的一个功能“扩展”,就是很自然的事了。
Cocos2d-x/quick-Cocos2dx 提供了 Lua 语言绑定,也是为了利用 Lua 语言小巧的特点,可以快速开发功能,而避免使用 C++ 编写时每次都需要编译的情况。
使用 lua_newstate() 创建一个 Lua 状态机,如果有需要的话,调用 luaL_openlibs() 加载Lua的标准库
lua_newstate()
luaL_openlibs()
使用 luaL_loadfile 加载 Lua 文件或者脚本到 Lua 引擎中,使用 lua_call 或 lua_pcall 等方法执行已加载的脚本
luaL_loadfile
lua_call
lua_pcall
调用 lua_close(L) 方法关闭引擎并释放资源。
lua_close(L)
C++ 文件如下:
//c++ file: main.cpp #include <stdio.h> #include <iostream> using namespace std; extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" }; lua_State *L; int main(int argc, char *argv[]) { L = luaL_newstate(); luaL_openlibs(L); luaL_loadfile(L, "main.lua"); lua_pcall(L, 0, LUA_MULTRET, 0); lua_close(L); return 0; }
Lua 文件如下:
-- Lua file: main.lua print "Hello World!"
在终端中使用 g++ 编译,需要指定包含的头文件路径和加载的动态库
g++ -o CallLua main.cpp -I<Lua_head_file_path> -L<Lua_lib_file_path> -lllua
当前本机的 Lua 头文件位置在 /usr/local/Cellar/lua/5.1.5/include,而动态库文件位置在 /usr/local/lia 中。
/usr/local/Cellar/lua/5.1.5/include
/usr/local/lia
编译后生成名为 CallLua 的可执行文件,执行后,终端输出 "Hello World!"。
在调用 Lua 引擎时,都会返回一个包含 Lua 引擎状态信息的数据结构( lua_State )指针,即成功调用 lua_newstate() 会返回一个 lua_State 指针,失败时则为 NULL,这个结构用来确定特定的 Lua 引擎会话,说明 Lua 脚本引擎可以允许同时存在多个会话,这些会话各自保有自己的运行环境。
lua_State
C/C++ 调用 Lua API 的方式,是 Lua 引擎虚拟了函数栈空间,通过对数据的压栈和弹栈实现语言间交互。
Lua 简洁轻量,本身就非常适合作为配置脚本,先来看读取配置(全局变量)的例子。
--Lua config file: main.lua FONT_SIZE = 20
// c++ file: main.cpp #include <stdio.h> #include <iostream> using namespace std; extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" }; lua_State *L; int main(int argc, char *argv[]) { L = luaL_newstate(); luaL_openlibs(L); if (luaL_loadfile(L, "main.lua") || lua_pcall(L, 0, 0, 0)) { printf("Error at load file of \"main.lua\", %s\n", lua_tostring(L, -1)); lua_close(L); return 0; } lua_getglobal(L, "FONT_SIZE"); if (!lua_isnumber(L, -1)) { printf("Error FONT SIZE should be a number \n"); lua_close(L); return 0; } int fontSize = lua_tointeger(L, -1); cout << "font size = " << fontSize << endl; lua_close(L); return 0; }
读取过程显而易见,通过 lua_getglobal(L, "FONT_SIZE") 将 Lua 脚本中的全局变量 FONT_SIZE 压入虚拟栈中,对于虚拟栈空间,栈顶索引为 -1,向栈底减小。通过 lua_tointeger(L, -1) 读取出栈顶元素并转换成整型。
lua_getglobal(L, "FONT_SIZE")
FONT_SIZE
lua_tointeger(L, -1)
调用 Lua 的函数,涉及到向 Lua 传递参数和接收返回值。
--Lua file : main.lua function add2int(x, y) return x + y end
// c++ file: main.cpp #include <stdio.h> #include <iostream> using namespace std; extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" }; lua_State *L; int callLuaAdd(lua_State *L, int x, int y) { lua_getglobal(L, "add2int"); lua_pushnumber(L, x); lua_pushnumber(L, y); lua_call(L, 2, 1); int sum = (int)lua_tonumber(L, -1); lua_pop(L, 1); return sum; } int main(int argc, char *argv[]) { L = luaL_newstate(); luaL_openlibs(L); luaL_loadfile(L, "main.lua"); int sum = callLuaAdd(10, 15); cout << "sum = " << sum << endl; lua_close(L); return 0; }
在 callLuaAdd 的函数中,先将 Lua 脚本中的 add2int 方法压栈,然后将 C++ 的参数 x,y 通过 lua_pushnumber 再压入栈中,然后 lua_call 的方法中,指出当前调用有 2 个参数和 1 个返回值,调用结束后,函数和参数均被自动弹栈,返回值被压倒栈顶,通过 lua_tonumber(L, -1) 得到栈顶元素,之后用 lua_pop(L, 1) 将返回值从栈中弹出,栈中元素从栈底向栈顶方向索引从 1 逐渐增大。
callLuaAdd
add2int
lua_pushnumber
lua_tonumber(L, -1)
lua_pop(L, 1)
table 是 Lua 中重要的数据结构,灵活度非常高,在 C++ 中可以通过虚拟栈空间读写 table 数据,使两种语言交互更加方便。
// c++ file: main.cpp #include <stdio.h> #include <iostream> using namespace std; extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" }; void createLuaTable(lua_State *L); void modLuaTable(lua_State *L); lua_State *L; int main(int argc, char *argv[]) { L = luaL_newstate(); luaL_openlibs(L); createLuaTable(L); modLuaTable(L); lua_close(L); return 0; } void createLuaTable(lua_State *L) { lua_newtable(L); lua_pushstring(L, "Gyan"); lua_setfield(L, -2, "name"); lua_pushnumber(L, 10); lua_setfield(L, -2, "age"); lua_pushstring(L, "M"); lua_setfield(L, -2, "sex"); lua_setglobal(L, "user"); } void modLuaTable(lua_State *L) { lua_getglobal(L, "user"); //push user if (!lua_istable(L, -1)) { printf("'user' is not a table.\n" ); return; } lua_getfield(L, -1, "name"); //push name if (!lua_isstring(L, -1)) { printf("Invalid component in user.\n"); return; } string name = (string)lua_tostring(L, -1); lua_pop(L, 1); //pop name lua_getfield(L, -1, "age"); //push age if (!lua_isnumber(L, -1)) { printf("Invalid component in user.\n"); return; } int age = (int)lua_tonumber(L, -1); cout << "age = " << age << endl; lua_pop(L, 1); //pop name lua_pushnumber(L, 20); //push new data lua_setfield(L, -2, "age"); //set age and pop new data lua_getfield(L, -1, "age"); //push age if (!lua_isnumber(L, -1)) { printf("Invalid component in user.\n"); return; } age = (int)lua_tonumber(L, -1); lua_pop(L, 1); //pop age lua_getfield(L, -1, "sex"); //push sex if (!lua_isstring(L, -1)) { printf("Invalid component in user.\n"); return; } string sex = (string)lua_tostring(L, -1); lua_pop(L, 1); //pop sex lua_pop(L, 1); //pop user cout << "user is " << name << ", " << age << " year old, " << sex << endl; }
先看构造 table 的方法 createLuaTable,其中,lua_newtable 是宏语句:
createLuaTable
lua_newtable
#define lua_newtable(L) lua_createtable(L, 0, 0)
调用宏后,Lua 引擎会生成一个新的 table 对象压入栈中。随后,lua_pushstring 将字符串变量继续压入栈中,而函数:
lua_pushstring
void lua_setfield(lua_State *L, int index, const char *key);
使将栈索引 index 处的值赋值到 key 中,函数调用成功后会将新值弹栈。结尾处的 lua_setglobal 也是宏:
lua_setglobal
#define lua_setglobal(L, s) lua_setfield(L, LUA_GLOBALSINDEX, s)
调用后,会将当前栈顶元素赋值给指定的全局变量名,执行成功后,栈顶元素被弹出。
在随后的读取 table 的方法中,lua_getglobal 仍为宏:
lua_getglobal
#define lua_getglobal(L, s) lua_getfield(L, LUA_GLOBALSINDEX, s)
将相应的全局变量压入栈中,随后出现的方法:
void lua_getfield(lua_State *L, int index, const char *key);
即通过传入的会话结构 L,通过栈元素索引 index,来获取 key 的 value。函数调用成功后,会将取出的值压栈。
导出 C++ 的函数工 Lua 调用就与 Cocos2d-x 的 Lua 绑定原理很类似了。
希望能被 Lua 调用的函数原型都必须形如:
typedef int (*lua_CallFunc)(lua_State *L);
即函数有且只有一个 lua_State* 类型的参数,同时返回一个整型值,表示当函数调用结束后,会有多少个返回值被压入栈中。
看一个例子:
// c++ file: main.cpp #include <stdio.h> #include <iostream> using namespace std; extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" }; int multi2int(lua_State *L); lua_State *L; int main(int argc, char *argv[]) { L = luaL_newstate(); luaL_openlibs(L); lua_register(L, "multi2int", multi2int); luaL_loadfile(L, "main.lua"); lua_pcall(L, 0, LUA_MULTRET, 0); lua_close(L); return 0; } int multi2int(lua_State *L) { int argc = lua_gettop(L); int result = 0; int x = (int)lua_tonumber(L, 1); int y = (int)lua_tonumber(L, 2); cout << "[C++] calc x * y" << endl; lua_pushnumber(L, x * y); return 1; }
local r = multi2int(2, 5); print("result = " .. tostring(r))
在上述示例中,定义了满足要求的整数求积方法,通过 lua_register 注册到来脚本引擎中,这条宏命令等价于:
lua_register
lua_pushcfunction(L, func); lua_setglobal(L, funcname);
这样即可以在 Lua 中使用该方法了。
更进一步,可以将 C++ 中的方法制作成 Lua 的模块,像系统库那样 require 进去使用,方法如下:
#include <iostream> #include <stdio.h> #include <lua.hpp> #include <lauxlib.h> #include <lualib.h> using namespace std; extern "C" int multi2int(lua_State *L) { int argc = lua_gettop(L); int result = 0; int x = (int)lua_tonumber(L, 1); int y = (int)lua_tonumber(L, 2); cout << "[C++] calc x * y" << endl; lua_pushnumber(L, x * y); return 1; } static luaL_Reg customlibs[] = { {"multi2int", multi2int}, {NULL, NULL}, }; extern "C" __declspec(dllexport) int luaopen_customlibs(lua_State* L) { const char* libName = "customlibs"; luaL_register(L, libName, customlibs); return 1; }
上例中,luaL_Reg 结构体的第一个字段为字符串,在注册时用于通知 Lua 该函数的名字。第一个字段为 C 函数指针。结构体数组中的最后一个元素的两个字段均为 NULL,用于提示 Lua 注册函数已经到达数组的末尾。而形如 luaopen_LibName 的函数则是该库的入口,自定义的库函数名称格式必须如此,LibName 将作为 lib 的名称,用于 Lua 文件中 require。
luaopen_LibName
上述 C++ 代码通过编译成动态链接库,放入 Lua 环境变量指定的路径中,即可在 Lua 代码中 require 使用,像系统库 os 等一样。
Great tutorial! : D
Thank U. :)
本文原写于 2014-10-25
了解脚本语言和编译语言的话,都知道脚本语言轻量和易修改维护的特点,Lua 本身又是由 C 编写而成,作为 C/C++ 的一个功能“扩展”,就是很自然的事了。
Cocos2d-x/quick-Cocos2dx 提供了 Lua 语言绑定,也是为了利用 Lua 语言小巧的特点,可以快速开发功能,而避免使用 C++ 编写时每次都需要编译的情况。
C++ 调用 Lua 流程
使用
lua_newstate()
创建一个 Lua 状态机,如果有需要的话,调用luaL_openlibs()
加载Lua的标准库使用
luaL_loadfile
加载 Lua 文件或者脚本到 Lua 引擎中,使用lua_call
或lua_pcall
等方法执行已加载的脚本调用
lua_close(L)
方法关闭引擎并释放资源。C++ 调用 Lua 示例
C++ 文件如下:
Lua 文件如下:
在终端中使用 g++ 编译,需要指定包含的头文件路径和加载的动态库
当前本机的 Lua 头文件位置在
/usr/local/Cellar/lua/5.1.5/include
,而动态库文件位置在/usr/local/lia
中。编译后生成名为 CallLua 的可执行文件,执行后,终端输出 "Hello World!"。
在调用 Lua 引擎时,都会返回一个包含 Lua 引擎状态信息的数据结构(
lua_State
)指针,即成功调用lua_newstate()
会返回一个lua_State
指针,失败时则为 NULL,这个结构用来确定特定的 Lua 引擎会话,说明 Lua 脚本引擎可以允许同时存在多个会话,这些会话各自保有自己的运行环境。C++ 与 Lua的交互
C/C++ 调用 Lua API 的方式,是 Lua 引擎虚拟了函数栈空间,通过对数据的压栈和弹栈实现语言间交互。
Lua 简洁轻量,本身就非常适合作为配置脚本,先来看读取配置(全局变量)的例子。
读取过程显而易见,通过
lua_getglobal(L, "FONT_SIZE")
将 Lua 脚本中的全局变量FONT_SIZE
压入虚拟栈中,对于虚拟栈空间,栈顶索引为 -1,向栈底减小。通过lua_tointeger(L, -1)
读取出栈顶元素并转换成整型。调用 Lua 的函数,涉及到向 Lua 传递参数和接收返回值。
在
callLuaAdd
的函数中,先将 Lua 脚本中的add2int
方法压栈,然后将 C++ 的参数 x,y 通过lua_pushnumber
再压入栈中,然后lua_call
的方法中,指出当前调用有 2 个参数和 1 个返回值,调用结束后,函数和参数均被自动弹栈,返回值被压倒栈顶,通过lua_tonumber(L, -1)
得到栈顶元素,之后用lua_pop(L, 1)
将返回值从栈中弹出,栈中元素从栈底向栈顶方向索引从 1 逐渐增大。table 是 Lua 中重要的数据结构,灵活度非常高,在 C++ 中可以通过虚拟栈空间读写 table 数据,使两种语言交互更加方便。
先看构造 table 的方法
createLuaTable
,其中,lua_newtable
是宏语句:#define lua_newtable(L) lua_createtable(L, 0, 0)
调用宏后,Lua 引擎会生成一个新的 table 对象压入栈中。随后,
lua_pushstring
将字符串变量继续压入栈中,而函数:void lua_setfield(lua_State *L, int index, const char *key);
使将栈索引 index 处的值赋值到 key 中,函数调用成功后会将新值弹栈。结尾处的
lua_setglobal
也是宏:#define lua_setglobal(L, s) lua_setfield(L, LUA_GLOBALSINDEX, s)
调用后,会将当前栈顶元素赋值给指定的全局变量名,执行成功后,栈顶元素被弹出。
在随后的读取 table 的方法中,
lua_getglobal
仍为宏:#define lua_getglobal(L, s) lua_getfield(L, LUA_GLOBALSINDEX, s)
将相应的全局变量压入栈中,随后出现的方法:
void lua_getfield(lua_State *L, int index, const char *key);
即通过传入的会话结构 L,通过栈元素索引 index,来获取 key 的 value。函数调用成功后,会将取出的值压栈。
导出C++ 接口到 Lua
导出 C++ 的函数工 Lua 调用就与 Cocos2d-x 的 Lua 绑定原理很类似了。
希望能被 Lua 调用的函数原型都必须形如:
typedef int (*lua_CallFunc)(lua_State *L);
即函数有且只有一个 lua_State* 类型的参数,同时返回一个整型值,表示当函数调用结束后,会有多少个返回值被压入栈中。
看一个例子:
在上述示例中,定义了满足要求的整数求积方法,通过
lua_register
注册到来脚本引擎中,这条宏命令等价于:这样即可以在 Lua 中使用该方法了。
更进一步,可以将 C++ 中的方法制作成 Lua 的模块,像系统库那样 require 进去使用,方法如下:
上例中,luaL_Reg 结构体的第一个字段为字符串,在注册时用于通知 Lua 该函数的名字。第一个字段为 C 函数指针。结构体数组中的最后一个元素的两个字段均为 NULL,用于提示 Lua 注册函数已经到达数组的末尾。而形如
luaopen_LibName
的函数则是该库的入口,自定义的库函数名称格式必须如此,LibName 将作为 lib 的名称,用于 Lua 文件中 require。上述 C++ 代码通过编译成动态链接库,放入 Lua 环境变量指定的路径中,即可在 Lua 代码中 require 使用,像系统库 os 等一样。