配置文件搞不定的,就得依赖脚本。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"
C++ 函数 SayHello
「导出」为 Lua 函数 sayHello
,然后通过 luaL_dostring
执行 Lua 代码,调用这个函数。
void SayHello() { std::cout << "Hello, World!" << std::endl; }
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 << "Hello, " << to << "!" << std::endl; }
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) { } size_t Length() const { return data_.length(); } private: std::string data_; };
导出构造函数用 addConstructor
,导出成员函数还是用 addFunction
:
luabridge::getGlobalNamespace(L) .beginClass<Line>("Line") .addConstructor<void(*)(const std::string&)>() .addFunction("getLength", &Line::Length) .endClass();
构造函数无法取址,调用 addConstructor
时需传递模板参数以指明类型。
测试:
const char* str = "line = Line('test')/n" "print(line:getLength())/n"; luaL_dostring(L, str);
输出:
如果有多个构造函数,比如还有一个缺省构造函数:
Line::Line();
则只能导出一个。下面这种写法,第二个会覆盖第一个:
luabridge::getGlobalNamespace(L) .beginClass<Line>("Line") .addConstructor<void(*)(void)>() // 被下一句覆盖 .addConstructor<void(*)(const std::string&)>() .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 < data_.size() && IsSpace(data_[i]); ++i) { } } if (data_.size() < i + str.size()) { return false; } if (strncmp(&data_[i], &str[0], str.size()) == 0) { return true; } return false; }
通过 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";
输出:
false true
现在为 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 < data_.size() && IsSpace(data_[i]); ++i) { } } if (data_.size() < i + str.size()) { return false; } if (strncmp(&data_[i], &str[0], str.size()) == 0) { if (off != NULL) { *off = static_cast<int>(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 < 3) { luaL_error(L, "incorrect argument number"); } if (!lua_isboolean(L, 2)) { luaL_error(L, "incorrect argument type"); } bool ignore_spaces = lua_toboolean(L, 2) != 0; bool result = false; int off = 0; // 逐个比较字符串变参,一旦匹配就跳出循环。 for (int i = 3; i <= n; ++i) { if (!lua_isstring(L, i)) { break; } std::string str(lua_tostring(L, i)); if (StartWith(str, ignore_spaces, &off)) { result = true; break; } } luabridge::push(L, result); luabridge::push(L, off); return 2; }
测试:
const char* str = "line = Line(' else ...')/n" "ok, off = line:startWith(true, 'if', 'else')/n" "print(ok, off)/n";
输出:
true 2
前面示例执行 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
通过 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<const char*>()); } 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'))");
输出:
true true false
如果 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/Us erdata.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 字符。
为了方便问题诊断和错误处理,有必要为内置的函数或宏做一些封装。
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 << error << std::endl; }
输出(试图调用一个空值):
[string "SayHello('Lua')"]:1: attempt to call a nil value (global 'SayHello')
与 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; }
LuaRef world = getGlobal(L, "world"); if (!world.isNil() && world.isString()) { // ... }
LuaRef say_hello = getGlobal(L, "sayHello"); if (!say_hello.isNil() && say_hello.isFunction()) { // ... }
如果 Lua 代码有什么问题,LuaBridge 会引发 LuaException
异常,相关代码最好放在 try...catch
中。
try { // ... } catch (const luabridge::LuaException& e) { std::cerr << e.what() << std::endl; }