shape_jit/ffi/generic_builtin.rs
1//! Generic Builtin FFI Trampoline
2//!
3//! A single FFI function that can execute ANY builtin function, eliminating
4//! the need for individual FFI wrappers for each of the ~170 builtins.
5//!
6//! Uses the same static trampoline pattern as async_ops: the VM registers a
7//! dispatch function before JIT execution, and the JIT calls it via
8//! `jit_generic_builtin`.
9
10use super::super::context::JITContext;
11use crate::nan_boxing::TAG_NULL;
12
13/// Static trampoline function pointer for generic builtin dispatch.
14///
15/// Signature: `fn(ctx: *mut JITContext, builtin_id: u16, arg_count: u16) -> u64`
16///
17/// The registered function should:
18/// 1. Read `arg_count` values from `ctx.stack` (popping them)
19/// 2. Dispatch to the appropriate builtin handler
20/// 3. Return the NaN-boxed result
21pub static GENERIC_BUILTIN_FN: std::sync::atomic::AtomicPtr<()> =
22 std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
23
24/// Register the generic builtin dispatch trampoline.
25///
26/// # Safety
27/// The function pointer must be valid for the duration of JIT execution and
28/// must have the signature: `extern "C" fn(*mut JITContext, u16, u16) -> u64`
29pub unsafe fn register_generic_builtin_fn(f: extern "C" fn(*mut JITContext, u16, u16) -> u64) {
30 GENERIC_BUILTIN_FN.store(f as *mut (), std::sync::atomic::Ordering::Release);
31}
32
33/// Clear the generic builtin dispatch registration.
34pub fn unregister_generic_builtin_fn() {
35 GENERIC_BUILTIN_FN.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
36}
37
38/// Execute any builtin function via the registered trampoline.
39///
40/// Called from JIT-compiled code when a builtin is not handled by a dedicated
41/// JIT lowering path. The JIT translator flushes args onto `ctx.stack` before
42/// calling this function.
43///
44/// # Arguments
45/// * `ctx` - JIT execution context (args are on ctx.stack)
46/// * `builtin_id` - The `BuiltinFunction` discriminant as u16
47/// * `arg_count` - Number of arguments on the stack
48///
49/// # Returns
50/// NaN-boxed result, or TAG_NULL on failure.
51#[unsafe(no_mangle)]
52pub extern "C" fn jit_generic_builtin(
53 ctx: *mut JITContext,
54 builtin_id: u16,
55 arg_count: u16,
56) -> u64 {
57 let f = GENERIC_BUILTIN_FN.load(std::sync::atomic::Ordering::Acquire);
58 if f.is_null() {
59 // No trampoline registered — pop args and return null
60 if !ctx.is_null() {
61 let ctx_ref = unsafe { &mut *ctx };
62 let pop_count = (arg_count as usize).min(ctx_ref.stack_ptr);
63 ctx_ref.stack_ptr -= pop_count;
64 }
65 return TAG_NULL;
66 }
67 let dispatch: extern "C" fn(*mut JITContext, u16, u16) -> u64 =
68 unsafe { std::mem::transmute(f) };
69 dispatch(ctx, builtin_id, arg_count)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn test_generic_builtin_null_trampoline() {
78 let mut ctx = JITContext::default();
79 // Push a dummy value on the stack
80 ctx.stack[0] = 42;
81 ctx.stack_ptr = 1;
82
83 let result = jit_generic_builtin(&mut ctx, 0, 1);
84 assert_eq!(result, TAG_NULL);
85 // Args should be popped
86 assert_eq!(ctx.stack_ptr, 0);
87 }
88}