Skip to main content

interstice_sdk_core/host_calls/
core.rs

1use interstice_abi::{
2    CallQueryRequest, CallQueryResponse, CallReducerRequest, CallReducerResponse, HostCall,
3    IndexKey, IndexQuery, InsertRowResponse, IntersticeValue, ModuleSelection, NodeSelection, Row,
4    ScheduleRequest, ScheduleResponse, TableGetByPrimaryKeyRequest, TableGetByPrimaryKeyResponse,
5    TableIndexScanRequest, TableIndexScanResponse, TableScanRequest, TableScanResponse, decode,
6    encode,
7};
8
9// Pre-allocated scratch buffer for serialising rows/keys before a direct host call.
10// WASM is single-threaded so a module-level static is safe to use as a reusable scratch area.
11static mut ENCODE_BUF: [u8; 16384] = [0u8; 16384];
12
13/// Holds encoded bytes for a direct host call — either borrowed from the static scratch buffer
14/// (zero alloc, common path) or owned on the heap (fallback for unusually large values).
15enum EncodedBuf<'a> {
16    Static(&'a [u8]),
17    Heap(Vec<u8>),
18}
19
20impl EncodedBuf<'_> {
21    #[inline]
22    fn ptr_len(&self) -> (i32, i32) {
23        match self {
24            Self::Static(s) => (s.as_ptr() as i32, s.len() as i32),
25            Self::Heap(v) => (v.as_ptr() as i32, v.len() as i32),
26        }
27    }
28}
29
30/// Serialize `value` into the static encode buffer when possible, falling back to a heap
31/// allocation for values that exceed the static buffer.
32#[inline]
33fn encode_scratch<T: serde::Serialize>(value: &T) -> Result<EncodedBuf<'static>, String> {
34    unsafe {
35        // Rust 2024: use addr_of_mut! to avoid creating a &mut reference to a static.
36        let buf_ptr = std::ptr::addr_of_mut!(ENCODE_BUF);
37        let scratch = std::slice::from_raw_parts_mut(buf_ptr as *mut u8, 16384);
38        match postcard::to_slice(value, scratch) {
39            Ok(slice) => {
40                // SAFETY: the static reference is valid for the duration of the call (WASM is
41                // single-threaded; nothing else can observe the buffer between here and the
42                // corresponding host call).
43                let static_slice: &'static [u8] = &*(slice as *const [u8]);
44                Ok(EncodedBuf::Static(static_slice))
45            }
46            Err(_) => {
47                let v = encode(value).map_err(|e| e.to_string())?;
48                Ok(EncodedBuf::Heap(v))
49            }
50        }
51    }
52}
53
54pub type NodeId = String;
55
56pub fn log(message: &str) {
57    let msg = message.as_bytes();
58    unsafe { crate::host_calls::interstice_log(msg.as_ptr() as i32, msg.len() as i32) };
59}
60
61pub fn current_node_id() -> NodeId {
62    let call = HostCall::CurrentNodeId;
63    let pack = host_call(call);
64    let response: NodeId = unpack(pack);
65    return response;
66}
67
68pub fn call_reducer(
69    node_selection: NodeSelection,
70    module_selection: ModuleSelection,
71    reducer_name: String,
72    input: IntersticeValue,
73) -> Result<(), String> {
74    let call = HostCall::CallReducer(CallReducerRequest {
75        node_selection,
76        module_selection,
77        reducer_name,
78        input,
79    });
80
81    let pack = host_call(call);
82    let response: CallReducerResponse = unpack(pack);
83    match response {
84        CallReducerResponse::Ok => Ok(()),
85        CallReducerResponse::Err(err) => Err(err),
86    }
87}
88
89pub fn schedule(reducer_name: String, delay_ms: u64) -> Result<(), String> {
90    let call = HostCall::Schedule(ScheduleRequest {
91        reducer_name,
92        delay_ms,
93    });
94
95    let pack = host_call(call);
96    let response: ScheduleResponse = unpack(pack);
97    match response {
98        ScheduleResponse::Ok => Ok(()),
99        ScheduleResponse::Err(err) => Err(err),
100    }
101}
102
103pub fn call_query(
104    node_selection: NodeSelection,
105    module_selection: ModuleSelection,
106    query_name: String,
107    input: IntersticeValue,
108) -> Result<IntersticeValue, String> {
109    let call = HostCall::CallQuery(CallQueryRequest {
110        node_selection,
111        module_selection,
112        query_name,
113        input,
114    });
115
116    let pack = host_call(call);
117    let response: CallQueryResponse = unpack(pack);
118    match response {
119        CallQueryResponse::Ok(value) => Ok(value),
120        CallQueryResponse::Err(err) => Err(err),
121    }
122}
123
124pub fn deterministic_random_u64() -> Result<u64, String> {
125    let v = unsafe { crate::host_calls::interstice_random() };
126    Ok(v as u64)
127}
128
129pub fn time_now_ms() -> Result<u64, String> {
130    let v = unsafe { crate::host_calls::interstice_time() };
131    Ok(v as u64)
132}
133
134pub fn insert_row(table_name: &str, row: Row) -> Result<Row, String> {
135    let table_bytes = table_name.as_bytes();
136    let row_buf = encode_scratch(&row)?;
137    let (row_ptr, row_len) = row_buf.ptr_len();
138
139    // Fast path: try the static response buffer first.
140    let written = unsafe {
141        let resp_ptr = std::ptr::addr_of_mut!(crate::host_calls::DIRECT_RESP_BUF) as *mut u8 as i32;
142        crate::host_calls::interstice_insert_row(
143            table_bytes.as_ptr() as i32,
144            table_bytes.len() as i32,
145            row_ptr,
146            row_len,
147            resp_ptr,
148            8192i32,
149        )
150    };
151    if written >= 0 {
152        let resp: InsertRowResponse = unsafe {
153            let buf = std::slice::from_raw_parts(
154                std::ptr::addr_of!(crate::host_calls::DIRECT_RESP_BUF) as *const u8,
155                written as usize,
156            );
157            decode(buf).map_err(|e| e.to_string())?
158        };
159        return match resp {
160            InsertRowResponse::Ok(None) => Ok(row),
161            InsertRowResponse::Ok(Some(modified)) => Ok(modified),
162            InsertRowResponse::Err(err) => Err(err),
163        };
164    }
165    // Slow path: host returned -(needed_size). Allocate a heap buffer and retry.
166    let needed = (-written) as usize;
167    let mut heap_buf: Vec<u8> = vec![0u8; needed];
168    let written2 = unsafe {
169        crate::host_calls::interstice_insert_row(
170            table_bytes.as_ptr() as i32,
171            table_bytes.len() as i32,
172            row_ptr,
173            row_len,
174            heap_buf.as_mut_ptr() as i32,
175            needed as i32,
176        )
177    };
178    if written2 < 0 {
179        return Err("insert_row: response buffer too small".into());
180    }
181    let resp: InsertRowResponse =
182        decode(&heap_buf[..written2 as usize]).map_err(|e| e.to_string())?;
183    return match resp {
184        InsertRowResponse::Ok(None) => Ok(row),
185        InsertRowResponse::Ok(Some(modified)) => Ok(modified),
186        InsertRowResponse::Err(err) => Err(err),
187    };
188}
189
190pub fn update_row(table_name: &str, row: Row) -> Result<(), String> {
191    let table_bytes = table_name.as_bytes();
192    let row_buf = encode_scratch(&row)?;
193    let (row_ptr, row_len) = row_buf.ptr_len();
194    let result = unsafe {
195        crate::host_calls::interstice_update_row(
196            table_bytes.as_ptr() as i32,
197            table_bytes.len() as i32,
198            row_ptr,
199            row_len,
200        )
201    };
202    if result < 0 {
203        Err("update_row failed".into())
204    } else {
205        Ok(())
206    }
207}
208
209pub fn delete_row(table_name: &str, primary_key: IndexKey) -> Result<(), String> {
210    let table_bytes = table_name.as_bytes();
211    let pk_buf = encode_scratch(&primary_key)?;
212    let (pk_ptr, pk_len) = pk_buf.ptr_len();
213    let result = unsafe {
214        crate::host_calls::interstice_delete_row(
215            table_bytes.as_ptr() as i32,
216            table_bytes.len() as i32,
217            pk_ptr,
218            pk_len,
219        )
220    };
221    if result < 0 {
222        Err("delete_row failed".into())
223    } else {
224        Ok(())
225    }
226}
227
228pub fn clear_table(module_selection: ModuleSelection, table_name: &str) -> Result<(), String> {
229    match module_selection {
230        ModuleSelection::Current => {
231            let table_bytes = table_name.as_bytes();
232            let result = unsafe {
233                crate::host_calls::interstice_clear_table(
234                    table_bytes.as_ptr() as i32,
235                    table_bytes.len() as i32,
236                )
237            };
238            if result < 0 {
239                Err("clear_table failed".into())
240            } else {
241                Ok(())
242            }
243        }
244        ModuleSelection::Other(_) => {
245            Err("clear_table with ModuleSelection::Other is not supported in ABI v2".into())
246        }
247    }
248}
249
250pub fn scan(module_selection: ModuleSelection, table_name: String) -> Result<Vec<Row>, String> {
251    let call = HostCall::TableScan(TableScanRequest {
252        module_selection,
253        table_name,
254    });
255
256    let pack = host_call(call);
257    let response: TableScanResponse = unpack(pack);
258    match response {
259        TableScanResponse::Ok { rows } => Ok(rows),
260        TableScanResponse::Err(err) => Err(err),
261    }
262}
263
264pub fn get_by_primary_key(
265    module_selection: ModuleSelection,
266    table_name: &str,
267    primary_key: IndexKey,
268) -> Result<Option<Row>, String> {
269    match module_selection {
270        ModuleSelection::Current => {
271            let table_bytes = table_name.as_bytes();
272            let pk_buf = encode_scratch(&primary_key)?;
273            let (pk_ptr, pk_len) = pk_buf.ptr_len();
274            let written = unsafe {
275                let resp_ptr =
276                    std::ptr::addr_of_mut!(crate::host_calls::DIRECT_RESP_BUF) as *mut u8 as i32;
277                let resp_cap = 8192i32;
278                crate::host_calls::interstice_get_by_pk(
279                    table_bytes.as_ptr() as i32,
280                    table_bytes.len() as i32,
281                    pk_ptr,
282                    pk_len,
283                    resp_ptr,
284                    resp_cap,
285                )
286            };
287            if written < 0 {
288                return Err("get_by_pk: response buffer too small".into());
289            }
290            let resp: TableGetByPrimaryKeyResponse = unsafe {
291                let buf = std::slice::from_raw_parts(
292                    std::ptr::addr_of!(crate::host_calls::DIRECT_RESP_BUF) as *const u8,
293                    written as usize,
294                );
295                decode(buf).map_err(|e| e.to_string())?
296            };
297            match resp {
298                TableGetByPrimaryKeyResponse::Ok(row) => Ok(row),
299                TableGetByPrimaryKeyResponse::Err(err) => Err(err),
300            }
301        }
302        other => {
303            let call = HostCall::TableGetByPrimaryKey(TableGetByPrimaryKeyRequest {
304                module_selection: other,
305                table_name: table_name.to_string(),
306                primary_key,
307            });
308            let pack = host_call(call);
309            let response: TableGetByPrimaryKeyResponse = unpack(pack);
310            match response {
311                TableGetByPrimaryKeyResponse::Ok(row) => Ok(row),
312                TableGetByPrimaryKeyResponse::Err(err) => Err(err),
313            }
314        }
315    }
316}
317
318pub fn scan_index(
319    module_selection: ModuleSelection,
320    table_name: String,
321    field_name: String,
322    query: IndexQuery,
323) -> Result<Vec<Row>, String> {
324    let call = HostCall::TableIndexScan(TableIndexScanRequest {
325        module_selection,
326        table_name,
327        field_name,
328        query,
329    });
330
331    let pack = host_call(call);
332    let response: TableIndexScanResponse = unpack(pack);
333    match response {
334        TableIndexScanResponse::Ok { rows } => Ok(rows),
335        TableIndexScanResponse::Err(err) => Err(err),
336    }
337}
338
339use crate::{QueryContext, ReducerContext};
340
341use crate::host_calls::{host_call, unpack};
342
343pub trait HostLog {
344    fn log(&self, message: &str);
345}
346
347pub trait HostCurrentNodeId {
348    fn current_node_id(&self) -> NodeId;
349}
350
351pub trait HostTime {
352    fn time_now_ms(&self) -> Result<u64, String>;
353}
354
355pub trait HostDeterministicRandom {
356    fn deterministic_random_u64(&self) -> Result<u64, String>;
357}
358
359pub trait HostSchedule {
360    fn schedule(&self, reducer_name: &str, delay_ms: u64) -> Result<(), String>;
361}
362
363impl<Caps> HostLog for ReducerContext<Caps> {
364    fn log(&self, message: &str) {
365        log(message);
366    }
367}
368
369impl<Caps> HostLog for QueryContext<Caps> {
370    fn log(&self, message: &str) {
371        log(message);
372    }
373}
374
375impl<Caps> HostCurrentNodeId for ReducerContext<Caps> {
376    fn current_node_id(&self) -> NodeId {
377        current_node_id()
378    }
379}
380
381impl<Caps> HostCurrentNodeId for QueryContext<Caps> {
382    fn current_node_id(&self) -> NodeId {
383        current_node_id()
384    }
385}
386
387impl<Caps> HostTime for ReducerContext<Caps> {
388    fn time_now_ms(&self) -> Result<u64, String> {
389        time_now_ms()
390    }
391}
392
393impl<Caps> HostTime for QueryContext<Caps> {
394    fn time_now_ms(&self) -> Result<u64, String> {
395        time_now_ms()
396    }
397}
398
399impl<Caps> HostDeterministicRandom for ReducerContext<Caps> {
400    fn deterministic_random_u64(&self) -> Result<u64, String> {
401        deterministic_random_u64()
402    }
403}
404
405impl<Caps> HostDeterministicRandom for QueryContext<Caps> {
406    fn deterministic_random_u64(&self) -> Result<u64, String> {
407        deterministic_random_u64()
408    }
409}
410
411impl<Caps> HostSchedule for ReducerContext<Caps> {
412    fn schedule(&self, reducer_name: &str, delay_ms: u64) -> Result<(), String> {
413        schedule(reducer_name.to_string(), delay_ms)
414    }
415}