Glenn Yonemitsu - software engineer. yonemitsu@gmail.com

Embedding Lua in Odin

Odin offers a great set of third party libraries out of the box. One of them is the Lua language which enables your application to be scriptable or configurable without the need of a custom DSL parser.

This post is an introduction to embedding Lua into your app. It assumes you are familiar with Odin, using Odin's vendor libraries, and Lua syntax.

Lua C API: the state

All processing in Lua is done on the Lua states we create. It is an opaque data struct that encapsulates a Lua environment including all of it's variables, functions, globals, and so on. No Lua state can cross contaminate another one.

From Odin we mutate, run, and interact with the Lua state with lua's C API. Every function requires the pointer to the state as it's first parameter.

As we go through the code we will review the new concepts as they come up. And for simplicity the source will be loaded from a string literal instead of reading a file in Odin.

Getting data from a Lua file

In this example we get a variable's data from a Lua file.

package tutorial

import "core:testing"
import lua "vendor:lua/5.4"

LUA_SOURCE :: `
message = "find me"
`

@(test)
lua_tutorial :: proc(t: ^testing.T) {
    L: ^lua.State = lua.L_newstate()
    defer lua.close(L)

    status: lua.Status = lua.L_loadstring(L, LUA_SOURCE)
    testing.expect(t, status == .OK, "Error loading source")

    call_status: c.int = lua.pcall(L, 0, 0, 0)
    testing.expect(t, lua.Status(call_status) == .OK, "Error running source")

    stack := lua.getglobal(L, "message")
    testing.expect(t, stack == i32(lua.TSTRING), "Cannot find variable")

    val := lua.tostring(L, 1)
    testing.expect(t, val == "find me", "Cannot convert stack to cstring")

    lua.settop(L, 0)
    testing.expect(t, lua.gettop(L) == 0, "Cannot clear stack")
}

Let's go through each step.

L: ^lua.State = lua.L_newstate()
defer lua.close(L)

This creates the Lua state. It is the instance of Lua environment that contains everything needed to run on it's own. This means your application can have many Lua states and they won't interfere with each other.

As mentioned, the function calls require the state as it's first parameter. This is a common pattern with the Lua C API.

status: lua.Status = lua.L_loadstring(L, LUA_SOURCE)
testing.expect(t, status == .OK, "Error loading source")

This loads the source into the state, but does not run it yet. The returned lua.Status value can be inspected to see if there are any issues, including syntax errors.

What happens under the hood is the source is processed into a "chunk" which is a function to be called. Calling this function essentially runs the source. This chunk is placed on the stack.

The Lua stack

The stack is the main way we interact with the Lua state. All variable assignments, reads, mutations, function embedding, etc. are done with the stack.

When we ran the L_loadstring function the stack has this pushed:

  +---+-------------------------------+
  | 1 | function to run loaded source |
--+---+-------------------------------+--

Then we can run it with the following lines in our example:

call_status: c.int = lua.pcall(L, 0, 0, 0)
testing.expect(t, lua.Status(call_status) == .OK, "Error running source")

This runs the loaded source in the previous step. The parameters 0, 0, 0 indicate the function's parameters, outputs, and error handler stack index. That last one is more for debugging which won't be covered in this tutorial.

Side note: here the return type is a c.int instead of lua.Status because the C API can return values other than the typical status codes.

At this point the Lua source has run and we now just need to get the variable data. We know the global variable name is "message" so we retrieve that and add it to the stack:

stack := lua.getglobal(L, "message")
testing.expect(t, stack == i32(lua.TSTRING), "Cannot find variable")

This looks for the global variable and puts it on the virtual stack of our Lua state.

When we call this function the stack looks like this:

  +---+-----------+
  | 1 | "find me" |
--+---+-----------+--

The only thing left is to inspect the stack to get the data we want. And we do that with:

val := lua.tostring(L, 1)
testing.expect(t, val == "find me", "Cannot convert stack to cstring")

This looks at the stack at index 1 and returns it as a cstring.

Since we are using a stack we can load up many values and retrieve them later.

  +---+-----------+
  | 3 | 42        |
  +---+-----------+
  | 2 | "Lua"     |
  +---+-----------+
  | 1 | "find me" |
--+---+-----------+--
val := lua.tostring(L, 1)
lang := lua.tostring(L, 2)
answer := lua.tonumber(L, 3)

Getting values from the stack does not change it. We can now clear the stack:

lua.settop(L, 0)

Enabling some default libs in Lua

Lua comes with standard libraries that provide convenient functionality to manipulate strings, tables, print, etc. This can be added to the state with a simple call.

lua.L_openlibs(L)

Embedding a callable Odin proc into Lua

Now let's extend our Lua capabilities a bit more. Let's provide Odin's commonmark vendor library to let the Lua script convert markdown text to html.

When we embed a function into the Lua state they must be of the lua.CFunction type.

CFunction :: proc "c" (L: ^State) -> i32

So our embedded function will look like so

lua_markdown :: proc "c" (L: ^lua.State) -> i32 {
    context = runtime.default_context()
    md := lua.tostring(L, 1)
    converted := cm.markdown_to_html(md, len(md), {.Unsafe})
    defer cm.free_cstring(converted)
    lua.pushstring(L, converted)
    return 1
}

Just as we used the stack to get the parameters into our Odin code, we also use the stack to set the return values for Lua. The i32 return integer tells the Lua state how many objects on the stack are the return values.

The key parts to returning to the Lua state are these lines. We don't pop or clear the stack:

lua.pushstring(L, converted)
return 1

Altogether it looks like this:

package tutorial

import "base:runtime"
import "core:c"
import "core:testing"
import lua "vendor:lua/5.4"
import cm "vendor:commonmark"

LUA_SOURCE :: `
content = markdown("hello!")
`

lua_markdown :: proc "c" (L: ^lua.State) -> i32 {
    context = runtime.default_context()
    md := lua.tostring(L, 1)
    converted := cm.markdown_to_html(md, len(md), {.Unsafe})
    defer cm.free_cstring(converted)
    lua.pushstring(L, converted)
    return 1
}

@(test)
lua_embed :: proc(t: ^testing.T) {
    L: ^lua.State = lua.L_newstate()
    defer lua.close(L)

    status: lua.Status = lua.L_loadstring(L, LUA_SOURCE)
    testing.expect(t, status == .OK, "Error loading source")

    lua.pushcfunction(L, lua_markdown)
    lua.setglobal(L, "markdown")

    call_status: c.int = lua.pcall(L, 0, 0, 0)
    testing.expect(t, lua.Status(call_status) == .OK, "Error running source")

    stack: c.int = lua.getglobal(L, "content")
    testing.expect(t, stack == i32(lua.TSTRING), "Cannot find variable")

    val: cstring = lua.tostring(L, 1)
    testing.expect(t, val == "<p>hello!</p>\n", "Cannot convert variable on stack to cstring")
}

One more thing about the stack, it's not infinite

When pushing things to the stack you should not assume it can all fit. For this tutorial we skipped it since the stack size does nto get very big, but for more complex Lua embeddings this should be checked with lua_checkstack.

Reference

What can we do with this so far?

Practically speaking any Lua file should be accessible as a config or data entry file in your application now.

The main thing to keep in mind is the stack is the main way to interact with the Lua state, including embedding Odin functions. You should get familiar with the lua_push*, lua_set*, and lua_to* functions in the C API reference.

Some additional hints to help you: