Wrapping instance methods

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.

Note

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.

How to do it…

Let's execute the following steps to wrap instance methods:

  1. Write a generic conversion function to and from your consistent type. You should use std.variant.Variant or std.conv.to directly, if possible, and use type families with std.traits if working with a custom type.
  2. Write a helper function generator that takes an existing function as a runtime delegate and returns the wrapped type. Constrain the compile-time argument with std.traits.isDelegate.
  3. Use std.traits.ParameterTypeTuple to loop over the function arguments and set them from the input data, using your conversion function to set the types.
  4. Use 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.
  5. Pass methods to the helper with &__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.
  6. Test it by building an associative array of all the methods on an object and try calling them through it.

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.

How it works…

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.

Note

Not all information about a function is available through a delegate type. If we needed more information such as the parameter names or default values, they will need to be passed to the wrapper function from the outside along with the delegate.

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.

See also

  • The Creating a command-line function caller recipe in Chapter 8, Reflection
  • The Creating a dynamic type recipe in Chapter 5, Resource Management
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.165.62