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:
-
Odin's Lua docs just list the
translated C API functions. However to understand what these functions do you
must look at the Lua C API docs.
-
All Lua C API functions or data structures start with lua_
or luaL_
in the
name. These are in Odin without the lua_
or luaL_
prefix. So for example
the C typedef lua_State
is in Odin as State
.
-
Lua functions starting with luaL_
are not part of the base set of functions
but can be considered helpful wrappers. Both lua_
and luaL_
functions can
be used together.
-
In the Lua function references there are triplets on the right side. They look
like [-2, +1, e]
. They indicate how the stack is impacted per call.
Section 4.6 - Functions and Types
has the full explanation on how to interpret these.