Callbacks

You can use function types in C declarations:

  1. lib X
  2. # In C:
  3. #
  4. # void callback(int (*f)(int));
  5. fun callback(f : Int32 -> Int32)
  6. end

Then you can pass a function (a Proc) like this:

  1. f = ->(x : Int32) { x + 1 }
  2. X.callback(f)

If you define the function inline in the same call you can omit the parameter types, the compiler will add the types for you based on the fun signature:

  1. X.callback ->(x) { x + 1 }

Note, however, that functions passed to C can’t form closures. If the compiler detects at compile-time that a closure is being passed, an error will be issued:

  1. y = 2
  2. X.callback ->(x) { x + y } # Error: can't send closure to C function

If the compiler can’t detect this at compile-time, an exception will be raised at runtime.

Refer to the type grammar for the notation used in callbacks and procs types.

If you want to pass NULL instead of a callback, just pass nil:

  1. # Same as callback(NULL) in C
  2. X.callback nil

Passing a closure to a C function

Most of the time a C function that allows setting a callback also provides a parameter for custom data. This custom data is then sent as an argument to the callback. For example, suppose a C function that invokes a callback at every tick, passing that tick:

  1. lib LibTicker
  2. fun on_tick(callback : (Int32, Void* ->), data : Void*)
  3. end

To properly define a wrapper for this function we must send the Proc as the callback data, and then convert that callback data to the Proc and finally invoke it.

  1. module Ticker
  2. # The callback for the user doesn't have a Void*
  3. @@box : Pointer(Void)?
  4. def self.on_tick(&callback : Int32 ->)
  5. # Since Proc is a {Void*, Void*}, we can't turn that into a Void*, so we
  6. # "box" it: we allocate memory and store the Proc there
  7. boxed_data = Box.box(callback)
  8. # We must save this in Crystal-land so the GC doesn't collect it (*)
  9. @@box = boxed_data
  10. # We pass a callback that doesn't form a closure, and pass the boxed_data as
  11. # the callback data
  12. LibTicker.on_tick(->(tick, data) {
  13. # Now we turn data back into the Proc, using Box.unbox
  14. data_as_callback = Box(typeof(callback)).unbox(data)
  15. # And finally invoke the user's callback
  16. data_as_callback.call(tick)
  17. }, boxed_data)
  18. end
  19. end
  20. Ticker.on_tick do |tick|
  21. puts tick
  22. end

Note that we save the boxed callback in @@box. The reason is that if we don’t do it, and our code doesn’t reference it anymore, the GC will collect it. The C library will of course store the callback, but Crystal’s GC has no way of knowing that.

Raises annotation

If a C function executes a user-provided callback that might raise, it must be annotated with the @[Raises] annotation.

The compiler infers this annotation for a method if it invokes a method that is marked as @[Raises] or raises (recursively).

However, some C functions accept callbacks to be executed by other C functions. For example, suppose a fictitious library:

  1. lib LibFoo
  2. fun store_callback(callback : ->)
  3. fun execute_callback
  4. end
  5. LibFoo.store_callback ->{ raise "OH NO!" }
  6. LibFoo.execute_callback

If the callback passed to store_callback raises, then execute_callback will raise. However, the compiler doesn’t know that execute_callback can potentially raise because it is not marked as @[Raises] and the compiler has no way to figure this out. In these cases you have to manually mark such functions:

  1. lib LibFoo
  2. fun store_callback(callback : ->)
  3. @[Raises]
  4. fun execute_callback
  5. end

If you don’t mark them, begin/rescue blocks that surround this function’s calls won’t work as expected.