🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Parameter pack expansion order (for automatic script-function wrapper)

Started by
6 comments, last by Juliean 3 years, 2 months ago

Hello,

so I'm at a point in my bytecode-compiler where I need to support binding functions from user-code. Lets take a simple function:

Vector2 multiply(const Vector2& v1, float v2);

Now this is how I would write a wrapper manually:

void multiplyWrapper(Stack& stack)
{
	const auto v2 = stack.Pop<float>();
	const auto& v1 = stack.Pop<const Vector2>();

	const auto result = multiply(v1, v2);
	stack.Push(result);
}

The scripting-language is stack-based, arguments are pushed in left-to-right order and thus need to be accessed in inverse order from the stackl (this is important and not something that can really be changed at this point unless there is no other way).

Now the issue: I don't want to have to write all those functions myself. Using templates, I can (almost) automate the process already:

template<typename Return, typename... Args, typename Functor>
void genericWrapper(Functor functor, Stack& stack)
{
	if constexpr (std::is_void_v<Return>)
		functor(stack.Pop<Args>()...);
	else
		stack.Push(functor(stack.Pop<Args>()...));
}

The problem here is that the order of stack.Pop in the pack-expansion as part of a function-call is not deterministic. Meaning it might end up calling Pop<const Vector2> first, which is bad.

A slightly more accurate solution would be this:

template<typename Return, typename... Args, typename Functor>
void genericWrapper(Functor functor, Stack& stack)
{
	const auto arguments = std::tuple{ stack.Pop<Args>()... };

	if constexpr (std::is_void_v<Return>)
		std::apply(functor, arguments);
	else
		stack.Push(std::apply(functor, arguments));
}

The usage of pack-expansion inside a bracket-initializer guarantees the order… however its exactly the wrong one. For a signature (const Vector2, float), it will call Pop in this order, meaning it will always access const Vector2& first, instead of float.

