Skip to main content

interstice_sdk_core/host_calls/
core.rs

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