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?