Does somebody have a clever solution for this? In my old system, The closest I can think is instead of relying on the order of pops, I calculate the absolute offset of arguments from the top of the stack, but that is also only a last resort (as it introduces a certain runtime-overhead as well as general complication - my old system used something like this and it is a nightmare; as the actual mechanism here are a little more complicated than what I've shown). I'm also looking for something that has as little runtime-overhead as possible, even for debug-builds.

Any ideas?

Advertisement

Juliean said:
Any ideas

Yeah, sure ?

I had need for something similar when I first wrote my in-game command line. It needed to grab the arguments parsed from the input and place them into a function call. Something even close to this is my Flex data type, a wrapper struct around Any, which also can have extension functions called from a parameter pack (void**).

In both cases I used a combination of several macros and templates to achieve what I want. You can probably do the same for pulling the parameters from your stack into local variables and inline a function call with them. I try to post some code from my solution (unfortunately it is very complex and not on GitHub yet as part of your game/engine SDK).

I assume you have some kind of register to your scripting language, so my starting point is to make that function a template function, which takes the signature of the native code you want to call. I did it for my delegates this way

    template<typename signature> struct StaticDelegate;

    #define ORDER 0
    #include <Delegate/StaticDelegate.Partial.h>
    #undef ORDER

    #define ORDER 1
    #include <Delegate/StaticDelegate.Partial.h>
    #undef ORDER

    ...

    #define ORDER 10
    #include <Delegate/StaticDelegate.Partial.h>
    #undef ORDER

Because I'm mostly writing in C/C++99 standard and don't have access to variable parameter that easy, I wrote a macro for it.

template<typename ret do_if(ORDER, _separator) variadic_decl(typename Args, ORDER)> struct StaticDelegate<ret(variadic_decl(Args, ORDER))>
{
    ....
    
    force_inline ret operator()(variadic_args(Args, a, ORDER)) const 
    { 
        return Invoke(variadic_decl(a, ORDER));
    }
};

It expands the variadic argument list into whatever I need. For example

force_inline Vector2 operator()(Args1 a1, Args2 a2) const 
{ 
    return Invoke(a1, a2);
}

where Args1 and Args2 are the template arguments (e.g. Vector2, float). Long story short, this way I was able to generate more complex calls and also declare local variables. This is for example how my Flex data-type calls extension methods

if (instance && ptr)
    {
        void* argsList[ORDER + 1] = { variadic_decl(&a, ORDER) do_if(ORDER, _separator) result };
        ptr(instance, argsList);
        return true;
    }
    else return false;

So to solve your problem, you could register a wrapper function over a delegate like type which automatically pulls the number of arguments from the stack by the number and kind of template parameter extracted from the function signature

variadic_stack_pop(Args, param, ORDER)
do_if(ORDER, JOIN(stack.Push, LEFT_BRACE)
Invoke(variadic_decl(param, ORDER));
do_if(ORDER, JOIN(RIGHT_BRACE, SEMICOLON))

then expans into

const Args1 param1 = stack.Pop<Args1>();
const Args0 param0 = stack.Pop<Args0>();

stack.Push(Invoke(param0, param1));

And everything else is then solved by the template

It should be possible to write a recursive function that reverses the order of a tuple using std::tuple_cat. Something like this:

template<std::size_t N> struct reverse_tuple_helper {
  template<class Tuple> apply(Tuple const &t) {
    return std::tuple_cat(
      std::make_tuple(std::get<N - 1>(t)),
      reverse_tuple_helper<N - 1>::apply(t));
  }
};

template<> struct reverse_tuple_helper<0> {
  template<class Tuple> apply(Tuple const &t) {
    return std::tuple<>();
  }
};

template<class Tuple> auto reverse_tuple(Tuple const &t) {
  return reverse_tuple_helper<std::tuple_size_v<Tuple> >::apply(t);
};

@a light breeze Thanks, but thats not quite it. Its not that my tuple is reversed (std::tuple<int, Vector2> instead of std::tuple<Vector2, int>) but that the evaluation-order of the Pop-functions for the tuple are executed left-to-right, resulting the elements from my stack (Vector2, int) being interpreted wrong - it would try to pop “Vector2” first, instead of int.

I guess I could construct an Args-pack where the elements are inversed, then construct that from the stack, and then inverse that, but thats a few too many operations for my liking (pretty sure all that would get optimized in release-build, but debug-performance is also a priority for me).

@shaarigan Thanks! That actually put me on the right track for the solution I was looking for. Now I'm not that good with macros, especially variadic ones (I've always had the privilege of working with c++11-parameter packs even before it was officially out), but I can really just write the overloads myself now:

template<typename Return, typename Arg1, typename Arg2, typename Functor>
Return genericWrapper(Functor functor, Stack& stack)
{
	Arg2 arg2 = stack.Pop<Arg2>();
	Arg1 arg1 = stack.Pop<Arg1>();

	return functor(arg1, arg2);
}

template<typename Return, typename Arg, typename Functor>
Return genericWrapper(Functor functor, Stack& stack)
{
	Arg arg = stack.Pop<Arg>();

	return functor(arg);
}

template<typename Return, typename Functor>
Return genericWrapper(Functor functor, Stack& stack)
{
	functor();
}

I'll probably look into the macro-solution you posted some other time, but right now I can't figure out how to that for the life of me. Its more important to get going now, and I don't mind the code-duplication here too much. I don't have functions with so many parameters anyway, so I'll probably have to write that up to 10 or so.

Legit!

My solution is less “real” variadic rather than a set of macros which call each other from a chain depending on the number specified. So for example

DO(a, n) JOIN(DO_, n)(a)
DO_0(a) JOIN(a, 0)
DO_1(a) DO_0(a) JOIN(a, 1)
DO_2(a) DO_1(a) JOIN(a, 2)
DO_3(a) DO_2(a) JOIN(a, 3)
...
DO_99(a) DO_98(a) JOIN(a, 99)

Ah, thanks again. I get it now. So instead of having to write an increasing number of “arg”-lines per specialization, I just write the set of macros for how many I want to have and then use those. I like it.

This topic is closed to new replies.

Advertisement