← Stephen Molyneaux

Rust Modules in LÖVE

Calling Rust code in Lua is great for calling optimized routines, and leveraging libraries from the Rust ecosystem. Packing Rust code for consumption in Lua requires learning how to:

This post covers all the steps needed to wrap the Rust regex crate into a library loadable by the Lua-based LÖVE game framework.

To help us understand the details of how the process works, this will be a from-scratch implementation – no lua-specific Rust modules will be used. This process will be demonstrated by wrapping up BurntSushi’s regex library into a Lua module. This article assumes the reader is familiar with the basics of Rust (up to the 8th chapter of the Rust book should be fine). I’ll also assume that you are willing for me to gloss over some of the aspects of using unsafe in Rust.

By the end of this article you should know how to:

If you’re looking to add Lua functionality to an existing Rust application you should check out rlua on crates.io instead.

Let’s get started!

Generating Bindings

To load a module into Lua we need to match the interface it expects. This interface is defined by lua.h and lauxlib.h. Strictly speaking, only lua.h is necessary, but the auxillary library provides many convenience functions that make it easier to interact with Lua, and helps remove boilerplate. This interface is defined in C code, but the Rust community has created bindgen to automatically convert existing C bindings into Rust!

The most recent version of LÖVE (version 11.2) uses Lua 5.1.5. Technically, LÖVE uses LuaJIT, which is an optimized version of Lua that’s ABI-compatible with Lua 5.1.5.

Before we generate the bindings, we first need to talk about how this project will be organized. First, we will create a library lua-51-sys which provides the interface we use to call Lua functions from Rust. This library will be used by our main project, which we’ll call lregex.

cargo new --lib lregex
cd lregex/
cargo new --lib lua-51-sys
cd lua-51-sys/

We’ll add the following lines to the end of our Cargo.toml file for the lua-51-sys library:

[build-dependencies]
bindgen = "0.37.4"

This section, [build-dependencies], defines a set of crates that we use at compile time. By defining a build.rs file at the root of our project, we can execute code that gives the Rust compiler additional steps to perform. Let’s add a build.rs file to lua-51-sys/:

extern crate bindgen;

use std::env;
use std::path::PathBuf;

