relon_codegen_llvm/vtable.rs
1//! Host-helper indirection table for the LLVM-native AOT backend.
2//! **Phase C.**
3//!
4//! The cranelift backend (`relon-codegen-cranelift::vtable`) is the gold
5//! standard: it centralises the runtime contract between emitted code
6//! and the host's sandbox helpers behind a fixed-layout
7//! `__relon_capability_vtable` data symbol the codegen indirects through
8//! and the host populates after `dlopen` / JIT-finalize.
9//!
10//! ## Why the LLVM table resolves by *symbol*, not by a data slot
11//!
12//! cranelift's object-emit path turns a direct `extern "C"` call into an
13//! ELF import that needs runtime resolution against the host's
14//! dynamic-symbol table — fragile unless the host links `-rdynamic`.
15//! Cranelift dodges that with a data-vtable the host fills after dlopen.
16//!
17//! The LLVM backend already resolves host helpers a different way: the
18//! emitted module declares each helper as an `extern` function under a
19//! stable symbol name, and the evaluator maps that name onto the host
20//! fn's address with `ExecutionEngine::add_global_mapping` before
21//! resolving the entry pointer (see `state::RELON_LLVM_CALL_NATIVE_SYMBOL`
22//! / `str_helpers::RELON_LLVM_STR_CONTAINS_ARENA_SYMBOL`). For the
23//! linked-after **native** object the same symbols become ordinary
24//! undefined externs the linker resolves against the host binary (the
25//! `relon-rs-shims` staticlib provides them), so no data-vtable
26//! indirection is needed.
27//!
28//! This module therefore ports cranelift's `VtableSlot` enum + populate
29//! surface to the LLVM model as a **symbol registry**: one [`VtableSlot`]
30//! per host helper, each carrying the stable symbol name the emitted
31//! module declares and the host address `add_global_mapping` binds. The
32//! slot *order* mirrors cranelift's so a side-by-side audit lines up;
33//! the carrier differs (symbol name vs data-section offset).
34
35use crate::state::{relon_llvm_call_native_addr, RELON_LLVM_CALL_NATIVE_SYMBOL};
36use crate::str_helpers::{
37 relon_llvm_f64_to_str_addr, relon_llvm_str_contains_arena_addr, RELON_LLVM_F64_TO_STR_SYMBOL,
38 RELON_LLVM_STR_CONTAINS_ARENA_SYMBOL,
39};
40
41/// One slot per host helper the LLVM codegen indirects through, in the
42/// same order cranelift pins them in its data-vtable. Adding a new
43/// helper appends a variant (NEVER reorder existing variants).
44///
45/// | Slot | cranelift analogue | LLVM symbol |
46/// |------|-------------------------------|--------------------------------------|
47/// | 0 | `RelonGlobMatch` (slot 3) | `relon_llvm_str_contains_arena` |
48/// | 1 | `RelonCallNative` (slot 4) | `relon_llvm_call_native` |
49/// | 2 | `RelonF64ToStr` (slot 5) | `relon_llvm_f64_to_str` |
50///
51/// cranelift's slots 0..=2 (`RelonNow` / `RelonRaiseTrap` /
52/// `RelonCapLookup`) have no LLVM counterpart: the LLVM gate is an
53/// inline `caps`-bitmask test baked into the object by `Op::CheckCap`
54/// (no `cap_lookup` helper), trap codes are written directly to
55/// `ArenaState::trap_code` by the helper / trap arm (no `raise_trap`
56/// helper), and the deadline clock (`now`) is reserved for the LLVM
57/// deadline work. Only the two helpers the LLVM emitter actually
58/// declares as externs are represented here.
59#[repr(u32)]
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum VtableSlot {
62 /// `extern "C" fn(s_ptr: *const u8, n_ptr: *const u8) -> i32`.
63 /// Tier-2 substring matcher; the LLVM mirror of cranelift's
64 /// `RelonGlobMatch`. Declared lazily on the first
65 /// `Op::Call { contains }` site.
66 RelonStrContains = 0,
67 /// `extern "C" fn(state: *const ArenaState, import_idx: u32,
68 /// args_ptr: *const i64, arg_count: u32) -> i64`. Dynamic host-fn
69 /// dispatch; the LLVM mirror of cranelift's `RelonCallNative`. See
70 /// [`crate::state::relon_llvm_call_native`].
71 RelonCallNative = 1,
72 /// `extern "C" fn(bits: i64, dest: *mut u8) -> i32`. Wave B float
73 /// Display renderer; the LLVM mirror of cranelift's
74 /// `RelonF64ToStr`. Declared lazily on the first `Op::FloatToStr`
75 /// site. See [`crate::str_helpers::relon_llvm_f64_to_str`].
76 RelonF64ToStr = 2,
77}
78
79impl VtableSlot {
80 /// Number of slots the LLVM emitter can declare. Mirrors cranelift's
81 /// `VtableSlot::COUNT`; bumping it needs a matching variant + a
82 /// `populate_global_mappings` arm.
83 pub const COUNT: u32 = 3;
84
85 /// All slots, in declaration order. Used by [`populate_global_mappings`]
86 /// and the parity tests.
87 pub const ALL: [VtableSlot; Self::COUNT as usize] = [
88 VtableSlot::RelonStrContains,
89 VtableSlot::RelonCallNative,
90 VtableSlot::RelonF64ToStr,
91 ];
92
93 /// Stable symbol name the emitted LLVM module declares this helper
94 /// under. The host binds it via `add_global_mapping` (JIT) or the
95 /// linker resolves it against `relon-rs-shims` (native object).
96 pub fn symbol(self) -> &'static str {
97 match self {
98 VtableSlot::RelonStrContains => RELON_LLVM_STR_CONTAINS_ARENA_SYMBOL,
99 VtableSlot::RelonCallNative => RELON_LLVM_CALL_NATIVE_SYMBOL,
100 VtableSlot::RelonF64ToStr => RELON_LLVM_F64_TO_STR_SYMBOL,
101 }
102 }
103
104 /// Host-side address of the helper backing this slot, as a `usize`
105 /// suitable for `ExecutionEngine::add_global_mapping`. The cranelift
106 /// analogue is the `*const u8` fn pointer `populate_vtable` writes
107 /// into the data slot.
108 pub fn host_addr(self) -> usize {
109 match self {
110 VtableSlot::RelonStrContains => relon_llvm_str_contains_arena_addr(),
111 VtableSlot::RelonCallNative => relon_llvm_call_native_addr(),
112 VtableSlot::RelonF64ToStr => relon_llvm_f64_to_str_addr(),
113 }
114 }
115}
116
117/// Resolve every slot to its `(symbol, host_addr)` binding. The
118/// evaluator iterates this to register `add_global_mapping`s for the
119/// helpers the emitted module actually references (it only declares a
120/// helper's extern on first use, so the caller filters by
121/// `module.get_function(symbol).is_some()` before binding). The LLVM
122/// analogue of cranelift's `populate_vtable`, which writes every active
123/// slot's fn pointer into the data section unconditionally.
124///
125/// Returned addresses are non-null (`&'static` host fn items) and stay
126/// valid for the host process lifetime.
127pub fn populate_global_mappings() -> [(&'static str, usize); VtableSlot::COUNT as usize] {
128 [
129 (
130 VtableSlot::RelonStrContains.symbol(),
131 VtableSlot::RelonStrContains.host_addr(),
132 ),
133 (
134 VtableSlot::RelonCallNative.symbol(),
135 VtableSlot::RelonCallNative.host_addr(),
136 ),
137 (
138 VtableSlot::RelonF64ToStr.symbol(),
139 VtableSlot::RelonF64ToStr.host_addr(),
140 ),
141 ]
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn slot_count_matches_variant_list() {
150 assert_eq!(VtableSlot::ALL.len() as u32, VtableSlot::COUNT);
151 }
152
153 #[test]
154 fn slot_indices_are_distinct_and_packed() {
155 assert_eq!(VtableSlot::RelonStrContains as u32, 0);
156 assert_eq!(VtableSlot::RelonCallNative as u32, 1);
157 assert_eq!(VtableSlot::RelonF64ToStr as u32, 2);
158 }
159
160 #[test]
161 fn symbols_are_stable_and_match_state_str_helpers() {
162 assert_eq!(
163 VtableSlot::RelonStrContains.symbol(),
164 RELON_LLVM_STR_CONTAINS_ARENA_SYMBOL
165 );
166 assert_eq!(
167 VtableSlot::RelonCallNative.symbol(),
168 RELON_LLVM_CALL_NATIVE_SYMBOL
169 );
170 assert_eq!(
171 VtableSlot::RelonF64ToStr.symbol(),
172 RELON_LLVM_F64_TO_STR_SYMBOL
173 );
174 }
175
176 #[test]
177 fn host_addrs_are_non_null_and_stable() {
178 for slot in VtableSlot::ALL {
179 let a = slot.host_addr();
180 assert_ne!(a, 0, "{slot:?} host addr must be non-null");
181 // Stable across calls (mirrors the `_addr()` helper contract).
182 assert_eq!(a, slot.host_addr(), "{slot:?} host addr must be stable");
183 }
184 }
185
186 #[test]
187 fn populate_global_mappings_covers_every_active_slot() {
188 let mappings = populate_global_mappings();
189 assert_eq!(mappings.len() as u32, VtableSlot::COUNT);
190 for (sym, addr) in mappings {
191 assert!(!sym.is_empty(), "symbol name must be non-empty");
192 assert_ne!(addr, 0, "host addr must be non-null for {sym}");
193 }
194 // Every slot maps to a distinct symbol.
195 assert_ne!(mappings[0].0, mappings[1].0);
196 assert_ne!(mappings[0].0, mappings[2].0);
197 assert_ne!(mappings[1].0, mappings[2].0);
198 }
199}