[Thiago Cafe] Programming is fun!

Class proxy in D - Remote invoke

Created by thiago on 2016-09-29 23:01:56

Tags:

I was thinking to myself some other day how could I do a wrapper for any class for, make it remote invokable or maybe auditable ?

For this, I'd like to have the following:

So, I'd like to have a syntax like that:

auto instance = wrapper!my_class;

But, first things first. For this, I'd need to go through the following steps:

  1. Get class name for specific type
  2. Get methods and parameters for type::method
  3. Invoke this method with passed parameters
  4. Generate this templated code and create an instance
  5. And have an example class to use

Example class and traits

Sometime ago I was buying some videogames for my daughters and one of them I bought from a teenager. Based on the conversation we had, I wrote my example class.

class teen_writer {
    public void say(string text) {
        writeln("Whatever, " ~ text);
    }
}

Now I need to use traits to get the methods from my temperamental class. After some fights, I found how to do it.

foreach(member_name; __traits(allMembers, teen_writer)) {
    writeln(member_name);
}

This simple for returned much more than I wanted: say, toString, toHash, opCmp, opEquals, Monitor, factory

Oops... I need to filter somehow. I decided to tag the methods with @invokable

enum invokable;
//.....

@invokable
public void say(string text) {
//...

foreach(member_name; __traits(allMembers, teen_writer)) {
    static if(hasUDA!(__traits(getMember, teen_writer, member_name), invokable)) {
        writeln(member_name);
    }
}

And now I have something nice already! This code will print say method only !

Ok. Time to start the magical part. We have two very interesting things in D - template mixin and mixin. It took me a while to understand what are those two.

Template mixin

Imagine that you can create a block of code that will be added to anywhere you want. That's template mixin.

From dlang documentation (based):

mixin template ContactType()
{
    string first_name;
    string last_name;
}

struct Person
{
    mixin ContactType;
}

Person p;
p.first_name = "The";
p.last_name = "Doctor";

Yes, it works !

Mixin

Mixins are processors that will compile and generate something.

We saw in previous example mixin being used to generate the body of Person struct. Another interesting use is create code based on strings.

From dlang documentation:

    template GenStruct(string Name, string M1)
    {
        const char[] GenStruct = "struct " ~ Name ~ "{ int " ~ M1 ~ "; }";
    }
    mixin(GenStruct!("Foo", "bar"));
    
    Foo f;
    f.bar = 1;

Yes, it also works. I have here everything I need.

Creating the wrapper

So, I created a template mixin caleed wrap_class

mixin template wrap_class(alias source_class, string instance_name)

Inside, a method _wrap to generate one string with a new class that will wrap whatever I pass:

private string _wrap(string class_name) {
    string code = "" ~
        "class _proxy_" ~ class_name ~ "_ { " ~
        "  " ~ class_name ~ " _inst_ = new " ~ class_name ~ "; ";
        
    foreach(member_name; __traits(allMembers, source_class)) {
        static if(hasUDA!(__traits(getMember, source_class, member_name), invokable)) {
            string f = "__traits(getMember, " ~ class_name ~ ", \"" ~ member_name ~ "\")";
            code ~= "std.traits.ReturnType!(" ~ f ~ ") " ~ member_name ~ "(std.traits.ParameterTypeTuple!(" ~ f ~ ") args) {
                    import std.stdio;
                    writeln(\"[Invoking " ~ member_name ~"]\");
                    /* return */
                    return _inst_." ~ member_name ~ "(args);
                }";
        }
    }
    code ~= "}";
    
    return code;
}

I have the type, but I don't have one instance. Let me create this instance adding a bit more of code

code ~= "auto " ~ instance_name ~ " = new _proxy_" ~ class_name ~ "_(); ";

Next step, get a class name based on it's type, call the method and generate the code.

Explaining the code

mixin(_wrap(typeof(new source_class).stringof));

Before anything, let's explain this code string. I'm use the code that will be generated to make it clearer

Here I retrieve all members of source_class (teen writer) in my case.

foreach(member_name; __traits(allMembers, teen_writer)) {

Now, filtering only what's tagged with invokable

static if(hasUDA!(__traits(getMember, teen_writer, member_name), invokable)) {

And this part is a bit more complicated

alias f => __traits(getMember, teen_writer, say)
std.traits.ReturnType!( f ) 
say 
(std.traits.ParameterTypeTuple!( f ) args)

This first part __traits(getMember, teen_writer, "say") will get the member say from teen_writer type. It's the same as teen_writer.say.

This second part std.traits.ReturnType!( f ) will get the return type of the function teen_writer::say

The last part std.traits.ParameterTypeTuple!( f ) args will get the parameters as a tuple. Almost the same vargs from C lang.

Generating the code

Now, generate the code is the easier part. First, we need to get the class name as a string and pass to our function

typeof(new source_class).stringof -> returns the class name

Just as a reminder. This is all compile time, therefore this class in not being allocated.

Now we just need to call this class name using mixin

mixin(_wrap(typeof(new source_class).stringof));

We have now the following code being executed by our mixin (supposing I passed inst as instance_name)

auto inst = new _proxy_teen_writer_();

This is clumsy. To brought this code to a perfect end, we just need an auxiliar function:

auto wrapped(T)() {
    mixin wrap_class!(T, "_local_var_");
    return _local_var_;
}

//...
auto teenager = wrapped!teen_writer;
teenager.say("Thiago");

Output:
[Invoking say]
Whatever, Thiago

Last but not least

I hope you enjoy as much as I enjoyed writing this. If you liked, disliked or even know how to improve it, please comment!

References:

Tell me your opinion!

Reach me on Twitter - @thiedri