コンテンツへスキップ

コールバック

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ブロックは期待通りに動作しません。