Skip to main content

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}