C++ 程序嵌 Lua(基于 LuaBridge)

配置文件搞不定的,就得依赖脚本。C++ 程序想嵌点脚本,Lua 几乎是首选。

Lua 的源码自带 Makefile,可以编译出静态库、解释器、编译器三个目标文件,作为宿主的 C++ 程序,除了要包含 Lua 头文件,还应该链接这个静态库。

如果 C++ 程序是由 CMake 来构建的,那么用 CMake 为 Lua 创建一个静态库,也不是什么难事。CMake 很好的解决了跨平台的问题。

其实脚本扩展的问题只有两个:一、怎么让 Lua 访问 C++ 对象?二、怎么让 C++ 访问 Lua 对象?当然所谓对象,是个宽泛的概念,包括变量、函数、类,等等。

通过 LuaBridge,可以很方便的解决这两个问题。

头文件

先交代一下头文件,后面就不提了。
首先包含 Lua 的几个头文件,因为是 C 代码,放在 extern "C" 里才能跟 C++ 程序混编。

extern "C" {

include "lua.h"

include "lualib.h"

include "lauxlib.h"

} // extern "C"
其次是 LuaBridge 头文件,LuaBridge 跟 STL 一样,只有头文件,直接包含使用。

# include "LuaBridge/LuaBridge.h"

Lua 访问 C++

函数

C++ 函数 SayHello 「导出」为 Lua 函数 sayHello,然后通过 luaL_dostring 执行 Lua 代码,调用这个函数。

void SayHello() {
std::cout
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);

luabridge::getGlobalNamespace(L)
.addFunction("sayHello", SayHello);

luaL_dostring(L, "sayHello()");

lua_close(L);
}
输出:

Hello, World!

为 SayHello 加个参数:

