Native MRI Callback
A few weeks ago after a discussion with raggi (yet again) about callback implementations for the Ruby language, we kicked off an attempt at a minimal native object that’s very close in performance to method dispatch.
Here’s a representation of the pure ruby version :
The game plan
The following inefficiencies would have to be addressed :
- Instance variables Instance variable lookups in general require some symbol table overhead.
- Block procedure arguments Block proc arguments are up to 6x slower than passing explicit objects.
- Method dispatch The splat operator and __send__, although efficient, does have some noticeable overheads.
- Kernel convenience method
Additional method dispatch in the critical path through this global function.
Ivar lookup overhead is mostly negated for Ruby 1.9 as the RObject struct stores the first three ivars for each object.
Implementation
The callback structure
We introduce a new callback struct with 2 members :
- object Ruby object to invoke the callback on
- method Method to call.Defaults to #call if no method argument given
Wrapping the C struct as T_DATA has the least overheads in accessing either the object and the method when our callback fires.
GC integration’s a piece of cake as well.
Allocation
Wrapping C structures requires an allocator function (Callback.allocate) to initialize our callback struct and register our mark and free functions with the GC.This is usually invoked prior to #initialize and is transparent for our use cases.
Definition
Three definition (setup) styles supported :
- Object and method arguments
- Block argument
- Farmed off an existing object
We handle each of the argument or block definition styles in an initialize method and handle edge cases such as not assigning a callback method that’s not defined for the object.It’s also important to ensure that our callback is frozen at this point ( I’m looking at you lifo, this one’s legit
) for consistency.
Our global function, Callback(), which is defined much like the popular Array() and Float() coercion helpers, and the farm out API reuse the alloc and init covered above.
Results
An intuitive callback API that’s slower than method dispatch, but faster than lambda callbacks.
Have a look at the source or install the gem to play around.
sudo gem install methodmissing-callback
Tested on MRI versions > 1.8.6 and 1.9.2 – please have a look through the test suite for further examples.
Follow me @ github or twitter if you enjoyed this article.Thanks for reading and special thanks to Aman Gupta for review !
4 Responses to “Native MRI Callback”
Leave a Reply

Compilação de Pequenas Novidades do Mundo Ruby - Setembro de 2009 on September 10th, 2009
[...] caras da MethodMissing desenvolveram uma biblioteca Ruby inteligente que oferece um sistema de callbacks nativo que tem performance superior aos lambdas, (embora não tão rápido quando o dispatch de métodos [...]
roger on September 11th, 2009
I like it. Interesting. Unfortunately at least on my browser the results are hard to read.
Thanks!
-r
roger on October 12th, 2009
As a note, your pure ruby example doesn’t allow for
‘hai’.callback(:gsub)
Anyway, here’s my wish list for Callback, if you don’t mind the suggestions:
class A
def go
callback(:go) # callback to self.go
a = ”
a.callback(:to_s) # a.to_s
a.callback { to_s } # instance_eval’ed for a when it is call’ed
c = callback(:go, ‘param1′, ‘param2′) # can pass it parameters to use so you can call it like
c.call # calls a.go(‘param1′, ‘param2′)
c.call(‘param3′) # calls a.go(‘param1′, ‘param2′, ‘parame’) — either that or be disallowed or replace the original params, like a.go(‘param1′)
Callback(A, :go) # this one works currently
A.callback(:class_method)
end
def A.class_method
end
end
Also shouldn’t
Callback { }
just be an alias for proc { }?
Also interesting would be if you could pass it for a block parameter, like:
>> A.go &Callback(A, :class_method)
TypeError: wrong argument type Callback (expected Proc)
from (irb):16
from :0
I guess you could fake it with
class Callback
def to_proc
@proc ||= proc {|*args| call *args}
end
end
however this still requires creating a proc at least once…which is slow, if I remember correctly.
If you knew a class and its methods were going to stick around forever, you could cache callbacks to each class, but not for instances you couldn’t cache.
Anyway I like it
-=r
roger on October 13th, 2009
Looks like the link to the source should be
http://github.com/methodmissing/cb