Skip to main content

nylon_ring_host/
types.rs

1//! Type definitions and aliases for the nylon-ring-host crate.
2
3use crate::error::NylonRingHostError;
4use crate::stream_channel::{StreamChannelReceiver, StreamSender};
5use crate::{PluginCallGuard, context, context::HostContext};
6use dashmap::DashMap;
7use nylon_ring::NrStatus;
8use rustc_hash::FxBuildHasher;
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::task::Waker;
12
13/// Result type alias for this crate.
14pub type Result<T> = std::result::Result<T, NylonRingHostError>;
15
16/// Pending request state.
17#[derive(Debug)]
18pub(crate) enum Pending {
19    Unary(UnaryPending),
20    Stream(StreamSender),
21}
22
23/// Completion state stored inline in the pending-request map.
24#[derive(Debug)]
25pub(crate) enum UnaryPending {
26    Waiting {
27        waker: Option<Waker>,
28        /// Host-owned buffer leased to the plugin; the plugin may
29        /// write into it until the call completes, so removal paths must
30        /// park it in the orphan list instead of freeing it.
31        lease: Option<Vec<u8>>,
32    },
33    Ready(NrStatus, ResponsePayload),
34}
35
36/// A plugin-owned buffer received over the ABI; releases it on drop.
37///
38/// Contract (`NrOwnedBytes`): the bytes stay valid and immutable until
39/// the consumer calls `release` exactly once, from any thread — which is
40/// what makes this Send + Sync. Dropping it runs plugin code, so the holder
41/// must guarantee the plugin library is still loaded (via a call guard or by
42/// being inside a plugin callback).
43#[derive(Debug)]
44pub(crate) struct ForeignBytes {
45    ptr: *const u8,
46    len: usize,
47    owner_ctx: *mut std::ffi::c_void,
48    release: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const u8, u64)>,
49}
50
51// SAFETY: see the type documentation — immutability until release plus a
52// thread-agnostic release callback are part of the ABI contract.
53unsafe impl Send for ForeignBytes {}
54unsafe impl Sync for ForeignBytes {}
55
56impl ForeignBytes {
57    /// Takes ownership of a payload received from a plugin.
58    pub(crate) fn from_abi(payload: nylon_ring::NrOwnedBytes) -> Self {
59        Self {
60            ptr: payload.ptr,
61            len: payload.len as usize,
62            owner_ctx: payload.owner_ctx,
63            release: payload.release,
64        }
65    }
66
67    pub(crate) fn as_slice(&self) -> &[u8] {
68        if self.ptr.is_null() || self.len == 0 {
69            return &[];
70        }
71        // SAFETY: the producer guarantees ptr..ptr+len stays valid and
72        // immutable until release (ABI contract).
73        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
74    }
75}
76
77impl Drop for ForeignBytes {
78    fn drop(&mut self) {
79        if let Some(release) = self.release {
80            // SAFETY: called exactly once with the values the producer
81            // handed over; Drop runs at most once.
82            unsafe { release(self.owner_ctx, self.ptr, self.len as u64) };
83        }
84    }
85}
86
87/// A response payload that is either host-owned or plugin-owned.
88#[derive(Debug)]
89pub(crate) enum ResponsePayload {
90    Owned(Vec<u8>),
91    Foreign(ForeignBytes),
92}
93
94impl ResponsePayload {
95    pub(crate) fn as_slice(&self) -> &[u8] {
96        match self {
97            Self::Owned(data) => data,
98            Self::Foreign(foreign) => foreign.as_slice(),
99        }
100    }
101
102    /// Converts into a host-owned `Vec`, copying (and releasing) a foreign
103    /// payload. Callers must hold the plugin alive across this call.
104    pub(crate) fn into_vec(self) -> Vec<u8> {
105        match self {
106            Self::Owned(data) => data,
107            Self::Foreign(foreign) => foreign.as_slice().to_vec(),
108        }
109    }
110}
111
112/// A unary response as returned by [`crate::PluginHandle::call_response_bytes`].
113///
114/// For a plugin that responded with plugin-owned bytes this is a
115/// zero-copy view: the buffer is released back to the plugin when the value
116/// drops, and an in-flight call guard held inside keeps the plugin library
117/// loaded until then.
118pub struct ResponseBytes {
119    payload: ResponsePayload,
120    /// Keeps the plugin loaded while a foreign payload is alive; dropped
121    /// after `payload` would be a bug, so field order matters (payload
122    /// first).
123    _guard: Option<PluginCallGuard>,
124}
125
126impl ResponseBytes {
127    pub(crate) fn new(payload: ResponsePayload, guard: Option<PluginCallGuard>) -> Self {
128        Self {
129            payload,
130            _guard: guard,
131        }
132    }
133
134    /// Converts into a host-owned `Vec<u8>` (copies only if the payload is
135    /// still plugin-owned).
136    pub fn into_vec(self) -> Vec<u8> {
137        // Field-by-field move keeps the guard alive until after the copy.
138        let Self { payload, _guard } = self;
139        payload.into_vec()
140    }
141}
142
143impl std::ops::Deref for ResponseBytes {
144    type Target = [u8];
145
146    fn deref(&self) -> &[u8] {
147        self.payload.as_slice()
148    }
149}
150
151impl AsRef<[u8]> for ResponseBytes {
152    fn as_ref(&self) -> &[u8] {
153        self.payload.as_slice()
154    }
155}
156
157impl std::fmt::Debug for ResponseBytes {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("ResponseBytes")
160            .field("len", &self.payload.as_slice().len())
161            .field(
162                "foreign",
163                &matches!(self.payload, ResponsePayload::Foreign(_)),
164            )
165            .finish()
166    }
167}
168
169impl UnaryPending {
170    pub(crate) const fn waiting() -> Self {
171        Self::Waiting {
172            waker: None,
173            lease: None,
174        }
175    }
176}
177
178/// A frame in a streaming response.
179#[derive(Debug)]
180pub struct StreamFrame {
181    pub status: NrStatus,
182    pub data: Vec<u8>,
183}
184
185/// A receiver for streaming responses.
186///
187/// Dropping the receiver unregisters its pending stream from the host.
188pub struct StreamReceiver {
189    inner: StreamChannelReceiver,
190    /// Populated only when no call guard is held (host-internal uses); with a
191    /// guard the context is reached through the guarded plugin instead of
192    /// paying a second shared refcount round-trip per stream.
193    host_ctx: Option<Arc<HostContext>>,
194    sid: u64,
195    call_guard: Option<PluginCallGuard>,
196}
197
198impl StreamReceiver {
199    pub(crate) fn new(
200        inner: StreamChannelReceiver,
201        host_ctx: Option<Arc<HostContext>>,
202        sid: u64,
203        call_guard: Option<PluginCallGuard>,
204    ) -> Self {
205        debug_assert!(
206            host_ctx.is_some() || call_guard.is_some(),
207            "stream receiver needs a context source"
208        );
209        Self {
210            inner,
211            host_ctx,
212            sid,
213            call_guard,
214        }
215    }
216
217    fn host_ctx(&self) -> &HostContext {
218        match (&self.call_guard, &self.host_ctx) {
219            (Some(guard), _) => &guard.plugin().host_ctx,
220            (None, Some(host_ctx)) => host_ctx,
221            (None, None) => unreachable!("checked at construction"),
222        }
223    }
224
225    /// Waits for the next stream frame.
226    pub async fn recv(&mut self) -> Option<StreamFrame> {
227        self.inner.recv().await
228    }
229}
230
231impl Drop for StreamReceiver {
232    fn drop(&mut self) {
233        // The call guard (if any) outlives this cleanup: Drop::drop runs
234        // before the struct's fields are dropped. Removing the pending entry
235        // drops the channel's sender under the shard lock, which is what
236        // makes recycling the receiving half sound.
237        context::cleanup_sid(self.host_ctx(), self.sid);
238        self.inner.recycle();
239    }
240}
241
242/// Fast hash map for pending requests using FxHash.
243pub(crate) type FastPendingMap = DashMap<u64, Pending, FxBuildHasher>;
244
245/// Fast hash map for per-SID state using FxHash.
246pub(crate) type FastStateMap = DashMap<u64, HashMap<String, Vec<u8>>, FxBuildHasher>;
247
248/// Result slot for an in-progress synchronous fast-path call.
249pub(crate) struct UnaryResultSlot {
250    pub(crate) sid: u64,
251    pub(crate) result: Option<(NrStatus, Vec<u8>)>,
252    /// Host-owned buffer leased to the plugin for this call.
253    pub(crate) lease: Option<Vec<u8>>,
254}