C + + program embedded Lua (based on luabridge)

Time:2020-8-13

If the configuration file is uncertain, you have to rely on scripts. Lua is almost the first choice for C + + programs to embed embedded scripts.

Lua’s source code comes with itMakefileThe static library, interpreter and compiler can be compiled. As the host C + + program, in addition to the Lua header file, it should also link the static library.

If C + + programs are built by cmake, it is not difficult to create a static library for Lua with cmake. Cmake solves the problem of cross platform.

In fact, there are only two problems with script extension: first, how can Lua access C + + objects? 2、 How to let C + + access Lua objects? Of course, the so-called object is a broad concept, including variables, functions, classes, and so on.

Through luabridge, these two problems can be easily solved.

Header file

I’ll give you the header file first, and I won’t mention it later.
First of all, it contains several header files of lua. Because it is C code, it is placed in theextern "C"In order to mix with C + + program.

extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}  // extern "C"

The second is luabridge header file. Luabridge, like STL, has only header file, which directly contains and uses.

#include "LuaBridge/LuaBridge.h"

Lua visits C++

function

C + + functionsSayHelloExport as Lua functionsayHelloAnd then through theluaL_dostringExecute Lua code and call this function.

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

Output:

Hello, World!

bySayHelloAdd a parameter:

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

luaL_dostring(L, "sayHello('Lua')");

Output:

Hello, Lua!

class

The C + + class is exported to Lua’s table, and the member function of the class corresponds to the member of the table. Suppose there is a classLine, represents a line in the text file:

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

  size_t Length() const {
    return data_.length();
  }

private:
  std::string data_;
};

Constructor

Export constructor withaddConstructor, export member functions or useaddFunction

luabridge::getGlobalNamespace(L)
  .beginClass<Line>("Line")
    .addConstructor<void(*)(const std::string&)>()
    .addFunction("getLength", &Line::Length)
  .endClass();

Constructor cannot address, calladdConstructorYou need to pass a template parameter to indicate the type.

Test:

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

luaL_dostring(L, str);

Output:

4

If you have multiple constructors, such as a default constructor:

Line::Line();

Only one can be exported. In this way, the second will cover the first:

luabridge::getGlobalNamespace(L)
  .beginClass<Line>("Line")
    . addconstructor < void (*) (void) > () // is covered by the next sentence
    .addConstructor<void(*)(const std::string&)>()
  .endClass();

You can’t make the same name refer to two things.

Member function

Consider a slightly more complex member function,StartWithTo determine whether a line of text begins with a string, the parameterignore_spacesDetermines whether to ignore spaces at the beginning of a line. Those that are not interested in implementation can be completely ignored.

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

adoptaddFunctionExport to Lua:

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

Test:

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

Output:

false
true

Output parameters

Now forStartWithAdd optional output parameters so thatignore_spacesbytrueThe offset information (subscript of the first non empty character) can be returned when the

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

Output parameters are very common in C / C + +, which can make a function return multiple values. But withaddFunctionExportedStartWithIt cannot be called by Lua because Lua has no output parameters. Fortunately, Lua’s functions can have multiple return values, so thatStartWithReturn multiple values, we have to do a layer of luaCFunctionPackaging.

// Lua CFunction wrapper for StartWith.
int Line::Lua_StartWith(lua_State* L) {
  //Get the number of parameters
  int n = lua_gettop(L);

  //Number of validation parameters
  if (n != 3) {
   luaL_error(L, "incorrect argument number");
  }

  //Verify parameter type
  if (!lua_isstring(L, 2) || !lua_isboolean(L, 3)) {
    luaL_error(L, "incorrect argument type");
  }

  //Get parameters
  std::string str(lua_tostring(L, 2));
  bool ignore_spaces = lua_toboolean(L, 3) != 0;

  //Start with
  int off = 0;
  bool result = StartWith(str, ignore_spaces, &off);

  //Return results
  luabridge::push(L, result);
  luabridge::push(L, off);
  Return 2; // there are two return values
}

The type isint (*) (lua_State*)The function is called LuaCFunction。 change to the use of sth.addCFunctionexportLua_StartWith

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

Test:

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

Output:

true   2

Variable parameter

Now that it’s doneCFunctionIt’s better to do it more thoroughly. In view of lua’s good support for variable parameters, we letstartWithSupport variable parameters. For example, you can judge whether the'if'Lead:

line:startWith(true, 'if')

We can also judge whether the'if'or'else'Lead:

line:startWith(true, 'if', 'else')

For this reason,ignore_spacesIt becomes the first parameter, followed by the variable parameter of string type. The specific implementation is as follows:

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;

  //Compare the string arguments one by one, and jump out of the loop once matched.
  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;
}

Test:

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

Output:

true   2

Execute Lua file

The previous example executes Lua code and uses all of themluaL_dostringIn the actual project, Lua code mainly exists in the form of file, so it is necessary toluaL_dofile

Test:

luaL_dofile(L, "test.lua);

filetest.luaThe contents are as follows:

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

Output:

true   2

C + + accessing Lua

adoptgetGlobalThe global Lua object of typeLuaRef

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

  {// to make luaref objects in Lua_ Deconstruction before 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);
}

Output:

Hello, World!

character string

Lua doesn’t have a character type, and it doesn’tUnicodeString (specificallywchar_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'))");

Output:

true
true
false

IfIsSpaceThe parameter iswchar_t:

bool IsSpace(wchar_t c) {
  return c == L' ' || c == L'\t';
}

Call in LuaisSpace(' ')Luabridge asserts that it failed:

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

The compromise is thatIsSpace(wchar_t c)A wrapper is provided for Lua to use.

bool Lua_IsSpace(char c) {
  return IsSpace((wchar_t)c);
}
luabridge::getGlobalNamespace(L)
  .addFunction("isSpace", Lua_IsSpace);

Of course, the premise is that Lua code callsisSpaceOnly ASCII characters are passed in.

error handling

In order to facilitate problem diagnosis and error handling, it is necessary to do some encapsulation for built-in functions or macros.

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) {
      //Get the error message from the top of the stack.
      if (lua_gettop(L) != 0) {
        *error = lua_tostring(L, -1);
      }
    }
    return false;
  }
  return true;
}

Test: deliberately calling a function that does not existSayHello(it should besayHello)。

std::string error;
if (!DoLuaString(L, "SayHello('Lua')", &error)) {
  std::cerr << error << std::endl;
}

Output (attempted to call a null value):

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

luaL_dofile

AndluaL_dostringThe package is similar.

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) {
      //Get the error message from the top of the stack.
      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

If there is a problem with Lua code, luabridge will triggerLuaExceptionException, the relevant code should be placed in thetry...catchMedium.

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