Integrate Lua into C++
Here is the thing, I’d like to have something really simple to enable scripting in my application written in a statically typed language. In my case, it’s C++. I’d also like it not to have a massive overhead, and mostly do very simple things, like basic math, basic string operations and so on, with possibility to extend later.
So I’ve heard about this supposably awesome programming language Lua used in games. So it must be good in terms of speed, as games do require speed.
Hello, Lua
It took me about half an hour to execute “Hello, Lua” from a clean C++ console app. This is my vcpkg.json
:
{
"name": "lua-showcase",
"version-string": "latest",
"dependencies": [
{
"name": "lua"
}
]
}
CMakeLists.txt
find_package(Lua REQUIRED)
add_executable (ls "ls.cpp" "ls.h")
target_include_directories(ls PRIVATE ${LUA_INCLUDE_DIR})
target_link_libraries(ls PRIVATE ${LUA_LIBRARIES})
and main C++ file:
#include "lua.hpp"
using namespace std;
int main() {
// execute a lua script
lua_State* L = luaL_newstate();
luaL_openlibs(L); // Opens all standard Lua libraries into the given state.
luaL_dostring(L, "print('Hello Lua!')"); // executes a string of Lua code
lua_close(L);
return 0;
}
This did print “Hello Lua” to the console indeed. Short and sweet.
Of course that’s not enough. Now I’ve got two primary questions - how do I pass a parameter to the Lua script, and how do I get a meaningful result back. Theoretically I could pass a parameter by injecting it’s value into the script itself. But then it quickly becomes really unstable - just one wrong character will ruin the script, and I’m not even sure yet what kind of rules for defining string constants Lua has as I’ve never used the language before.
Side note - binary sizes
When compiled in release configuration, the total size of x64 executable on Windows is 232 kb. When not using Lua and just clean executable, the size is 13 kb, therefore Lua overhead so far is 219 kb. Not bad so far, I can live with that (and that’s just default configuration with absolutely no tuning whatsoever, if this tuning even exists)
Passing arguments
See, the previous simple example shows the basic workflow:
lua_State
is some kind of script state, I’m not sure yet what it is, but is seems to be important.luaL_openlibs
sort of loads essential Lua libraries, like string and math and so on (I guess).luaL_dostring
executes a string of code in Lua.
However, the code can be immediate code, which executes straight away as is, just like we have it, and it can be a declaration of something, like a function.
Therefore, I can declare a function in Lua this way (I don’t know much about Lua functions yet, I just googled a sample):
luaL_dostring(L, "function add(a, b) return a + b end");
This declares function called add
with two numeric arguments. Then, looking inside luaL_dostring
it’s actually a macro performing two things - loading a string, and executing it using lua_pcall
function, which is described in Lua documentation with these particular interesting information bits:
To call a function you must use the following protocol: first, the function to be called is pushed onto the stack; then, the arguments to the function are pushed in direct order; that is, the first argument is pushed first. Finally you call
lua_call
;nargs
is the number of arguments that you pushed onto the stack. All arguments and the function value are popped from the stack when the function is called. The function results are pushed onto the stack when the function returns. The number of results is adjusted tonresults
, unlessnresults
isLUA_MULTRET
. In this case, all results from the function are pushed; Lua takes care that the returned values fit into the stack space, but it does not ensure any extra space in the stack. The function results are pushed onto the stack in direct order (the first result is pushed first), so that after the call the last result is on the top of the stack. blah blah blah…
Applying this layman’s knowledge I could create and call the function like so:
L = luaL_newstate();
luaL_openlibs(L);
luaL_dostring(L, "function add(a, b) return a + b end");
lua_getglobal(L, "add");
lua_pushnumber(L, 10);
lua_pushnumber(L, 20);
lua_call(L, 2, 1);
cout << "Result: " << lua_tonumber(L, -1) << endl;
lua_pop(L, 1);
lua_close(L);
and it did indeed print Result: 30
to the terminal! This is becoming interesting!
Lua basics and how to find what you need
Now that I can theoretically pass arguments, execute a function and get back a result, it would be interesting to build a scriptable library of functions for my program. In my case I’d like users to be able to script a custom function with the following structure:
- Input arguments
URL
- string.Process Name
- string.Window Title
- string.
- Output
true/false
- boolean.
Essentially, I’d like to give them control on deciding whether a combination of three input parameters is good or not for other purposes.
To develop test functions, I would need some kind of IDE or editor. As I’m using IDEA loads, it was worth giving a go to EmmyLua plugin. Of course the plugin relies on Lua being available locally, which you can download from Lua Binaries website. To do that, click on version you want to download i.e. “Lua 5.4.2 - Release 1”, then “Tools Executables”, and version for your platform, for instance lua-5.4.2_Win64_bin.zip
and unpack somewhere. Then you can add SDK from IDEA:
This didn’t work though, and I was getting an error “Cannot detect SDK version. Probably SDK installed in … is corrupt”. What he hell is this SDK then? After 10 minutes of digging I still couldn’t figure out how to make this work, there is simply no documentation on either Lua website or plugin documentation on how to make this work, therefore the plugin and Lua download went straight into bin.
I’ve ended up downloading ZeroBrane Studio and that just worked out of the box with no issues at all or any complicated configuration or extra downloads.
You can also experiment online, there are plenty of tools, but I went for myCompiler because it allows sharing the code with others.
At the end, I have decided to use VS Code with Lua plugin as it’s more aesthetically pleasing.
Input arguments and globals
At the end I’ve decided not to use input arguments for my function at all. I need to be able to develop this further without introducing breaking changes to function signatures, and also in my use case I might want to modify one of the arguments, like URL.
So I’ve decided to use global variables in Lua instead - this code will be single-threaded and executed in sequence, which is fine by me.
You can manipulate globals using lua-getglobal
and lua_setglobal
functions, but I went further by learning about Lua tables, which are something like hash-maps but can contain keys of any type and value of any type. The structure that functions need as an input is therefore simple:
classDiagram
class p {
+string url
+string wt
+string pn
}
Lua’s documentation has plenty of info on how tables work. To set this table as a global variable:
// set global table "p" with 3 members: url, window_title, process_name
lua_newtable(L);
lua_pushstring(L, url.c_str());
lua_setfield(L, -2, "url");
lua_pushstring(L, window_title.c_str());
lua_setfield(L, -2, "wt");
lua_pushstring(L, process_name.c_str());
lua_setfield(L, -2, "pn");
lua_setglobal(L, "p");
Then, say I want to write a function which return true
when domain part of the URL is “microsoft.com” and process name is “slack.exe”:
function is_work_slack()
-- extract domain part from p.url
domain = string.match(p.url, "https://(.-)/")
print("domain is " .. domain)
-- check if domain is microsoft.com and process name is slack.exe, then return true
if domain == "microsoft.com" and p.pn == "slack.exe" then
return true
end
return false
end
My Lua skills are still quite bad at this point, so there was a lot of googling and experimentation to write above. However, Lua’s docs are just fine, for instance string functions.
My users should be able to change p.url
optionally as well, and I want the calling code to pick this up. Therefore, the full code will look like this:
bool call(url_payload& up, const string& function_name) {
// set global table "p" with 3 members: url, window_title, process_name
lua_newtable(L);
lua_pushstring(L, up.match_url.c_str());
lua_setfield(L, -2, "url");
lua_pushstring(L, up.window_title.c_str());
lua_setfield(L, -2, "wt");
lua_pushstring(L, up.process_name.c_str());
lua_setfield(L, -2, "pn");
lua_setglobal(L, "p");
// call function
lua_getglobal(L, function_name.c_str());
if(lua_pcall(L, 0, 1, 0)) {
// get error message from the stack
error = lua_tostring(L, -1); // "error" is class member of type std::string
lua_pop(L, 1);
return false;
}
// read return value
bool r = lua_toboolean(L, -1);
lua_pop(L, 1);
// read back "url" from global table "p" (in case it's changed)
lua_getglobal(L, "p");
lua_getfield(L, -1, "url");
lua_pop(L, 1);
return r;
}
And that’s all I need for now.
cmake and vcpkg
In vcpkg the lua
dependency is pretty much enough to embed the Lua interpreter. The vcpkg.json
can be as simple as:
{
"name": "lua-showcase",
"version-string": "latest",
"dependencies": [
{
"name": "lua"
}
]
}
Cmake needs extra 3 lines to reference the dependency, which I have outlined here:
# 1. Reference the package
find_package(Lua REQUIRED)
add_executable (myproject "ls.cpp")
# 2. Include Lua header directories
target_include_directories(myproject PRIVATE ${LUA_INCLUDE_DIR})
# 3. Link with Lua library
target_link_libraries(ls PRIVATE ${LUA_LIBRARIES})
Once this is done, start using Lua after performing
#include "lua.hpp"
I think that’s pretty much it.
A week passed and…
I’m really happy with Lua/C++ integration. I think the only roadblock was lack of Lua knowledge as a language, hence some frustrations related to writing simple things. I have found that reading “Programming in Lua” book by Roberto Ierusawhatever really helped to get this to the next level withing a few days, as it explains Lua bits quickly and in detail.
It even includes a section on C API integration, including how to perform error handling, pass function arguments, handle results and so on. That’s where I learned also how to pass Lua tables from C to Lua.
It explains how to also call C functions from Lua (i.e. backwards integration) which I’m looking forward to exploring when the need comes.
Good luck!
P.S. Browser Tamer is a browser automation system utility done in spare time by myself. Official page is located here.
To contact me, send an email anytime or leave a comment below.