Callbacks
Vous pouvez utiliser des types fonction dans vos déclarations C:
lib X
# In C:
#
# void callback(int (*f)(int));
fun callback(f : Int32 -> Int32)
end
Vous pouvez ensuite passer une fonction (un Proc) ainsi:
f = ->(x : Int32) { x + 1 }
X.callback(f)
Si vous définissez la fonction comme inline dans le même appel vous pouvez omettre les types des arguments,
le compilateur ajoutera pour vous les types en se basant sur la signature de fun
:
X.callback ->(x) { x + 1 }
Remarquez, néanmoins, que les fonctions passées au C ne peuvent former des closures. Si le compilateur détecte à la compilation qu'une closure est passée, une erreur sera renvoyée:
y = 2
X.callback ->(x) { x + y } # Error: can't send closure
# to C function
Si le compilateur ne peut détecter ça à la compilation, une exception sera levée à l'exécution.
Reportez-vous à la grammaire de type pour la notation utilisée dans les types callbacks et procs.
Si vous voulez passer NULL
au lieu d'un callback, passez simplement nil
:
# Same as callback(NULL) in C
X.callback nil
Passer une closure à une fonction C
La plupart du temps une fonction C qui permet de définir une callback fournit aussi un argument pour des données personnalisées. Ces données personnalisées sont ensuite envoyées comme argument au callback. Par exemple, supposez qu'une fonction C qui invoque une callback à chaque toc d'horloge, pour passer ce toc d'horloge:
lib LibTicker
fun on_tick(callback : (Int32, Void* ->), data : Void*)
end
Pour définir proprement un wrapper pour cette fonction nous devons envoyer le Proc comme données du callback, puis convertir ces données de callback en le Proc et finalement l'invoquer.
module Ticker
# La callback pour l'utilisateur n'a pas de Void*
def self.on_tick(&callback : Int32 ->)
# Nous devons enregistrer ça dans l'espace Crystal afin que le ramasse-miettes ne le rammasse pas (*)
@@callback = callback
# Etant donné que Proc est un {Void*, Void*}, on ne peut le transformer en Void*,
# alors nous l'"empaquetons": on alloue de la mémoire et on y stocke le Proc
boxed_data = Box.box(callback)
# Nous passons une callback qui ne forme pas de closure, et passons la boxed_data
# comme données du callback
LibTicker.on_tick(->(tick, data) {
# Maintenant nous transformons les données en le Proc, en utilisant Box.unbox
data_as_callback = Box(typeof(callback)).unbox(data)
# Et on invoque au final le callback utilisateur
data_as_callback.call(tick)
}, boxed_data)
end
end
Ticker.on_tick do |tick|
puts tick
end
Remarquez que nous sauvons le callback dans @@callback
.
La raison est que si nous ne le faisons pas, et que notre code n'y fait plus référence, le ramasse-miettes va le récupérer.
La librairie C va bien sûr stocker le callback, mais le ramasse-miettes de Crystal n'a aucun moyen de le savoir.
Attribut Raises
Si une fonction C exécute une callback utilisateur qui peut lever une exception,
elle doit être annotée avec l'attribut @[Raises]
.
Le compilateur infére cet attribut en tant que méthode s'il invoque une méthode qui est marquée comme @[Raises]
ou qui lève une exception (récursivement).
Néanmoins, certaines fonctions C acceptent des callbacks qui sont exécutées par des fonctions C. Par exemple, prenons une librairie fictive:
lib LibFoo
fun store_callback(callback : ->)
fun execute_callback
end
LibFoo.store_callback ->{ raise "OH NO!" }
LibFoo.execute_callback
Si la callback passée à store_callback
lève une exception, alors execute_callback
en lévera une aussi.
Néanmoins, le compilateur ne sait pas que execute_callback
peut potentiellement lever une exception car elle n'est pas marquée avec @[Raises]
et le compilateur n'a aucun moyen de le savoir. Dans ces cas vous devez marquer manuellement de telles fonctions:
lib LibFoo
fun store_callback(callback : ->)
@[Raises]
fun execute_callback
end
Si vous ne les marquez pas, les blocs begin/rescue
délimitant les appels de cette fonction n'auront pas le résultat attendu.