Adventures in Lua Binding
I’ve mentioned before (actually, probably only in that last post) about a game I’ve been working on on and off for the last few years (I think I said three, but it’s actually been five, with it being inactive for most of that time.) I had decided long ago that it would probably be scripted with Lua and I’m just now finally getting around to experimenting with that. I’ve ultimately decided it’s going to either be using OOLua or Luabind. I’ve given both a fairly quick look over and attempted to use each to bind a specific class in my project.
For the sake of conversation I’m going to use a very basic C++ class that’s based on my project’s Configuration class. I’ll go through and show how both libraries bind the class to Lua.
First, here’s the C++ class:
class Configuration {
public:
enum ConfigurationValue {
// String values
DATADIR, MODE,
// Int Values
RESOLUTIONX, RESOLUTIONY, DEPTH, WINDOWED, SOUND, MUSIC,
ENUMCONFLENGTH
};
Configuration(std::string directory = "") {
// if we don't have a path, save the config in the same dir
if(directory == "") {
m_Cfg = "config.cfg";
}
else {
m_Cfg = directory + "/config.cfg";
}
// Then we'd do things like loading the configuration/creating a new one
}
void set(ConfigurationValue cfg, std::string value) {
m_ConfStr[cfg] = value;
}
void set(ConfigurationValue cfg, int value) {
m_ConfInt[cfg] = value;
}
std::string getStr(ConfigurationValue cfg) const {
return m_ConfStr[cfg];
}
int getInt(ConfigurationValue cfg) const {
return m_ConfInt[cfg];
}
bool save() {
std::cout << "Saving (but not really) Configuration" << std::endl;
std::cout << "datadir = " << getStr(DATADIR) << std::endl
<< "mode = " << getStr(MODE) << std::endl
<< "resolution_x = " << getInt(RESOLUTIONX) << std::endl
<< "resolution_y = " << getInt(RESOLUTIONY) << std::endl
<< "depth = " << getInt(DEPTH) << std::endl
<< "windowed = " << getInt(WINDOWED) << std::endl
<< "sound = " << getInt(SOUND) << std::endl
<< "music = " << getInt(MUSIC) << std::endl;
return true;
}
bool load() {
set(DATADIR, "/usr/local/game/some_game_dir");
set(MODE, "opengl");
set(RESOLUTIONX, 800);
set(RESOLUTIONY, 600);
set(DEPTH, 32);
set(WINDOWED, 1);
set(SOUND, 1);
set(MUSIC, 1);
return true;
}
private:
std::string m_Cfg;
std::string m_ConfStr[ENUMCONFLENGTH];
int m_ConfInt[ENUMCONFLENGTH];
};
This code should look quite trivial to anyone with a background in C++, but not so trivial that it didn’t introduce some challenges in binding them with each library (explained later.) All this class is doing is mimicing what a real configuration class might do, and that is, store and load configuration values. In this case, the data isn’t saved to any kind of persistance. For the sake of an example, that would only complitate things.
Here’s the basic rundown of the code:
- A constructor with an argument (in this case, one that doesn’t do anything useful.)
- An enumeration which is used to represent the values available in the configuration.
- Getters and setters for ints and strings.
- A Save function which prints out the configuration.
- A load function which sets some defaults to the configuration values.
- A few private variables to save the fake configuration path, and two arrays to store the configuration in memory.
Now, we can start to get an idea of what the binding libraries are going to need to handle.
- Constructors & overloaded constructors
- Enumerations (or at least being able to let the decay to ints)
- Member functions
- Overloaded member functions with differing parameters
So, let’s start with Luabind.
using namespace luabind;
// assume s in defined as a lua_State*
open(s);
module(s)
[
class_<Configuration>("Configuration")
.def(constructor<std::string>())
.def("getStr", &Configuration::getStr)
.def("getInt", &Configuration::getInt)
.def("setInt", (void(Configuration::*)(Configuration::ConfigurationValue, int))&Configuration::set)
.def("setStr", (void(Configuration::*)(Configuration::ConfigurationValue, std::string))&Configuration::set)
.def("save", &Configuration::save)
.def("load", &Configuration::load)
.enum_("ConfigurationValue")
[
value("DATADIR", Configuration::DATADIR),
value("MODE", Configuration::MODE),
value("RESOLUTIONX", Configuration::RESOLUTIONX),
value("RESOLUTIONY", Configuration::RESOLUTIONY),
value("DEPTH", Configuration::DEPTH),
value("WINDOWED", Configuration::WINDOWED),
value("SOUND", Configuration::SOUND),
value("MUSIC", Configuration::MUSIC)
]
];
So lets go through the above requirements:
- Supported. Luabind looks to provide good support for constructors and multiple constructors.
- Supported. Again, Luabind has a built-in method for dealing with enumerations.
- Supported. This one should obviously be supported by any C++ Lua binding library worth a damn.
- Supported. As you can see, Luabind allows/requires you to tell it what the function will be called in Lua, thus allowing overloaded member functions without any issue.
Other thoughts: Luabind seems to have pretty good support for binding C++ classes, functions, and enums to Lua. The syntax for the binding seems quite clean and well thought out.
So now lets do a binding to the same class in OOLua
// I use the Varadic macros, otherwise, I'd have to give the length of each
// function/constructor definition (and also to the EXPORT* macros)
OOLUA_CLASS_NO_BASES(Configuration)
// most likely not really needed in our example here, but it forces the use
// of Configuration(std::string)
OOLUA_TYPEDEFS No_default_constructor OOLUA_END_TYPES
OOLUA_CONSTRUCTORS_BEGIN
OOLUA_CONSTRUCTOR(std::string)
OOLUA_CONSTRUCTORS_END
OOLUA_MEM_FUNC(bool, load)
OOLUA_MEM_FUNC(bool, save)
OOLUA_MEM_FUNC_CONST(std::string, getStr, Configuration::ConfigurationValue)
OOLUA_MEM_FUNC_CONST(int, getInt, Configuration::ConfigurationValue)
OOLUA_MEM_FUNC_RENAME(setInt, void, set, Configuration::ConfigurationValue, int)
OOLUA_MEM_FUNC_RENAME(setStr, void, set, Configuration::ConfigurationValue, std::string)
OOLUA_CLASS_END
EXPORT_OOLUA_FUNCTIONS_CONST(Configuration, getStr, getInt)
EXPORT_OOLUA_FUNCTIONS_NON_CONST(Configuration, load, save, setInt, setStr)
OOLUA::Script s;
s.register_class<Configuration>();
s.register_class_static<Configuration>("DATADIR", Configuration::DATADIR);
s.register_class_static<Configuration>("MODE", Configuration::MODE);
s.register_class_static<Configuration>("RESOLUTIONX", Configuration::RESOLUTIONX);
s.register_class_static<Configuration>("RESOLUTIONY", Configuration::RESOLUTIONY);
s.register_class_static<Configuration>("DEPTH", Configuration::DEPTH);
s.register_class_static<Configuration>("WINDOWED", Configuration::WINDOWED);
s.register_class_static<Configuration>("SOUND", Configuration::SOUND);
s.register_class_static<Configuration>("MUSIC", Configuration::MUSIC);
Again, we’ll go through my above requirements:
- Supported. OOLua too has support for multiple constructors.
- Supported. At the time I started this post, it was actually broken. After asking about it on the mailing list, a bug report was filed for it, and it was fixed shortly after.
- Supported. Again, this should obviously be provided by any C++ to Lua library.
- Supported. OOLua uses a different macro than what’s used to define normal functions.
Other thoughts: What I really like about OOLua is how fast the developer’s response time is for fixing bugs/issues. It’s also nice that it strives to be as fast as possible. I also really like the Script class. Script->run_file(string) just feels like a natural way to run a Lua script. OOLua works, and works well. That said, I’m not personally very fond of the syntax for declaring/exporting classes. Mostly, I’d like to, just as an example, not have to type OOLUA_ as a prefix for every macro.
For my pretty basic example both OOLua and Luabind support all four requirements. So I suppose you’re now asking yourself, “which is better?” Ultimately, I think Luabind supports using more of C++'s features than OOLua. For instance, I don’t believe OOLua supports overloaded operators, but Luabind does. Depending on the classes you’re binding, OOLua may not be able to fully handle it, and you may be better off with Luabind. In most cases, Luabind will probably work for you. OOLua should not be overlooked though. Even though it may not support quite as many things as Luabind does, what it does support, it supports well and is fast. I also believe there’s a lot of potential for OOLua. So go try them out for yourself.
I’ve included the full source to the above test bindings with a quick run of each. The code is licensed under the MIT liscense (just like Lua, Luabind, and OOLua.) It’s also only been tested on Linux. The makefile should work for any POSIX environment, but the source is should be easy enough to compile anywhere. Lua, Luabind, and OOLua all need to have their headers & libraries installed or put in the project path (or really anywhere) to be included by the compiler, as pointed out by Liam.