luaguides

Embedding Lua in a C Application

Introduction

Embedding Lua into a C application gives you a powerful, lightweight scripting layer that your users can use to configure behaviour, automate tasks, or extend functionality without recompiling anything. The Lua C API is a thin, well-designed bridge — you get a Lua state, push and pull values through a virtual stack, and call Lua code from C with almost no overhead.

This tutorial walks you through the complete embedding workflow: creating and destroying the Lua state, loading and running Lua code, exchanging data via the stack, and exposing your own C functions to Lua scripts. By the end you’ll have a working skeleton you can build on.

Setting Up the Lua State

Every Lua operation starts with a lua_State — a pointer to an isolated Lua interpreter instance. You create it with luaL_newstate(), optionally load the standard library with luaL_openlibs(), and close it with lua_close() when you’re done.

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void) {
    lua_State *L = luaL_newstate();   // Create a new Lua state
    luaL_openlibs(L);                  // Load the standard library (print, io, etc.)

    /* Run Lua code here */

    lua_close(L);  // Clean up
    return 0;
}

Compile this with:

gcc -o myapp myapp.c $(pkg-config --cflags --libs lua5.4)

If pkg-config is not available, use -I/usr/include/lua5.4 -llua5.4 directly. The exact library name depends on your installation — on some systems it is lua5.4, on others simply lua.

Running Lua Code from C

There are two convenient helpers for executing Lua code: luaL_dofile() runs a .lua file, and luaL_dostring() executes a string. Both return 0 on success and a non-zero error code on failure.

int ret = luaL_dofile(L, "script.lua");
if (ret != 0) {
    fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);  // Remove the error message from the stack
}

luaL_dostring() works the same way for inline code:

ret = luaL_dostring(L, "print('Hello from embedded Lua!')");
if (ret != 0) {
    fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}

For production code you will usually want to use lua_pcall() instead, because it lets you set a custom error handler and gives you more control. The _pcall variant takes four arguments: the Lua state, the number of arguments on the stack, the number of return values you expect, and an optional error handler index (0 means no handler).

lua_getglobal(L, "print");       // Push the print function
lua_pushstring(L, "via pcall");  // Push an argument

