hlua’s stack handling

Introduction to the Lua stack

Lua is a programming language, and one of its goals is to be easy to embed in another language. Executing some code is as easy as creating a new Lua environment with luaL_newstate and calling luaL_do_string. But executing Lua code in itself is not very interesting if it can’t interact with Rust.

// initializes the environment
let lua = luaL_newstate();
// parse and execute some string
luaL_do_string(lua, “a = 5;”);
// this is pseudo-code ^ ; in reality we need to pass a CString
// push the value of `a` on the stack
lua_getglobal(lua, “a”);
// get the value ; `-1` means the “the top element”
let value = lua_tonumber(lua, -1);
lua_pop(lua, 1);
lua_getglobal(lua, “a”);
lua_pushnumber(lua, 1);
lua_gettable(lua, -2);
// ^ again, this supposes that `a` is indeed an array
// the key has been poped
// the stack now contains the array and the value
let value = lua_tonumber(lua, -1);lua_pop(lua, 2); // we pop both the value and the array
lua_getglobal(lua, “a”);
lua_pushnil(lua);
while lua_next(lua, -2) != 0 {
// the stack now contains the array, the key and the value
let key = lua_tonumber(lua, -2)
let value = lua_tonumber(lua, -1);
lua_pop(lua, 1);
// the stack now contains the array and the key
// we are ready for the next iteration
}

The Rust wrapper

In order to wrap around all these operations in Rust, we are going to create a struct named Lua with a basic constructor and destructor:

pub struct Lua {
context: *mut lua_State,
}
impl Lua {
pub fn new() -> Lua {
Lua {
context: luaL_newstate(), // skipping "unsafe" for clarity
}
}
}
impl Drop for Lua {
fn drop(&mut self) {
lua_close(self.context);
}
}
impl Lua {
pub fn get_global_number(&self, name: &str) -> i32 {
lua_getglobal(self.context, name);
let value = lua_tonumber(self.context, -1);
lua_pop(self.context, 1);
value
}
}

Arrays

Thread-safety issues aside, the question that you probably now have in mind is: how to handle arrays elegantly? First, how do we load a single value out of an array? With the Lua API, we need to load the array with lua_getglobal, push the key, load the value, get the value, and then clean the stack.

pub struct LuaTable<’a> {
lua: &’a mut Lua,
}
impl Lua {
pub fn load_array(&mut self, name: &str) -> LuaTable {
lua_getglobal(self.context, name);
assert!(lua_istable(self.context, -1));
LuaTable { lua: self }
}
}
impl<’a> LuaTable<’a> {
fn get_number(&mut self, key: i32) {
lua_pushnumber(self.lua.context, key);
let value = lua_gettable(self.lua.context, -2);
lua_pop(self.lua.context, 1);
}
}
impl<’a> Drop for LuaTable<’a> {
fn drop(&mut self) {
lua_pop(self.lua.context, 1);
}
}
let mut table_access = lua.load_array(“a”);
let value = table_access.get_number(1);
drop(table_access);

Being more generic

Before going forward, we need to make some changes to our wrapper. Having functions named get_global_number, load_array or get_number is not very elegant. Instead we’re going to write a trait named Read that represents types that can be loaded from a Lua environment.

pub trait Read<’a> {
fn read(lua: &’a mut Lua, index: i32) -> Result<Self, ReadError>;
}
impl<’a> Read<’a> for i32 {
fn read(lua: &’a mut Lua, index: i32) -> Result<i32, ReadError> {
if lua_isnumber(lua.context, index) {
let value = lua_tonumber(lua.context, index);
lua_pop(lua.context, 1);
Ok(value)
} else {
lua_pop(lua.context, 1);
Err(ReadError::WrongType)
}
}
}
impl<’a> Read<’a> for LuaTable<’a> {
fn read(lua: &’a mut Lua, index: i32)
-> Result<LuaTable<’a>, ReadError>
{
if lua_istable(lua, index) {
Ok(LuaTable { lua: lua })
} else {
lua_pop(lua.context, 1);
Err(ReadError::WrongType)
}
}
}
impl<’a> Read<’a> for i8 { … }
impl<’a> Read<’a> for i16 { … }
impl<’a> Read<’a> for String { … }
impl<’a> Read<’a> for bool { … }
impl Lua {
pub fn get<T>(&mut self, name: &str) -> Result<T, ReadError>
where T: Read
{
lua_getglobal(self.context, name);
Read::read(self.context, -1)
}
}
let a: i32 = lua.get("a");
let b: LuaTable = lua.get("b");

