Skip to main content

byteflow/
natives.rs

1//! Standard native (FFI) table shipped with the facade.
2//!
3//! # Why Message make/unpack lives here (not as new opcodes)
4//!
5//! [`crate::Value::Message`] is already a first-class wire/register value
6//! (ABI v2). Building and tearing it apart in bytecode could be ISA ops
7//! (`MakeMessage`, `MsgSender`, …), but that would burn opcode space and
8//! force every embedder that never does request-reply to carry the
9//! dispatch cost. Natives keep the envelope **generic in the core** while
10//! the std table opts into the helpers — same pattern as `print` / `now_ms`
11//! (design notes §30-31): host Rust owns the operation, bytecode only
12//! picks a slot.
13//!
14//! # Stable indices are an embed contract
15//!
16//! Flash / separately-built `.bf` modules hard-code `CallNative` indices.
17//! Renumbering an existing slot is a **breaking** change. Keep
18//! [`std_native_map`] and [`std_native_table`] in lockstep; append only.
19//!
20//! | Index | Name | Role |
21//! |------:|------|------|
22//! | 0 | `print` | host stdout log line (flow-visible) |
23//! | 1 | `now_ms` | wall-clock millis as `Value::Int` |
24//! | 2 | `make_msg` | build [`crate::Message`] from four scalars (**untrusted** `sender`) |
25//! | 3 | `msg_sender` | extract `sender` → `Value::Pid` (authenticated **after** delivery) |
26//! | 4 | `msg_request_id` | extract `request_id` → `Value::Int` |
27//! | 5 | `msg_tag` | extract `tag` → `Value::Int` |
28//! | 6 | `msg_payload` | extract `payload` → `Value::Int` |
29//! | 7 | `msg_reply_cap` | extract `reply_cap` → `Value::Cap` (SEND grant) |
30//!
31//! # `CallNative` argument layout
32//!
33//! `CallNative ra, fb, nc` takes args from `r[a .. a+nc]` and writes the
34//! result back into `r[a]` — so a call **destroys** the first argument
35//! register. Samples that need to keep a `Message` while unpacking must
36//! `Move` it into the dest register first (see
37//! [`crate::samples::atomic_request_reply`]).
38
39use std::collections::HashMap;
40use std::sync::Arc;
41use std::time::{SystemTime, UNIX_EPOCH};
42
43use crate::bytecode::{Message, Value};
44use crate::vm::{expect_message, expect_u64, Fault, NativeTable};
45
46/// Stable `CallNative` indices for [`std_native_table`]. Bytecode embeds these
47/// once assembled — append only; never renumber.
48pub mod std_native {
49    pub const PRINT: u32 = 0;
50    pub const NOW_MS: u32 = 1;
51    pub const MAKE_MSG: u32 = 2;
52    pub const MSG_SENDER: u32 = 3;
53    pub const MSG_REQUEST_ID: u32 = 4;
54    pub const MSG_TAG: u32 = 5;
55    pub const MSG_PAYLOAD: u32 = 6;
56    pub const MSG_REPLY_CAP: u32 = 7;
57}
58
59/// Name → `CallNative` index for documentation / host-side lookups.
60///
61/// Prefer this (or [`std_natives`]) over hard-coding integers in host code
62/// so renames stay discoverable; bytecode itself still embeds the numeric
63/// slot once assembled.
64pub fn std_native_map() -> HashMap<String, u32> {
65    HashMap::from([
66        ("print".to_owned(), 0),
67        ("now_ms".to_owned(), 1),
68        ("make_msg".to_owned(), 2),
69        ("msg_sender".to_owned(), 3),
70        ("msg_request_id".to_owned(), 4),
71        ("msg_tag".to_owned(), 5),
72        ("msg_payload".to_owned(), 6),
73        ("msg_reply_cap".to_owned(), 7),
74    ])
75}
76
77/// Default FFI table: print, clock, and Message make/unpack.
78///
79/// Pass this to [`crate::Runtime::with_natives`] (or
80/// `with_natives_and_config`) whenever the chunk emits the Message helpers
81/// or `print` / `now_ms`. Chunks that never `CallNative` can keep
82/// [`crate::NativeTable::empty`].
83pub fn std_native_table() -> Arc<NativeTable> {
84    let built = NativeTable::builder()
85        .register("print", |args| {
86            // Space-separated Display forms, trailing newline — mirrors a
87            // tiny "println!("{:?}", …)" for bytecode without allocating a
88            // format string in the VM.
89            let mut first = true;
90            for value in args {
91                if !first {
92                    print!(" ");
93                }
94                print!("{value}");
95                first = false;
96            }
97            println!();
98            Ok(Value::Unit)
99        })
100        .and_then(|b| {
101            b.register("now_ms", |_| {
102                let duration = SystemTime::now()
103                    .duration_since(UNIX_EPOCH)
104                    .map_err(|e| Fault::NativeError(format!("system clock error: {e}")))?;
105                let millis = i64::try_from(duration.as_millis())
106                    .map_err(|_| Fault::NativeError("system clock value exceeds i64".into()))?;
107                Ok(Value::Int(millis))
108            })
109        })
110        .and_then(|b| {
111            b.register("make_msg", |args| {
112                // Args: sender, request_id, tag, payload — each Int≥0, Pid, Cap, or Bool.
113                // Tag must fit `u16` (protocol discriminator width on the wire).
114                //
115                // # Security (crates.io contract)
116                //
117                // The first argument is retained so existing `.bf` modules and
118                // samples keep a stable CallNative layout (indices 0–6 frozen;
119                // 7 = msg_reply_cap appended). It is **not** an authentication
120                // primitive:
121                //
122                // - Before `Send` / `Ask`, `sender` is ordinary register data.
123                // - At delivery, the worker stamps `Message.sender` and mints
124                //   `reply_cap` (`Message::authenticate`).
125                // - After a hop is received, `msg_sender` / `msg_reply_cap`
126                //   reflect runtime identity and the SEND grant (S1 + FlowCap).
127                //
128                // Host code that only builds messages in memory (never sends)
129                // still sees the constructed field unchanged.
130                let sender = expect_u64(args, 0, "make_msg")?;
131                let request_id = expect_u64(args, 1, "make_msg")?;
132                let tag = expect_u64(args, 2, "make_msg")?;
133                let payload = expect_u64(args, 3, "make_msg")?;
134                let tag = u16::try_from(tag).map_err(|_| {
135                    Fault::NativeError(format!("make_msg: tag {tag} does not fit in u16"))
136                })?;
137                Ok(Value::Message(Message::new(sender, request_id, tag, payload)))
138            })
139        })
140        .and_then(|b| {
141            b.register("msg_sender", |args| {
142                // After mailbox delivery this is the runtime-stamped origin.
143                // Identity only — not a Send/Ask address (use `msg_reply_cap`).
144                Ok(Value::Pid(expect_message(args, 0, "msg_sender")?.sender))
145            })
146        })
147        .and_then(|b| {
148            b.register("msg_request_id", |args| {
149                Ok(Value::Int(
150                    expect_message(args, 0, "msg_request_id")?.request_id as i64,
151                ))
152            })
153        })
154        .and_then(|b| {
155            b.register("msg_tag", |args| {
156                Ok(Value::Int(i64::from(expect_message(args, 0, "msg_tag")?.tag)))
157            })
158        })
159        .and_then(|b| {
160            b.register("msg_payload", |args| {
161                Ok(Value::Int(
162                    expect_message(args, 0, "msg_payload")?.payload as i64,
163                ))
164            })
165        })
166        .and_then(|b| {
167            b.register("msg_reply_cap", |args| {
168                Ok(Value::Cap(
169                    expect_message(args, 0, "msg_reply_cap")?.reply_cap,
170                ))
171            })
172        });
173    // Unique sequential names cannot hit DuplicateName / SlotOccupied.
174    // Empty table on Err: fail-closed (CallNative → BadNative), never panic.
175    match built {
176        Ok(b) => b.build(),
177        Err(_) => NativeTable::empty(),
178    }
179}
180
181/// Build the default native table and its matching name map together.
182///
183/// Prefer this when the host both registers natives and resolves names to
184/// indices for `ChunkBuilder` — one source of truth, no drift between map
185/// and table.
186pub fn std_natives() -> (Arc<NativeTable>, HashMap<String, u32>) {
187    let table = std_native_table();
188    let map: HashMap<String, u32> = table
189        .names()
190        .filter_map(|n| table.index_of(n).map(|i| (n.to_string(), i)))
191        .collect();
192    (table, map)
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn std_native_indices_are_stable() {
201        let map = std_native_map();
202        assert_eq!(map.get("print"), Some(&0));
203        assert_eq!(map.get("now_ms"), Some(&1));
204        assert_eq!(map.get("make_msg"), Some(&2));
205        assert_eq!(map.get("msg_sender"), Some(&3));
206        assert_eq!(map.get("msg_request_id"), Some(&4));
207        assert_eq!(map.get("msg_tag"), Some(&5));
208        assert_eq!(map.get("msg_payload"), Some(&6));
209        assert_eq!(map.get("msg_reply_cap"), Some(&7));
210    }
211
212    #[test]
213    fn std_native_table_matches_map() {
214        let (table, map) = std_natives();
215        assert_eq!(table.len(), 8);
216        for (name, idx) in &map {
217            assert_eq!(table.index_of(name), Some(*idx));
218        }
219    }
220
221    #[test]
222    fn now_ms_returns_non_negative_int() -> Result<(), Box<dyn std::error::Error>> {
223        let table = std_native_table();
224        let now_ms = table.get(1).ok_or("now_ms native")?;
225        let value = now_ms(&[])?;
226        assert!(matches!(value, Value::Int(ms) if ms >= 0));
227        Ok(())
228    }
229
230    #[test]
231    fn make_msg_and_unpack_round_trip() -> Result<(), Box<dyn std::error::Error>> {
232        let table = std_native_table();
233        let make = table.get(2).ok_or("make_msg")?;
234        let msg = make(&[
235            Value::Pid(9),
236            Value::Int(3),
237            Value::Int(7),
238            Value::Int(42),
239        ])?;
240        assert_eq!(msg.as_message(), Some(Message::new(9, 3, 7, 42)));
241        let sender = table.get(3).ok_or("msg_sender")?;
242        let req = table.get(4).ok_or("msg_request_id")?;
243        let tag = table.get(5).ok_or("msg_tag")?;
244        let payload = table.get(6).ok_or("msg_payload")?;
245        let cap = table.get(7).ok_or("msg_reply_cap")?;
246        assert_eq!(sender(std::slice::from_ref(&msg))?, Value::Pid(9));
247        assert_eq!(req(std::slice::from_ref(&msg))?, Value::Int(3));
248        assert_eq!(tag(std::slice::from_ref(&msg))?, Value::Int(7));
249        assert_eq!(payload(std::slice::from_ref(&msg))?, Value::Int(42));
250        assert_eq!(cap(std::slice::from_ref(&msg))?, Value::Cap(0));
251        Ok(())
252    }
253
254    #[test]
255    fn print_accepts_all_current_values() -> Result<(), Box<dyn std::error::Error>> {
256        let table = std_native_table();
257        let print = table.get(0).ok_or("print native")?;
258        let values = [
259            Value::Unit,
260            Value::Bool(true),
261            Value::Int(42),
262            Value::Float(1.5),
263            Value::Pid(7),
264            Value::Message(Message::new(1, 2, 3, 4)),
265            Value::Cap(9),
266            Value::str("hello"),
267            Value::bytes([1u8, 2, 3]),
268        ];
269        assert_eq!(print(&values)?, Value::Unit);
270        Ok(())
271    }
272
273    #[test]
274    fn print_with_no_args_still_returns_unit() -> Result<(), Box<dyn std::error::Error>> {
275        let table = std_native_table();
276        let print = table.get(0).ok_or("print native")?;
277        assert_eq!(print(&[])?, Value::Unit);
278        Ok(())
279    }
280}