on master help sign in
skrepka/core/src/ffi.rs
96 lines 3.1 kB history raw
1//! The FFI surface the Swift shell calls, via BoltFFI. Mirrors the Crux
2//! counter example's `ffi.rs`: a `Bridge` that takes/returns bincode bytes.
3//!
4//! Nothing here may unwind. A panic crossing the Rust/Swift boundary is
5//! undefined behavior, not a crash report — so every exported method runs its
6//! body inside `catch_unwind` and turns a panic into an empty response. The
7//! shell already tolerates an empty effect list (it just does nothing) and an
8//! empty view (it renders the previous one), so a bug degrades into a stalled
9//! UI rather than a corrupted process.
10//!
11//! Note the release profile deliberately does *not* set `panic = "abort"`:
12//! aborting would fire before `catch_unwind` ever ran, which is strictly worse
13//! than catching.
15#![allow(clippy::used_underscore_items)]
17use std::panic::{catch_unwind, AssertUnwindSafe};
19use crux_core::{
20 bridge::{Bridge, EffectId},
21 Core,
22};
24use crate::Skrepka;
26/// The main interface used by the shell.
27pub struct CoreFFI {
28 core: Bridge<Skrepka>,
31impl Default for CoreFFI {
32 fn default() -> Self {
33 Self::new()
34 }
37/// Run `f`, converting a panic (or a bridge error) into an empty byte vec.
38///
39/// `AssertUnwindSafe` is sound here because a poisoned `Bridge` is never read
40/// again for correctness: the worst case is a dropped effect batch, and the next
41/// event starts a fresh `update`.
42///
43/// NOTE: A panic mid-`update` leaves the `Model` in a partially-mutated state.
44/// The next `update` call operates on this potentially-corrupted state. A
45/// proper fix requires snapshotting the Model before `update` and restoring on
46/// panic, or marking the Bridge as poisoned. This is an accepted limitation.
47fn guard(f: impl FnOnce() -> Vec<u8>) -> Vec<u8> {
48 catch_unwind(AssertUnwindSafe(f)).unwrap_or_default()
51#[boltffi::export]
52impl CoreFFI {
53 #[must_use]
54 pub fn new() -> Self {
55 Self {
56 core: Bridge::new(Core::new()),
57 }
58 }
60 /// Send an event to the app and return the resulting effects (bincode).
61 /// Empty on a serialization failure — see the module docs.
62 #[must_use]
63 pub fn update(&self, data: &[u8]) -> Vec<u8> {
64 guard(|| {
65 let mut effects = Vec::new();
66 match self.core.update(data, &mut effects) {
67 Ok(()) => effects,
68 Err(_) => Vec::new(),
69 }
70 })
71 }
73 /// Resolve an effect request and return the next batch of effects.
74 #[must_use]
75 pub fn resolve(&self, id: u32, data: &[u8]) -> Vec<u8> {
76 guard(|| {
77 let mut effects = Vec::new();
78 match self.core.resolve(EffectId(id), data, &mut effects) {
79 Ok(()) => effects,
80 Err(_) => Vec::new(),
81 }
82 })
83 }
85 /// Get the current serialized `ViewModel`.
86 #[must_use]
87 pub fn view(&self) -> Vec<u8> {
88 guard(|| {
89 let mut view_model = Vec::new();
90 match self.core.view(&mut view_model) {
91 Ok(()) => view_model,
92 Err(_) => Vec::new(),
93 }
94 })
95 }