Earlier, we looked at a dynamic type in D which included the capability to wrap native functions with dynamic typing and property replacement. Now, it is time to see exactly how that works and how automated code generation makes it a lot easier.
By unifying types, we enable both dynamic transformations as well as create an array of delegates to access functions of varying types. We cannot declare an array of function pointers that all take different arguments, but we can declare an array of pointers to helper functions that take one input array and transform the values for use.
Let's execute the following steps to wrap instance methods:
std.variant.Variant
or std.conv.to
directly, if possible, and use type families with std.traits
if working with a custom type.std.traits.isDelegate
.std.traits.ParameterTypeTuple
to loop over the function arguments and set them from the input data, using your conversion function to set the types.static if(is(ReturnType!func == void))
to branch on the presence of a return value. If it is of the void
type, call the function with the args
argument. If it is not of the void
type, transform the return value.&__traits(getMember, object, memberName)
. Do not use an alias
helper in this step, and be sure to get the member of an object and not the type.The code is as follows:
import std.traits; // this alias represents the unified type our wrapper yields alias WrapperFunctionType = string delegate(string[]); // step 2: this returns the wrapped function, given the original WrapperFunctionType wrap(Func)(Func func) if(isDelegate!Func) { // it immediately returns the wrapped function here return delegate string(string[] args) { import std.conv; ParameterTypeTuple!Func funcArgs; // step 3: populating the arguments foreach(idx, ref arg; funcArgs) { if(idx >= args.length) break; // the cast allows us to write to const // arguments. to!() performs conversions. cast() arg = to!(typeof(arg))(args[idx]); } // step 4: call the function, handling the // return value. string ret; static if(is(ReturnType!func == void)) func(funcArgs); else ret = to!(typeof(return))(func(funcArgs)); return ret; }; // remember, this is a return statement, needing ; } WrapperFunctionType[string] wrapAll(T)(ref T obj) { WrapperFunctionType[string] wrappers; foreach(memberName; __traits(allMembers, T)) { // only wrapping methods static if(is(typeof(__traits(getMember, T, memberName)) == function)) // wrap with a delegate from obj wrappers[memberName] = wrap(&__traits(getMember, obj, memberName)); } return wrappers; } // a test structure with various method types struct Test { int a; void foo() { import std.stdio; writeln("foo called"); } int setA(int a) { return this.a = a; } int getA() { return this.a; } } void main() { Test t; // Wrap functions into a uniform array with string keys auto functions = wrapAll(t); // call foo with no arguments functions["foo"](null); functions["setA"](["10"]); // calls t.setA(10); import std.stdio; // confirm we can get the value and that it matches // the original object too. writeln(functions["getA"](null), " == ", t.getA()); }
Running the program will confirm that foo
is called and the getter and setter functions work correctly.
The code generation here barely looks like code generation. It works very similar to any regular closure that you can write in D and many other languages. The difference is that the wrapper here is aware of the specific type of the delegate passed to it, and thus it can customize its behavior. A separate wrapper will be automatically generated for each different type passed to it, allowing us to use all the compile-time features such as static if
, introspection on types, and generic functions such as std.conv.to
.
The function wrapper uses ParameterTypeTuple
and ReturnType
to perform the transformations needed to present a uniform interface. ParameterTypeTuple
yields a list of all the function's arguments in a form that we can use to declare a variable. Looping over it with foreach
and ref
lets us set each value from the input array. Using typeof
with the generic function to
, the type conversion is performed quickly, easily, and generically. The cast on the left-hand side of the assignment is used to remove const
. Generally, casting away const
is a bad idea, but here it can be necessary to populate the full argument list on some functions.
The ReturnType
check is necessary because void
can never be converted to any other type. If a function returns void
, we must not try to use its return value. This is the same as we did in the compile-time function caller previously.
The major difference between wrapping an object method and the compile-time function caller we wrote in the previous chapter is that the object wrapper needs an object which is only available at runtime. Thus, this function needs to work with compile-time data and runtime data together. It works with delegates instead of the alias
arguments, while still templating the outer function on the type of the delegate given.
If we tried to use an alias
argument, the compiler will issue an error saying need this to access member foo
when we tried to call it. An alias
parameter doesn't include a pointer to the this
object; it will only be a reference to the function itself. A delegate, on the other hand, includes both the necessary pieces of information to call an object method.
Similarly, we have often used an alias helper
function in the past to refer to a member when doing compile-time reflection. Here, it is not important because the alias helper
function will also lose the reference to this, yielding a plain function instead of a delegate. The plain function that it gives doesn't know that the this
pointer is even necessary, so while the code may compile, it will crash when it tries to actually use the object! This is also why wrap
is specifically constrained to the isDelegate
method instead of the isCallable
method, to catch this mistake at compile time by means of a type mismatch error. We specifically need a delegate because only delegates keep a reference to the object instance.
Once the wrapper is working, we can use it to populate arrays. Here, we used compile-time reflection to get all the methods on a struct
type and populate an associative array. Since the wrapper interface is the same for all functions, this can now be used to consistently call any method on the given object.
18.191.165.62