fn main() {
    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .generate()
        .expect("Unable to generate bindings");

    // Write the bindings to the $OUT_DIR/bindings.rs file.
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

This code (at compile time) generates bindings from a file we call wrapper.h, and produces a file bindings.rs which contains our Rust bindings. Let’s add wrapper.h now:

#include "lua.h"
#include "lauxlib.h"

These files, lua.h and lauxlib.h define the interface we need to use to talk to Lua. You can grab them here. Put lua.h and lauxlib.h into lua-51-sys/.

Lastly, replace lua-51-sys/src/lib.rs with the following:

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

At this point, you can run cargo build to verify that everything is working.

BONUS: Run cargo doc to generate documentation that you can view in your browser. The output will be in lua-51-sys/target/doc/lua_51_sys/index.html.

Unfortunately, we’re going to have to do something a little bit messy here. First, the Lua ↔ C Interface relies on C-style strings. Since Rust uses it’s own form of strings, it would be nice to be able to easily define constant C-style strings. There are crates with macros to do this on crates.io, but since it’s small I’ve included my own version here.

#[macro_export]
macro_rules! cstr {
    ($s:expr) => {
        concat!($s, "\0") as *const str as *const u8 as *const i8
    };
}

Second, bindgen can’t determine that the C macro lua_pop can be called like a function (like a compiler would be able to do in C). This means that any macros that we want to access from Rust will need to be reimplemented. We’ll add lua_pop to lib.rs:

/// Redefine lua_pop macro from lua.h
/// Pops n elements from the stack
pub unsafe fn lua_pop(L: *mut lua_State, n: i32) {
    lua_settop(L, -(n)-1);
}

Lastly, Lua 5.1 turns out to be a fairly old version of Lua. Lua 5.2 includes standard functions for making it easy to create and load new modules into Lua. In an effort to not repeat work that has already been done, we’ll port a few of these functions over to lib.rs: luaL_setfuncs, luaL_newlib, and luaL_newlibtable.

// Ported from Lua 5.2
unsafe fn luaL_newlibtable(L: *mut lua_State, l: &[luaL_Reg]) {
    lua_createtable(L, 0, l.len() as i32);
}

// Ported from Lua 5.2
// For the original function see:
// https://www.lua.org/source/5.2/lauxlib.c.html#luaL_setfuncs
unsafe fn luaL_setfuncs(L: *mut lua_State, l: &[luaL_Reg], nup: i32) {
    luaL_checkstack(L, nup, cstr!("too many upvalues"));
    for reg in l {
        for _ in 0..nup {
            lua_pushvalue(L, -nup);
        }
        lua_pushcclosure(L, reg.func, nup);
        lua_setfield(L, -(nup + 2), reg.name);
    }
    lua_pop(L, nup);
}

// Ported from Lua 5.2
// Creates a new table and registers there the functions in list l
pub unsafe fn luaL_newlib(L: *mut lua_State, l: &[luaL_Reg]) {
    luaL_newlibtable(L, l);
    luaL_setfuncs(L,l,0);
}

Optional: When you run cargo test on lua-51-sys, you’ll run into a failing test, bindgen_test_layout_max_align_t. If you want, you can remove this type completely (without issue) by modifying build.rs and blacklisting the type:

    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .blacklist_type("max_align_t")
        .generate()
        .expect("Unable to generate bindings");

Creating a Basic Lua Module

Let’s go back to the base project lregex, and modify its Cargo.toml file. Replace the [dependencies] section with the following:

[lib]
crate-type = ["dylib"]

[dependencies]
lua-51-sys = { path = "lua-51-sys" }

Now, let’s modify src/lib.rs. First, clear it of any test code, then add the following:

#[macro_use]
extern crate lua_51_sys;
use lua_51_sys::*;

#[no_mangle]
pub unsafe extern "C" fn foo(state: *mut lua_State) -> i32 {
    let d = lua_tonumber(state, 1);
    lua_pushnumber(state, d + 42.);
    1
}

Then add this:

#[no_mangle]
pub unsafe extern "C" fn luaopen_liblregex(state: *mut lua_State) -> i32 {
    luaL_newlib(
        state,
        &[
            luaL_Reg { name: cstr!("foo"), func: Some(foo), }
        ],
    );
    1
}

Then run cargo build.

Then put the following in main.lua:

local regex = require 'liblregex'

function love.draw()
    love.graphics.print("Foo is: " .. regex.foo(12), 400, 300)
end

Then link the library ln -s target/debug/liblregex.so (different on Windows).

Run love .

Wrapping the Regex Crate

Now that we have a framework set up for creating and loading Lua modules, let’s get our Lua module to do some real work! Let’s expose regex::Regex::find to Lua. First we’ll add the regex crate to our toml file:

regex = "1.0.2"

Then, we’ll create our new function:

use regex::Regex;

#[no_mangle]
pub unsafe extern "C" fn find(state: *mut lua_State) -> i32 {
    let mut pattern_len = 0;
    let pattern_ptr = lua_tolstring(state, 1, &mut pattern_len) as *mut u8;
    let pattern = std::str::from_utf8_unchecked(
        std::slice::from_raw_parts(pattern_ptr, pattern_len)
    );

    let mut text_len = 0;
    let text_ptr = lua_tolstring(state, 2, &mut text_len) as *mut u8;
    let text = std::str::from_utf8_unchecked(
        std::slice::from_raw_parts(text_ptr, text_len)
    );

    // Error flag
    let mut error = false;

    // We only want to return in _one_ place to make sure we don't forget to
    // free memory
    let return_stack_size = match Regex::new(&pattern) {
        Ok(re) => {
            if let Some(match_bytes) = re.find(&text) {
                let (start, end) = (match_bytes.start(), match_bytes.end());
                lua_pushnumber(state, start as f64);
                lua_pushnumber(state, end as f64);
                2
            } else {
                lua_pushnil(state);
                1
            }
        }
        Err(e) => {
            match e {
                regex::Error::Syntax(_) => {
                    lua_pushstring(state, cstr!("Syntax error :("));
                    error = true;
                }
                _ => {
                    lua_pushstring(state, cstr!("Regex internal error :("));
                    error = true;
                }
            }
            1
        }
    };

    if error {
        // This never returns
        lua_error(state);
    }

    return_stack_size
}

Don’t forget to modify luaopen_liblregex with the new function.

Now let’s modify our main.lua to test this:

local regex = require 'liblregex'

function love.draw()
    local pattern = '([abc])*d'
    local text = 'abbbcd'

    head, tail = regex.find(pattern, text)

    if head then
        love.graphics.print("Start: " .. head .. " End: " .. tail , 400, 300)
    else
        love.graphics.print("No match for pattern '" .. pattern .. "'", 100, 300)
        love.graphics.print("in text '" .. text .. "'", 100, 320)
    end
end

And you should see Start: 0 End: 6 on the screen!

Conclusion

At this point you should have learned a few things:

Try wrapping different libraries, and send me a tweet @shmolyneaux with your results!