iChienWei / iChienWei.github.io

blog
GNU General Public License v3.0
1 stars 0 forks source link

Lua 与 C++ 的交互 #8

Open iChienWei opened 6 years ago

iChienWei commented 6 years ago

本文原写于 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_calllua_pcall 等方法执行已加载的脚本

调用 lua_close(L) 方法关闭引擎并释放资源。

C++ 调用 Lua 示例

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 中。

编译后生成名为 CallLua 的可执行文件,执行后,终端输出 "Hello World!"。

在调用 Lua 引擎时,都会返回一个包含 Lua 引擎状态信息的数据结构( lua_State )指针,即成功调用 lua_newstate() 会返回一个 lua_State 指针,失败时则为 NULL,这个结构用来确定特定的 Lua 引擎会话,说明 Lua 脚本引擎可以允许同时存在多个会话,这些会话各自保有自己的运行环境。

C++ 与 Lua的交互

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 的函数,涉及到向 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 逐渐增大。

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 是宏语句:

#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* 类型的参数,同时返回一个整型值,表示当函数调用结束后,会有多少个返回值被压入栈中。

看一个例子:

// 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_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。

上述 C++ 代码通过编译成动态链接库,放入 Lua 环境变量指定的路径中,即可在 Lua 代码中 require 使用,像系统库 os 等一样。

Dinaya commented 5 years ago

Great tutorial! : D

iChienWei commented 5 years ago

Great tutorial! : D

Thank U. :)