void SayHello(const char* to) {
std::cout
luabridge::getGlobalNamespace(L)
.addFunction("sayHello", SayHello);

luaL_dostring(L, "sayHello('Lua')");
输出:

Hello, Lua!

C++ 的类导出为 Lua 的表,类的成员函数对应于表的成员。假如有一个类 Line,表示文本文件中的一行:

class Line {
public:
Line(const std::string& data)
: data_(data) {
}

sizet Length() const {
return data
.length();
}

private:
std::string data_;
};

构造函数

导出构造函数用 addConstructor,导出成员函数还是用 addFunction:

luabridge::getGlobalNamespace(L)
.beginClass("Line")
.addConstructor()
.addFunction("getLength", &Line::Length)
.endClass();
构造函数无法取址,调用 addConstructor 时需传递模板参数以指明类型。

测试:

const char* str =  "line = Line('test')\n"  "print(line:getLength())\n";luaL_dostring(L, str);

输出:

4

如果有多个构造函数,比如还有一个缺省构造函数:

Line::Line();
则只能导出一个。下面这种写法,第二个会覆盖第一个:

luabridge::getGlobalNamespace(L)
.beginClass("Line")
.addConstructor() // 被下一句覆盖
.addConstructor()
.endClass();
你不可能让同一个名字指代两件事情。

成员函数

考虑一个稍微复杂的成员函数,StartWith 判断一行文本是否以某个字符串打头,参数 ignore_spaces 决定是否忽略行首的空格。对实现不感兴趣的可以完全忽略。

bool Line::StartWith(const std::string& str,
bool ignore_spaces) const {
size_t i = 0;

if (ignore_spaces && !IsSpace(str[0])) {
for (; i
通过 addFunction 导出到 Lua:

addFunction("startWith", &Line::StartWith)
测试:

const char* str =
"line = Line(' if ...')\n"
"print(line:startWith('if', false))\n"
"print(line:startWith('if', true))\n";
输出:

falsetrue

输出参数

现在为 StartWith 添加可选的输出参数,以便 ignore_spaces 为 true 时能够返回偏移信息(第一个非空字符的下标):

bool Line::StartWith(const std::string& str,
bool ignore_spaces,
int* off = NULL) const {
size_t i = 0;

if (ignore_spaces && !IsSpace(str[0])) {
for (; i (i);
}
return true;
}

return false;
}
输出参数在 C/C++ 里是很常见的用法,可以让一个函数返回多个值。但是用 addFunction 导出的 StartWith 并不能被 Lua 调用,因为 Lua 没有输出参数。幸运的是,Lua 的函数可以有多个返回值,为了让 StartWith 返回多个值,我们得做一层 Lua CFunction 的包装。

// Lua CFunction wrapper for StartWith.
int Line::Lua_StartWith(lua_State* L) {
// 获取参数个数
int n = lua_gettop(L);

// 验证参数个数
if (n != 3) {
luaL_error(L, "incorrect argument number");
}

// 验证参数类型
if (!lua_isstring(L, 2) || !lua_isboolean(L, 3)) {
luaL_error(L, "incorrect argument type");
}

// 获取参数
std::string str(lua_tostring(L, 2));
bool ignore_spaces = lua_toboolean(L, 3) != 0;

// 转调 StartWith
int off = 0;
bool result = StartWith(str, ignore_spaces, &off);

// 返回结果
luabridge::push(L, result);
luabridge::push(L, off);
return 2; // 返回值有两个
}
类型为 int () (lua_State) 的函数就叫 Lua CFunction。改用 addCFunction 导出 Lua_StartWith:

addCFunction("startWith", &Line::Lua_StartWith)
测试:

const char* str =
"line = Line(' if ...')\n"
"ok, off = line:startWith('if', true)\n"
"print(ok, off)\n";
输出:

true   2

变参

既然已经做了 CFunction 的封装,不如做得更彻底一些。鉴于 Lua 对变参的良好支持,我们让 startWith 支持变参,比如既可以判断是否以 'if' 打头:

line:startWith(true, 'if')
也可以判断是否以 'if' 或 'else' 打头:

line:startWith(true, 'if', 'else')
为此,ignore_spaces 变成了第一个参数,后面是字符串类型的变参,具体实现如下:

int Line::Lua_StartWith(lua_State* L) {
int n = lua_gettop(L);

if (n
测试:

const char* str =
"line = Line(' else ...')\n"
"ok, off = line:startWith(true, 'if', 'else')\n"
"print(ok, off)\n";
输出:

true   2

执行 Lua 文件

前面示例执行 Lua 代码全部使用 luaL_dostring,实际项目中,Lua 代码主要以文件形式存在,就需要 luaL_dofile。

测试:

luaL_dofile(L, "test.lua);
文件 test.lua 的内容为:

line = Line(' else ...')
ok, off = line:startWith(true, 'if', 'else')
print(ok, off)
输出:

true   2

C++ 访问 Lua

通过 getGlobal 函数可以拿到「全局」的 Lua 对象,类型为 LuaRef。

int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);

{ // 为了让 LuaRef 对象在 lua_close(L) 之前析构

const char* str =  "world = 'World'\n"  "sayHello = function(to)\n"  "    print('Hello, ' .. to .. '!')\n"  "end\n";luaL_dostring(L, str);using namespace luabridge;LuaRef world = getGlobal(L, "world");LuaRef say_hello = getGlobal(L, "sayHello");say_hello(world.cast());

}

lua_close(L);
}
输出:

Hello, World!

字符串

Lua 没有字符类型,也没有 Unicode 字符串(特指 wchar_t*)。

bool IsSpace(char c) {
return c == ' ' || c == '\t';
}
luabridge::getGlobalNamespace(L)
.addFunction("isSpace", IsSpace);

luaL_dostring(L, "print(isSpace(' '))");
luaL_dostring(L, "print(isSpace(' '))");
luaL_dostring(L, "print(isSpace('c'))");
输出:

truetruefalse

如果 IsSpace 参数为 wchar_t:

bool IsSpace(wchar_t c) {
return c == L' ' || c == L'\t';
}
在 Lua 里调用 isSpace(' ') 时,LuaBridge 便会断言失败:

Assertion failed: lua_istable (L, -1), file e:\proj\lua_test\third_party\include\luabridge\detail/Userdata.h, line 189

折中的办法是,为 IsSpace(wchar_t c) 提供一个 wrapper,专供 Lua 使用。

bool Lua_IsSpace(char c) {
return IsSpace((wchar_t)c);
}
luabridge::getGlobalNamespace(L)
.addFunction("isSpace", Lua_IsSpace);
当然前提是,Lua 代码调用 isSpace 时,只会传入 ASCII 字符。

错误处理

为了方便问题诊断和错误处理,有必要为内置的函数或宏做一些封装。

luaL_dostring

bool DoLuaString(lua_State L,
const std::string& str,
std::string
error = NULL) {
if (luaL_dostring(L, str.c_str()) != LUA_OK) {
if (error != NULL) {
// 从栈顶获取错误消息。
if (lua_gettop(L) != 0) {
*error = lua_tostring(L, -1);
}
}
return false;
}
return true;
}
测试:故意调用一个不存在的函数 SayHello(应该是 sayHello)。

std::string error;
if (!DoLuaString(L, "SayHello('Lua')", &error)) {
std::cerr
输出(试图调用一个空值):

[string "SayHello('Lua')"]:1: attempt to call a nil value (global 'SayHello')

luaL_dofile

与 luaL_dostring 的封装类似。

bool DoLuaFile(lua_State L,
const std::string& file,
std::string
error = NULL) {
if (luaL_dofile(L, file.c_str()) != LUA_OK) {
if (error != NULL) {
// 从栈顶获取错误消息。
if (lua_gettop(L) != 0) {
*error = lua_tostring(L, -1);
}
}
return false;
}

return true;
}

luabridge::LuaRef

LuaRef world = getGlobal(L, "world");
if (!world.isNil() && world.isString()) {
// ...
}
LuaRef say_hello = getGlobal(L, "sayHello");
if (!say_hello.isNil() && say_hello.isFunction()) {
// ...
}

luabridge::LuaException

如果 Lua 代码有什么问题,LuaBridge 会引发 LuaException 异常,相关代码最好放在 try...catch 中。

try {
// ...
} catch (const luabridge::LuaException& e) {
std::cerr

c++# lua, embed

版权声明

本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部