[C/C++] A Lua C API Cheat Sheet
Find this example here on my GitHub repository.
Lua is a very good way to extend your C/C++ application. The integration in your project is easy and working with Lua is in my opinion fun.
However, Lua was first released in 1993 and is still a C library. This doesn't mean it's bad, but writing proper bindings to enhance usage in a modern C++ project would still be nice. We don't want to mix modern C++ code with plain C. And speaking of libraries, we want to keep external dependencies isolated (see my article about frameworks).
With this article you have a first cheat sheet about the Lua C API and how the access variables, functions, arrays, etc. I know there are many existing Lua bindings available, and I recommend using one of them. But maybe you want to write your own bindings or you want to understand the C API in general, so this cheat sheet can be quite helpful.
Before I present the examples I'll briefly explain how to compile Lua and the syntax in general. Followed by a brief explanation of the Lua stack, which we'll need to understand when we work with the Lua C API.
In case you are already familiar with Lua you can jump directly to the examples by using this menu:
Compiling Lua
I added all the Lua sources to this project and to embed Lua in your application, just add all Lua source files (except lua.c
) to your target. If you want to compile the Lua interpreter, build all source files from Lua. And that's it. Check out the CMakeLists.txt
at GitHub.
Lua Syntax
Consider the following code snippet, if you want to read a more detailed manual about the Lua language itself, take a look into the official Lua manual .
-- comments begin with two dash --
print("Lua: Hello console output")
global_var = 10
local local_var = "defined in the local scope"
if global_var == 5 then
-- something complex happens here
elseif local_var == "defined in the local scope" then
-- something else here
else
error("invalid operation")
end
-- add all elements of array `a'
function add (a)
local sum = 0
for i,v in ipairs(a) do
sum = sum + v
end
return sum
end
The Lua Stack
Before I start with the actual example of this article, I'll present the Lua stack with which you'll work. This may seem confusing at first, but it's not too difficult, let's take a look at the following illustration.
This is a very short illustration of the Lua stack, but i think you will get it. You have the stack, you call some getters from C/C++ each requested value is pushed to the top of the stack.
To access the stack you need to create a lua_State
and to use built libraries inside the scripts you need to open those. Usually the code snippet looks like this:
lua_State* L = luaL_newstate();
luaL_openlibs(L);
// do something here...
lua_close(L);
1. Running a Lua script from file
This first example script just prints a Hello Lua to the console by using print
, like in other languages.
example_1_run_script.lua
:
print("[Lua] Hello Lua")
With luaL_dofile(L, "./scripts/example_1_run_script.lua") == LUA_OK
we are running the script. If something goes wrong, we wouldn't get LUA_OK
and can print/log the error from Lua.
example_1_run_script.hpp:
if (luaL_dofile(L, "./scripts/example_1_run_script.lua") == LUA_OK) {
std::cout << "[C] Executed example_1_run_script.lua\n";
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
Terminal output:
--- example_1_run_script ---
[Lua] Hello Lua
[C] Executed example_1_run_script.lua
2. Running a Lua script from string
Same applies by running a script from string, use luaL_dostring
.
example_2_run_string.hpp
:
const char* s = "print(\"[Lua] Hello from this string\")";
if (luaL_dostring(L, s) == LUA_OK) {
std::cout << "[C] Executed string s\n";
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
Terminal output:
--- example_2_run_string ---
[Lua] Hello from this string
[C] Executed string s
3. Access to variables
Now we are creating a variable foo = 99
in Lua.
example_3_access_lua_variables.lua
:
foo = 99;
print("[Lua] foo = ".. foo)
With lua_getglobal
we get foo
on the stack. Once it is on the stack we use lua_tonumber(L, -1)
to access it. The -1
is the stack index, which is the top of the stack.
if (luaL_dofile(L, "./scripts/example_3_access_lua_variables.lua") == LUA_OK) {
lua_getglobal(L, "foo"); // get foo on the stack
lua_Number foo_in_c = lua_tonumber(L, -1); // foo is on top of the stack, use -1
std::cout << "[C] Received lua's foo with value " << foo_in_c << '\n';
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
And there we have foo
printed in C:
--- example_3_access_lua_variables ---
[Lua] foo = 99
[C] Received lua's foo with value 99
4. Passing variables to Lua
The other way around, we'll print bar
in the following script. Note: bar
was not declared in this script.
example_4_passing_variable_to_lua.lua
:
print("[Lua] Can access bar Lua = " .. bar )
In the C implementation we need to declare the value and push the value. Therfore we need following steps:
- Use
lua_pushnumber
to push any value on top of the stack and - Use
lua_setglobal
to declare the variable
Note: with lua_pushnumber
you just push a value to the stack which hasn't asigned. As the documentation for lua_setglobal
goes: [..] Pops a value from the stack and sets it as the new value of global name. [..]
example_4_passing_variable_to_lua.hpp
:
int bar = 199;
std::cout << "[C] Declared bar = " << bar << '\n';
lua_pushnumber(L, bar); // push the number to the stack
lua_setglobal(L, "bar"); // assign a variable to the value on top of the stack, which is bar
if (luaL_dofile(L, "./scripts/example_4_passing_variable_to_lua.lua") == LUA_OK) {
std::cout << "[C] Executed example_4_passing_variable_to_lua.lua\n";
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
With the terminal output:
--- example_4_passing_variable_to_lua ---
[C] Declared bar = 199
[Lua] Can access bar Lua = 199.0
[C] Executed example_4_passing_variable_to_lua.lua
5. Access a Lua function
Now we call a Lua function from C. In the script we only declared the function, we don't call it.
example_5_access_lua_function.lua
:
function add_2_numbers(value_1, value_2)
print("[Lua] I got 2 numbers to add: " .. value_1 .. " + " .. value_2 .. " = " .. value_1+value_2)
return value_1 + value_2
end
In C/C++ it goes like following:
- Read the Lua script with
luaL_dofile
- Use
lua_getglobal
with the global name of the function - Push step by step each argument
- Call the function with:
lua_pcall(L, arguments_count, returnvalues_count)
(you can return multiple values in Lua functions) - The result is available on the Lua stock (top)
Note: you need to push each function argument separatly to the Lua stack.
example_5_access_lua_function.hpp
:
if (luaL_dofile(L, "./scripts/example_5_access_lua_function.lua") == LUA_OK) {
lua_getglobal(L, "add_2_numbers"); // get the function on the stack
if (lua_isfunction(L, -1)) {
lua_pushnumber(L, 55); // first function arg: one
lua_pushnumber(L, 17); // second function arg: two
const int arguments_count = 2 ; // we have 2 arguments, which we just pushed to the lua stack
const int returnvalues_count = 1; // function returns 0 values
lua_pcall(L, arguments_count, returnvalues_count, 0); // now call the function
lua_Number result = lua_tonumber(L, -1);
std::cout << "[C] The result from Lua is: " << static_cast<int>(result) << '\n';
} else {
std::cout << "[C] Error: didn't find a function on top of Lua stack\n";
}
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
Gives the following output:
--- example_5_access_lua_function ---
[C] Reading script done, now we're getting the function
[Lua] I got 2 numbers to add: 55.0 + 17.0 = 72.0
[C] The result from Lua is: 72
6. Calling a C function from Lua
Now the other way around, we are accessing a C function from a Lua script.
example_6_calling_c_function_from_lua.lua
result = multiply_2_numbers(3,4)
print("[Lua] Called multiply_2_numbers(..) and the result is: " .. result)
In general for all C functions which we want to call from Lua:
- Returntype
int
- Signature
int any_function(lua_State* L)
- Returns the number of values to return (remember Lua can return multiple numbers from functions)
- Before running the script: Expose the function and assign it (like with variables)
// the function we'll call from lua:
int multiply_2_numbers(lua_State* L) {
// function args are on the lua stack, last arg is on top
lua_Number arg_2 = lua_tonumber(L, -1);
lua_Number arg_1 = lua_tonumber(L, -2);
std::cout << "[C] executing multiply_2_numbers with two arguments: "
<< arg_1 << " and " << arg_2 << '\n';
// calculate the result
lua_Number result = arg_2 * arg_1;
// push the result back to the stack
lua_pushnumber(L, result);
// return 1 because we have 1 return value (i know magic number ...)
return 1;
}
// to expose the function, do almost the same as with variables:
lua_pushcfunction(L, multiply_2_numbers);
lua_setglobal(L, "multiply_2_numbers");
if (luaL_dofile(L, "./scripts/example_6_calling_c_function_from_lua.lua") == LUA_OK) {
std::cout << "[C] Executed example_6_calling_c_function_from_lua.lua\n";
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
And this gives the following output:
--- example_6_calling_c_function_from_lua ---
[C] executing multiply_2_numbers with two arguments: 3 and 4
[Lua] Called multiply_2_numbers(..) and the result is: 12.0
[C] Executed example_6_calling_c_function_from_lua.lua
7. Access structs in Lua (creating userdata)
Arbitrary structs can also passed to Lua, those are usertypes
. Unfortunately Lua can't access the struct members directly, you need getter and setter functions. There are other libraries which make structs/classes accessible but this is not part of this article (check out for instance LuaBridge if you want something in that way).
example_7_access_structs_in_lua.lua
foo = create_foo()
print("[Lua] Created userdata Foo")
Now we have almost the same implementation as in example 6, because we are using a create function to create a struct Foo
. Consider the comments in the following file.
example_7_access_structs_in_lua.hpp
// any struct defined to pass to lua
struct Foo {
int m_x;
int m_y;
};
// inside our function we create newuserdata
// we still return one because we return one instance of
int create_foo(lua_State* L) {
Foo* foo = static_cast<Foo*>(lua_newuserdata(L, sizeof(Foo)));
foo->m_x = 0;
foo->m_y = 0;
std::cout << "[C] Created Foo with x = " << foo->m_x << " and y = " << foo->m_y << '\n';
return 1;
}
// before executing the script we push the create function to the lua stack and assign it to make it callable
lua_pushcfunction(L, create_foo);
lua_setglobal(L, "create_foo");
if (luaL_dofile(L, "./scripts/example_7_access_structs_in_lua.lua") == LUA_OK) {
std::cout << "[C] Executed example_7_access_structs_in_lua.lua\n";
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
Gives then the output:
--- example_7_access_structs_in_lua ---
[C] Created Foo with x = 0 and y = 0
[Lua] Created userdata Foo
[C] Executed example_7_access_structs_in_lua.lua
8. Reading a Lua table in C
We also can define tables in Lua which are similar to C structs. Consider the following table foo
with the according fields.
example_8_reading_lua_table_in_c.lua
foo = {
foo_number = 1,
bar_number = 2,
foo_string = "foo",
bar_string = "bar"
}
To access foo
and all the single values, we need to push at first foo
to the stack, and then push each field one by one to the stack. If you have a Lua table on the stack, you can use lua_getfield(L, <index>, <fieldname>)
with:
<index>
: Index on the Lua stack of the table<fieldname>
: Name of the field from the table we want to access
Consider the comments to see how the table moves down on the stack by accessing each field one by one.
example_8_reading_lua_table_in_c.hpp
:
if (luaL_dofile(L, "./scripts/example_8_reading_lua_table_in_c.lua") == LUA_OK) {
std::cout << "[C] Executed example_8_reading_lua_table_in_c.lua\n";
lua_getglobal(L, "foo"); // get global struct foo on the stack
if (lua_istable(L, -1)) { // verify if its a table
lua_getfield(L, -1, "foo_number"); // get the field"foo_number", table is on top of the stack
auto foo_number = lua_tonumber(L, -1); // after get_field, the field "foo_number" is on top
lua_getfield(L, -2, "bar_number"); // now the table is on -2, we access "bar_number" from the table
auto bar_number = lua_tonumber(L, -1); // we receive "bar_number"
lua_getfield(L, -3, "foo_string"); // the table is now on -3 and we use getfield for "foo_string"
auto foo_string = lua_tostring(L, -1); // and receive the string from the top of the stack
lua_getfield(L, -4, "bar_string"); // and once more table is now on -4, we use getfield again
auto bar_string = lua_tostring(L, -1); // the table is now on -5, but we already have all fields.
std::cout << "[C] Got table foo from lua with:\n" <<
"foo_number = " << foo_number << '\n' <<
"bar_number = " << bar_number << '\n' <<
"foo_string = " << foo_string << '\n' <<
"bar_string = " << bar_string << '\n';
} else {
std::cout << "[C] There's apparently no table on the stack top\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
This prints then:
--- example_8_reading_lua_table_in_c ---
[C] Executed example_8_reading_lua_table_in_c.lua
[C] Got table foo from lua with:
foo_number = 1
bar_number = 2
foo_string = foo
bar_string = bar
9. Reading a Lua array in C
Now we declare an array foo
in Lua and access it's values in C.
example_9_reading_lua_array_in_c.lua
:
foo = {11, 22, 33}
print("[Lua] foo[1] = " .. foo[1])
print("[Lua] foo[2] = " .. foo[2])
print("[Lua] foo[3] = " .. foo[3])
Since we can put different values in the array (which makes it more like a list), we need to use rawgeti
. As the name states, we are accessing raw values and we need to know what we are receiving on each index.
The syntax is following: rawgeti(lua_State *L, int index, int n)
where:
- L = Lua stack which we access
- index = index on the Lua stack where the array is
- n = index of the array, in this case
foo[n]
Consider following code and the comments I put into the snippet.
example_10_reading_lua_tablearray_in_c.hpp
:
if (luaL_dofile(L, "./scripts/example_9_reading_lua_array_in_c.lua") == LUA_OK) {
std::cout << "[C] Executed example_9_reading_lua_array_in_c.lua\n";
lua_getglobal(L, "foo");
lua_rawgeti(L, -1, 1); // foo is on the top, access with -1 and access to foo[1] with 1
auto first = lua_tonumber(L, -1); // foo[1] is now on the top of the stack
lua_rawgeti(L, -2, 2); // "foo" is on -2, and we want foo[2] with 2
auto second = lua_tonumber(L, -1); // foo[2] is now on the top of the stack
lua_rawgeti(L, -3, 3); // "foo" is on -3, and we want foo[3] with 3
auto third = lua_tonumber(L, -1); // foo[3] is now on the top of the stack
std::cout << "[C] Got table foo from lua with:\n" <<
"[C] foo[1] = " << first << '\n' <<
"[C] foo[2] = " << second << '\n' <<
"[C] foo[3] = " << third << '\n' ;
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
If there were strings in foo
we'd need to use lua_tostring
instead of lua_tonumber
.
The output gives:
--- example_9_reading_lua_array_in_c ---
[Lua] foo[1] = 11
[Lua] foo[2] = 22
[Lua] foo[3] = 33
[C] Executed example_9_reading_lua_array_in_c.lua
[C] Got table foo from lua with:
[C] foo[1] = 11
[C] foo[2] = 22
[C] foo[3] = 33
10. Reading a Lua table array in C
Now we're putting all together. Accessing a table array from Lua. Here is the definition of foo
.
example_10_reading_lua_tablearray_in_c.lua
:
foo = {
[1] = {
bar = 123,
baz = "baz",
},
[2] = {
bar = 456,
baz = "another baz",
}
}
For the access in C/C++ we use all the function we already used. Now you can check if you understood the Lua stack. Consider the following code snippet. I left some comments to explain it step by step.
if (luaL_dofile(L, "./scripts/example_10_reading_lua_tablearray_in_c.lua") == LUA_OK) {
std::cout << "[C] Executed example_10_reading_lua_tablearray_in_c.lua\n";
lua_getglobal(L, "foo"); // getting foo on top of the stack
if (lua_istable(L, -1)) // verifying if it's a table
{
lua_pushnumber(L, 1); // we push the index 1 to access the first element, foo is now on -2
lua_gettable(L, -2); // we get the table from stack index -2, index to access is 1 on top
if (lua_istable(L, -1)) { // verifying this is a table too
lua_getfield(L, -1, "bar"); // now we get the values like in example 8
auto bar = lua_tonumber(L, -1);
lua_getfield(L, -2, "baz");
auto baz = lua_tostring(L, -1);
std::cout << "[C] foo[1].bar = " << bar << '\n' <<
"[C] foo[1].baz = " << baz << '\n' ;
} else {
std::cout << "[C] There's apparently no table on the stack top\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
lua_pushnumber(L, 2); // we push the next array index we want to read
lua_gettable(L, -5); // meanwhile we pushed four other values, the table is on top-5
if (lua_istable(L, -1)) { // now the same procedure as above
lua_getfield(L, -1, "bar");
auto bar = lua_tonumber(L, -1);
lua_getfield(L, -2, "baz");
auto baz = lua_tostring(L, -1);
std::cout << "[C] foo[2].bar = " << bar << '\n' <<
"[C] foo[2].baz = " << baz << '\n' ;
} else {
std::cout << "[C] There's apparently no table on the stack top\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
} else {
std::cout << "[C] There's apparently no table on the stack top\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
And you'll see this output:
--- example_10_reading_lua_tablearray_in_c ---
[C] Executed example_10_reading_lua_tablearray_in_c.lua
[C] foo[1].bar = 123
[C] foo[1].baz = baz
[C] foo[2].bar = 456
[C] foo[2].baz = another baz
11. Reading a Lua table array in C (alternative)
This is an alternative declaration of a table array, where names are used instead of indices.
example_11_reading_lua_tablearray_alternative.lua
foo = {
foo_1 = {
bar = 123,
baz = "baz",
},
foo_2 = {
bar = 456,
baz = "another baz",
}
}
And a alternative C implementation to access the members, which is pretty similar to example 10.
example_11_reading_lua_tablearray_alternative.hpp
:
if (luaL_dofile(L, "./scripts/example_11_reading_lua_tablearray_alternative.lua") == LUA_OK) {
std::cout << "[C] Executed example_11_reading_lua_tablearray_alternative.lua\n";
lua_getglobal(L, "foo"); // get the global table array foo on top of the stack
if (lua_istable(L, -1)) { // verify it is a table
lua_getfield(L, -1, "foo_1"); // get the first element by name
if (lua_istable(L, -1)) { // verify it is a table
lua_getfield(L, -1, "bar"); // get the values like in the examples before
auto bar = lua_tonumber(L, -1);
lua_getfield(L, -2, "baz");
auto baz = lua_tostring(L, -1);
std::cout << "[C] foo.foo_1.bar = " << bar << '\n' <<
"[C] foo.foo_1.baz = " << baz << '\n' ;
}
lua_getfield(L, -4, "foo_2"); // meanwhile the table itself is on top-4
if (lua_istable(L, -1)) { // and the same again
lua_getfield(L, -1, "bar");
auto bar = lua_tonumber(L, -1);
lua_getfield(L, -2, "baz");
auto baz = lua_tostring(L, -1);
std::cout << "[C] foo.foo_2.bar = " << bar << '\n' <<
"[C] foo.foo_2.baz = " << baz << '\n' ;
}
}
} else {
std::cout << "[C] Error reading script\n";
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
Gives then this output:
--- example_11_reading_lua_tablearray_alternative ---
[C] Executed example_11_reading_lua_tablearray_alternative.lua
[C] foo.foo_1.bar = 123
[C] foo.foo_1.baz = baz
[C] foo.foo_2.bar = 456
[C] foo.foo_2.baz = another baz
Conclusion
So thats it for starters with Lua and the C API. I hope this helps to understand the Lua stack and how to pass values, arrays, tables, etc. from Lua to C and vice versa.
As mentioned in the beginning I recommend to isolate the Lua implementations in your project and write proper bindings to access it. Means wrapping it in proper classes and functions, manage the lifetime of the lua_State* L
, and so on. But under the hood you'll have sort of those implementations.
I worked already with Sol, a C++ library binding to Lua. It is really comfortable to use ist, but sometimes you need to customize things or just want to understand it. So for whatever reason you read this article, I hope it helped.
You can find all examples on my GitHub repository.
But that's it for now.
Best Thomas