Skip to content
JakobOvrum edited this page Feb 27, 2012 · 11 revisions

LuaD Tutorial

(this tutorial is a work in progress)

LuaD is a bridge between the D and Lua programming languages. Unlike many other libraries built on the Lua C API, LuaD doesn't expose the Lua stack - instead, it has wrappers for references to Lua objects, and supports seamlessly and directly converting any D type into a Lua type and vice versa. This makes it very easy to use and encourages a much less error-prone style of programming, as well as boosting productivity by a substantial order. Due to D's powerful generic programming capabilities, performance remains the same as the equivalent using the C API.

LuaD is designed to be easy to use regardless of your knowledge of the Lua stack and the Lua C API - no usage of LuaD assumes any knowledge of the C API, and neither does this tutorial. But LuaD isn't just easy to use. Even if you are familiar with the C API, LuaD can drastically increase productivity and reduce bugs in your Lua handling code.

Typically, only code specifically interacting with a Lua state (see below) needs to use the LuaD API - the rest can be written as regular D code and the glue will be handled by LuaD. Sometimes you want to interact with Lua in ways that aren't clearly expressible in D; types wrapping Lua references are provided for these cases.

The Lua State

From the Lua manual:

Opaque structure that keeps the whole state of a Lua interpreter. The Lua library is fully reentrant: it has no global variables. All information about a state is kept in this structure.

In LuaD, a Lua state is provided by the LuaState class. Using Lua in a program always begins by creating a Lua state:

auto lua = new LuaState;

LuaState is the entry point to all LuaD functionality. It exposes the two basic windows into Lua: the global table and the registry table. It also exposes the ability to run Lua code or Lua scripts in the current instance. To use the LuaD API, import luad.all. Although not very interesting, a complete program could look like this:

import luad.all;

void main()
{
    auto lua = new LuaState;
}

The global table is used for looking up all non-local variables in Lua code. We can fill the global table with the functionality of the Lua standard library by calling the openLibs method. The print function, which writes to the standard output pipe, is one of the introduced globals. Let's expand the above example:

lua.openLibs();
lua.doString(`print("hello, world!")`);

As seen above, doString lets us run Lua code in our instance. Any errors which may have occurred during execution which weren't handled by Lua are thrown as a D exception.

Introducing new entries in a table is done by using the index assign operator, reminiscent of doing the same operation in Lua. The global table is available as the globals property of the LuaState:

lua.globals["message"] = "hello, world!";
lua.doString(`print(message)`);

The globals and registry members of LuaState are of type LuaTable. Through the globals table, we can read and write global variables and perform various other table-specific operations. The get, set, opIndex and opIndexAssign methods of LuaState are forwarded to globals for convenience; the next example is equivalent of the previous one:

lua["message"] = "hello, world!";
lua.doString(`print(message)`);

Type Conversions

In the previous section, string was the type of both the key and value in the call to the index assign operator. LuaD supports any D type for operations like this, some shown below:

lua[1] = true;
lua["test"] = 1.5;
lua["globals"] = lua.globals;
lua["array"] = [2];
lua.doString(`
    -- note that _G is a standard loopback reference to the global table
    assert(test == 1.5)
    assert(globals == _G)
    assert(_G[1] == true)
    assert(array[1] == 2)
`);

Getting Lua values into D is just as easy:

lua.doString(`
    test = 1.5
    _G[1] = true
    array = {2}
`);

assert(lua.get!double("test") == 1.5);
assert(lua.get!bool(1));
assert(lua.get!(int[])("array")[0] == 2);

The get function of LuaState actually just forwards to globals.get. With most types, it returns copies of the data. Hence, if we want to operate on a value and have the changes reflected in Lua, the code would look like this:

lua.doString(`foo = 1`);

int foo = lua.get!int("foo");
++foo;

lua.doString(`
    assert(foo == 1) -- 'foo' is still 1 on this side
`);

lua["foo"] = foo;

lua.doString(`
    assert(foo == 2) -- updated
`);

Some Lua types are not as easily expressible in D. For example, how do we get a large table like _G into D from Lua? We sure don't want to make a copy of it, and we'd like to be able to mutate it and see the effects immediately in Lua. This leads us back to the LuaTable reference wrapper type:

auto t = lua.get!LuaTable("_G");
assert(t == lua.globals);

// Introduce a new global
lua.doString(`foo = "bar"`);

auto foo = t.get!string("foo");
assert(foo == "bar"); // 't' is a reference to the global table, not a copy!

Wchar and dchar

