There is this cool library by _why called Mixico. Its main purpose (I think) is to get around the issue of instance_eval replacing self, which makes it (instance_eval) kinda sucky to use in DSLs. Example:
def x &blk SomeProxyClass.new(self).instance_eval(&blk) end @i = 5 x do p @i # => NOT 5 end
@i won’t be 5 in the above example because @i will be searched for in
SomeProxyClass.new, not in the
self in which the block was created, due to the fact instance_eval hijacks
self. Mixico helps you get around this.
require 'mixico' def x &blk Module.mix_eval(SomeProxyModule, &blk) end @i = 5 x do p @i # => 5 !! end
Mixico will make all the methods in the proxy module available to your block, but it won’t hijack self. Pretty sweet deal! Except it’s not thread safe =[.
Why not? Well essentially what it does is call
self.extend SomeProxyModule. Now we could do this without mixico and all its magic. But what mixico does allow us to do is unextend the module after the block is finished. This required some C extensions. So the problem is that extending an object or a class is not a thread local operation. It will affect any thread dealing with that object or class. So say you had two mix_eval’s going on in different threads on the same object at the same time… fail.
So at first I was like, yea that’s too bad. But then I thought of a clever way to fix it. Instead of extending
dup it and then extend the dup. But now we wan’t to run the block in the context of the duped self, so how do we do that? We call
instance_eval on the duped self. But wait, doesn’t
instance_eval hijack self and that’s bad? Well yes, but this time we’re hijacking self and setting it to the dup’ed self which is almost exactly the same thing.
But not exactly. The dup’ed self will have different instance var and method tables. So you’ll be able to access all the methods and instance vars of the original but if you were to modify them the changes wouldn’t persist outside the block. So how do we fix this? We change the dup’ed self’s
m_tbl pointers to point to the original self’s tables (C hax). This way any modification of ivars or methods will actually be on the original, even though we have a distinct class hierarchy which includes our temporarily mixed in module.
Here’s the thread safe version of mix_eval:
def safe_mix_eval mod, &blk duped_context = blk.context.dup # make sure the singleton class is in existence class << duped_context; self; end duped_context.redirect_tbls(blk.context) duped_context.extend mod begin m = duped_context.is_a?(Module) ? :class_eval : :instance_eval duped_context.send(m, &blk) ensure duped_context.restore_tbls (class << duped_context; self; end).disable_mixin mod end end
This will even handle the special case of adding methods to a class, which needs class_eval instead of instance_eval. What this still doesn’t handle is if you were to try to do something like this:
class X Module.safe_mix_eval Module.new do class << self def x; :x; end end end end X.x # no method error!
This wouldn’t work because the
class << self refers to the singleton class of the dup of X not the singelton class of X itself. Therefore we add the singleton method to the wrong class and it doesn’t persist. I think this case is probably rare, but maybe not. I doubt many people use mixico anyway so whatever :P
Benchmarks currently show the thread safe version being about twice as slow as the original.
Props to http://banisterfiend.wordpress.com/ for working through this fix with me.