int ret = lua_pcall(L, 1, 0, 0);  // 1 arg, 0 returns, no error handler
if (ret != 0) {
    fprintf(stderr, "pcall failed: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}

lua_call() does the same job without any error handling — if the called code raises an error, it propagates directly to the caller. Use it only when you are certain the code cannot fail, or when you are already inside a protected call.

Exchanging Data Through the Stack

The Lua stack is the central mechanism for all data exchange between C and Lua. C code pushes Lua values onto the stack to pass them to Lua, and pops values from the stack to read results back. The stack also works in reverse: you can push values from C and then read them from Lua.

Pushing Values Onto the Stack

The API provides one lua_push* function per type:

lua_pushnil(L);           // Push nil
lua_pushboolean(L, 1);    // Push a boolean (int, 0 or non-zero)
lua_pushinteger(L, 42);  // Push an integer
lua_pushnumber(L, 3.14); // Push a double
lua_pushstring(L, "hello"); // Push a string (Lua copies it)

Lua manages the memory for these values. Strings in particular are copied into Lua’s internal buffer, so you do not need to keep your original C string alive after pushing it.

Reading Values from the Stack

Use the lua_to* family to read stack values back into C types:

int b = lua_toboolean(L, -1);       // Returns 0 or 1
lua_Integer i = lua_tointeger(L, -1);
double n = lua_tonumber(L, -1);
const char *s = lua_tostring(L, -1); // Returns NULL if not a string

All of these functions are safe to call even if the value at the given index is not of the expected type — they return a sensible default (0, NULL, or false).

Stack Indexes

Indexes into the stack can be positive or negative. Positive indexes count from the bottom of the stack (where index 1 is the first element). Negative indexes count from the top, so -1 is the top element, -2 is the one below it, and so on.

// Assuming the stack contains: [1, 2, 3]  (3 on top)
// lua_gettop(L) returns 3

lua_pushinteger(L, 100);
// Stack: [1, 2, 3, 100]  -- top is 100

int top = lua_gettop(L);  // Returns 4
lua_pop(L, 1);            // Remove top element
// Stack: [1, 2, 3]  -- top is back to 3

For most operations you will use relative indexes like -1 to refer to the top, and 1 to refer to the bottom when iterating over all stack elements.

Exposing C Functions to Lua

One of the most useful things you can do when embedding Lua is to register your own C functions so Lua scripts can call them. This is how you give scripts access to your application’s functionality.

A C function callable from Lua has this signature:

static int my_add(lua_State *L) {
    int a = (int)luaL_checkinteger(L, 1);  // Get 1st argument
    int b = (int)luaL_checkinteger(L, 2);  // Get 2nd argument
    lua_pushinteger(L, a + b);              // Push the result
    return 1;  // Number of return values
}

luaL_checkinteger validates that the argument at the given position is an integer and returns it. If it is not, it raises a Lua error with a formatted message. There is also luaL_checkstring for strings.

The function returns the number of values it pushed onto the stack — Lua uses this to know how many return values to collect.

Register the function with lua_register, which is a convenience macro that combines lua_pushcfunction and lua_setglobal:

lua_register(L, "my_add", my_add);

After this call, a Lua script can call my_add(3, 4) and receive 7.

Here is a complete example that registers a function and calls it from Lua:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>

static int say_hello(lua_State *L) {
    const char *name = luaL_checkstring(L, 1);
    printf("Hello, %s!\n", name);
    return 0;  // No return values
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_register(L, "say_hello", say_hello);

    /* Call the function we just registered */
    lua_getglobal(L, "say_hello");
    lua_pushstring(L, "World");
    lua_pcall(L, 1, 0, 0);

    lua_close(L);
    return 0;
}

When you run this program it prints Hello, World!.

Raising Errors from C

When something goes wrong inside a C function that you have exposed to Lua, use luaL_error() to raise a Lua error. It formats a message using printf-style arguments and throws it, just as if Lua itself had raised an error.

static int safe_div(lua_State *L) {
    int a = (int)luaL_checkinteger(L, 1);
    int b = (int)luaL_checkinteger(L, 2);
    if (b == 0) {
        return luaL_error(L, "division by zero");
    }
    lua_pushinteger(L, a / b);
    return 1;
}

The error propagates up through lua_pcall and lands on the stack as a string, where your C code can read it and handle it.

A Complete Worked Example

Putting the pieces together, here is a minimal but complete C program that creates a Lua state, registers a C function, evaluates a Lua expression from a string, and prints the result:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>

static int square(lua_State *L) {
    int n = (int)luaL_checkinteger(L, 1);
    lua_pushinteger(L, n * n);
    return 1;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_register(L, "square", square);

    const char *code = "local x = 5; local y = square(x); print(y)";
    int ret = luaL_dostring(L, code);
    if (ret != 0) {
        fprintf(stderr, "Error running Lua: %s\n", lua_tostring(L, -1));
        lua_pop(L, 1);
    }

    lua_close(L);
    return 0;
}

Compile and run:

gcc -o square square.c $(pkg-config --cflags --libs lua5.4)
./square

The output is 25, because square(5) returns 25 from our C function.

Common Pitfalls

A few mistakes come up repeatedly when you are first embedding Lua:

Stack overflow. Lua has a stack size limit (typically a few thousand entries). If you push too many values without popping, Lua will abort with a stack overflow. Use lua_gettop(L) to check the current stack size and lua_checkstack(L, n) to ensure there is room before a large push.

String lifetime. When you push a string with lua_pushstring(), Lua copies the string into its own memory. However, if you push a luaL_Buffer (a temporary string buffer API), the underlying buffer must not be freed before the buffer is completed — the Lua API documentation calls this out explicitly.

Garbage collection for userdata. If you allocate memory from C and want Lua’s garbage collector to manage it, create a userdata object with a custom metatable that has a __gc metamethod. This is an advanced topic but important for long-running applications that create many such objects.

Thread safety. Each lua_State is single-threaded. If you need to run Lua from multiple OS threads, each thread must have its own lua_State. Lua coroutines (threads within a single lua_State) are fine, but sharing a state across OS threads requires explicit synchronisation and is generally not safe.

Summary

Embedding Lua comes down to a small, consistent set of operations:

  • Create the state with luaL_newstate(), load libraries with luaL_openlibs(), and close with lua_close().
  • Run code with luaL_dofile(), luaL_dostring(), or lua_pcall() for protected execution.
  • Push values to send data to Lua and lua_to* functions to read values back.
  • Register C functions with lua_register() so Lua scripts can call your code.
  • Handle errors with lua_pcall() on the Lua side and luaL_error() on the C side.

The Lua 5.4 API is stable and well documented. Once you are comfortable with the stack model, you will find that adding a Lua scripting layer to a C project takes only a few dozen lines of code — and gives your users a great deal of flexibility in return.

See Also