logicaffeine_compile/vm/aot_tier.rs
1//! AOT-native tier loader (HOTSWAP §Axis-3 / P15): `dlopen` a rustc-built cdylib
2//! (produced by [`crate::compile::build_native_cdylib`]) and wrap an exported
3//! `logos_native_<fn>` symbol as a [`NativeFn`] the VM dispatches through the existing
4//! `NativeSlot::Ready` path — no new dispatch, the hot-swap seam already exists.
5//!
6//! Sound subset: ALL-INT signatures (every param + the return `Int`). Those cross by
7//! value in general-purpose registers, so a single `i64` calling convention is exact.
8//! Float/Bool need per-type fn-pointer ABIs (f64 rides XMM) and are deferred — a
9//! function outside the subset simply gets no AOT fn and stays on VM+JIT (no gap).
10//!
11//! The interpreter is the only caller of the loaded symbol, and ownership of the
12//! `Library` is held for the program's lifetime (an `Arc` inside the `NativeFn`), so
13//! the code stays mapped while any call can occur.
14
15#![cfg(not(target_arch = "wasm32"))]
16
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::sync::Arc;
19
20use super::native_tier::{NativeFn, NativeOutcome, NativeRet, SlotKind};
21
22/// One AOT-native function: the resolved symbol address, its arity, the `Library` it
23/// lives in (kept alive), and a call counter for observability/tests. `Send + Sync`
24/// because the address is immutable and the code is read-only once loaded.
25pub struct AotNativeFn {
26 addr: usize,
27 arity: usize,
28 _lib: Arc<libloading::Library>,
29 calls: Arc<AtomicU64>,
30}
31
32// SAFETY: `addr` is an immutable code address in a mapped, read-only library kept
33// alive by `_lib`; calling it is the same on any thread (the interpreter is the sole
34// caller). `calls` is atomic.
35unsafe impl Send for AotNativeFn {}
36unsafe impl Sync for AotNativeFn {}
37
38impl AotNativeFn {
39 /// Number of times this AOT function has been invoked (observability/tests).
40 pub fn call_count(&self) -> u64 {
41 self.calls.load(Ordering::Relaxed)
42 }
43}
44
45impl NativeFn for AotNativeFn {
46 fn call(&self, args: &[i64], _pins: &[i64], _depth: usize) -> NativeOutcome {
47 self.calls.fetch_add(1, Ordering::Relaxed);
48 let a = self.addr;
49 // SAFETY: `a` is the address of a `extern "C" fn(i64..) -> i64` (the
50 // `logos_native_<fn>` shim) for an all-Int signature of this arity; the VM's
51 // entry guard already proved every `args[k]` is an `Int`.
52 let r = unsafe {
53 match self.arity {
54 0 => std::mem::transmute::<usize, extern "C" fn() -> i64>(a)(),
55 1 => std::mem::transmute::<usize, extern "C" fn(i64) -> i64>(a)(args[0]),
56 2 => std::mem::transmute::<usize, extern "C" fn(i64, i64) -> i64>(a)(args[0], args[1]),
57 3 => std::mem::transmute::<usize, extern "C" fn(i64, i64, i64) -> i64>(a)(
58 args[0], args[1], args[2],
59 ),
60 4 => std::mem::transmute::<usize, extern "C" fn(i64, i64, i64, i64) -> i64>(a)(
61 args[0], args[1], args[2], args[3],
62 ),
63 // Higher arities aren't loaded yet — bail to the bytecode path.
64 _ => return NativeOutcome::Deopt,
65 }
66 };
67 NativeOutcome::Return(r)
68 }
69
70 fn ret(&self) -> NativeRet {
71 NativeRet::Scalar(SlotKind::Int)
72 }
73
74 fn entry_ptr(&self) -> i64 {
75 // AOT functions are reached via the Rust-level `NativeSlot::Ready` dispatch,
76 // NOT the FnTable stencil fast-path (whose raw-pointer ABI differs). They are
77 // never published to the FnTable, so this is unused.
78 0
79 }
80
81 fn published_regc(&self) -> i64 {
82 0
83 }
84}
85
86/// `dlopen` `path` and resolve the AOT-native `symbol` (`logos_native_<fn>`) as an
87/// all-Int function of `arity`, wrapping it as a [`NativeFn`] plus its shared call
88/// counter. Returns `None` if the library or symbol cannot be loaded (the caller then
89/// keeps the function on VM+JIT — no gap at the seam).
90pub fn load_aot_native(
91 path: &std::path::Path,
92 symbol: &str,
93 arity: usize,
94) -> Option<(Box<dyn NativeFn>, Arc<AtomicU64>)> {
95 // SAFETY: loading an arbitrary library runs its initializers; the caller supplies a
96 // path it just built (or a cache entry validated by toolchain hash).
97 let lib = unsafe { libloading::Library::new(path).ok()? };
98 // Probe the symbol exists (typed as a fn so resolution is a function lookup).
99 let addr = unsafe {
100 let sym: libloading::Symbol<'_, extern "C" fn() -> i64> = lib.get(symbol.as_bytes()).ok()?;
101 (*sym) as usize
102 };
103 let calls = Arc::new(AtomicU64::new(0));
104 let nf = AotNativeFn {
105 addr,
106 arity,
107 _lib: Arc::new(lib),
108 calls: calls.clone(),
109 };
110 Some((Box::new(nf), calls))
111}