转载

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 << "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_spacestrue 时能够返回偏移信息(第一个非空字符的下标):

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 文件

前面示例执行 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<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 字符。

错误处理

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

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 << error << std::endl; }

输出(试图调用一个空值):

[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 << e.what() << std::endl; }
原文  https://segmentfault.com/a/1190000005765697
正文到此结束
Loading...