Skip to content

Callbacks

You can use function types in C declarations:

lib X
  # In C:
  #
  #    void callback(int (*f)(int));
  fun callback(f : Int32 -> Int32)
end

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

f = ->(x : Int32) { x + 1 }
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:

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:

y = 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:

# Same as callback(NULL) in C
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:

lib LibTicker
  fun on_tick(callback : (Int32, Void* ->), data : Void*)
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.

module Ticker
  # The callback for the user doesn't have a Void*
  @@box : Pointer(Void)?

  def self.on_tick(&callback : Int32 ->)
    # Since Proc is a {Void*, Void*}, we can't turn that into a Void*, so we
    # "box" it: we allocate memory and store the Proc there
    boxed_data = Box.box(callback)

    # We must save this in Crystal-land so the GC doesn't collect it (*)
    @@box = boxed_data

    # We pass a callback that doesn't form a closure, and pass the boxed_data as
    # the callback data
    LibTicker.on_tick(->(tick, data) {
      # Now we turn data back into the Proc, using Box.unbox
      data_as_callback = Box(typeof(callback)).unbox(data)
      # And finally invoke the user's callback
      data_as_callback.call(tick)
    }, boxed_data)
  end
end

Ticker.on_tick do |tick|
  puts tick
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:

lib LibFoo
  fun store_callback(callback : ->)
  fun execute_callback
end

LibFoo.store_callback ->{ raise "OH NO!" }
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:

lib LibFoo
  fun store_callback(callback : ->)

  @[Raises]
  fun execute_callback
end

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