Automagical Language Bindings
Using C++ Template Meta-Programming to expose C functions in Lua
by John Ryland © 2016
Summary
Here I will present how I create automagically the wrappers needed to expose native calls to lua code.
I won't go in to the details of how to integrate a lua VM in to a C/C++ program, it is assumed you either have this already or can refer to one of the many existing guides for this.
Introduction
For the problem of exposing C/C++ functions to be able to be called from Lua, the typical approach is to wrap the given function with a function which has a signature such as the following:
static int functionCallableFromLua(lua_State* L);
So if one was to have a function which had a signature like the following:
void testFunction(const char* msg);
We would need to adapt it to conform to the signature which is callable from Lua, so the typical approach is to wrap it with a function such as the following:
static int testFunctionLuaWrapper(lua_State* L) { assert(lua_isstring(L,1)); const char *msg = lua_tostring(L, 1); testFunction(msg); }Some have made implemented means to minimize the work required for implementing the wrappers by creating as a string a description of the parameters, such as in this case, it is one argument which is a string, so the description of arguments might be "s". If our test function took an int and a double and then the const char *, it might be something like "ids" as an example, and this could be used to expand with macros or with external code generation tools to make the required code.
Solution
But with about 50 lines of modern template meta-programming code we can do away with needing external tools, no need for macros, and can avoid the error prone and tedious work of writing such wrappers by hand.
So I think without further ado, I present the code, and will discuss it after to explain what it does:
// If we are wrapping/binding a function that returns an int, propagate the result to lua template <typename... As, typename... Ps> int MakeCallableFunc(lua_State*, int, int (*fn)(Ps... values), int (*)(), As... params) { return fn(std::forward<As>(params)...); } // Otherwise if the return type is not int, return 0 template <typename R, typename... As, typename... Ps> int MakeCallableFunc(lua_State*, int, R (*fn)(Ps... values), R (*)(), As... params) { fn(std::forward<As>(params)...); return 0; } // Handle int arguments template <typename R, typename... As, typename... Ts, typename... Ps> int MakeCallableFunc(lua_State* L, int i, R (*fn)(As... values), R (*)(int, Ts... values), Ps... params) { R (*fn3)(Ts... values); assert(lua_isinteger(L,i)); return MakeCallableFunc(L, i+1, fn, fn3, std::forward<Ps>(params)..., (int)lua_tointeger(L, i)); } // Handle double arguments template <typename R, typename... As, typename... Ts, typename... Ps> int MakeCallableFunc(lua_State* L, int i, R (*fn)(As... values), R (*)(double, Ts... values), Ps... params) { R (*fn3)(Ts... values); assert(lua_isnumber(L,i)); return MakeCallableFunc(L, i+1, fn, fn3, std::forward<Ps>(params)..., (double)lua_tonumber(L, i)); } // Handle string arguments template <typename R, typename... As, typename... Ts, typename... Ps> int MakeCallableFunc(lua_State* L, int i, R (*fn)(As... values), R (*)(const char*, Ts... values), Ps... params) { R (*fn3)(Ts... values); assert(lua_isstring(L,i)); return MakeCallableFunc(L, i+1, fn, fn3, std::forward<Ps>(params)..., (const char*)lua_tostring(L, i)); } // Add helper for entry in to creating the binding template <typename R, typename... As> int MakeCallableFunc(lua_State* L, R (*fn)(As... values)) { return MakeCallableFunc(L, 1, fn, fn); }
This last template function signature is the intended entry point in to these templates.
Discussion
To use this, there are a few options, but the particulars may depend on which version of Lua you have.
For the latest version of Lua, there are 2 functions to call to register a function, these are lua_pushclosure, and lua_setglobal. You may wish to wrap those two calls in a single function, but in my case I created a table of functions to map, and iterated this calling those functions.
To expose our C function so it is wrapped with the required signature we saw earlier of functionCallableFromLua I created those in place with C++11's lambda syntax, but that is optional, depending what you want to do, eg:
[](lua_State*L)->int { return MakeCallableFunc(L, func); }
The details of the table I used looked like this:
// Import table static const struct luaL_Reg scriptCallableFunctions[] = { { "someFunc", [](lua_State*lua)->int { return MakeCallableFunc(lua, someFunc); } }, { "otherFunc", [](lua_State*lua)->int { return MakeCallableFunc(lua, otherFunc); } }, { "blah", [](lua_State*lua)->int { return MakeCallableFunc(lua, blah); } }, };
Because I didn't want to make the code as verbose, and make it less error prone in the mapping of the name given to the function in lua and the name in C, I compromised and used a macro to make adding these entries in the table easier:
#define AUTO_BIND_C_FUNTION_TO_LUA(func) \ { #func, [](lua_State*lua)->int { return MakeCallableFunc(lua, func); } },
Iterating the table to register the bindings looks like this (using C++11's range based looping on a plain C array):
void registerLuaBindings(lua_State* L) { // Register all of our bindings for (luaL_Reg r : scriptCallableFunctions) { lua_pushcclosure(L, r.func, 0); lua_setglobal(L, r.name); } }
And that is all there is to it.
Details
So for a discussion about how it works, the basic idea is that the templates consume out of the function definition the types in the signature of the function and as it does that, it accumulates the parameter list needed for making the function call to the function being wrapped. The templates are recursive but terminate when it reaches the point that the function definition that is being consumed has no arguments left to consume. At the terminating templates, I have two versions, just for niceness, so if the wrapped function signature does return an int, it passes that back, but I've not really tested the return value handling back in lua, my goal is just to invoke some native code from lua, so it is sufficient for my current needs.
So as part of this process of consuming the definition of the function which is to be called, I need to actually pass it in twice, once for maintaining a pointer to the function with its full definition, and a second time for being consumed. This is received in the parameter list defined like this:
R (*fn2)(T arg0, Ts... values)
This is a pointer to a function with a signature of having at least one argument, arg0, and any number of parameters after this. We don't directly use fn2, so drop the 'fn2'. Also because we want to actually process specific argument types, we make specializations of this, such as:
R (*)(int arg0, Ts... values)
R (*)(double arg0, Ts... values)
R (*)(const char* arg0, Ts... values)
Obviously to support other types, additional template functions would need to be added along those lines if there is support in lua in some way for those types.
Now we ask lua to give us from the lua_State the value of this parameter of the given type which need to know to call the appropriate lua function for getting it.
We can then knowing the template parameters, generate a function signature of the remaining parameters:
R (*fn3)(Ts... values)
This is dropping the argument we have just processed, so we now have a signature with one less parameter.
We continue doing this until we have no parameters left, and so these template functions no longer match, and what matches is the template which has as the 2nd function signature this:
R (*)()
The 'L' and 'i' parameters are needed for asking lua for the values so are passed through all the functions.
Conclusion
No longer is there an excuse to have 1000s of lines long C/C++ code to wrap functions with error prone manually constructed code that needs to be maintained as function signatures change. Obviously if you wrap the function manually or automagically, if the signature changes, you will need to change the lua code, but at least when it is automagical, there is no need to change the bindings. This can be a cause of errors. If speed is a concern, probably reconsider using lua and do everything in C, but if you value the productivity gain of using lua, you may appreciate the productivity gain from having an easier, less error prone and more maintainable way to expose native C calls to your lua code.
No comments:
Post a Comment