module
UI::CallbackRegistry
Overview
Module-level registry that prevents Crystal Proc closures from being
garbage collected while native code holds references to them.
Problem
When Crystal passes a Proc as a C function pointer (e.g., to the ObjC
target-action mechanism or JNI callback bridge), BoehmGC may collect the
closure if no Crystal-side reference remains. This causes a use-after-free
crash when the native side later invokes the callback.
Solution
CallbackRegistry holds a strong reference to every registered Proc in
a module-level Hash. Each registration returns a unique UInt64 ID that
the native side stores. When the native callback fires, it passes the ID
back to Crystal, which looks up and invokes the live Proc.
Lifecycle
- Register a callback:
id = CallbackRegistry.register(proc) - Pass
idto the native side (stored in SEL name, JNI callback ID, etc.) - Native fires: calls
crystal_ui_callback_dispatch(id)->CallbackRegistry.call(id) - On teardown:
CallbackRegistry.unregister(id)removes the strong reference
Thread Safety
The registry itself is not thread-safe. All registration, unregistration, and callback dispatch should happen on the main thread, which matches the normal UI callback model for AppKit/UIKit/Android.
Defined in:
ui/native/callback_registry.crClass Method Summary
-
.call(id : UInt64) : Nil
Invoke the callback registered under the given ID.
-
.call_bool(id : UInt64, value : Bool) : Nil
Invoke the Bool callback registered under the given ID.
-
.call_float(id : UInt64, value : Float64) : Nil
Invoke the Float64 callback registered under the given ID.
-
.call_int(id : UInt64, value : Int32) : Nil
Invoke the Int32 callback registered under the given ID.
-
.call_string(id : UInt64, value : String) : Nil
Invoke the String callback registered under the given ID.
-
.call_string_bool(id : UInt64, value : String) : Bool
Invoke the String -> Bool callback registered under the given ID.
-
.call_time(id : UInt64, value : Time) : Nil
Invoke the Time callback registered under the given ID.
-
.clear : Nil
Remove all registered callbacks from all typed hashes.
-
.invoke_swiftkit(token : UInt64, value : Float64) : Nil
Dispatch a SwiftKit action by token.
-
.invoke_swiftkit_string(token : UInt64, value : String) : Nil
Phase 6.10 Rem 4 (Item 1) — string-valued counterpart to
.invoke_swiftkit. -
.register(callback : Proc(Nil)) : UInt64
Register a callback proc and return its unique ID.
-
.register(&block : -> Nil) : UInt64
Register a callback block and return its unique ID.
-
.register_action(&block : -> Nil) : UInt64
Register a no-arg SwiftKit action (Button#on_tap, MenuItem activation).
-
.register_action_with_value(&block : Float64 -> Nil) : UInt64
Register a Float64-valued SwiftKit action (Slider#on_change, Stepper#on_change, Toggle#on_change after Bool→Float64 coercion).
-
.register_bool(callback : Proc(Bool, Nil)) : UInt64
Register a Bool callback proc and return its unique ID.
-
.register_float(callback : Proc(Float64, Nil)) : UInt64
Register a Float64 callback proc and return its unique ID.
-
.register_int(callback : Proc(Int32, Nil)) : UInt64
Register an Int32 callback proc and return its unique ID.
-
.register_string(callback : Proc(String, Nil)) : UInt64
Register a String callback proc and return its unique ID.
-
.register_string_bool(callback : Proc(String, Bool)) : UInt64
Register a String -> Bool callback proc and return its unique ID.
-
.register_time(callback : Proc(Time, Nil)) : UInt64
Register a Time callback proc and return its unique ID.
-
.size : Int32
Returns the number of currently registered callbacks across all typed hashes.
-
.unregister(id : UInt64) : Nil
Remove the callback registered under the given ID.
-
.unregister(ids : Array(UInt64)) : Nil
Remove multiple callbacks by their IDs.
Class Method Detail
Invoke the callback registered under the given ID.
If the ID is not found (e.g., it was already unregistered), this is a safe no-op. This prevents crashes if a native callback fires after the Crystal side has torn down.
Invoke the Bool callback registered under the given ID.
Invoke the Float64 callback registered under the given ID.
Invoke the Int32 callback registered under the given ID.
Invoke the String callback registered under the given ID.
Invoke the String -> Bool callback registered under the given ID.
Missing IDs default to true so native policy delegates stay permissive
instead of failing closed when a view has already been torn down.
Invoke the Time callback registered under the given ID.
Remove all registered callbacks from all typed hashes.
Intended for use in test cleanup (Spec.after_each). Do NOT call
this in production code -- use .unregister for targeted cleanup.
Dispatch a SwiftKit action by token. Called from the
ap_swiftkit_invoke_action C trampoline.
Looks the token up across the relevant typed registries:
- The no-arg Proc(Nil) registry (Button taps fire here).
- The Float64 registry (Slider/Stepper/Toggle fire here).
Unknown tokens fall through silently — mirrors the existing
crystal_ui_*_callback_dispatch convention so a stale callback
fired by Swift after Crystal teardown does not crash.
Phase 6.10 Rem 4 (Item 1) — string-valued counterpart to
.invoke_swiftkit. Routes Proc(String, Nil) callbacks fired by
AssetPipelineSwiftKit's string trampoline. Unknown tokens fall
through silently — mirrors the float channel's policy.
Register a callback proc and return its unique ID.
The proc is held by strong reference until .unregister is called.
Register a callback block and return its unique ID.
Convenience overload that wraps a block in a Proc.
Register a no-arg SwiftKit action (Button#on_tap, MenuItem activation).
Returns the opaque UInt64 token the Swift facade keeps.
Register a Float64-valued SwiftKit action (Slider#on_change, Stepper#on_change, Toggle#on_change after Bool→Float64 coercion).
Register a Bool callback proc and return its unique ID.
Register a Float64 callback proc and return its unique ID.
Register an Int32 callback proc and return its unique ID.
Register a String callback proc and return its unique ID.
Register a String -> Bool callback proc and return its unique ID.
Register a Time callback proc and return its unique ID.
Returns the number of currently registered callbacks across all typed hashes.
Remove the callback registered under the given ID.
After this call, the Proc is eligible for GC and the ID will no
longer resolve. Safe to call with an ID that was already unregistered.
Checks all typed hashes.
Remove multiple callbacks by their IDs.
Convenience method for bulk cleanup during NativeView#teardown!.