[MLton] Callback functions: how?

Matthew Fluet fluet at tti-c.org
Wed Feb 14 11:36:54 PST 2007

Wesley W. Terpstra wrote:
> What's the most efficient way to implement callback functions in MLton?

Good question.

> As I understand it, the code
>> val fns : (int -> unit) list ref = ref [ fn _ => () ]
>> val register f = fns := f :: !fns
>> val runAll x = List.app (fn f => f x) (!fns)
> will prevent flow-analysis and the runAll method will have a loop over a 
> giant switch statement that could call all possible functions of type 
> 'int -> unit'. Is this correct? Or can MLton recognize that only methods 
> passed to 'register' need to be in the switch statement?

I believe that MLton does something in between the two.  That is, 
flow-analysis is (purposefully) conservative on functions that escape 
into mutable objects, but it does distinguish between escaping and 
non-escaping functions.  So, you won't get a dispatch among all possible 
functions of type 'int -> unit'.  On the other hand, if you had 
'register1' and 'register2' that added to different refs, then you would 
get a dispatch among the set of functions passed to either 'register1' 
or 'register2'.

There are ways of improving the precision, but they add analysis time 
and didn't demonstrate a lot of improvement; see the 
Cejtin/Jagannathan/Weeks; ESOP 2000 paper.

> I'm trying to wrap the standard C idiom of 'void registercb(const char* 
> name, void (*cb)(void* uarg, ...), void* uarg);'. My best idea so far is 
> to use the 'uarg' as a word that is the index into some SML-side vector 
> of callback functions. eg:
>> local
>>   val fns = GrowingVector.empty
>>   fun runOne (id, x) = GrowingVector.sub (fns, Word.toInt id) x
>>   val () = _export "mlton_lib_ufnhook": (word * ... -> unit) -> unit; 
>> runOne
>>   val runOne_addr = _address "mlton_lib_ufnhook" : MLton.Pointer.t;
>>   val Cregistercb = _import "registercb" : string * MLton.Pointer.t * 
>> word -> unit;
>> in
>>   fun registercb (name, f) = registercb (name, runOne_addr, 
>> Word.fromInt (GrowingVector.insert f))
>> end
> Is there a better way? Can MLton (still) recognize that only 
> registercb'd methods need be in the switch?

I think that this is the currently best known solution.  As noted above, 
MLton will only merge escaping functions of the same type.  If the only 
way for a function of type 'word * int -> unit' to escape is to be 
passed to 'registercb', then you'll get a switch of exactly those.  But, 
if you have different ways of registering functions with the same type, 
you'll get a switch over all of them.

> This idiom appears in enough C libraries that we should really have a 
> good solution for this in the FFI section of the wiki. If a good 
> solution is relatively complex, perhaps we should offer a small library 
> (I'l volunteer to write it). For my scenario, it's quite important that 
> this be as fast as possible---callbacks are invoked inside a tight loop, 
> and the callbacks themselves are very simple.

It's hard to come up with a general library, since the type of the 
callback functions often change from C library to C library.

To complicate matters, some C callbacks don't give you a void* to hang 
callback specific data; I think John Reppy has mentioned that some 
OpenGL bindings are of that form.  For that, we'd like to eventually 
support an "indirect export" mechanism:

   _export * : (int -> unit) -> MLton.Pointer.t;

which, upon each execution, would allocate a new code stub (not in the 
ML heap, since we can't move the code pointer after giving it to C) for 
calling the provided ML function.  Under the hood, it would work very 
much like your solution, using some sort of key lookup, but the key 
would be written directly into the code stub.

> Also, what's the best known way to implement GrowingVector? I've been using
>> datatype 'a used = FREE of int | USED of 'a
>> type 'a t = { free: int, buf: 'a used array }
> where buf doubles in size when free = ~1. This isn't such a big deal, 
> since 'registercb' is rarely invoked compared to 'runOne'. However, I'd 
> like to know a better solution.

That's pretty close to the resizeable-array.fun implementation in the 
MLton sources.

More information about the MLton mailing list