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::{Cap, CapRights, NativeIdx, NativeMask, RevocationCell, 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/// Host error while building a [`NativeTable`] (duplicate name or occupied slot).
75///
76/// Category A: the embedder misconfigured FFI. Never a panic — `std_native_table`
77/// and host tables must surface this as `Result`.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum NativeTableError {
80    DuplicateName(String),
81    SlotOccupied { index: u32, name: String },
82}
83
84impl std::fmt::Display for NativeTableError {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self {
87            NativeTableError::DuplicateName(name) => {
88                write!(f, "duplicate native function registered: '{name}'")
89            }
90            NativeTableError::SlotOccupied { index, name } => {
91                write!(f, "native slot {index} already occupied (registering '{name}')")
92            }
93        }
94    }
95}
96
97impl std::error::Error for NativeTableError {}
98
99/// Fluent builder for a [`NativeTable`].
100///
101/// - [`Self::register`] — next sequential slot (host builds table + chunk together).
102/// - [`Self::register_at`] — fixed ABI slot (MCU / separately flashed bytecode).
103pub struct NativeTableBuilder {
104    entries: Vec<Option<(String, NativeFn)>>,
105}
106
107impl NativeTableBuilder {
108    pub fn new() -> Self {
109        Self {
110            entries: Vec::new(),
111        }
112    }
113
114    /// Register `f` under `name` at the next sequential slot.
115    pub fn register<F>(self, name: impl Into<String>, f: F) -> Result<Self, NativeTableError>
116    where
117        F: Fn(&[Value]) -> NativeResult + Send + Sync + 'static,
118    {
119        let index = self.entries.len() as u32;
120        self.register_at(index, name, f)
121    }
122
123    /// Register `f` under `name` at explicit `index`, padding lower gaps as
124    /// unregistered (`get` → `None` → [`Fault::BadNative`]).
125    pub fn register_at<F>(
126        mut self,
127        index: u32,
128        name: impl Into<String>,
129        f: F,
130    ) -> Result<Self, NativeTableError>
131    where
132        F: Fn(&[Value]) -> NativeResult + Send + Sync + 'static,
133    {
134        let name = name.into();
135        for (n, _) in self.entries.iter().flatten() {
136            if n == &name {
137                return Err(NativeTableError::DuplicateName(name));
138            }
139        }
140        let index_usize = index as usize;
141        if index_usize >= self.entries.len() {
142            self.entries.resize_with(index_usize + 1, || None);
143        }
144        if self.entries[index_usize].is_some() {
145            return Err(NativeTableError::SlotOccupied { index, name });
146        }
147        self.entries[index_usize] = Some((name, Arc::new(f)));
148        Ok(self)
149    }
150
151    pub fn build(self) -> Arc<NativeTable> {
152        Arc::new(NativeTable {
153            entries: self.entries,
154        })
155    }
156}
157
158impl Default for NativeTableBuilder {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164/// Require `args[index]` to exist.
165pub fn expect_arg<'a>(
166    args: &'a [Value],
167    index: usize,
168    fn_name: &str,
169) -> Result<&'a Value, Fault> {
170    args.get(index).ok_or(Fault::NativeError(format!(
171        "{fn_name}: missing argument {index}"
172    )))
173}
174
175/// Require `args[index]` to coerce to an int (`Value::as_int`).
176pub fn expect_int(args: &[Value], index: usize, fn_name: &str) -> Result<i64, Fault> {
177    expect_arg(args, index, fn_name)?
178        .as_int()
179        .ok_or(Fault::NativeError(format!(
180            "{fn_name}: argument {index} is not an int"
181        )))
182}
183
184/// Require `args[index]` to be a bool (ints: nonzero = true).
185pub fn expect_bool(args: &[Value], index: usize, fn_name: &str) -> Result<bool, Fault> {
186    match expect_arg(args, index, fn_name)? {
187        Value::Bool(b) => Ok(*b),
188        Value::Int(i) => Ok(*i != 0),
189        other => Err(Fault::NativeError(format!(
190            "{fn_name}: argument {index} is not a bool/int (got {})",
191            other.type_name()
192        ))),
193    }
194}
195
196/// Require `args[index]` to be a [`crate::Message`].
197///
198/// Used by the std `msg_*` natives. A wrong type becomes
199/// [`Fault::NativeError`] (category B — Flow fault), not a host panic.
200pub fn expect_message(
201    args: &[Value],
202    index: usize,
203    fn_name: &str,
204) -> Result<crate::Message, Fault> {
205    expect_arg(args, index, fn_name)?
206        .as_message()
207        .cloned()
208        .ok_or(Fault::NativeError(format!(
209            "{fn_name}: argument {index} is not a message"
210        )))
211}
212
213/// Coerce `args[index]` to `u64` from `Int` (≥ 0), `Pid`, `Cap`, or `Bool`.
214///
215/// `make_msg` accepts these so bytecode can pass a `SelfPid` / Spawn Cap
216/// result, a `Pid` identity, or a `LoadImm` without an extra conversion.
217/// Negative ints are rejected — envelope fields are unsigned on the wire.
218pub fn expect_u64(args: &[Value], index: usize, fn_name: &str) -> Result<u64, Fault> {
219    match expect_arg(args, index, fn_name)? {
220        Value::Pid(p) => Ok(*p),
221        Value::Int(i) if *i >= 0 => Ok(*i as u64),
222        Value::Bool(b) => Ok(u64::from(*b)),
223        other => Err(Fault::NativeError(format!(
224            "{fn_name}: argument {index} is not a non-negative int/pid (got {})",
225            other.type_name()
226        ))),
227    }
228}
229
230/// Per-flow native allowlist snapshot, checked before indexing the table.
231#[derive(Clone, Debug)]
232pub struct NativeGate {
233    pub has_native: bool,
234    pub mask: NativeMask,
235    pub authority_epoch: u64,
236    pub flow_cell: Arc<RevocationCell>,
237    pub native_epoch: u64,
238    pub native_cell: Arc<RevocationCell>,
239}
240
241impl NativeGate {
242    /// No native right — unit tests and chunks that never call out.
243    pub fn deny(native_count: usize) -> Self {
244        Self {
245            has_native: false,
246            mask: NativeMask::empty(native_count),
247            authority_epoch: 0,
248            flow_cell: Arc::new(RevocationCell::new()),
249            native_epoch: 0,
250            native_cell: Arc::new(RevocationCell::new()),
251        }
252    }
253
254    pub fn from_authority(
255        cap: &Cap,
256        flow_cell: Arc<RevocationCell>,
257        native_cell: Arc<RevocationCell>,
258        native_count: usize,
259    ) -> Self {
260        let mask = match &cap.native_mask {
261            Some(m) => m.clone(),
262            None => NativeMask::empty(native_count),
263        };
264        Self {
265            has_native: cap.rights.contains(CapRights::NATIVE),
266            mask,
267            authority_epoch: cap.epoch(),
268            flow_cell,
269            native_epoch: native_cell.epoch(),
270            native_cell,
271        }
272    }
273
274    pub fn is_live(&self) -> bool {
275        self.flow_cell.epoch() == self.authority_epoch
276            && self.native_cell.epoch() == self.native_epoch
277    }
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub enum NativeCallError {
282    NoNativeRight,
283    IndexNotAllowlisted(NativeIdx),
284    IndexOutOfRange(NativeIdx),
285    Revoked,
286}
287
288impl std::fmt::Display for NativeCallError {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        match self {
291            NativeCallError::NoNativeRight => f.write_str("CALL_NATIVE: flow lacks NATIVE right"),
292            NativeCallError::IndexNotAllowlisted(idx) => {
293                write!(f, "CALL_NATIVE: index {idx} not on allowlist")
294            }
295            NativeCallError::IndexOutOfRange(idx) => {
296                write!(f, "CALL_NATIVE: index {idx} out of range")
297            }
298            NativeCallError::Revoked => f.write_str("CALL_NATIVE: capability revoked"),
299        }
300    }
301}
302
303impl std::error::Error for NativeCallError {}
304
305/// Pure check — call from `CALL_NATIVE` **before** indexing `table.entries`.
306pub fn check_native_call(
307    cap: &Cap,
308    table: &NativeTable,
309    idx: NativeIdx,
310) -> Result<(), NativeCallError> {
311    if (idx as usize) >= table.len() {
312        return Err(NativeCallError::IndexOutOfRange(idx));
313    }
314    if !cap.rights.contains(CapRights::NATIVE) {
315        return Err(NativeCallError::NoNativeRight);
316    }
317    match &cap.native_mask {
318        Some(mask) if mask.allows(idx) => Ok(()),
319        _ => Err(NativeCallError::IndexNotAllowlisted(idx)),
320    }
321}
322
323pub fn check_native_gate(
324    gate: &NativeGate,
325    table: &NativeTable,
326    idx: NativeIdx,
327) -> Result<(), NativeCallError> {
328    if (idx as usize) >= table.len() {
329        return Err(NativeCallError::IndexOutOfRange(idx));
330    }
331    if !gate.is_live() {
332        return Err(NativeCallError::Revoked);
333    }
334    if !gate.has_native {
335        return Err(NativeCallError::NoNativeRight);
336    }
337    if gate.mask.allows(idx) {
338        Ok(())
339    } else {
340        Err(NativeCallError::IndexNotAllowlisted(idx))
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn register_at_leaves_holes_as_none() -> Result<(), Box<dyn std::error::Error>> {
350        let table = NativeTable::builder()
351            .register_at(10, "answer", |_| Ok(Value::Int(42)))?
352            .build();
353        assert_eq!(table.len(), 11);
354        assert_eq!(table.index_of("answer"), Some(10));
355        let f = table.get(10).ok_or("missing native")?;
356        assert!(matches!(f(&[])?, Value::Int(42)));
357        assert!(table.get(2).is_none());
358        Ok(())
359    }
360
361    #[test]
362    fn register_at_errors_on_duplicate_slot() {
363        let result = NativeTable::builder()
364            .register_at(3, "a", |_| Ok(Value::Unit))
365            .and_then(|b| b.register_at(3, "b", |_| Ok(Value::Unit)));
366        assert!(matches!(
367            result,
368            Err(NativeTableError::SlotOccupied { index: 3, .. })
369        ));
370    }
371
372    #[test]
373    fn register_errors_on_duplicate_name() {
374        let result = NativeTable::builder()
375            .register("x", |_| Ok(Value::Unit))
376            .and_then(|b| b.register("x", |_| Ok(Value::Unit)));
377        assert!(matches!(result, Err(NativeTableError::DuplicateName(_))));
378    }
379
380    #[test]
381    fn denies_unlisted_index_even_with_native_right() {
382        use crate::bytecode::{CapTarget, RevocationCell};
383
384        let cell = RevocationCell::new();
385        let mask = NativeMask::from_indices(16, &[2, 4]);
386        let cap = Cap::root(CapTarget::Flow(1), CapRights::NATIVE, Some(mask), &cell);
387        let table = NativeTable {
388            entries: Vec::new(),
389        };
390        assert!(matches!(
391            check_native_call(&cap, &table, 4),
392            Err(NativeCallError::IndexOutOfRange(_))
393        ));
394    }
395}