On my regular programming stream I’ve been figuring out how to build a Multi-User Dungeon (or MUD) server in Haskell.
Part of that project is to learn how to embed Lua into Haskell applications. The idea is that the game developer can write their game in Lua and not have to worry about networking, concurrency, databases, caching, simulation, and all of that. They can focus on the game logic and use the provided API to interact with the server, called Bakamud, which is written in Haskell. In your application it might allow users to write plugins the extend existing functionality.
In this post I’m going to share the basics of loading some Lua code, calling some Lua functions in that code, and exporting Haskell functions for Lua code to call.
Relevant Packages
I didn’t want to learn how to write bindings to Lua itself so I searched for a package I could use. I came across the lua package and started with that. I discovered that this is a low level set of bindings and that calling Lua code and pushing Haskell functions into the Lua interpreter required a lot of low-level stack manipulation and pain-staking calls and checks.
Fortunately you can avoid most this by using the hslua-core and related packages. They provide a higher-level and friendlier interface so that you can avoid having to write your own marshalling and common error handling code.
However it was worthwhile going this route because you will have to be
familiar with the bindings in order to understand how to effectively
use hslua-core. I find myself bouncing between the documentation
for both. If I could start again however I would definitely recommend
hslua-core first and dive into lua as needed.
Calling Lua Functions
It is straight-forward to call functions from the Lua standard libraries from Haskell. The documentation gives us some examples. I managed to get these working in Bakamud rather quickly. However this was not what Bakamud needed.
There doesn’t appear to be a way to load a module of Lua code into the interpreter and then call specific functions using the interpreter. This was frustrating to me at first because the server wants to be able to call the game developers’ “update” function every tick of the simulation loop or call some event handlers. These would be written as Lua functions and they would have to pass arguments and return values.
Fortunately there is a work around!
The first thing to understand is lua_load:
Loads a Lua chunk (without running it). If there are no errors, lua_load pushes the compiled chunk as a Lua function on top of the stack. Otherwise, it pushes an error message.
This the primary interface for “loading” user-written Lua code in the interpreter. It doesn’t begin immediately executing that code. It pushes the compiled, “chunk,” onto the interpreter’s stack as a function.
That function takes no arguments but it can return a value. And that’s going to be the key to our work around. If we write a bit of Lua code like this:
-- Define a local table to serve as our module interface
local M = {}
-- We can define public functions on that module like this
function M.foo (items)
print("Hello, from Lua!")
for i, item in ipairs(items) do
print(i, item)
end
end
-- And we then return that module, this is important
return MWe can load this chunk in Haskell, call the function on the top of the
stack to get the module M, and then access the fields of that
module to call the users’ functions.
It would be so much better if a Lua, “chunk,” was treated as a module and any global names in the chunk become fields in the table… but I digress.
Here’s a simple Haskell program that will demonstrate the whole process:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Monad (when)
import qualified HsLua.Core as Lua
import qualified HsLua.Marshalling.Push as Lua
main :: IO ()
main = do
-- This might not be necessary in a single-threaded application or
-- one where you only have a single instance of a Lua interpreter at
-- a time. For long-running programs and multi-threaded ones,
-- managing handles on interpreter state will be useful.
luaState <- Lua.newstate
-- Here we run the Lua interpreter with our state. In this case the function
-- requires an annotation of `Lua.Exception` to satisfy the constraints of
-- the `Lua.getfield` function.
_ <- Lua.runWith @Lua.Exception luaState $ do
Lua.openlibs -- Open all of the default Lua libraries and add them
-- to the global namespace
loadResult <- Lua.loadfile (Just "main.lua") -- This file will be
-- relative to our
-- executable.
when (loadResult /= Lua.OK) (error "Unable to load Lua chunk")
-- We then load the chunk of Lua code and have a function on top
-- of the Lua stack.
callResult <- Lua.pcallTrace 0 1 -- The function takes no
-- arguments and returns a single
-- result.
when (callResult /= Lua.OK) (error "Unable to run Lua module")
-- We now have the returned `M` table on the top of the stack.
fieldType <- Lua.getfield 1 "foo" -- This will push the value at
-- M["foo"] on top of the stack.
when (fieldType /= Lua.TypeFunction) (error "Expected 'foo' to be a function")
Lua.pushList Lua.pushText ["Foo", "Bar", "Baz"]
-- This comes from the hslua-marshalling package and is really helpful!
funcallResult <- Lua.pcallTrace 1 0
when (funcallResult /= Lua.OK) (error "Error calling 'foo' function")
putStrLn "Hello from Haskell!"When we compile and run this program we get:
Hello, from Lua!
1 Foo
2 Bar
3 Baz
Hello from Haskell!
What’s interesting about embedding Lua in your applications like this is that we don’t need Lua to assert any binary calling conventions on the host language other than the widely used SystemV ABI. Basically, if your host platform has a FFI you’re good to go. Interacting with Lua and calling Lua functions is done through the Lua stack. A concept you will become intimate with as you learn more about embedding Lua in your application.
In fact, you saw an example of that “calling convention” in this
example. In order to pass arguments to a Lua function we have to push
them on to the Lua stack before we call the pcallTrace function. We
have to tell pcallTrace how many arguments we pushed onto the stack
and how many values we are expecting to be returned. The reason is so
that pcallTrace can ensure the stack is in the state we expect it to
be after executing the Lua function so that we can retrieve our data
from it.
Adding Haskell Functions to Lua
Here’s where things get really useful. You can push Haskell functions onto the Lua interpreter stack and make them available for the Lua scripts to call! It’s all rather straight forward:
local M = {}
function M.foo ()
-- We're going to implement this function in Haskell
hello("from Lua!")
end
return MWe can then implement hello in Haskell using the HaskellFunction e
type alias provided by the library:
hello :: Lua.HaskellFunction e
hello = do
-- We have to get our function arguments off of the Lua stack
maybeName <- Lua.tostring (Lua.nthBottom 1)
-- nthBottom is a stack helper function, the arguments from
-- left/right are pushed onto the stack in first/last order.
case maybeName of
Nothing -> do
Lua.liftIO . putStrLn $ "Hello, programmer!"
Just name -> do
Lua.liftIO . putStrLn $ "Hello, " ++ C.unpack name
pure 0 -- And we always have to return the number of result values
-- we will leave on the stack.We can then push the function onto the interpreter stack and add a global name to refer to it like so:
-- Mostly the same program as before...
main :: IO ()
main = do
luaState <- Lua.newstate
_ <- Lua.runWith @Lua.Exception luaState $ do
Lua.openlibs
-- Here is where we push `hello` onto the stack
Lua.pushHaskellFunction hello
Lua.setglobal "hello" -- And add the global name to refer to it
loadResult <- Lua.loadfile (Just "main.lua")
when (loadResult /= Lua.OK) (error "Unable to load Lua chunk")
callResult <- Lua.pcallTrace 0 1
when (callResult /= Lua.OK) (error "Unable to run Lua module")
fieldType <- Lua.getfield 1 "foo"
when (fieldType /= Lua.TypeFunction) (error "Expected 'foo' to be a function")
funcallResult <- Lua.pcallTrace 1 0
when (funcallResult /= Lua.OK) (error "Error calling 'foo' function")
putStrLn "Hello from Haskell!"And when we compile and run this program we get:
Hello, from Lua!
Hello from Haskell!
If you need to pass in some arguments to the HaskellFunction e
function you can partially apply them in the pushHaskellFunction
call and it will work. This can be useful for passing in database
handles and such.
Conclusion
Adding scripting capabilities with Lua to your Haskell application is well supported by a collection of great libraries by the folks at hslua.org.
Lua is a fully re-entrant interpreter and is a widely known scripting language. It can be useful for users to extend your software with such a language. In the case of Bakamud it is used to write the game logic. You could use it to enable users to write custom filters, plugins, and so forth without having to recompile the entire program.
I’m going to continue exploring this space further with Bakamud and hope you learned something useful for your projects today. Thanks for reading!