Using Lua with C++ (and C)
Related:
- How to implement action sequences and cutscenes in Lua
- Making and storing references to game objects (entities) in C++ and Lua
- LuaVela - the LuaJIT fork I’ve worked on
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
- It’s simple. The simplest language I know. It doesn’t have many “gotchas”. Writing the code in it feels like writing pseudocode/prototyping.
- It’s easy to integrate Lua scripts into C/C++ projects. You just compile a bunch of C files, link with the library and you’re ready to go - you don’t need to rewrite half of your program/game or make your build script 10 times more difficult.
- It’s stable. New versions come out once in a while, but you can comfortably stay on Lua 5.2 not loose much. Many 3rd party Lua libraries usually work with Lua 5.2/5.3/5.4.
- It’s fast. You’re unlikely to run into performance issues, unless you’re making a AA/AAA game or a complex simulation program.
- Even if you do, with LuaJIT you’ll be able to achieve C/C++ level performance.
- Lua doesn’t impose one style of writing code on you. You can do procedural, functional, OO programming.
- You can even create small domain specific languages thanks to Lua’s syntax sugar.
- You don’t need to compile Lua code, which means that you can change parts of your program/game logic without recompiling and even without reloading your program.
- If you’re writing a game and want to make it possible for people to create mods, many of your players are likely to know Lua thanks to such games as Minecraft, Factorio, Garry’s Mod, World of Warcraft, Roblox and many others.
- The same can be said about making your program expandable with plugins. E.g. NeoVim, which uses Lua scripts for user script code.
- Sandboxing is easy. You choose what Lua code can and can’t do. You can easily create a Lua state which won’t have any functions from the standard library (I/O, coroutines, tables etc.)
- You don’t need a specialized IDE or tools to write Lua comfortably. If you need a debugger, I recommend checking out ZenoBrane Studio.
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 if you need maximum performance.
-
Vanilla Lua, the latest version, otherwise.
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
- The fastest and the most feature-complete Lua/C++ binding library.
- Easy to start using, the interface is user friendly and feels similar to C++ standard library.
- Can do a lot of “automagic” and save you from writing tons of boilerplate code, which you need to do if you use other bindings or Lua C API.
Cons/gotchas:
- It uses heavy meta-programming to get things done.
- This can result in slow compilation times, unreadable error messages and some interfaces are not that friendly (e.g. it’s hard to know if what you’re passing to Lua will be copied or passed by a reference).
- Not safe by default, you need to use
protected_
versions of functions/objects to not crash constantly.
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:
- middleclass for OOP.
- inspect for debugging.
- bump.lua for simple collision detection/resolution.
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?
- Check out this engine for making Zelda-likes for inspiration of how to write game code in Lua: https://www.solarus-games.org/.
- Minetest (a Minecraft-like game) has a great API and a lot of docs.
- I also found Don’t Starve Lua scripts/code inspiring (purchase the game, go to game dir, they’re in plain text!).
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
-
Most of the time, you only need one Lua state (
lua_State
in Lua C API andsol::state
in sol2) for everything. -
Think of Lua scripts as functions - you can call them with
require
ordo_file
and they can return variables. -
Don’t put too much code into one script - it’s easy to separate everything into smaller scripts.
-
After your basic game engine is implemented, start with writing new code in Lua. Then, see if you really need to move this code (or parts of it) to C++.
-
If you think that your game became “slow” because of Lua code, profile first before rewriting it in C++. Gut feeling is usually wrong.
-
Your life will be easier if you have a small C/C++ API. The less data is shared between C++ and Lua, the better.
-
What to write in C++: collision detection, path finding, physics, renderer and other things that are performance critical.
-
What to write in Lua: gameplay logic, FSMs, cutscenes, etc. - basically everything else that doesn’t interact with low-level OS APIs much.
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.