You know how I said earlier that LuaD supports any D type? I lied. Exceptions are made for the types wchar and dchar and derivatives such as their array equivalents (e.g. wstring, dstring): they are explicitly rejected at compile-time. All Lua strings are 8-bit with no assumptions made about their encoding. This makes them a good fit for UTF-8 strings, represented in D by arrays of char (e.g. string). However, wchar and dchar are 16 and 32 bits respectively, making conversion to a Lua type ambiguous. The ambiguity must be solved by the programmer, for example by choosing to transcode to UTF-8 first:

wstring str = "foobar";
lua["str"] = to!string(str);

The rationale is that transcoding is an expensive operation, both in terms of computational complexity and actual time, which shouldn't be silently hidden away in the LuaD library. Also, depending on what the strings are given to Lua for, it may make more sense to push them as tables of numbers (representing code points) or as binary data; there is no clear default solution.

However, apart from these two types, any type combination is allowed. We will take a look at these various types later in the tutorial.

Functions

Things become more interesting when you start using more complex types, like functions. As an exercise, let's provide our own print function instead of using the Lua standard library. Lua's print takes a variable amount of arguments and writes them all to the standard output pipe separated by the tab character:

import std.stdio;
import luad.all;

void print(const(char)[][] params...)
{
    if(params.length > 0)
    {
        foreach(param; params[0..$-1])
            write(param, '\t');
        write(params[$-1]);
    }
    writeln();
}

void main()
{
    auto lua = new LuaState;
    lua["print"] = &print;
    lua.doString(`print("hello", "world!")`);
}

Our print function works as well in D as it does in Lua. One problem remains - Lua's print function actually accepts Lua values of any type, not just strings:

print(2)

Output:

luad.error.LuaErrorException@..\luad\state.d(52): [string "print(2)"]:1: bad argument #1 to 'print' (string expected, got number)

In D, variadic templates are typically used for variadic functions with differing parameter types (e.g. writeln). However, templates are instantiated at compile-time, making them inaccessible from Lua. We could use a specific template instance as our print function:

lua["print"] = &writeln!int;
lua.doString(`
    print(2) -- OK
    print(true) -- Error: bad argument #1 to 'print' (number expected, got boolean)
`);

But as we can see, this does not help us with our goal. In comes the LuaD reference wrappers:

void print(LuaObject[] params...)
{
    if(params.length > 0)
    {
        foreach(param; params[0..$-1])
            write(param.toString(), '\t');
        write(params[$-1].toString());
    }
    writeln();
}

void main()
{
    auto lua = new LuaState;
    lua["print"] = &print;
    lua.doString(`
        print("hello", "world!") -- OK
        print(true, 2) -- OK
    `);
}

LuaObject represents a reference to any Lua value, including nil. It provides the bare minimum functionality that is applicable to all Lua types; such as the toString method, which creates a text representation of the referenced value exactly like Lua's tostring function does. Our print function is now identical in functionality to the one provided in Lua's standard library.

What about the other way around, using Lua functions in D? Let's say we wanted to use the Lua standard library function os.execute. Here's the documentation on it from the Lua manual:

os.execute ([command])

This function is equivalent to the C function system. It passes command to be executed by an operating system shell. It returns a status code, which is system-dependent. If command is absent, then it returns nonzero if a shell is available and zero otherwise.

The most straight-forward approach is to fetch it as a D delegate:

auto execute = lua.get!(int delegate(in char[] command))("os", "execute");

int statusCode = execute("echo hello, world!");

writeln(statusCode == 0? "success!" : "failed!");

This also shows that you can use get to conveniently retrieve values nested deeper than one level. This is very important for efficiency too; getting each LuaTable in the link manually results in an unnecessary amount of boilerplate dealing with creating and releasing table references.

You can also use the reference wrapper type LuaFunction. This type is convenient, but also indispensable in various situations, such as when the behavior of the Lua function depends on the types of the arguments passed:

lua.doString(`
    function func(a)
        if type(a) == "number" then
            return a * 2
        elseif type(a) == "string" then
            return "hello, " .. a
        end

        error("unsupported argument type!")
    end
`);

auto func = lua.get!LuaFunction("func");

int integerResult = func.call!int(2);
assert(integerResult == 4);

string stringResult = func.call!string("world!");
assert(stringResult == "hello, world!");

We can also take a look at what happens if we let our Lua function raise an error, such as with this code:

func.call(true);
luad.error.LuaErrorException@..\luad\state.d(51): [string "..."]:9: unsupported argument type!

With the default constructor of LuaState, Lua errors are converted to D exceptions when crossing into D. You can catch LuaErrorException like you would with any other exception.

TODO: finish tutorial

Clone this wiki locally