Skip to main content

byteflow/vm/
native.rs

1//! Native (FFI) table for `Opcode::CallNative`.
2//!
3//! # The one rule: never block
4//!
5//! `CallNative` runs **inline** on the worker thread. A native that blocks
6//! stalls every other Flow on that worker. Slow I/O belongs in a dedicated
7//! Flow (`Send`/`Receive`), not here.
8
9use std::sync::Arc;
10
11use crate::bytecode::Value;
12
13use super::fault::Fault;
14
15/// Result type for a native function.
16pub type NativeResult = Result<Value, Fault>;
17
18/// A host-side function callable from bytecode via `Opcode::CallNative`.
19pub type NativeFn = Arc<dyn Fn(&[Value]) -> NativeResult + Send + Sync>;
20
21/// Immutable, indexable set of natives. Gaps left by [`NativeTableBuilder::register_at`]
22/// are `None` — calling them faults with [`Fault::BadNative`].
23pub struct NativeTable {
24    entries: Vec<Option<(String, NativeFn)>>,
25}
26
27impl NativeTable {
28    pub fn builder() -> NativeTableBuilder {
29        NativeTableBuilder {
30            entries: Vec::new(),
31        }
32    }
33
34    /// Empty table — valid for chunks that never emit `CallNative`.
35    pub fn empty() -> Arc<NativeTable> {
36        Arc::new(NativeTable {
37            entries: Vec::new(),
38        })
39    }
40
41    #[inline]
42    pub fn get(&self, index: u32) -> Option<&NativeFn> {
43        self.entries
44            .get(index as usize)
45            .and_then(|slot| slot.as_ref())
46            .map(|(_, f)| f)
47    }
48
49    pub fn index_of(&self, name: &str) -> Option<u32> {
50        self.entries
51            .iter()
52            .enumerate()
53            .find(|(_, slot)| slot.as_ref().is_some_and(|(n, _)| n == name))
54            .map(|(i, _)| i as u32)
55    }
56
57    /// Slot count including reserved-but-empty gaps (one past the highest
58    /// touched index), not the count of registered functions.
59    pub fn len(&self) -> usize {
60        self.entries.len()
61    }
62
63    pub fn is_empty(&self) -> bool {
64        self.entries.is_empty()
65    }
66
67    pub fn names(&self) -> impl Iterator<Item = &str> {
68        self.entries
69            .iter()
70            .filter_map(|slot| slot.as_ref().map(|(n, _)| n.as_str()))
71    }
72}
73
74/// Fluent builder for a [`NativeTable`].
75///
76/// - [`Self::register`] — next sequential slot (host builds table + chunk together).
77/// - [`Self::register_at`] — fixed ABI slot (MCU / separately flashed bytecode).
78pub struct NativeTableBuilder {
79    entries: Vec<Option<(String, NativeFn)>>,
80}
81
82impl NativeTableBuilder {
83    pub fn new() -> Self {
84        Self {
85            entries: Vec::new(),
86        }
87    }
88
89    /// Register `f` under `name` at the next sequential slot.
90    /// Panics on duplicate `name`.
91    pub fn register<F>(self, name: impl Into<String>, f: F) -> Self
92    where
93        F: Fn(&[Value]) -> NativeResult + Send + Sync + 'static,
94    {
95        let index = self.entries.len() as u32;
96        self.register_at(index, name, f)
97    }
98
99    /// Register `f` under `name` at explicit `index`, padding lower gaps as
100    /// unregistered (`get` → `None` → [`Fault::BadNative`]).
101    ///
102    /// Panics if the slot is occupied or `name` is already registered.
103    pub fn register_at<F>(mut self, index: u32, name: impl Into<String>, f: F) -> Self
104    where
105        F: Fn(&[Value]) -> NativeResult + Send + Sync + 'static,
106    {
107        let name = name.into();
108        assert!(
109            !self
110                .entries
111                .iter()
112                .any(|slot| slot.as_ref().is_some_and(|(n, _)| n == &name)),
113            "byteflow: duplicate native function registered: '{name}'"
114        );
115        let index = index as usize;
116        if index >= self.entries.len() {
117            self.entries.resize_with(index + 1, || None);
118        }
119        assert!(
120            self.entries[index].is_none(),
121            "byteflow: native slot {index} already occupied (registering '{name}')"
122        );
123        self.entries[index] = Some((name, Arc::new(f)));
124        self
125    }
126
127    pub fn build(self) -> Arc<NativeTable> {
128        Arc::new(NativeTable {
129            entries: self.entries,
130        })
131    }
132}
133
134impl Default for NativeTableBuilder {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140/// Require `args[index]` to exist.
141pub fn expect_arg<'a>(
142    args: &'a [Value],
143    index: usize,
144    fn_name: &str,
145) -> Result<&'a Value, Fault> {
146    args.get(index).ok_or_else(|| {
147        Fault::NativeError(format!("{fn_name}: missing argument {index}"))
148    })
149}
150
151/// Require `args[index]` to coerce to an int (`Value::as_int`).
152pub fn expect_int(args: &[Value], index: usize, fn_name: &str) -> Result<i64, Fault> {
153    expect_arg(args, index, fn_name)?
154        .as_int()
155        .ok_or_else(|| {
156            Fault::NativeError(format!("{fn_name}: argument {index} is not an int"))
157        })
158}
159
160/// Require `args[index]` to be a bool (ints: nonzero = true).
161pub fn expect_bool(args: &[Value], index: usize, fn_name: &str) -> Result<bool, Fault> {
162    match expect_arg(args, index, fn_name)? {
163        Value::Bool(b) => Ok(*b),
164        Value::Int(i) => Ok(*i != 0),
165        other => Err(Fault::NativeError(format!(
166            "{fn_name}: argument {index} is not a bool/int (got {})",
167            other.type_name()
168        ))),
169    }
170}
171
172/// Require `args[index]` to be a [`crate::Message`].
173///
174/// Used by the std `msg_*` natives. A wrong type becomes
175/// [`Fault::NativeError`] (category B — Flow fault), not a host panic.
176pub fn expect_message(
177    args: &[Value],
178    index: usize,
179    fn_name: &str,
180) -> Result<crate::Message, Fault> {
181    expect_arg(args, index, fn_name)?
182        .as_message()
183        .ok_or_else(|| {
184            Fault::NativeError(format!("{fn_name}: argument {index} is not a message"))
185        })
186}
187
188/// Coerce `args[index]` to `u64` from `Int` (≥ 0), `Pid`, `Cap`, or `Bool`.
189///
190/// `make_msg` accepts these so bytecode can pass a `SelfPid` / Spawn Cap
191/// result, a `Pid` identity, or a `LoadImm` without an extra conversion.
192/// Negative ints are rejected — envelope fields are unsigned on the wire.
193pub fn expect_u64(args: &[Value], index: usize, fn_name: &str) -> Result<u64, Fault> {
194    match expect_arg(args, index, fn_name)? {
195        Value::Pid(p) => Ok(*p),
196        Value::Cap(c) => Ok(*c),
197        Value::Int(i) if *i >= 0 => Ok(*i as u64),
198        Value::Bool(b) => Ok(u64::from(*b)),
199        other => Err(Fault::NativeError(format!(
200            "{fn_name}: argument {index} is not a non-negative int/pid/cap (got {})",
201            other.type_name()
202        ))),
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn register_at_leaves_holes_as_none() {
212        let table = NativeTable::builder()
213            .register_at(10, "answer", |_| Ok(Value::Int(42)))
214            .build();
215        assert_eq!(table.len(), 11);
216        assert_eq!(table.index_of("answer"), Some(10));
217        assert!(table.get(10).unwrap()(&[]).unwrap() == Value::Int(42));
218        assert!(table.get(2).is_none());
219    }
220
221    #[test]
222    #[should_panic(expected = "already occupied")]
223    fn register_at_panics_on_duplicate_slot() {
224        let _ = NativeTable::builder()
225            .register_at(3, "a", |_| Ok(Value::Unit))
226            .register_at(3, "b", |_| Ok(Value::Unit));
227    }
228
229    #[test]
230    #[should_panic(expected = "duplicate native")]
231    fn register_panics_on_duplicate_name() {
232        let _ = NativeTable::builder()
233            .register("x", |_| Ok(Value::Unit))
234            .register("x", |_| Ok(Value::Unit));
235    }
236}