582 lines
12 KiB
D
582 lines
12 KiB
D
module luad.state;
|
|
|
|
import std.array, std.range;
|
|
|
|
import std.string : toStringz;
|
|
import std.typecons : isTuple;
|
|
|
|
import luad.c.all;
|
|
import luad.stack;
|
|
import luad.conversions.classes;
|
|
|
|
import luad.base, luad.table, luad.lfunction, luad.dynamic, luad.error;
|
|
|
|
/// Specify error handling scheme for $(MREF LuaState.doString) and $(MREF LuaState.doFile).
|
|
enum LuaErrorHandler
|
|
{
|
|
None, /// No extra error handler.
|
|
Traceback /// Append a stack traceback to the error message.
|
|
}
|
|
|
|
/**
|
|
* Represents a Lua state instance.
|
|
*/
|
|
class LuaState
|
|
{
|
|
private:
|
|
lua_State* L; // underlying state
|
|
LuaTable _G, _R; // global and registry tables
|
|
LuaFunction traceback; // debug.traceback, set in openLibs()
|
|
bool owner = false; // whether or not to close the underlying state in the finalizer
|
|
|
|
public:
|
|
/**
|
|
* Create a new, empty Lua state. The standard library is not loaded.
|
|
*
|
|
* If an uncaught error for any operation on this state
|
|
* causes a Lua panic for the underlying state,
|
|
* an exception of type $(DPREF error, LuaErrorException) is thrown.
|
|
*
|
|
* See_Also: $(MREF LuaState.openLibs)
|
|
*/
|
|
this()
|
|
{
|
|
lua_State* L = luaL_newstate();
|
|
owner = true;
|
|
|
|
extern(C) static int panic(lua_State* L)
|
|
{
|
|
size_t len;
|
|
const(char)* cMessage = lua_tolstring(L, -1, &len);
|
|
string message = cMessage[0 .. len].idup;
|
|
|
|
lua_pop(L, 1);
|
|
|
|
throw new LuaErrorException(message);
|
|
}
|
|
|
|
lua_atpanic(L, &panic);
|
|
|
|
this(L);
|
|
}
|
|
|
|
/**
|
|
* Create a D wrapper for an existing Lua state.
|
|
*
|
|
* The new $(D LuaState) object does not assume ownership of the state.
|
|
* Params:
|
|
* L = state to wrap
|
|
* Note:
|
|
* The panic function is not changed - a Lua panic will not throw a D exception!
|
|
* See_Also:
|
|
$(MREF LuaState.setPanicHandler)
|
|
*/
|
|
this(lua_State* L)
|
|
{
|
|
this.L = L;
|
|
_G = LuaTable(L, LUA_GLOBALSINDEX);
|
|
_R = LuaTable(L, LUA_REGISTRYINDEX);
|
|
|
|
lua_pushlightuserdata(L, cast(void*)this);
|
|
lua_setfield(L, LUA_REGISTRYINDEX, "__dstate");
|
|
}
|
|
|
|
~this()
|
|
{
|
|
if(owner)
|
|
{
|
|
_R.release();
|
|
_G.release();
|
|
traceback.release();
|
|
lua_close(L);
|
|
}
|
|
else // Unregister state
|
|
{
|
|
lua_pushnil(L);
|
|
lua_setfield(L, LUA_REGISTRYINDEX, "__dstate");
|
|
}
|
|
}
|
|
|
|
/// The underlying $(D lua_State) pointer for interfacing with C.
|
|
@property lua_State* state() nothrow pure @safe
|
|
{
|
|
return L;
|
|
}
|
|
|
|
/**
|
|
* Get the $(D LuaState) instance for a Lua state.
|
|
* Params:
|
|
* L = Lua state
|
|
* Returns:
|
|
* $(D LuaState) for the given $(D lua_State*), or $(D null) if a $(D LuaState) is not currently attached to the state
|
|
*/
|
|
static LuaState fromPointer(lua_State* L) @trusted
|
|
{
|
|
lua_getfield(L, LUA_REGISTRYINDEX, "__dstate");
|
|
auto lua = cast(LuaState)lua_touserdata(L, -1);
|
|
lua_pop(L, 1);
|
|
return lua;
|
|
}
|
|
|
|
/// Open the standard library.
|
|
void openLibs() @trusted
|
|
{
|
|
luaL_openlibs(L);
|
|
traceback = _G.get!LuaFunction("debug", "traceback");
|
|
}
|
|
|
|
/// The global table for this instance.
|
|
@property LuaTable globals() @trusted
|
|
{
|
|
return _G;
|
|
}
|
|
|
|
/// The _registry table for this instance.
|
|
@property LuaTable registry() @trusted
|
|
{
|
|
return _R;
|
|
}
|
|
|
|
/**
|
|
* Set a new panic handler.
|
|
* Params:
|
|
* onPanic = new panic handler
|
|
* Examples:
|
|
* ----------------------
|
|
auto L = luaL_newstate(); // found in luad.c.all
|
|
auto lua = new LuaState(L);
|
|
|
|
static void panic(LuaState lua, in char[] error)
|
|
{
|
|
throw new LuaErrorException(error.idup);
|
|
}
|
|
|
|
lua.setPanicHandler(&panic);
|
|
* ----------------------
|
|
*/
|
|
void setPanicHandler(void function(LuaState, in char[]) onPanic) @trusted
|
|
{
|
|
extern(C) static int panic(lua_State* L)
|
|
{
|
|
size_t len;
|
|
const(char)* message = lua_tolstring(L, -1, &len);
|
|
auto error = message[0 .. len];
|
|
|
|
lua_getfield(L, LUA_REGISTRYINDEX, "__dpanic");
|
|
auto callback = cast(void function(LuaState, in char[]))lua_touserdata(L, -1);
|
|
assert(callback);
|
|
|
|
scope(exit) lua_pop(L, 2);
|
|
|
|
callback(LuaState.fromPointer(L), error);
|
|
return 0;
|
|
}
|
|
|
|
lua_pushlightuserdata(L, onPanic);
|
|
lua_setfield(L, LUA_REGISTRYINDEX, "__dpanic");
|
|
|
|
lua_atpanic(L, &panic);
|
|
}
|
|
|
|
/*
|
|
* push debug.traceback error handler to the stack
|
|
*/
|
|
private void pushErrorHandler()
|
|
{
|
|
if(traceback.isNil)
|
|
throw new Exception("LuaErrorHandler.Traceback requires openLibs()");
|
|
traceback.push();
|
|
}
|
|
|
|
/*
|
|
* a variant of luaL_do(string|file) with advanced error handling
|
|
*/
|
|
private void doChunk(alias loader)(in char[] s, LuaErrorHandler handler)
|
|
{
|
|
if(handler == LuaErrorHandler.Traceback)
|
|
pushErrorHandler();
|
|
|
|
if(loader(L, toStringz(s)) || lua_pcall(L, 0, LUA_MULTRET, handler == LuaErrorHandler.Traceback? -2 : 0))
|
|
lua_error(L);
|
|
|
|
if(handler == LuaErrorHandler.Traceback)
|
|
lua_remove(L, 1);
|
|
}
|
|
|
|
/**
|
|
* Compile a string of Lua _code.
|
|
* Params:
|
|
* code = _code to compile
|
|
* Returns:
|
|
* Loaded _code as a function.
|
|
*/
|
|
LuaFunction loadString(in char[] code) @trusted
|
|
{
|
|
if(luaL_loadstring(L, toStringz(code)) != 0)
|
|
lua_error(L);
|
|
|
|
return popValue!LuaFunction(L);
|
|
}
|
|
|
|
/**
|
|
* Compile a file of Lua code.
|
|
* Params:
|
|
* path = _path to file
|
|
* Returns:
|
|
* Loaded code as a function.
|
|
*/
|
|
LuaFunction loadFile(in char[] path) @trusted
|
|
{
|
|
if(luaL_loadfile(L, toStringz(path)) != 0)
|
|
lua_error(L);
|
|
|
|
return popValue!LuaFunction(L);
|
|
}
|
|
|
|
/**
|
|
* Execute a string of Lua _code.
|
|
* Params:
|
|
* code = _code to run
|
|
* handler = error handling scheme
|
|
* Returns:
|
|
* Any _code return values
|
|
* See_Also:
|
|
* $(MREF LuaErrorHandler)
|
|
*/
|
|
LuaObject[] doString(in char[] code, LuaErrorHandler handler = LuaErrorHandler.None) @trusted
|
|
{
|
|
auto top = lua_gettop(L);
|
|
|
|
doChunk!(luaL_loadstring)(code, handler);
|
|
|
|
auto nret = lua_gettop(L) - top;
|
|
|
|
return popStack(L, nret);
|
|
}
|
|
|
|
/**
|
|
* Execute a file of Lua code.
|
|
* Params:
|
|
* path = _path to file
|
|
* handler = error handling scheme
|
|
* Returns:
|
|
* Any script return values
|
|
* See_Also:
|
|
* $(MREF LuaErrorHandler)
|
|
*/
|
|
LuaObject[] doFile(in char[] path, LuaErrorHandler handler = LuaErrorHandler.None) @trusted
|
|
{
|
|
auto top = lua_gettop(L);
|
|
|
|
doChunk!(luaL_loadfile)(path, handler);
|
|
|
|
auto nret = lua_gettop(L) - top;
|
|
|
|
return popStack(L, nret);
|
|
}
|
|
|
|
/**
|
|
* Create a new, empty table.
|
|
* Returns:
|
|
* The new table
|
|
*/
|
|
LuaTable newTable()() @trusted
|
|
{
|
|
return newTable(0, 0);
|
|
}
|
|
|
|
/**
|
|
* Create a new, empty table with pre-allocated space for members.
|
|
* Params:
|
|
* narr = number of pre-allocated array slots
|
|
* nrec = number of pre-allocated non-array slots
|
|
* Returns:
|
|
* The new table
|
|
*/
|
|
LuaTable newTable()(uint narr, uint nrec) @trusted
|
|
{
|
|
lua_createtable(L, narr, nrec);
|
|
return popValue!LuaTable(L);
|
|
}
|
|
|
|
/**
|
|
* Create a new table from an $(D InputRange).
|
|
* If the element type of the range is $(D Tuple!(T, U)),
|
|
* then each element makes up a key-value pair, where
|
|
* $(D T) is the key and $(D U) is the value of the pair.
|
|
* For any other element type $(D T), a table with sequential numeric
|
|
* keys is created (an array).
|
|
* Params:
|
|
* range = $(D InputRange) of key-value pairs or elements
|
|
* Returns:
|
|
* The new table
|
|
*/
|
|
LuaTable newTable(Range)(Range range) @trusted if(isInputRange!Range)
|
|
{
|
|
alias ElementType!Range Elem;
|
|
|
|
static if(hasLength!Range)
|
|
{
|
|
immutable numElements = range.length;
|
|
assert(numElements < int.max, "lua_createtable only supports int.max many elements");
|
|
}
|
|
else
|
|
{
|
|
immutable numElements = 0;
|
|
}
|
|
|
|
static if(isTuple!Elem) // Key-value pairs
|
|
{
|
|
static assert(range.front.length == 2, "key-value tuple must have exactly 2 values.");
|
|
|
|
lua_createtable(L, 0, cast(int)numElements);
|
|
|
|
foreach(pair; range)
|
|
{
|
|
pushValue(L, pair[0]);
|
|
pushValue(L, pair[1]);
|
|
lua_rawset(L, -3);
|
|
}
|
|
}
|
|
else // Sequential table
|
|
{
|
|
lua_createtable(L, cast(int)numElements, 0);
|
|
|
|
int i = 1;
|
|
|
|
foreach(value; range)
|
|
{
|
|
pushValue(L, value);
|
|
lua_rawseti(L, -2, i);
|
|
++i;
|
|
}
|
|
}
|
|
|
|
return popValue!LuaTable(L);
|
|
}
|
|
|
|
/**
|
|
* Wrap a D value in a Lua reference.
|
|
*
|
|
* Note that using this method is only necessary in certain situations,
|
|
* such as when you want to act on the reference before fully exposing it to Lua.
|
|
* Params:
|
|
* T = type of reference. Must be $(D LuaObject), $(D LuaTable), $(D LuaFunction) or $(D LuaDynamic).
|
|
* Defaults to $(D LuaObject).
|
|
* value = D value to _wrap
|
|
* Returns:
|
|
* A Lua reference to value
|
|
*/
|
|
T wrap(T = LuaObject, U)(U value) @trusted if(is(T : LuaObject) || is(T == LuaDynamic))
|
|
{
|
|
pushValue(L, value);
|
|
return popValue!T(L);
|
|
}
|
|
|
|
/**
|
|
* Register a D class or struct with Lua.
|
|
*
|
|
* This method exposes a type's constructors and static interface to Lua.
|
|
* Params:
|
|
* T = class or struct to register
|
|
* Returns:
|
|
* Reference to the registered type in Lua
|
|
*/
|
|
LuaObject registerType(T)() @trusted
|
|
{
|
|
pushStaticTypeInterface!T(L);
|
|
return popValue!LuaObject(L);
|
|
}
|
|
|
|
/**
|
|
* Same as calling $(D globals._get) with the same arguments.
|
|
* See_Also:
|
|
* $(DPREF table, LuaTable._get)
|
|
*/
|
|
T get(T, U...)(U args)
|
|
{
|
|
return globals.get!T(args);
|
|
}
|
|
|
|
/**
|
|
* Same as calling $(D globals.get!LuaObject) with the same arguments.
|
|
* See_Also:
|
|
* $(DPREF table, LuaTable._opIndex)
|
|
*/
|
|
LuaObject opIndex(T...)(T args)
|
|
{
|
|
return globals.get!LuaObject(args);
|
|
}
|
|
|
|
/**
|
|
* Same as calling $(D globals._set) with the same arguments.
|
|
* See_Also:
|
|
* $(DPREF table, LuaTable._set)
|
|
*/
|
|
void set(T, U)(T key, U value)
|
|
{
|
|
globals.set(key, value);
|
|
}
|
|
|
|
/**
|
|
* Same as calling $(D globals._opIndexAssign) with the same arguments.
|
|
* See_Also:
|
|
* $(DPREF table, LuaTable._opIndexAssign)
|
|
*/
|
|
void opIndexAssign(T, U...)(T value, U args)
|
|
{
|
|
globals()[args] = value;
|
|
}
|
|
}
|
|
|
|
version(unittest)
|
|
{
|
|
import luad.testing;
|
|
import std.string : splitLines;
|
|
private LuaState lua;
|
|
}
|
|
|
|
unittest
|
|
{
|
|
lua = new LuaState;
|
|
assert(LuaState.fromPointer(lua.state) == lua);
|
|
|
|
lua.openLibs();
|
|
|
|
//default panic handler
|
|
try
|
|
{
|
|
lua.doString(`error("Hello, D!")`, LuaErrorHandler.Traceback);
|
|
assert(false);
|
|
}
|
|
catch(LuaErrorException e)
|
|
{
|
|
auto lines = splitLines(e.msg);
|
|
assert(lines.length > 1);
|
|
assert(lines[0] == `[string "error("Hello, D!")"]:1: Hello, D!`);
|
|
}
|
|
|
|
lua.set("success", false);
|
|
assert(!lua.get!bool("success"));
|
|
|
|
lua.doString(`success = true`);
|
|
assert(lua.get!bool("success"));
|
|
|
|
auto foo = lua.wrap!LuaTable([1, 2, 3]);
|
|
foo[4] = "test"; // Lua tables start at 1
|
|
lua["foo"] = foo;
|
|
unittest_lua(lua.state, `
|
|
for i = 1, 3 do
|
|
assert(foo[i] == i)
|
|
end
|
|
assert(foo[4] == "test")
|
|
`);
|
|
|
|
LuaFunction multipleReturns = lua.loadString(`return 1, "two", 3`);
|
|
LuaObject[] results = multipleReturns();
|
|
|
|
assert(results.length == 3);
|
|
assert(results[0].type == LuaType.Number);
|
|
assert(results[1].type == LuaType.String);
|
|
assert(results[2].type == LuaType.Number);
|
|
}
|
|
|
|
unittest // LuaState.newTable(range)
|
|
{
|
|
import std.algorithm;
|
|
|
|
auto input = [1, 2, 3];
|
|
|
|
lua["tab"] = lua.newTable(input);
|
|
|
|
unittest_lua(lua.state, `
|
|
assert(#tab == 3)
|
|
for i = 1, 3 do
|
|
assert(tab[i] == i)
|
|
end
|
|
`);
|
|
|
|
lua["tab"] = lua.newTable(filter!(i => i == 2)(input));
|
|
|
|
unittest_lua(lua.state, `
|
|
assert(#tab == 1)
|
|
assert(tab[1] == 2)
|
|
`);
|
|
|
|
auto keys = iota(7, 14);
|
|
auto values = repeat(42);
|
|
|
|
lua["tab"] = lua.newTable(zip(keys, values));
|
|
|
|
unittest_lua(lua.state, `
|
|
assert(not tab[1])
|
|
assert(not tab[6])
|
|
for i = 7, 13 do
|
|
assert(tab[i] == 42)
|
|
end
|
|
assert(not tab[14])
|
|
`);
|
|
}
|
|
|
|
unittest
|
|
{
|
|
static class Test
|
|
{
|
|
private:
|
|
/+ Not working as of 2.062
|
|
static int priv;
|
|
static void priv_fun() {}
|
|
+/
|
|
|
|
public:
|
|
static int pub = 123;
|
|
|
|
static string foo() { return "bar"; }
|
|
|
|
this(int i)
|
|
{
|
|
_bar = i;
|
|
}
|
|
|
|
int bar(){ return _bar; }
|
|
int _bar;
|
|
}
|
|
|
|
lua["Test"] = lua.registerType!Test();
|
|
|
|
unittest_lua(lua.state, `
|
|
assert(type(Test) == "table")
|
|
-- TODO: private members are currently pushed too...
|
|
--assert(Test.priv == nil)
|
|
--assert(Test.priv_fun == nil)
|
|
assert(Test._foo == nil)
|
|
assert(Test._bar == nil)
|
|
|
|
local test = Test(42)
|
|
assert(test:bar() == 42)
|
|
|
|
assert(Test.pub == 123)
|
|
assert(Test.foo() == "bar")
|
|
`);
|
|
}
|
|
|
|
unittest
|
|
{
|
|
// setPanicHandler, keep this test last
|
|
static void panic(LuaState lua, in char[] error)
|
|
{
|
|
throw new Exception("hijacked error!");
|
|
}
|
|
|
|
lua.setPanicHandler(&panic);
|
|
|
|
try
|
|
{
|
|
lua.doString(`error("test")`);
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
assert(e.msg == "hijacked error!");
|
|
}
|
|
}
|