1
//! Host-language callback invoker registry.
2
//!
3
//! Managed-FFI bindings (Lua, Ruby, Perl, PHP, OCaml, Node, C#, Java, …) can't
4
//! generate C-ABI trampolines for callback typedefs that take aggregate args
5
//! by value — that's a libffi / LuaJIT FFI / ruby-ffi limitation we can't fix
6
//! at the host. This module provides the alternative the user's analysis
7
//! settled on: each language registers **one** generic invoker function at
8
//! module load time, plus a releaser that fires when a host-language handle
9
//! goes out of use.
10
//!
11
//! Every callback the host registers becomes a `Callback { cb, ctx }` pair
12
//! whose `cb` is a *static thunk* in libazul (so by-value args land on a
13
//! native frame the way the framework already expects), and whose `ctx` is
14
//! a `RefAny` payload that carries an opaque host-language `u64` handle.
15
//! The thunk reads `info.get_ctx()`, extracts the handle, and dispatches to
16
//! the registered per-kind invoker — which, on the host side, looks up the
17
//! callable by id in a host-managed table and runs it. When the RefAny's
18
//! refcount drops to zero, the destructor calls back through the registered
19
//! releaser so the host can drop its table entry, mirroring Python's
20
//! `Py<PyAny>` lifetime story without making libazul link against any host
21
//! runtime.
22
//!
23
//! ## API surface
24
//!
25
//! - [`AzApp_setHostHandleReleaser`] — register the host's "drop this id"
26
//!   callback once per process. Fires when a host-handle [`RefAny`] is
27
//!   collected.
28
//! - Per callback kind, [`crate::impl_managed_callback!`] expands to:
29
//!   - A static thunk (`extern "C" fn`) compiled into libazul.
30
//!   - A `<Wrapper>::create_from_host_handle(u64)` constructor.
31
//!   - An `AzApp_set<Kind>Invoker(...)` setter for the host-side per-kind
32
//!     pointer-arg invoker.
33
//!
34
//! ## Why a single shared releaser
35
//!
36
//! Per-kind invokers are necessarily distinct — each callback typedef has
37
//! a different signature, so the host has to register a libffi closure per
38
//! typedef anyway. The releaser, on the other hand, has the same signature
39
//! for every kind (`extern "C" fn(u64)`), so we can share one slot across
40
//! all callbacks; the host registers it once and every kind's destructor
41
//! routes through it.
42

            
43
use core::ffi::c_void;
44
use core::sync::atomic::{AtomicUsize, Ordering};
45

            
46
use azul_css::AzString;
47

            
48
use crate::refany::RefAny;
49

            
50
/// RTTI id stamped into every RefAny created via [`host_handle_to_refany`].
51
///
52
/// Hosts must not reuse this id for their own user-data RefAnys, otherwise
53
/// `refany_to_host_handle` would mis-identify their data as a host handle
54
/// and the destructor would call the registered releaser with a bogus id.
55
/// The high 32 bits are reserved for azul-internal RTTI ids; the low 32
56
/// spell `'H','S','T','H'` so the value reads `0xA20A_4853_5448_5F44`.
57
pub const AZ_HOST_HANDLE_RTTI_ID: u64 = 0xA20A_4853_5448_5F44;
58

            
59
/// Heap payload stored inside the [`RefAny`] returned by
60
/// [`host_handle_to_refany`]. Just the opaque host-language id — the actual
61
/// host callable lives on the host side keyed by this id.
62
#[repr(C)]
63
pub struct HostHandlePayload {
64
    pub id: u64,
65
}
66

            
67
/// A single atomic-pointer slot for one registered host-side function
68
/// pointer. `0` means "not registered"; the static thunks bail out (returning
69
/// the kind's default value) when they see an unregistered slot rather than
70
/// transmuting `0` into a fn pointer and crashing.
71
#[repr(C)]
72
pub struct InvokerSlot {
73
    fn_ptr: AtomicUsize,
74
}
75

            
76
impl InvokerSlot {
77
    /// Create an empty slot. `const` so it can be used to declare `static`
78
    /// per-kind slots in `impl_managed_callback!` expansions.
79
    pub const fn new() -> Self {
80
        Self {
81
            fn_ptr: AtomicUsize::new(0),
82
        }
83
    }
84

            
85
    /// Replace the registered function pointer.
86
    ///
87
    /// `SeqCst` because the slot is read on every callback fire and we
88
    /// don't want any stale-pointer windows after the host swaps invokers
89
    /// (rare but legal — e.g. unloading a Lua module that registered).
90
68
    pub fn set(&self, ptr: usize) {
91
68
        self.fn_ptr.store(ptr, Ordering::SeqCst);
92
68
    }
93

            
94
    /// Read the current function pointer; `0` if unregistered.
95
85
    pub fn get(&self) -> usize {
96
85
        self.fn_ptr.load(Ordering::SeqCst)
97
85
    }
98
}
99

            
100
impl Default for InvokerSlot {
101
    fn default() -> Self {
102
        Self::new()
103
    }
104
}
105

            
106
/// Process-global slot for the host's "drop a handle id" callback. Set via
107
/// [`AzApp_setHostHandleReleaser`]. Read by [`host_handle_destructor`]
108
/// when a host-handle [`RefAny`]'s last clone drops.
109
pub static HOST_HANDLE_RELEASER: InvokerSlot = InvokerSlot::new();
110

            
111
/// Process-global slot for the host's *generic* invoker. Set via
112
/// [`AzApp_setGenericInvoker`]. Used as a fallback in macro-generated
113
/// per-kind thunks when the per-kind invoker is not registered, and as
114
/// the **only** dispatch path for user-defined custom callback kinds in
115
/// libffi-restricted hosts (Lua, PHP, koffi, …) that can't easily ship
116
/// an upstream `impl_managed_callback!` invocation.
117
///
118
/// Signature on the host side:
119
///
120
/// ```c
121
/// typedef void (*AzGenericInvoker)(
122
///     uint64_t           handle,    /* host-handle id from the RefAny ctx */
123
///     const char*        kind,      /* null-terminated wrapper name */
124
///     const void* const* args,      /* array of pointers, one per arg, in declared order */
125
///     size_t             n_args,    /* args[] length */
126
///     void*              ret        /* where to write the return value (kind-specific size) */
127
/// );
128
/// extern void AzApp_setGenericInvoker(AzGenericInvoker);
129
/// ```
130
///
131
/// The args array carries pointers into the framework's by-value frame
132
/// — host code must not retain them past the call. The host decides what
133
/// to do per kind from the `kind` string (which matches the wrapper
134
/// struct name, e.g. `"Callback"`, `"LayoutCallback"`,
135
/// `"ButtonOnClickCallback"`).
136
pub static GENERIC_INVOKER: InvokerSlot = InvokerSlot::new();
137

            
138
/// Type alias for the generic invoker callable. Hosts cast a libffi
139
/// closure to this signature once at module load.
140
pub type AzGenericInvoker = extern "C" fn(
141
    handle: u64,
142
    kind: *const core::ffi::c_char,
143
    args: *const *const c_void,
144
    n_args: usize,
145
    ret: *mut c_void,
146
);
147

            
148
/// Register the generic invoker for user-defined custom callback kinds
149
/// or as a fallback for per-kind dispatch. Called once at module load;
150
/// subsequent registrations replace the previous slot.
151
///
152
/// Safety: `invoker` must be a valid [`AzGenericInvoker`] function
153
/// pointer for the lifetime of any callback that might be dispatched
154
/// through it — typically the whole process.
155
#[no_mangle]
156
17
pub extern "C" fn AzApp_setGenericInvoker(invoker: AzGenericInvoker) {
157
17
    GENERIC_INVOKER.set(invoker as usize);
158
17
}
159

            
160
/// Register the host-language releaser. Hosts call this once at module
161
/// load time; subsequent registrations replace the previous slot.
162
///
163
/// `releaser` will be invoked as `releaser(id)` whenever a host-handle
164
/// `RefAny` (the kind built by [`host_handle_to_refany`]) drops its last
165
/// reference. The host should remove `id` from whatever id→callable table
166
/// it maintains.
167
///
168
/// Safety: `releaser` must be a valid `extern "C" fn(u64)` for the lifetime
169
/// of any host-handle [`RefAny`] that may still be alive — typically the
170
/// whole process. Passing a function pointer that becomes invalid (e.g.,
171
/// from an unloaded library) without first re-registering will cause a
172
/// crash on the next collection.
173
#[no_mangle]
174
51
pub extern "C" fn AzApp_setHostHandleReleaser(releaser: extern "C" fn(u64)) {
175
51
    HOST_HANDLE_RELEASER.set(releaser as usize);
176
51
}
177

            
178
/// Destructor stamped into every host-handle [`RefAny`]. Reads the payload's
179
/// `id` and forwards it to the registered releaser; if no releaser has been
180
/// registered (e.g., host hasn't initialized yet, or this is a release-build
181
/// dll loaded by a non-managed-FFI consumer) the destructor is a no-op so
182
/// the C side doesn't crash.
183
51
extern "C" fn host_handle_destructor(ptr: *mut c_void) {
184
51
    if ptr.is_null() {
185
        return;
186
51
    }
187
    // SAFETY: the destructor only runs for RefAnys built via
188
    // host_handle_to_refany, whose payload type is HostHandlePayload.
189
51
    let payload = unsafe { &*(ptr as *const HostHandlePayload) };
190

            
191
51
    let releaser_addr = HOST_HANDLE_RELEASER.get();
192
51
    if releaser_addr == 0 {
193
        return;
194
51
    }
195
    // SAFETY: HOST_HANDLE_RELEASER only ever holds a value that came from
196
    // `releaser as usize` in `AzApp_setHostHandleReleaser`, where `releaser`
197
    // is an `extern "C" fn(u64)`.
198
51
    let releaser: extern "C" fn(u64) = unsafe { core::mem::transmute(releaser_addr) };
199
51
    releaser(payload.id);
200
51
}
201

            
202
/// Wrap a host-language `u64` handle in a [`RefAny`] suitable for storing
203
/// in a callback wrapper's `ctx` field.
204
///
205
/// The returned RefAny's destructor calls back through the registered
206
/// host releaser when the last clone is dropped, giving the host an
207
/// opportunity to release whatever its `id` was keying.
208
51
pub fn host_handle_to_refany(id: u64) -> RefAny {
209
51
    let payload = HostHandlePayload { id };
210
51
    let type_name: AzString = "AzHostHandle".into();
211
51
    RefAny::new_c(
212
51
        &payload as *const HostHandlePayload as *const c_void,
213
51
        core::mem::size_of::<HostHandlePayload>(),
214
51
        core::mem::align_of::<HostHandlePayload>(),
215
        AZ_HOST_HANDLE_RTTI_ID,
216
51
        type_name,
217
51
        host_handle_destructor,
218
        0,
219
        0,
220
    )
221
51
}
222

            
223
/// Read the host-language id back out of a [`RefAny`] previously created
224
/// via [`host_handle_to_refany`]. Returns `None` for any other RefAny, so
225
/// a static thunk that mistakenly receives a non-host-handle ctx falls
226
/// back to the kind's default value rather than reading random bytes.
227
51
pub fn refany_to_host_handle(refany: &RefAny) -> Option<u64> {
228
51
    if !refany.is_type(AZ_HOST_HANDLE_RTTI_ID) {
229
17
        return None;
230
34
    }
231
34
    let ptr = refany.get_data_ptr() as *const HostHandlePayload;
232
34
    if ptr.is_null() {
233
        return None;
234
34
    }
235
    // SAFETY: type-id check above guarantees the payload was a HostHandlePayload.
236
34
    Some(unsafe { (*ptr).id })
237
51
}
238

            
239
/// C-ABI: build a [`RefAny`] wrapping a host-language id. Lets managed-FFI
240
/// bindings use the same machinery for user data that callbacks already use
241
/// — one releaser, one id-keyed table, one lifetime story.
242
///
243
/// The returned RefAny's destructor fires the releaser registered via
244
/// [`AzApp_setHostHandleReleaser`] once the last clone drops, so the host
245
/// can drop its `id → value` entry.
246
#[no_mangle]
247
pub extern "C" fn AzRefAny_newHostHandle(id: u64) -> RefAny {
248
    host_handle_to_refany(id)
249
}
250

            
251
/// C-ABI: read the host-language id from a [`RefAny`] previously built via
252
/// [`AzRefAny_newHostHandle`] (or any other host-handle constructor).
253
///
254
/// Returns `0` if `refany` is null or wasn't a host handle. Host bindings
255
/// must reserve `0` as "no value" — [`host_handle_to_refany`] never produces
256
/// `0` if the host's id allocator starts at `1` (the convention used by
257
/// every binding in this repo).
258
#[no_mangle]
259
pub extern "C" fn AzRefAny_getHostHandle(refany: *const RefAny) -> u64 {
260
    if refany.is_null() {
261
        return 0;
262
    }
263
    // SAFETY: caller's responsibility per `*const` signature.
264
    let r = unsafe { &*refany };
265
    refany_to_host_handle(r).unwrap_or(0)
266
}
267

            
268
/// Macro that expands to the per-callback-kind boilerplate: a static thunk
269
/// (compiled into libazul) that the framework calls with by-value args, a
270
/// `<Wrapper>::create_from_host_handle(u64)` constructor, and an
271
/// `AzApp_set<Kind>Invoker` setter the host calls once at module load.
272
///
273
/// All identifiers are passed in explicitly so we don't need a proc-macro
274
/// dependency just to concatenate idents. Codegen emits invocations of this
275
/// macro from `ir.callback_typedefs`.
276
///
277
/// Caller responsibilities:
278
///
279
/// - The wrapper type must have public fields `cb: <typedef>` and
280
///   `ctx: OptionRefAny` — that's the standard shape every callback wrapper
281
///   in the framework already follows.
282
/// - `info_ty` must expose a `.get_ctx() -> OptionRefAny` method (also
283
///   standard for `*CallbackInfo` types).
284
/// - `default_ret` is returned when:
285
///   - the framework invokes the thunk with `OptionRefAny::None` ctx
286
///     (host called the typedef directly without going through this path),
287
///   - the ctx isn't a host-handle (host registered the wrapper but the
288
///     ctx came from somewhere else),
289
///   - or no invoker has been registered yet for this kind. Pick a value
290
///     that can't be confused with a "real" return — typically the kind's
291
///     "do nothing" / "empty body" default.
292
#[macro_export]
293
macro_rules! impl_managed_callback {
294
    // Form 1: simple two-argument callbacks `(RefAny, info) -> ret` —
295
    // matches `Callback`, `LayoutCallback`, `ButtonOnClickCallback`,
296
    // and the bulk of widget event callbacks. Identical to the
297
    // extras-form below with an empty extra-args list.
298
    (
299
        wrapper:        $wrapper:ty,
300
        info_ty:        $info_ty:ty,
301
        return_ty:      $ret:ty,
302
        default_ret:    $default:expr,
303
        invoker_static: $invoker_static:ident,
304
        invoker_ty:     $invoker_ty:ident,
305
        thunk_fn:       $thunk_fn:ident,
306
        setter_fn:      $setter_fn:ident,
307
        from_handle_fn: $from_handle_fn:ident,
308
    ) => {
309
        $crate::impl_managed_callback! {
310
            wrapper:        $wrapper,
311
            info_ty:        $info_ty,
312
            return_ty:      $ret,
313
            default_ret:    $default,
314
            invoker_static: $invoker_static,
315
            invoker_ty:     $invoker_ty,
316
            thunk_fn:       $thunk_fn,
317
            setter_fn:      $setter_fn,
318
            from_handle_fn: $from_handle_fn,
319
            extra_args:     [],
320
        }
321
    };
322
    // Form 2: callbacks that take additional state after info — e.g.
323
    // `CheckBoxOnToggleCallback(RefAny, CallbackInfo, CheckBoxState)`.
324
    // The extras list is forwarded by reference into the host invoker
325
    // so libffi-style runtimes never have to handle aggregate-by-value
326
    // returns OR aggregate-by-value args.
327
    (
328
        wrapper:        $wrapper:ty,
329
        info_ty:        $info_ty:ty,
330
        return_ty:      $ret:ty,
331
        default_ret:    $default:expr,
332
        invoker_static: $invoker_static:ident,
333
        invoker_ty:     $invoker_ty:ident,
334
        thunk_fn:       $thunk_fn:ident,
335
        setter_fn:      $setter_fn:ident,
336
        from_handle_fn: $from_handle_fn:ident,
337
        extra_args:     [ $( $extra_name:ident : $extra_ty:ty ),* $(,)? ] $(,)?
338
    ) => {
339
        /// Process-global slot for this callback kind's host-side invoker.
340
        pub static $invoker_static: $crate::host_invoker::InvokerSlot =
341
            $crate::host_invoker::InvokerSlot::new();
342

            
343
        /// Pointer-arg variant of this callback kind's typedef.
344
        ///
345
        /// The host's libffi closure casts to this signature (which all
346
        /// managed-FFI runtimes can handle — args and return are passed
347
        /// by pointer, no aggregate-by-value anywhere). The static thunk
348
        /// in libazul does the by-value plumbing on the C ABI side.
349
        ///
350
        /// LuaJIT FFI in particular cannot return aggregates larger than
351
        /// 8 bytes from a callback, so we use an out-pointer for the
352
        /// return value uniformly across kinds — even for `Update` which
353
        /// would fit in a register, so the macro stays homogeneous.
354
        pub type $invoker_ty = extern "C" fn(
355
            handle: u64,
356
            data: *const $crate::refany::RefAny,
357
            info: *const $info_ty,
358
            $( $extra_name : *const $extra_ty , )*
359
            out: *mut $ret,
360
        );
361

            
362
        /// Register the host-side invoker for this callback kind.
363
        #[no_mangle]
364
        pub extern "C" fn $setter_fn(invoker: $invoker_ty) {
365
            $invoker_static.set(invoker as usize);
366
        }
367

            
368
        /// Static thunk compiled into libazul. The framework calls this
369
        /// with by-value args; we extract the host handle from `info.ctx`,
370
        /// allocate space for the return value on our stack, and forward
371
        /// pointers to the registered invoker.
372
        extern "C" fn $thunk_fn(
373
            data: $crate::refany::RefAny,
374
            info: $info_ty,
375
            $( $extra_name : $extra_ty , )*
376
        ) -> $ret {
377
            let ctx = info.get_ctx();
378
            let handle = match ctx {
379
                $crate::refany::OptionRefAny::Some(ref refany) => {
380
                    match $crate::host_invoker::refany_to_host_handle(refany) {
381
                        Some(id) => id,
382
                        None => return $default,
383
                    }
384
                }
385
                _ => return $default,
386
            };
387
            let invoker_addr = $invoker_static.get();
388
            if invoker_addr == 0 {
389
                // Per-kind invoker not registered — fall back to the
390
                // generic invoker for hosts that wired up only the
391
                // single `AzApp_setGenericInvoker` slot (or for custom
392
                // user-defined kinds emitted by a downstream
393
                // `impl_managed_callback!` whose host hasn't shipped a
394
                // per-kind invoker setter yet).
395
                let generic_addr = $crate::host_invoker::GENERIC_INVOKER.get();
396
                if generic_addr == 0 {
397
                    return $default;
398
                }
399
                // SAFETY: GENERIC_INVOKER only ever holds an address that
400
                // came from `invoker as usize` in `AzApp_setGenericInvoker`,
401
                // whose parameter is typed as `AzGenericInvoker`.
402
                let generic: $crate::host_invoker::AzGenericInvoker =
403
                    unsafe { core::mem::transmute(generic_addr) };
404

            
405
                // Wrapper name as a null-terminated C string. `stringify!`
406
                // expands `$wrapper:ty` to e.g. `Callback`,
407
                // `ButtonOnClickCallback`, etc. — matching what the host's
408
                // dispatch table keys on.
409
                const KIND_STR: &str = concat!(stringify!($wrapper), "\0");
410

            
411
                // Build the args array: pointers to each by-value frame
412
                // arg, in declared order (data, info, extras…). Lifetime
413
                // is the scope of this thunk; the host MUST NOT retain
414
                // these pointers past the call. Array size is inferred
415
                // (2 base args + however many extras the macro forwarded).
416
                let args = [
417
                    &data as *const _ as *const core::ffi::c_void,
418
                    &info as *const _ as *const core::ffi::c_void,
419
                    $( & $extra_name as *const _ as *const core::ffi::c_void , )*
420
                ];
421

            
422
                let mut out: $ret = $default;
423
                generic(
424
                    handle,
425
                    KIND_STR.as_ptr() as *const core::ffi::c_char,
426
                    args.as_ptr(),
427
                    args.len(),
428
                    &mut out as *mut _ as *mut core::ffi::c_void,
429
                );
430
                return out;
431
            }
432
            // SAFETY: $invoker_static only ever holds a value that came from
433
            // `invoker as usize` in `$setter_fn`, where `invoker` has type
434
            // `$invoker_ty`.
435
            let invoker: $invoker_ty = unsafe { core::mem::transmute(invoker_addr) };
436

            
437
            // Pre-fill `out` with the kind's default so a host that fails
438
            // to write to the out-pointer (e.g. a buggy invoker) leaves us
439
            // with a sane value rather than uninitialized memory.
440
            let mut out: $ret = $default;
441
            invoker(
442
                handle,
443
                &data as *const $crate::refany::RefAny,
444
                &info as *const $info_ty,
445
                $( & $extra_name as *const $extra_ty , )*
446
                &mut out as *mut $ret,
447
            );
448
            out
449
        }
450

            
451
        impl $wrapper {
452
            /// Build a wrapper whose `cb` is the static thunk above and
453
            /// whose `ctx` carries the host's `u64` handle. The host
454
            /// language is responsible for keeping its id→callable table
455
            /// in sync with the releaser registered via
456
            /// `AzApp_setHostHandleReleaser`.
457
17
            pub fn create_from_host_handle(handle: u64) -> Self {
458
17
                Self {
459
17
                    cb: $thunk_fn,
460
17
                    ctx: $crate::refany::OptionRefAny::Some(
461
17
                        $crate::host_invoker::host_handle_to_refany(handle),
462
17
                    ),
463
17
                }
464
17
            }
465
        }
466

            
467
        /// C-ABI export wrapping `<Wrapper>::create_from_host_handle`.
468
        #[no_mangle]
469
        pub extern "C" fn $from_handle_fn(handle: u64) -> $wrapper {
470
            <$wrapper>::create_from_host_handle(handle)
471
        }
472
    };
473
}