C++ in Lua

Using user defined types (“usertype”s, or just “udt”s) is simple with sol. If you don’t call any member variables or functions, then you don’t even have to ‘register’ the usertype at all: just pass it through. But if you want variables and functions on your usertype inside of Lua, you need to register it. We’re going to give a short example here that includes a bunch of information on how to work with things.

Take this player struct in C++ in a header file:

player.hpp
 1#include <iostream>
 2
 3struct player {
 4public:
 5	int bullets;
 6	int speed;
 7
 8	player() : player(3, 100) {
 9	}
10
11	player(int ammo) : player(ammo, 100) {
12	}
13
14	player(int ammo, int hitpoints)
15	: bullets(ammo), hp(hitpoints) {
16	}
17
18	void boost() {
19		speed += 10;
20	}
21
22	bool shoot() {
23		if (bullets < 1)
24			return false;
25		--bullets;
26		return true;
27	}
28
29	void set_hp(int value) {
30		hp = value;
31	}
32
33	int get_hp() const {
34		return hp;
35	}
36
37private:
38	int hp;
39};
40
41int main() {
42	std::cout << "=== usertype_advanced ===" << std::endl;
43	sol::state lua;

It’s a fairly minimal class, but we don’t want to have to rewrite this with metatables in Lua. We want this to be part of Lua easily. The following is the Lua code that we’d like to have work properly:

player_script.lua
 1p1 = player.new(2)
 2
 3-- p2 is still here from being 
 4-- set with lua["p2"] = player(0); below
 5local p2shoots = p2:shoot()
 6assert(not p2shoots)
 7-- had 0 ammo
 8	
 9-- set variable property setter
10p1.hp = 545
11-- get variable through property unqualified_getter
12print(p1.hp)
13assert(p1.hp == 545)
14
15local did_shoot_1 = p1:shoot()
16print(did_shoot_1)
17print(p1.bullets)
18local did_shoot_2 = p1:shoot()
19print(did_shoot_2)
20print(p1.bullets)
21local did_shoot_3 = p1:shoot()
22print(did_shoot_3)
23	
24-- can read
25print(p1.bullets)
26-- would error: is a readonly variable, cannot write
27-- p1.bullets = 20
28
29p1:boost()
30-- call the function we define at runtime from a Lua script
31p1:brake()

To do this, you bind things using the new_usertype and method as shown below. These methods are on both table and state(_view), but we’re going to just use it on state:

main.cpp
 1#define SOL_ALL_SAFETIES_ON 1
 2#include <sol/sol.hpp>
 3
 4#include "player.hpp"
 5
 6#include <iostream>
 7
 8
 9	// userdata before you register a usertype,
10	// and it will still carry
11	// the right metatable if you register it later
12
13	// set a variable "p2" of type "player" with 0 ammo
14	lua["p2"] = player(0);
15
16	// make usertype metatable
17	sol::usertype<player> player_type
18	     = lua.new_usertype<player>("player",
19	          // 3 constructors
20	          sol::constructors<player(),
21	               player(int),
22	               player(int, int)>());
23
24	// typical member function that returns a variable
25	player_type["shoot"] = &player::shoot;
26	// typical member function
27	player_type["boost"] = &player::boost;
28
29	// gets or set the value using member variable syntax
30	player_type["hp"]
31	     = sol::property(&player::get_hp, &player::set_hp);
32
33	// read and write variable
34	player_type["speed"] = &player::speed;
35	// can only read from, not write to
36	// .set(foo, bar) is the same as [foo] = bar;
37	player_type.set("bullets", sol::readonly(&player::bullets));
38
39	// You can also add members to the code, defined in Lua!
40	/*
41	lua.script_file("prelude_script.lua");
42
43	return 0;
44}

There is one more method used in the script that is not in C++ or defined on the C++ code to bind a usertype, called brake. Even if a method does not exist in C++, you can add methods to the class table in Lua:

prelude_script.lua
1function player:brake ()
2	self.speed = 0
3	print("we hit the brakes!")
4end

That script should run fine now, and you can observe and play around with the values. Even more stuff you can do is described elsewhere, like initializer functions (private constructors / destructors support), “static” functions callable with name.my_function( ... ), and overloaded member functions. You can even bind global variables (even by reference with std::ref) with sol::var. There’s a lot to try out!

This is a powerful way to allow reuse of C++ code from Lua beyond just registering functions, and should get you on your way to having more complex classes and data structures! In the case that you need more customization than just usertypes, however, you can customize sol to behave more fit to your desires by using the desired customization and extension structures.

You can check out this code and more complicated code at the examples directory by looking at the usertype_-prefixed examples.