Using Lua with C++ (and C)

Related:


I’ve written many articles about using Lua with C++ on my old blog many years ago. I also worked on a fork of LuaJIT professionally and described my experiences here - this experience made me learn a lot about LuaJIT and how vanilla Lua works (and how it can be used in production for servers with a very high QPS).

This is not a tutorial or step-by-step guide. It’s a road map for learning Lua and my advice for how to integrate it with C/C++ code.

Why use Lua

Learning Lua

To learn Lua, read “Programming in Lua” by Roberto Ierusalimschy.

You can find Lua’s reference manual here.

That’s basically all you’ll ever need to know about Lua.

Building/integrating with Lua

Download the official Lua source code here: https://www.lua.org/download.html.

Lua is written in ANSI C, so it can compile almost everywhere. It has a GNU Make build, so building it is easy.

Integrating it with CMake is somewhat trickier. I’ve used LuaDist’s CMake build scripts for Lua for many years, but LuaDist authors have stopped supporting newer Lua versions. It’s a good starting point for making your own CMake build for Lua, though (or if you use older versions of Lua, they work well).

See a simple example of how to build Lua with CMake and use it with sol2 here.

Which Lua version to choose?

LuaJIT is Lua’s fork essentially and is stuck almost at Lua 5.2. It adds its own extensions to Lua, so porting code which works in LuaJIT to vanilla Lua is sometimes tricky, if not impossible.

Which binding to use?

Option A. Lua C API

If you only want to call some C/C++ functions from Lua (or some small Lua functions from C/C++) - use Lua C API. It’s minimalistic, but requires a lot of boilerplate and manual error-checking.

You can learn about how to use Lua C API here (or in the latest version of “Programming in Lua”) - the online version is written for Lua 5.1 and some things have changed since 5.1, but most concepts have stayed the same.

The reference manual for Lua 5.4’s C API is here: https://www.lua.org/manual/5.4/manual.html#4

Option B. sol2 (C++ only, requires a compiler which supports C++17)

Github repo: https://github.com/ThePhD/sol2

sol2 is a fantastic library which I’ve used for many years. It can do everything you’d ever want from Lua and even more. The docs are fantastic too.

Pros

Cons/gotchas:

Use the minimal subset of sol2 features which gets things done for you first. Try to avoid things which are not used in “vanilla” Lua code (e.g. function overloading). Try to avoid passing STL containers/strings to/from Lua too much.

Resources

Enrique García Cota has a lot of great articles about Lua: http://kiki.to/

He has also implemented a number of incredible little libraries which I’ve used in my projects:

Many other great libraries can be found via LuaRocks: https://luarocks.org/

However, you don’t need to use LuaRocks as the package manager. You can just download the .lua script(s), drop them into your source dir and call it a day. Make sure that you do it in the way that LICENSE permits - if you distribute your game/program with these libraries, you might also need to provide a LICENSE file somewhere nearby.

You might also find Moonscript interesting. itch.io was written with it and it compiles to vanilla Lua. It’s neat.

Where to see Lua in practice?

And finally, see what LÖVE and PICO-8 do. They’re mostly written in C/C++ and only expose a bunch of Lua functions/tables to the user.

You can write entire games in Lua without ever touching C/C++. Isn’t it great?

Ask yourself - do you need to write any C/C++ code at all? Maybe you can even start writing your game with LÖVE now? You can later replace parts of it with custom C++ code/engine if you need performance/extra portability in the future.

Using Lua in practice

And now, for more detailed advice…

1. Avoid creating C++ objects in Lua if they’re not PODs (simple structs)

Dealing with C++ objects in Lua is much harder than dealing with “native” types.

function someFunc()
    -- If it's on heap, you need to make sure it's properly deallocated!
    local obj = createCppObj()

    -- If you only needed this object inside this C++ functions, maybe you
    -- could have created it inside the C++ function itself?
    someCppFunc(obj)

    -- Do you really need a C++ string?
    local str = createCppString()
    someCppFunc2(str)

    -- Much better
    local str2 = "hello"
    someCppFunc3(str) -- accept it in C/C++ as "const char*"
end

2. Avoid passing Lua tables to C++ functions and returning tables from C++ functions

function someFunc()
    local t = { a = 5, b = 10 }

    -- Now you'll need to write additional C++ code which
    -- will check for "a" and "b" keys existence and their types.
    -- Maybe it's easier to implement someCppFunc2 in Lua?
    someCppFunc2(t)

    -- Better, but what will you do if your C++ function signature is
    -- "int someCppFunc3(int a,int b)", and nulls get passed to it?
    someCppFunc3(t.a, t.b)
end

It’s much easier to test if something is non-nil or has a specific type/fields in Lua than in C++. So it’s better to do it on Lua side, unless you make a C++ API which has to accept tables for ease-of-use - then you don’t have much choice (but you can still wrap them into Lua functions and call C++ function internally).

3. If you’re calling functions too often, maybe it’s time to reconsider your API design

For example (C++ functions have “cpp” postfix):

function someLuaScript(hero, monster)
    setAnimation_cpp(hero, "attack")
    setHP_cpp(monster, getHP_cpp(monster) - 1)
    say_cpp(hero, "Take this!")
end

Ask yourself: do all these functions need to be in C++? Can they be rewritten in Lua, perhaps? This will make your life easier, because you would be able to write less “binding” code and be able to stay “inside” Lua code as much as possible.

4. Avoid calling Lua from C++ too often

This code is OK (calling C++ from Lua is cheap):