Arrays over arrays

But wait… this works for the get function of the Lua struct, but our LuaTable struct also needs a get function.

pub trait AsMutLua {
unsafe fn as_mut_lua(&mut self) -> *mut lua_State;
}
impl<'a> AsMutLua for &'a mut Lua {
unsafe fn as_mut_lua(&mut self) -> *mut lua_State {
self.context
}
}
pub trait Read<T> where T: AsMutLua {
fn read(lua: T, index: i32) -> Result<Self, ReadError>;
}
impl Lua {
pub fn get<’a, T>(&’a mut self, name: &str)
-> Result<T, ReadError>
where T: Read<&’a mut Lua>
{
let ctxt = self.context;
lua_getglobal(ctxt, name);
Read::read(self, -1)
}
}
pub struct LuaTable<T> where T: AsMutLua {
lua: T,
}
impl<'a, T> AsMutLua for &'a mut LuaTable<T> where T: AsMutLua {
unsafe fn as_mut_lua(&mut self) -> *mut lua_State {
self.lua.as_mut_lua()
}
}
impl<T> LuaTable<T> where T: AsMutLua {
pub fn get<’a, U>(&’a mut self, key: i32)
-> Result<U, ReadError>
where T: Read<&’a mut LuaTable<T>>
{
let ctxt = self.lua.as_mut_lua();
lua_pushnumber(ctxt, key);
lua_gettable(ctxt, -2);
Read::read(self, -1)
}
}
impl<T> Drop for LuaTable<T> where T: AsMutLua {
fn drop(&mut self) {
lua_pop(self.lua.as_mut_lua(), 1);
}
}

Push guard

But wait, there’s another problem with this whole design. When iterating over a table for example, we want to read the key but not pop it, despite what our Read trait does. In some situations we want to read-and-pop, and in other situations we only want to read.

pub struct PushGuard<T> where T: AsMutLua {
lua: T,
size: i32,
}
impl<T> Drop for PushGuard<T> where T: AsMutLua {
fn drop(&mut self) {
if self.size != 0 {
lua_pop(self.lua.as_mut_lua(), self.size);
}
}
}
impl<T> AsMutLua for PushGuard<T> where T: AsMutLua {
fn as_mut_lua(&mut self) -> *mut lua_State {
self.lua.as_mut_lua()
}
}
impl Lua {
pub fn get<’a, T>(&’a mut self, name: &str)
-> Result<T, ReadError>
where T: Read<PushGuard<&’a mut Lua>>
{
let ctxt = self.context;
lua_getglobal(ctxt, name);
let guard = PushGuard { lua: self, size: 1 };
Read::read(guard, -1)
}
}
  1. We load the value that we want to examine on the stack.
  2. We create a PushGuard that corresponds to this value.
  3. We check that the value really is a table and build a LuaTable.
  4. Let’s say that we want to examine a sub-table. We load this sub-table on the stack over the outer table.
  5. We create a PushGuard that corresponds to this sub-table.
  6. We check that the sub-table really is a table a build a LuaTable.
  7. This LuaTable is destroyed. As it had ownership of the PushGuard, this PushGuard is destroyed too and the subtable is poped.
  8. The outer LuaTable is destroyed too. Again, the PushGuard is destroyed and the table is poped. The stack is clean again and the Lua is unborrowed.

Conclusion

With a clever usage of traits and generics, the Rust programming language allows you to write an abstraction over Lua that is almost free and remains safe at the same time, thanks to RAII.

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store