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:
Runtime coupling - No, nay, never ! It MUST be validated in compile time
Easy syntax
Totally generic code
In D, because I'm learning this language and I want to keep comfort zone away
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:
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.
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 !
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.
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.
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.
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
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: