[C/C++] A Lua C API Cheat Sheet

 

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.

  • Presume you have some Lua getter function in you C/C++ application on the left. The first call is to access foo, the second for bar and the third for baz

  • foo, bar and baz are variables from your Lua script on the right.

  • In the middle you have the Lua stack

    • Each value is pushed to the stack with the C/C++ getter call.

    • Each value has one index, regardless of the type (like an array, struct/table, string, integer, etc.)

    • Each value is then accessible with the according index

    • Positive indices starting from bottom to top

    • Negative indices starting from top to bottom

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

Previous
Previous

[2D Game Engine] Starting A New Project - The Project Setup

Next
Next

[C++] An Eventsystem Without std::bind / std::function