配置文件搞不定的,就得依赖脚本。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; }