Skip to main content

vox_core/
lib.rs

1//! Core implementations for the vox connectivity layer.
2//!
3//! This crate provides concrete implementations of the traits defined in
4//! [`vox_types`]. The only conduit shape is [`BareConduit`]: wraps a raw
5//! `Link` with postcard serialization. No reconnect, no reliability —
6//! reconnect was removed (StableConduit deleted) because the abstraction
7//! had no real users.
8
9mod bare_conduit;
10pub use bare_conduit::*;
11pub use vox_types::TransportMode;
12
13mod handshake;
14pub use handshake::*;
15
16mod into_conduit;
17pub use into_conduit::*;
18
19mod operation_store;
20pub use operation_store::*;
21
22mod transport_prologue;
23pub use transport_prologue::*;
24
25mod link_source;
26pub use link_source::*;
27
28#[cfg(not(target_arch = "wasm32"))]
29mod memory_link;
30#[cfg(not(target_arch = "wasm32"))]
31pub use memory_link::*;
32
33#[cfg(not(target_arch = "wasm32"))]
34use std::panic::AssertUnwindSafe;
35
36mod session;
37pub use session::*;
38
39mod driver;
40pub use driver::*;
41
42use vox_types::{Backing, SelfRef};
43
44/// Pre-built translation plan for deserializing the `Message` wire type.
45///
46/// Built once from the peer's schema (received during handshake) and our
47/// local schema. Stored in the conduit's Rx half and used for every
48/// incoming message.
49pub struct MessagePlan {
50    pub remote_schema_id: u64,
51    pub plan: vox_postcard::plan::TranslationPlan,
52    pub registry: vox_types::SchemaRegistry,
53}
54
55impl MessagePlan {
56    /// Build a message plan from the handshake result's schema exchange.
57    pub fn from_handshake(result: &vox_types::HandshakeResult) -> Result<Self, String> {
58        use vox_postcard::plan::{PlanInput, SchemaSet, build_plan};
59
60        if result.peer_schema.is_empty() || result.our_schema.is_empty() {
61            // No schemas exchanged — fall back to identity plan
62            let plan = vox_postcard::build_identity_plan(
63                <vox_types::Message<'static> as facet::Facet<'static>>::SHAPE,
64            );
65            return Ok(MessagePlan {
66                remote_schema_id: 0,
67                plan,
68                registry: vox_types::SchemaRegistry::new(),
69            });
70        }
71
72        let remote = SchemaSet::from_schemas(result.peer_schema.clone());
73        let local = SchemaSet::from_schemas(result.our_schema.clone());
74
75        let plan = build_plan(&PlanInput {
76            remote: &remote,
77            local: &local,
78        })
79        .map_err(|e| format!("failed to build message translation plan: {e}"))?;
80
81        Ok(MessagePlan {
82            remote_schema_id: remote.root.id.0,
83            plan,
84            registry: remote.registry,
85        })
86    }
87}
88
89/// Deserialize postcard-encoded `backing` bytes into `T` in place, returning
90/// a [`vox_types::SelfRef`] that keeps the backing storage alive for the
91/// value. Uses the identity plan; for plan-aware decoding, use
92/// [`deserialize_postcard_with_plan`].
93// r[impl zerocopy.framing.value]
94#[allow(dead_code)]
95pub(crate) fn deserialize_postcard<T: facet::Facet<'static>>(
96    backing: Backing,
97) -> Result<SelfRef<T>, vox_postcard::DeserializeError> {
98    let plan = vox_postcard::build_identity_plan(T::SHAPE);
99    let registry = vox_types::SchemaRegistry::new();
100    deserialize_postcard_with_plan(backing, &plan, &registry)
101}
102
103/// Deserialize postcard-encoded `backing` bytes into `T` using a pre-built
104/// translation plan and schema registry for the remote peer's type layout.
105// r[impl zerocopy.framing.value]
106#[allow(dead_code)]
107pub(crate) fn deserialize_postcard_with_plan<T: facet::Facet<'static>>(
108    backing: Backing,
109    plan: &vox_postcard::plan::TranslationPlan,
110    registry: &vox_types::SchemaRegistry,
111) -> Result<SelfRef<T>, vox_postcard::DeserializeError> {
112    #[cfg(not(target_arch = "wasm32"))]
113    {
114        SelfRef::try_new(backing, |bytes| {
115            match std::panic::catch_unwind(AssertUnwindSafe(|| {
116                vox_jit::global_runtime().try_decode_owned::<T>(bytes, 0, plan, registry)
117            })) {
118                Ok(Some(result)) => result,
119                Ok(None) => vox_postcard::from_slice_with_plan::<T>(bytes, plan, registry),
120                Err(payload) => {
121                    tracing::warn!(
122                        shape = %T::SHAPE,
123                        panic = %panic_payload_message(&payload),
124                        "vox message JIT decode panicked; falling back"
125                    );
126                    vox_postcard::from_slice_with_plan::<T>(bytes, plan, registry)
127                }
128            }
129        })
130    }
131    #[cfg(target_arch = "wasm32")]
132    {
133        SelfRef::try_new(backing, |bytes| {
134            vox_postcard::from_slice_with_plan::<T>(bytes, plan, registry)
135        })
136    }
137}
138
139/// Like [`deserialize_postcard`] but uses an already-resolved JIT decoder,
140/// skipping the global cache lookup. Used by conduits that resolved their
141/// decoder at construction.
142#[cfg(not(target_arch = "wasm32"))]
143pub(crate) fn deserialize_postcard_with_decoder<T: facet::Facet<'static>>(
144    backing: Backing,
145    decoder: Option<&'static vox_jit::cache::CompiledDecoder>,
146    plan: &vox_postcard::plan::TranslationPlan,
147    registry: &vox_types::SchemaRegistry,
148) -> Result<SelfRef<T>, vox_postcard::DeserializeError> {
149    SelfRef::try_new(backing, |bytes| {
150        let Some(decoder) = decoder else {
151            return vox_postcard::from_slice_with_plan::<T>(bytes, plan, registry);
152        };
153        let Some(decode_fn) = decoder.owned_fn_ptr() else {
154            tracing::warn!(
155                shape = %T::SHAPE,
156                "vox message JIT decoder missing function pointer; falling back"
157            );
158            return vox_postcard::from_slice_with_plan::<T>(bytes, plan, registry);
159        };
160        match std::panic::catch_unwind(AssertUnwindSafe(|| {
161            vox_jit::decode_owned_with::<T>(decode_fn, bytes)
162        })) {
163            Ok(result) => result,
164            Err(payload) => {
165                tracing::warn!(
166                    shape = %T::SHAPE,
167                    panic = %panic_payload_message(&payload),
168                    "vox message JIT decode panicked; falling back"
169                );
170                vox_postcard::from_slice_with_plan::<T>(bytes, plan, registry)
171            }
172        }
173    })
174}
175
176#[cfg(not(target_arch = "wasm32"))]
177fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
178    if let Some(message) = payload.downcast_ref::<&'static str>() {
179        (*message).to_owned()
180    } else if let Some(message) = payload.downcast_ref::<String>() {
181        message.clone()
182    } else {
183        "non-string panic payload".to_owned()
184    }
185}
186
187pub mod testing;
188
189#[cfg(test)]
190mod tests;