Monkey Patching CLR Objects

code 0 comments suggest edit

In my last post I set the stage for this post by discussing some of my personal opinions around integrating a dynamic language into a .NET application. Using a DSL written in a dynamic language, such as IronRuby, to set up configuration for a .NET application is an interesting approach to application configuration.

With that in mind, I was playing around with some IronRuby interop with the CLR recently. Ruby has this concept called Monkey Patching. You can read the definition in the Wikipedia link I provided, but in short, it is a way to modify the behavior of a class or instance of a class at runtime without changing the source of that class or instance. Kind of like extension methods in C#, but more powerful. Let me provide a demonstration.

I want to pass a C# object instance that happens to have an indexer to a Ruby script via IronRuby. In C#, you can access an indexer property using square brackets like so:

object value = indexer["key"];

Being able to use braces to access this property is merely syntactic sugar by the C# language. Under the hood, this gets compiled to IL as a method named get_Item.

So when passing this object to IronRuby, I need to do the following:

value = $indexer.get_Item("key");

That’s not soooo bad (ok, maybe it is), but we’re not taking advantage of any of the power of Ruby. So what I did was monkey patch the method_missing method onto my object and used the method name as the key to the dictionary. This method allows you to handle unknown method calls on an object instance. You can read this post for a nice brief explanation.

So this allows me now to access the indexer from within Ruby as if it were a simple property access like so:

value = $indexerObject.key

The code for doing this is the following, based on the latest IronRuby code in RubyForge.

ScriptRuntime runtime = IronRuby.CreateRuntime();
ScriptEngine rubyengine = IronRuby.GetEngine(runtime);
RubyExecutionContext ctx = IronRuby.GetExecutionContext(runtime);

ctx.DefineGlobalVariable("indexer", new Indexer());
string requires = 
@"require 'My.NameSpace, Version=1.0.0.0, Culture=neutral, PublicKeyToken=...'

def $indexer.method_missing(methodname)
  $indexer.get_Item(methodname.to_s)
end
";

//pretend we got the ruby script I really want to run from somewhere else
string rubyScript = GetRubyCode();

string script = requires + rubyScript;
ScriptSource source = rubyengine.CreateScriptSourceFromString(script);
runtime.ExecuteSourceUnit(source);

What’s going on here is that we instantiate the IronRuby runtime and script engine and context (I still need to learn exactly what each of these things are responsible for apart from each other). I then set a global variable and set it to an instance of a CLR object written in C#.

After that, I start constructing a string that contains the beginning of the Ruby script I want to execute. I will pre-append this beginning section with the actual script I want to run.

The beginning of the Ruby script imports the .NET namespace that contains my CLR type to IronRuby (I believe that by default you don’t need to import mscorlib and System).

I then added a missing_method method to that CLR instance within the Ruby code via this snippet.

def $indexer.method_missing(methodname);
  $indexer.get_Item(methodname.to_s)
end

At that point now, when I execute the rest of the ruby script, any calls from within Ruby to this CLR object can take advantage of this new method we patched onto the instance.

Pretty nifty, eh?

In my next post, I will show you the concrete instance of using this and supply source code.

Technorati Tags: DLR,IronRuby,DSL

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

8 responses

  1. Avatar for Chris Chandler
    Chris Chandler April 18th, 2008

    I love the creativity of this, I have been doing c# work for a long time, and have recently done 3 months of ruby work. Monkey patching is awesome, it really allows you to think meta. If you use a lot of reflection and attributes, monkey patching is totally your cup of tea. I am hoping extension methods with c# using reflection will be able to handle method_missing in the future. Although clearly some kind of dispatch abstraction will have to be put in place, at the same cost as DLR, it would be a really neat experiment.

  2. Avatar for Paul Batum
    Paul Batum April 18th, 2008

    I'm still fairly new to ruby and have no experience using IronRuby so please correct me if I'm wrong, but couldn't you have simply redefined the [] operator on the $indexer object? From what I have read so far, overriding method_missing is something you want to be pretty careful about doing.
    def $indexer.[](key)
    return get_Item(key)
    end
    That would let you write:
    value = $indexer["key"]
    Wouldn't it?

  3. Avatar for Haacked
    Haacked April 18th, 2008

    @Paul I tried that, but looking at what you did, I realized I totally got the syntax wrong. I'll try what you did and report back.

  4. Avatar for Haacked
    Haacked April 18th, 2008

    @Paul ah, but I didn't want to use an indexer in Ruby in this particular case. I just wanted to do indexer.foo or indexer.bar where foo and bar are the keys to the dictionary.
    I understand the qualms about method_missing, but in this case, I wrote the class I'm monkey patching in C#. So I have control over both sides.

  5. Avatar for Paul Batum
    Paul Batum April 19th, 2008

    I did a bit more experimentation and noticed that your method_missing trick will fail if you try to do an assignment using the same syntax, e.g :
    $indexer.key = "foo"
    However if you check the method name to see if it ends with an equals sign (=) then you could instead do a set operation. I managed to get this working fine in normal ruby, here is a slightly modified version that will hopefully work in IronRuby:

    def $indexer.method_missing(name, *args)
    # If $indexer.key = "foo" was executed then the name argument
    # here would be the symbol :key= and *args would be the string "foo".
    # Use a regex to detect these cases and capture the portion before the =
    # and do a store operation instead of a fetch operation.
    md = name.to_s.match(/(.+)=$/)
    if(md)
    # In normal ruby this would be: store(md[1], *args)
    set_Item(md[1], *args)
    else
    # In normal ruby this would be: fetch(name.to_s)
    get_Item(name.to_s)
    end
    end

  6. Avatar for Haacked
    Haacked April 19th, 2008

    @Paul Batum nice! Thanks for the tip. It just so happens in the context I'm using it, I wantned it to be read-only on the Ruby side.

  7. Avatar for Christopher Bennage
    Christopher Bennage April 20th, 2008

    I think there's some similiar stuff going on in Dynamic Silverlight (IronRuby integrated into SL). You can use the dot syntax to reference XAML elements, and it use missing_method to look them up. Cool stuff.

  8. Avatar for Christopher Bennage
    Christopher Bennage April 20th, 2008

    :-P The problem with RSS feeds is that I read the posts in reverse order. I just saw that inspiration for this came from John Lam.