跳至内容

回调

您可以在 C 声明中使用函数类型

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

然后您可以传递一个函数(一个 Proc)就像这样

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

如果您在同一个调用中内联定义函数,您可以省略参数类型,编译器会根据 fun 签名为您添加类型

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

但是,请注意,传递给 C 的函数不能形成闭包。如果编译器在编译时检测到正在传递闭包,则会发出错误

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

如果编译器在编译时无法检测到这一点,则会在运行时引发异常。

请参考 类型语法 以了解回调和 proc 类型中使用的符号。

如果您想传递 NULL 而不是回调,只需传递 nil

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

将闭包传递给 C 函数

大多数情况下,允许设置回调的 C 函数还提供一个用于自定义数据的参数。然后,此自定义数据将作为参数发送到回调函数。例如,假设一个 C 函数在每次滴答时调用回调函数,并将该滴答值作为参数传递

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

为了正确定义此函数的包装器,我们必须将 Proc 作为回调数据发送,然后将该回调数据转换为 Proc,最后调用它。

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

请注意,我们将盒装的回调保存到 @@box 中。原因是,如果我们不这样做,并且我们的代码不再引用它,GC 将会收集它。C 库当然会存储回调,但 Crystal 的 GC 无法知道这一点。

Raises 注解

如果一个 C 函数执行可能引发的用户提供的回调,则必须使用 @[Raises] 注解进行注释。

如果方法调用标记为 @[Raises] 或引发(递归地)的方法,编译器会为此方法推断此注解。

但是,某些 C 函数接受回调函数,这些回调函数由其他 C 函数执行。例如,假设一个虚拟库

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

LibFoo.store_callback ->{ raise "OH NO!" }
LibFoo.execute_callback

如果传递给 store_callback 的回调函数引发,则 execute_callback 将引发。但是,编译器不知道 execute_callback 可能引发,因为它没有标记为 @[Raises],并且编译器无法找出这一点。在这种情况下,您必须手动标记此类函数

lib LibFoo
  fun store_callback(callback : ->)

  @[Raises]
  fun execute_callback
end

如果您不标记它们,则围绕此函数调用的 begin/rescue 代码块将无法按预期工作。