I want to share some code that I’ve written for working with NativeFunctionArgs
. If you work with JavascriptEngine
, I think you’ll find this code to be very useful.
If you use JavascriptEngine
for scripting, then you’ll know that a DynamicObject
function receives all of its arguments in the form of an Array<var>
. I wanted to make something that would organize this into a more manageable form, so that you can quickly specify which type of arguments you are expecting a function to receive, and easily access them.
Let’s look at what ArgumentOrganizer
does.
struct ScriptObject : public DynamicObject
{
static var someFunction(const var::NativeFunctionArgs& a) // Here is the function that we can run from within Javascript
{
ArgumentOrganizer<String, int> o(a); // We construct an ArgumentOrganizer from the NativeFunctionArgs, specifying which types or argument you are expecting.
auto firstArgument = o.getArgument<0>(); // this is a String
auto secondArgument = o.getArgument<1>(); // this is an int
DBG(firstArgument + " " + String(secondArgument)); // prints "hello world 6"
return var::undefined();
}
ScriptObject()
{ setMethod("f", someFunction); }
};
int main()
{
JavascriptEngine js;
js.registerNativeObject("a", new ScriptObject());
// Here is the function we're executing in JS.
// It has one String argument, followed by an int.
js.execute("a.f(\"hello world\", 6)");
return 0;
}
This is really useful! It saves you a lot of looping and type checking every time you want to extract arguments.
Now let’s look at the code.
#include "../JuceLibraryCode/JuceHeader.h"
#include <utility>
#include <tuple>
#include <type_traits>
template<typename... T>
class ArgumentOrganizer
{
public:
ArgumentOrganizer(const var::NativeFunctionArgs& a) :
thisObject(a.thisObject.getDynamicObject()),
tpl(populate_tuple<T...>(a, std::make_index_sequence<sizeof...(T)>{})) {}
template <int n>
auto getArgument() const
{ return std::get<n>(tpl); }
private:
DynamicObject::Ptr thisObject;
const std::tuple<T...> tpl;
template<typename... Args, int... i>
auto populate_tuple(const var::NativeFunctionArgs& a, const std::index_sequence<i...>&)
{ return std::make_tuple((VariantConverter<Args>::fromVar(i < a.numArguments ? a.arguments[i] : var()))...); }
};
I was really pleased with how concise this I got this. As you can see, it loads the arguments into a tuple, meaning that a lot of the heavy lifting is done at compile time, and that the compiler won’t let you do stupid things like trying to access arguments terms don’t exist.
A second version, ComplexArgumentOrganizer
, organizes things even further. Instead of matching the arguments types by position, it scans through all of the arguments and categorizes them, allowing for “lazy evaluation”.
struct ScriptObject : public DynamicObject
{
static var someFunction(const var::NativeFunctionArgs& a)
{
ComplexArgumentOrganizer<String, int64> o(a);
DBG(o.getArgument<0>(0)); // Access the 0th argument of type 0 (String)
DBG(o.getArgument<0>(1)); // Access the 1st argument, etc...
DBG(o.getArgument<0>(2));
DBG(o.getArgument<1>(0)); // Access the 0th argument of type 1 (int)
DBG(o.getArgument<1>(1)); // Access the 1st argument, etc...
DBG(o.getArgument<1>(2));
return var::undefined();
}
ScriptObject()
{ setMethod("f", someFunction); }
};
int main()
{
JavascriptEngine js;
js.registerNativeObject("a", new ScriptObject());
js.execute("a.f(1, \"a\", 2, \"b\", 3, \"c\")"); // Prints "a b c 1 2 3"
return 0;
}
This is more convenient, but a bit less efficient: every extra type you specify constructs an Array and loops through the argument list.
You can get the whole code base here. There are a few more features there that I haven’t mentioned, mostly to help you work with DynamicObject
and numeric arguments (so that you don’t have to distinguish between ints and doubles).
I hope it’s useful to you! I’ve been testing it for a couple of days, but there might still be bugs. Comments and improvements are welcome.