Files
lunch-games/source/luad/conversions/functions.d

671 lines
14 KiB
D
Raw Normal View History

2025-04-29 01:59:15 +02:00
/**
Internal module for pushing and getting _functions and delegates.
LuaD allows for pushing of all D function or delegate types with return type and parameter types compatible with LuaD (see $(DPMODULE stack)).
For a fixed number of multiple return values, return a $(STDREF typecons,Tuple) or a static array. For a variable number of return values, return $(MREF LuaVariableReturn).
As a special case for $(D const(char)[]) parameter types in _functions pushed to Lua, no copy of the string is made when called; take care not to escape such references, they are effectively $(D scope) parameters.
When a copy is desired, use $(D char[]) or $(D string), or $(D dup) or $(D idup) the string manually.
If a function with the $(D lua_CFunction) signature is encountered, it is pushed directly with no inserted conversions or overhead.
Typesafe varargs is supported when pushing _functions to Lua, but as of DMD 2.054, compiler bugs prevent getting delegates with varargs from Lua (use $(DPREF lfunction,LuaFunction) instead).
*/
module luad.conversions.functions;
import core.memory;
import std.range;
import std.string : toStringz;
import std.traits;
import std.typetuple;
import luad.c.all;
import luad.stack;
private void argsError(lua_State* L, int nargs, ptrdiff_t expected)
{
lua_Debug debugInfo;
lua_getstack(L, 0, &debugInfo);
lua_getinfo(L, "n", &debugInfo);
luaL_error(L, "call to %s '%s': got %d arguments, expected %d",
debugInfo.namewhat, debugInfo.name, nargs, expected);
}
template StripHeadQual(T : const(T*))
{
alias const(T)* StripHeadQual;
}
template StripHeadQual(T : const(T[]))
{
alias const(T)[] StripHeadQual;
}
template StripHeadQual(T : immutable(T*))
{
alias immutable(T)* StripHeadQual;
}
template StripHeadQual(T : immutable(T[]))
{
alias immutable(T)[] StripHeadQual;
}
template StripHeadQual(T : T[])
{
alias T[] StripHeadQual;
}
template StripHeadQual(T : T*)
{
alias T* StripHeadQual;
}
template StripHeadQual(T : T[N], size_t N)
{
alias T[N] StripHeadQual;
}
template StripHeadQual(T)
{
alias T StripHeadQual;
}
template FillableParameterTypeTuple(T)
{
alias staticMap!(StripHeadQual, ParameterTypeTuple!T) FillableParameterTypeTuple;
}
template BindableReturnType(T)
{
alias StripHeadQual!(ReturnType!T) BindableReturnType;
}
//Call with or without return value, propagating Exceptions as Lua errors.
//This should rather be throwing a userdata with __tostring and a reference to
//the thrown exception, as it is now, everything but the error type and message is lost.
int callFunction(T)(lua_State* L, T func, ParameterTypeTuple!T args)
if(!is(BindableReturnType!T == const) &&
!is(BindableReturnType!T == immutable))
{
alias BindableReturnType!T RetType;
enum hasReturnValue = !is(RetType == void);
static if(hasReturnValue)
RetType ret;
try
{
static if(hasReturnValue)
ret = func(args);
else
func(args);
}
catch(Exception e)
{
luaL_error(L, "%s", toStringz(e.toString()));
}
static if(hasReturnValue)
return pushReturnValues(L, ret);
else
return 0;
}
// Ditto, but wrap the try-catch in a nested function because the return value's
// declaration and initialization cannot be separated.
int callFunction(T)(lua_State* L, T func, ParameterTypeTuple!T args)
if(is(BindableReturnType!T == const) ||
is(BindableReturnType!T == immutable))
{
auto ref call()
{
try
return func(args);
catch(Exception e)
luaL_error(L, "%s", e.toString().toStringz());
}
return pushReturnValues(L, call());
}
private:
// TODO: right now, virtual functions on specialized classes can be called with base classes as 'self', not safe!
extern(C) int methodWrapper(T, Class, bool virtual)(lua_State* L)
{
alias ParameterTypeTuple!T Args;
static assert ((variadicFunctionStyle!T != Variadic.d && variadicFunctionStyle!T != Variadic.c),
"Non-typesafe variadic functions are not supported.");
//Check arguments
int top = lua_gettop(L);
static if (variadicFunctionStyle!T == Variadic.typesafe)
enum requiredArgs = Args.length;
else
enum requiredArgs = Args.length + 1;
if(top < requiredArgs)
argsError(L, top, requiredArgs);
Class self = *cast(Class*)luaL_checkudata(L, 1, toStringz(Class.mangleof));
static if(virtual)
{
alias ReturnType!T function(Class, Args) VirtualWrapper;
VirtualWrapper func = cast(VirtualWrapper)lua_touserdata(L, lua_upvalueindex(1));
}
else
{
T func;
func.ptr = cast(void*)self;
func.funcptr = cast(typeof(func.funcptr))lua_touserdata(L, lua_upvalueindex(1));
}
//Assemble arguments
static if(virtual)
{
ParameterTypeTuple!VirtualWrapper allArgs;
allArgs[0] = self;
alias allArgs[1..$] args;
}
else
{
Args allArgs;
alias allArgs args;
}
foreach(i, Arg; Args)
args[i] = getArgument!(T, i)(L, i + 2);
return callFunction!(typeof(func))(L, func, allArgs);
}
extern(C) int functionWrapper(T)(lua_State* L)
{
alias FillableParameterTypeTuple!T Args;
static assert ((variadicFunctionStyle!T != Variadic.d && variadicFunctionStyle!T != Variadic.c),
"Non-typesafe variadic functions are not supported.");
//Check arguments
int top = lua_gettop(L);
static if (variadicFunctionStyle!T == Variadic.typesafe)
enum requiredArgs = Args.length - 1;
else
enum requiredArgs = Args.length;
if(top < requiredArgs)
argsError(L, top, requiredArgs);
//Get function
static if(isFunctionPointer!T)
T func = cast(T)lua_touserdata(L, lua_upvalueindex(1));
else
T func = *cast(T*)lua_touserdata(L, lua_upvalueindex(1));
//Assemble arguments
Args args;
foreach(i, Arg; Args)
args[i] = getArgument!(T, i)(L, i + 1);
return callFunction!T(L, func, args);
}
extern(C) int functionCleaner(lua_State* L)
{
GC.removeRoot(lua_touserdata(L, 1));
return 0;
}
public:
void pushFunction(T)(lua_State* L, T func) if (isSomeFunction!T)
{
static if(isFunctionPointer!T)
lua_pushlightuserdata(L, func);
else
{
T* udata = cast(T*)lua_newuserdata(L, T.sizeof);
*udata = func;
GC.addRoot(udata);
if(luaL_newmetatable(L, "__dcall") == 1)
{
lua_pushcfunction(L, &functionCleaner);
lua_setfield(L, -2, "__gc");
}
lua_setmetatable(L, -2);
}
lua_pushcclosure(L, &functionWrapper!T, 1);
}
// TODO: optimize for non-virtual functions
void pushMethod(Class, string member)(lua_State* L) if (isSomeFunction!(__traits(getMember, Class, member)))
{
alias typeof(mixin("&Class.init." ~ member)) T;
// Delay vtable lookup until the right time
static ReturnType!T virtualWrapper(Class self, ParameterTypeTuple!T args)
{
return mixin("self." ~ member)(args);
}
lua_pushlightuserdata(L, &virtualWrapper);
lua_pushcclosure(L, &methodWrapper!(T, Class, true), 1);
}
/**
* Currently this function allocates a reference in the registry that is never deleted,
* one for each call... see code comments
*/
T getFunction(T)(lua_State* L, int idx) if (is(T == delegate))
{
auto func = new class
{
int lref;
this()
{
lua_pushvalue(L, idx);
lref = luaL_ref(L, LUA_REGISTRYINDEX);
}
//Alright... how to fix this?
//The problem is that this object tends to be finalized after L is freed (by LuaState's destructor or otherwise).
//If you have a good solution to the problem of dangling references to a lua_State,
//please contact me :)
/+~this()
{
luaL_unref(L, LUA_REGISTRYINDEX, lref);
}+/
void push()
{
lua_rawgeti(L, LUA_REGISTRYINDEX, lref);
}
};
alias ReturnType!T RetType;
alias ParameterTypeTuple!T Args;
return delegate RetType(Args args)
{
func.push();
foreach(arg; args)
pushValue(L, arg);
return callWithRet!RetType(L, args.length);
};
}
/**
* Type for efficiently returning a variable number of return values
* from a function.
*
* Use $(D variableReturn) to instantiate it.
* Params:
* Range = any input range
*/
struct LuaVariableReturn(Range) if(isInputRange!Range)
{
alias WrappedType = Range; /// The type of the wrapped input range.
Range returnValues; /// The wrapped input range.
}
/**
* Create a LuaVariableReturn object for efficiently returning
* a variable number of values from a function.
* Params:
* returnValues = any input range
* Example:
-----------------------------
LuaVariableReturn!(uint[]) makeList(uint n)
{
uint[] list;
foreach(i; 1 .. n + 1)
{
list ~= i;
}
return variableReturn(list);
}
lua["makeList"] = &makeList;
lua.doString(`
local one, two, three, four = makeList(4)
assert(one == 1)
assert(two == 2)
assert(three == 3)
assert(four == 4)
`);
-----------------------------
*/
LuaVariableReturn!Range variableReturn(Range)(Range returnValues)
if(isInputRange!Range)
{
return typeof(return)(returnValues);
}
version(unittest)
{
import luad.testing;
import std.typecons;
private lua_State* L;
}
unittest
{
L = luaL_newstate();
luaL_openlibs(L);
//functions
static const(char)[] func(const(char)[] s)
{
return "Hello, " ~ s;
}
pushValue(L, &func);
assert(lua_isfunction(L, -1));
lua_setglobal(L, "sayHello");
unittest_lua(L, `
local ret = sayHello("foo")
local expect = "Hello, foo"
assert(ret == expect,
("sayHello return type - got '%s', expected '%s'"):format(ret, expect)
)
`);
static uint countSpaces(const(char)[] s)
{
uint n = 0;
foreach(dchar c; s)
if(c == ' ')
++n;
return n;
}
pushValue(L, &countSpaces);
assert(lua_isfunction(L, -1));
lua_setglobal(L, "countSpaces");
unittest_lua(L, `
assert(countSpaces("Hello there, world!") == 2)
`);
//delegates
double curry = 3.14 * 2;
double closure(double x)
{
return curry * x;
}
pushValue(L, &closure);
assert(lua_isfunction(L, -1));
lua_setglobal(L, "circle");
unittest_lua(L, `
assert(circle(2) == 3.14 * 4, "closure return type mismatch!")
`);
// Const parameters
static bool isEmpty(const(char[]) str) { return str.length == 0; }
static bool isEmpty2(in char[] str) { return str.length == 0; }
pushValue(L, &isEmpty);
lua_setglobal(L, "isEmpty");
pushValue(L, &isEmpty2);
lua_setglobal(L, "isEmpty2");
unittest_lua(L, `
assert(isEmpty(""))
assert(isEmpty2(""))
assert(not isEmpty("a"))
assert(not isEmpty2("a"))
`);
// Immutable parameters
static immutable(char[]) returnArg(immutable(char[]) str) { return str; }
pushValue(L, &returnArg);
lua_setglobal(L, "returnArg");
unittest_lua(L, `assert(returnArg("foo") == "foo")`);
}
version(unittest) import luad.base;
// multiple return values
unittest
{
// tuple returns
auto nameInfo = ["foo"];
auto ageInfo = [42];
alias Tuple!(string, "name", uint, "age") GetInfoResult;
GetInfoResult getInfo(int idx)
{
GetInfoResult result;
result.name = nameInfo[idx];
result.age = ageInfo[idx];
return result;
}
pushValue(L, &getInfo);
lua_setglobal(L, "getInfo");
unittest_lua(L, `
local name, age = getInfo(0)
assert(name == "foo")
assert(age == 42)
`);
// static array returns
static string[2] getName()
{
string[2] ret;
ret[0] = "Foo";
ret[1] = "Bar";
return ret;
}
pushValue(L, &getName);
lua_setglobal(L, "getName");
unittest_lua(L, `
local first, last = getName()
assert(first == "Foo")
assert(last == "Bar")
`);
// variable length returns
LuaVariableReturn!(uint[]) makeList(uint n)
{
uint[] list;
foreach(i; 1 .. n + 1)
{
list ~= i;
}
return variableReturn(list);
}
auto makeList2(uint n)
{
return variableReturn(iota(1, n + 1));
}
pushValue(L, &makeList);
lua_setglobal(L, "makeList");
pushValue(L, &makeList2);
lua_setglobal(L, "makeList2");
unittest_lua(L, `
for i, f in pairs{makeList, makeList2} do
local one, two, three, four = f(4)
assert(one == 1)
assert(two == 2)
assert(three == 3)
assert(four == 4)
end
`);
}
// Variadic function arguments
unittest
{
static string concat(const(char)[][] pieces...)
{
string result;
foreach(piece; pieces)
result ~= piece;
return result;
}
pushValue(L, &concat);
lua_setglobal(L, "concat");
unittest_lua(L, `
local whole = concat("he", "llo", ", ", "world!")
assert(whole == "hello, world!")
`);
//Test with zero parameters.
unittest_lua(L, `
local blank = concat()
assert (string.len(blank) == 0)
`);
static const(char)[] concat2(char separator, const(char)[][] pieces...)
{
if(pieces.length == 0)
return "";
string result;
foreach(piece; pieces[0..$-1])
result ~= piece ~ separator;
return result ~ pieces[$-1];
}
pushValue(L, &concat2);
lua_setglobal(L, "concat2");
unittest_lua(L, `
local whole = concat2(",", "one", "two", "three", "four")
assert(whole == "one,two,three,four")
`);
//C- and D-style variadic versions of concat for
//future use if/when these are supported.
//C varargs require at least one fixed argument.
import core.vararg;
// C-style varargs broken on Linux for 2.066.1?
version(none) extern(C) static string concat_cvar (size_t count, ...)
{
string result;
va_list args;
va_start(args, count);
foreach(immutable i; 0 .. count)
{
auto arg = va_arg!LuaObject(args);
result ~= arg.toString();
}
va_end(args);
return result;
}
//D-style variadics have an _arguments array that specifies
//the type of each passed argument.
static string concat_dvar (...) {
string result;
foreach (argtype; _arguments) {
assert (argtype == typeid(LuaObject));
auto arg = va_arg!(LuaObject)(_argptr);
result ~= arg.toString();
}
return result;
}
}
// get delegates from Lua
unittest
{
lua_getglobal(L, "string");
lua_getfield(L, -1, "match");
auto match = popValue!(string delegate(string, string))(L);
lua_pop(L, 1);
auto result = match("foobar@example.com", "([^@]+)@example.com");
assert(result == "foobar");
// multiple return values
luaL_dostring(L, `function multRet(a) return "foo", a end`);
lua_getglobal(L, "multRet");
auto multRet = popValue!(Tuple!(string, int) delegate(int))(L);
auto results = multRet(42);
assert(results[0] == "foo");
assert(results[1] == 42);
}
// Nested call stack testing
unittest
{
alias string delegate(string) MyFun;
MyFun[string] funcs;
pushValue(L, (string name, MyFun fun) {
funcs[name] = fun;
});
lua_setglobal(L, "addFun");
pushValue(L, (string name, string arg) {
auto top = lua_gettop(L);
auto result = funcs[name](arg);
assert(lua_gettop(L) == top);
return result;
});
lua_setglobal(L, "callFun");
auto top = lua_gettop(L);
luaL_dostring(L, q{
addFun("echo", function(s) return s end)
local result = callFun("echo", "test")
assert(result == "test")
});
assert(lua_gettop(L) == top);
}
unittest
{
assert(lua_gettop(L) == 0);
lua_close(L);
}