-- in Lua
for i=1,1000 do
    someFunc_cpp(i)
end

This code is not that good (calling Lua from C++ is slow):

// in C++
sol::function luaFunc = lua["someFunc"];
for (int i = 0; i < 1000; i++) {
    luaFunc(i);
}

Profile first. If you notice that your C++ function has a huge overhead because of Lua calls, consider using this pattern:

// in C++
sol::table t = lua.create_table();
for (int i = 0; i < 1000; i++) {
    ... // do stuff in C++, push data to Lua table occasionally
}
luaFunc(t);
-- in Lua
function someFunc(t)
    for k,v in pairs(t) do
      ... -- do stuff
    end
end

5. Try to avoid global variables

If you don’t use “local” to define/init a variable, it becomes a global by default. It stays global and doesn’t get collected by a GC until it has been set to nil.

Keep your global state to a minimum and variable scope as small as possible. Don’t forget that you can do this:

-- script.lua
local someConst = 5

local function someFunc(x)
    return x + someConst
end

local function someFunc2()
    someFunc(42)
end

return someFunc2

Now, when you load/execute this script, you will only get one function as the result:

// in C++
sol::function f = lua.do_file("script.lua");
f(); // you can call it

All the internal script state will be hidden and non-modifable and won’t overwrite any global state in your current Lua state, which is great.

6. Don’t make classes global

Similarly to the previous advice, when defining classes (e.g. via middleclass library), don’t do it like this:

-- some_class.lua
local class = require 'middleclass'

SomeClass = class('SomeClass') -- global, bad!

function SomeClass:initialize()
    ...
end
...

Do it like this instead:

-- some_class.lua
local class = require 'middleclass'

local SomeClass = class('SomeClass')

function SomeClass:initialize()
    ...
end
...

return SomeClass

and then, when you want to create an instance of this class, you do this:

-- script.lua
local SomeClass = require 'some_class'
local obj = SomeClass:new()

Why do it like this? Because it makes it much easier to track dependencies between various script files and makes them load each other when they’re needed. If you used classes as “globals” (which I’ve seen in many tutorials and real code), you might have a script like this:

-- script.lua
local obj = SomeClass:new()

Suppose that you didn’t execute some_class.lua first (which defines a global SomeClass). Then, executing script.lua would resullt in an error: SomeClass is nil. This makes dependencies between files hard to track and require you to manually execute some_class.lua before you can execute script.lua.

However, in the example where SomeClass is returned as a local variable, you won’t need to do this - if you just load the script.lua, it would execute some_class.lua when you call require (it would only do this if another script didn’t call it, otherwise it would return a “cached” SomeClass variable).

7. Don’t rely on “external” scripts too much in initialization/“base” code

For example, suppose that you have C++ code like this:

void Game::start() {
    ... // init C++ stuff
    lua.do_file("init.lua");
    ... // do more stuff
}

Your engine will crash on startup if init.lua is not found when your game starts (or contains errors). Of course, you can check for this file’s existence and handle errors appropriately (e.g. by displaying a message box/writing to a log), but if your init.lua is not meant to be modified externally, maybe you can do it like this?

std::string luaInitCode = "...";

void Game::start() {
    ... // init C++ stuff
    lua.do_string(luaInitCode);
    ... // do more stuff
}

Much better! Now you don’t need to ship this “init.lua” with your binary and life becomes easier.

Writing code like this will become better with new #embed directive in future C/C++ versions, e.g.:


void Game::start() {
    constexpr const char luaInitCode[] = {
#embed <init.lua>
    };
    ... // init C++ stuff
    lua.do_string(luaInitCode);
    ... // do more stuff
}

8. Be careful when storing references to Lua objects on the C++ side (e.g. via sol::object)

Overusing them makes code harder to understand because now you need to make sure that the object you’re are pointing to doesn’t change unexpectedly.

And be especially careful when using coroutines. If you create a local object inside the coroutine and store the reference to it in C++. In short, if your coroutine gets GC’ed and you hold the reference to this local variable as a sol::object on the C++ side, it won’t prevent the variable from getting GC’ed by Lua and your program crashing because of that.

See the explanation on how to do concurrency in Lua in sol2 here (especially about lua_xmove and sol::main_object).

Basically, you need to tell Lua explicitly that something you’ve created in the coroutine needs to outlive this coroutine (and this is done with lua_xmove).

Stumbling upon this without knowing about this was a traumatic experience… Many cups of coffee were drank that night and several keyboards got destroyed…

9. Use inspect.lua for printf debugging

https://github.com/kikito/inspect.lua

You can easily print table contents with it:

-- too much code :(
print(t.a, t.b, t.x.str)

-- nice!
-- prints "{ a = ..., b = ..., x = { str = ... } }"
print(inspect(t))

10. Try to avoid library bindings

For example, I’d recommend to avoid SDL/glfw/SFML bindings and writing them manually for the stuff you need. The problem with 3rd party bindings is that they usually tend to get unmaintained or only support a specific version of the library. With your own bindings, you can control what version you use and how the exposed API looks in Lua.

And you can do even better, you can hide library details and not expose them to Lua at all which will make switching between lower-level frameworks much easier, for example:

playSound("test.wav")
drawRect(10, 20, 32, 32)
if isKeyboardKeyPressed(key.Enter) then
    ...
end

If you want to use a C library and you’re using LuaJIT, you don’t even need to create a binding for it at all. You can call it with LuaJIT’s FFI, see the tutorial here.


Comments (GitHub)

Comments (Hacker News)

<- back to the front