跳至內容

回呼 (Callbacks)

您可以在 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 區塊將無法如預期般運作。