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

c/c++

浏览数:252

2019-3-29

AD:资源代下载服务

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

输出:

4

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

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;
}