Skip to main content

nylon_ring_host/
lib.rs

1//! Nylon Ring Host - A high-performance plugin host for the nylon-ring ABI.
2//!
3//! This crate provides the host-side implementation for loading and managing
4//! plugins that conform to the nylon-ring ABI. It supports multiple execution
5//! modes including fire-and-forget calls, request-response patterns, and
6//! bidirectional streaming.
7
8mod call_tracker;
9mod callbacks;
10mod context;
11mod error;
12mod extensions;
13#[cfg(test)]
14mod loom_tests;
15mod sid;
16mod types;
17
18use call_tracker::CallTracker;
19use callbacks::{get_state_callback, send_result_vec_callback, set_state_callback};
20use context::{CURRENT_UNARY_RESULT, HostContext};
21use libloading::{Library, Symbol};
22use nylon_ring::{
23    ABI_VERSION, NrBytes, NrHostExt, NrHostVTable, NrPluginInfo, NrPluginVTable, NrStr,
24};
25use sid::next_sid;
26use std::collections::HashMap;
27use std::ffi::c_void;
28use std::sync::Arc;
29use std::time::Duration;
30
31pub use error::NylonRingHostError;
32pub use extensions::Extensions;
33pub use nylon_ring::NrStatus;
34pub use types::{Result, StreamFrame, StreamReceiver};
35
36/// Default number of frames buffered for each response stream.
37pub const DEFAULT_STREAM_CAPACITY: usize = 64;
38
39/// Point-in-time host metrics suitable for application monitoring hooks.
40#[derive(Debug, Copy, Clone, PartialEq, Eq)]
41pub struct HostMetrics {
42    pub loaded_plugins: usize,
43    pub pending_requests: usize,
44    pub state_sessions: usize,
45    pub in_flight_calls: usize,
46}
47
48struct PendingGuard<'a> {
49    host_ctx: &'a HostContext,
50    sid: u64,
51    armed: bool,
52}
53
54impl<'a> PendingGuard<'a> {
55    fn new(host_ctx: &'a HostContext, sid: u64) -> Self {
56        Self {
57            host_ctx,
58            sid,
59            armed: true,
60        }
61    }
62
63    fn disarm(&mut self) {
64        self.armed = false;
65    }
66}
67
68impl Drop for PendingGuard<'_> {
69    fn drop(&mut self) {
70        if self.armed {
71            context::cleanup_sid(self.host_ctx, self.sid);
72        }
73    }
74}
75
76struct FastSlotBinding;
77
78impl FastSlotBinding {
79    fn bind(slot: &mut types::UnaryResultSlot) -> Result<Self> {
80        let bound = CURRENT_UNARY_RESULT.with(|cell| {
81            if !cell.get().is_null() {
82                return false;
83            }
84            cell.set(slot as *mut _);
85            true
86        });
87        if bound {
88            Ok(Self)
89        } else {
90            Err(NylonRingHostError::FastPathReentrant)
91        }
92    }
93}
94
95impl Drop for FastSlotBinding {
96    fn drop(&mut self) {
97        CURRENT_UNARY_RESULT.with(|cell| cell.set(std::ptr::null_mut()));
98    }
99}
100
101/// A loaded plugin instance.
102pub struct LoadedPlugin {
103    _lib: Library,
104    vtable: &'static NrPluginVTable,
105    host_ctx: Arc<HostContext>,
106    path: String,
107    call_tracker: CallTracker,
108}
109
110impl LoadedPlugin {
111    fn begin_call(&self) -> Result<BorrowedPluginCallGuard<'_>> {
112        let shard = self
113            .call_tracker
114            .try_begin()
115            .ok_or(NylonRingHostError::PluginUnloaded)?;
116        Ok(BorrowedPluginCallGuard {
117            tracker: &self.call_tracker,
118            shard,
119        })
120    }
121
122    fn begin_owned_call(self: &Arc<Self>) -> Result<PluginCallGuard> {
123        let shard = self
124            .call_tracker
125            .try_begin()
126            .ok_or(NylonRingHostError::PluginUnloaded)?;
127        Ok(PluginCallGuard {
128            plugin: self.clone(),
129            shard,
130        })
131    }
132
133    fn stop_accepting_calls(&self) {
134        self.call_tracker.stop();
135    }
136}
137
138struct BorrowedPluginCallGuard<'a> {
139    tracker: &'a CallTracker,
140    shard: usize,
141}
142
143impl Drop for BorrowedPluginCallGuard<'_> {
144    fn drop(&mut self) {
145        self.tracker.finish(self.shard);
146    }
147}
148
149pub(crate) struct PluginCallGuard {
150    plugin: Arc<LoadedPlugin>,
151    shard: usize,
152}
153
154impl Drop for PluginCallGuard {
155    fn drop(&mut self) {
156        self.plugin.call_tracker.finish(self.shard);
157    }
158}
159
160impl Drop for LoadedPlugin {
161    fn drop(&mut self) {
162        if let Some(shutdown_fn) = self.vtable.shutdown {
163            unsafe {
164                shutdown_fn();
165            }
166        }
167    }
168}
169
170/// A handle to a specific plugin for making calls.
171#[derive(Clone)]
172pub struct PluginHandle {
173    plugin: Arc<LoadedPlugin>,
174}
175
176impl PluginHandle {
177    fn status_error(status: NrStatus) -> NylonRingHostError {
178        if status == NrStatus::Panic {
179            NylonRingHostError::PluginPanicked
180        } else {
181            NylonRingHostError::PluginHandleFailed(status)
182        }
183    }
184
185    /// Call a plugin entry point with a request-response pattern.
186    pub async fn call_response(&self, entry: &str, payload: &[u8]) -> Result<(NrStatus, Vec<u8>)> {
187        let _call_guard = self.plugin.begin_call()?;
188        let sid = next_sid();
189
190        context::insert_pending(
191            &self.plugin.host_ctx,
192            sid,
193            types::Pending::Unary(types::UnaryPending::waiting()),
194        );
195        let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
196
197        let payload_bytes = NrBytes::from_slice(payload);
198        let handle_raw_fn = match self.plugin.vtable.handle {
199            Some(f) => f,
200            None => return Err(NylonRingHostError::MissingRequiredFunctions),
201        };
202
203        let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
204
205        if status != NrStatus::Ok {
206            return Err(Self::status_error(status));
207        }
208
209        let result = context::wait_for_unary(&self.plugin.host_ctx, sid)
210            .await
211            .ok_or(NylonRingHostError::PluginUnloaded);
212        if result.is_ok() {
213            pending_guard.disarm();
214        }
215        result
216    }
217
218    /// Like [`PluginHandle::call_response`] but bounded by a timeout.
219    ///
220    /// If the plugin does not deliver a response within `timeout`, the pending
221    /// entry is removed from the host's tracking map and `Err(Timeout)` is
222    /// returned. Use this for any production caller that cannot afford to
223    /// hang indefinitely on a misbehaving plugin.
224    pub async fn call_response_timeout(
225        &self,
226        entry: &str,
227        payload: &[u8],
228        timeout: std::time::Duration,
229    ) -> Result<(NrStatus, Vec<u8>)> {
230        let _call_guard = self.plugin.begin_call()?;
231        let sid = next_sid();
232
233        context::insert_pending(
234            &self.plugin.host_ctx,
235            sid,
236            types::Pending::Unary(types::UnaryPending::waiting()),
237        );
238        let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
239
240        let payload_bytes = NrBytes::from_slice(payload);
241        let handle_raw_fn = match self.plugin.vtable.handle {
242            Some(f) => f,
243            None => return Err(NylonRingHostError::MissingRequiredFunctions),
244        };
245
246        let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
247        if status != NrStatus::Ok {
248            return Err(Self::status_error(status));
249        }
250
251        match tokio::time::timeout(timeout, context::wait_for_unary(&self.plugin.host_ctx, sid))
252            .await
253        {
254            Ok(Some(v)) => {
255                pending_guard.disarm();
256                Ok(v)
257            }
258            Ok(None) => Err(NylonRingHostError::PluginUnloaded),
259            Err(_) => Err(NylonRingHostError::Timeout),
260        }
261    }
262
263    /// Ultra-fast unary call for synchronous plugins.
264    pub async fn call_response_fast(
265        &self,
266        entry: &str,
267        payload: &[u8],
268    ) -> Result<(NrStatus, Vec<u8>)> {
269        let _call_guard = self.plugin.begin_call()?;
270        let sid = next_sid();
271        let mut slot = types::UnaryResultSlot { sid, result: None };
272
273        let binding = FastSlotBinding::bind(&mut slot)?;
274
275        let payload_bytes = NrBytes::from_slice(payload);
276
277        let handle_raw_fn = match self.plugin.vtable.handle {
278            Some(f) => f,
279            None => return Err(NylonRingHostError::MissingRequiredFunctions),
280        };
281
282        let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
283
284        drop(binding);
285        self.plugin.host_ctx.remove_state(sid);
286
287        if status != NrStatus::Ok {
288            return Err(Self::status_error(status));
289        }
290
291        match slot.result {
292            Some((st, data)) => Ok((st, data)),
293            None => Err(NylonRingHostError::MissingSynchronousResponse),
294        }
295    }
296
297    /// Fire-and-forget call to a plugin entry point.
298    pub async fn call(&self, entry: &str, payload: &[u8]) -> Result<NrStatus> {
299        let _call_guard = self.plugin.begin_call()?;
300        // Use Fast SID
301        let sid = next_sid();
302
303        let payload_bytes = NrBytes::from_slice(payload);
304        let handle_raw_fn = match self.plugin.vtable.handle {
305            Some(f) => f,
306            None => {
307                return Err(NylonRingHostError::MissingRequiredFunctions);
308            }
309        };
310
311        let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
312
313        // Fire-and-forget calls have no later response lifecycle in which to
314        // clean up extension state.
315        self.plugin.host_ctx.remove_state(sid);
316
317        if status != NrStatus::Ok {
318            return Err(Self::status_error(status));
319        }
320        Ok(status)
321    }
322
323    /// Call a plugin entry point with a streaming response pattern.
324    pub async fn call_stream(&self, entry: &str, payload: &[u8]) -> Result<(u64, StreamReceiver)> {
325        let call_guard = self.plugin.begin_owned_call()?;
326        let sid = next_sid();
327
328        let (tx, rx) =
329            tokio::sync::mpsc::channel::<StreamFrame>(self.plugin.host_ctx.stream_capacity());
330
331        // Register the stream channel (Map)
332        context::insert_pending(&self.plugin.host_ctx, sid, types::Pending::Stream(tx));
333
334        let payload_bytes = NrBytes::from_slice(payload);
335
336        let handle_raw_fn = match self.plugin.vtable.handle {
337            Some(f) => f,
338            None => {
339                context::cleanup_sid(&self.plugin.host_ctx, sid);
340                return Err(NylonRingHostError::MissingRequiredFunctions);
341            }
342        };
343
344        let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
345
346        if status != NrStatus::Ok {
347            context::cleanup_sid(&self.plugin.host_ctx, sid);
348            return Err(Self::status_error(status));
349        }
350
351        Ok((
352            sid,
353            StreamReceiver::new(rx, self.plugin.host_ctx.clone(), sid, Some(call_guard)),
354        ))
355    }
356
357    /// Send data to an active stream.
358    pub fn send_stream_data(&self, sid: u64, data: &[u8]) -> Result<NrStatus> {
359        let stream_data_fn = match self.plugin.vtable.stream_data {
360            Some(f) => f,
361            None => return Err(NylonRingHostError::MissingRequiredFunctions),
362        };
363        let payload = NrBytes::from_slice(data);
364        Ok(unsafe { stream_data_fn(sid, payload) })
365    }
366
367    /// Close an active stream from the host side.
368    pub fn close_stream(&self, sid: u64) -> Result<NrStatus> {
369        let stream_close_fn = match self.plugin.vtable.stream_close {
370            Some(f) => f,
371            None => return Err(NylonRingHostError::MissingRequiredFunctions),
372        };
373        Ok(unsafe { stream_close_fn(sid) })
374    }
375}
376
377/// The main host for loading and managing nylon-ring plugins.
378pub struct NylonRingHost {
379    plugins: HashMap<String, Arc<LoadedPlugin>>,
380    host_ctx: Arc<HostContext>,
381    host_vtable: Box<NrHostVTable>,
382}
383
384impl Default for NylonRingHost {
385    fn default() -> Self {
386        Self::new()
387    }
388}
389
390impl NylonRingHost {
391    /// Create a new empty host.
392    pub fn new() -> Self {
393        Self::with_stream_capacity(DEFAULT_STREAM_CAPACITY)
394    }
395
396    /// Create a host with a bounded per-stream frame capacity.
397    pub fn with_stream_capacity(stream_capacity: usize) -> Self {
398        assert!(
399            stream_capacity > 0,
400            "stream capacity must be greater than zero"
401        );
402        let host_ctx = Arc::new(HostContext::new(
403            NrHostExt {
404                set_state: set_state_callback,
405                get_state: get_state_callback,
406            },
407            stream_capacity,
408        ));
409
410        let host_vtable = Box::new(NrHostVTable {
411            send_result: send_result_vec_callback,
412        });
413
414        Self {
415            plugins: HashMap::new(),
416            host_ctx,
417            host_vtable,
418        }
419    }
420
421    /// Load a plugin from the specified path with a given name.
422    pub fn load(&mut self, name: &str, path: &str) -> Result<()> {
423        unsafe {
424            let lib = Library::new(path).map_err(NylonRingHostError::FailedToLoadLibrary)?;
425
426            let get_plugin: Symbol<extern "C" fn() -> *const NrPluginInfo> =
427                lib.get(b"nylon_ring_get_plugin_v1\0").map_err(|_| {
428                    NylonRingHostError::MissingSymbol("nylon_ring_get_plugin_v1".to_string())
429                })?;
430
431            let info_ptr = get_plugin();
432            if info_ptr.is_null() {
433                return Err(NylonRingHostError::NullPluginInfo);
434            }
435            let abi_version = std::ptr::read_unaligned(info_ptr.cast::<u32>());
436            let struct_size = std::ptr::read_unaligned(info_ptr.cast::<u32>().add(1));
437
438            if abi_version != ABI_VERSION {
439                return Err(NylonRingHostError::IncompatibleAbiVersion {
440                    expected: ABI_VERSION,
441                    actual: abi_version,
442                });
443            }
444
445            let expected_size = std::mem::size_of::<NrPluginInfo>() as u32;
446            if struct_size < expected_size {
447                return Err(NylonRingHostError::IncompatiblePluginInfoSize {
448                    expected: expected_size,
449                    actual: struct_size,
450                });
451            }
452
453            let info = &*info_ptr;
454
455            if info.vtable.is_null() {
456                return Err(NylonRingHostError::NullPluginVTable);
457            }
458            let plugin_vtable = &*info.vtable;
459
460            if plugin_vtable.init.is_none() || plugin_vtable.handle.is_none() {
461                return Err(NylonRingHostError::MissingRequiredFunctions);
462            }
463
464            // Initialize plugin
465            if let Some(init_fn) = plugin_vtable.init {
466                let status = init_fn(
467                    Arc::as_ptr(&self.host_ctx) as *mut c_void,
468                    &*self.host_vtable,
469                );
470                if status != NrStatus::Ok {
471                    return Err(NylonRingHostError::PluginInitFailed(status));
472                }
473            }
474
475            let loaded = LoadedPlugin {
476                _lib: lib,
477                vtable: plugin_vtable,
478                host_ctx: self.host_ctx.clone(),
479                path: path.to_string(),
480                call_tracker: CallTracker::new(),
481            };
482
483            if let Some(previous) = self.plugins.insert(name.to_string(), Arc::new(loaded)) {
484                previous.stop_accepting_calls();
485            }
486            Ok(())
487        }
488    }
489
490    /// Unload a plugin by name.
491    pub fn unload(&mut self, name: &str) -> Result<()> {
492        let plugin = self
493            .plugins
494            .remove(name)
495            .ok_or_else(|| NylonRingHostError::PluginNotFound(name.to_owned()))?;
496        plugin.stop_accepting_calls();
497        Ok(())
498    }
499
500    /// Stop accepting new calls, then wait for tracked calls to drain.
501    pub async fn unload_with_grace(&mut self, name: &str, grace: Duration) -> Result<()> {
502        let plugin = self
503            .plugins
504            .remove(name)
505            .ok_or_else(|| NylonRingHostError::PluginNotFound(name.to_owned()))?;
506        plugin.stop_accepting_calls();
507        Self::wait_for_drain(&plugin, grace).await
508    }
509
510    /// Reload all plugins.
511    pub fn reload(&mut self) -> Result<()> {
512        let active_calls: usize = self
513            .plugins
514            .values()
515            .map(|plugin| plugin.call_tracker.active_calls())
516            .sum();
517        if active_calls != 0 {
518            return Err(NylonRingHostError::PluginBusy { active_calls });
519        }
520        let mut plugins_to_reload = Vec::new();
521        for (name, plugin) in &self.plugins {
522            plugins_to_reload.push((name.clone(), plugin.path.clone()));
523        }
524
525        // Load new versions - insert() will atomically replace old ones
526        // This ensures zero downtime (plugin() always returns a value)
527        for (name, path) in plugins_to_reload {
528            self.load(&name, &path)?;
529        }
530
531        Ok(())
532    }
533
534    /// Reload all plugins and wait for calls on replaced instances to drain.
535    pub async fn reload_with_grace(&mut self, grace: Duration) -> Result<()> {
536        let old_plugins: Vec<_> = self
537            .plugins
538            .drain()
539            .map(|(name, plugin)| {
540                plugin.stop_accepting_calls();
541                (name, plugin)
542            })
543            .collect();
544        for (_, plugin) in &old_plugins {
545            Self::wait_for_drain(plugin, grace).await?;
546        }
547        let plugins_to_reload: Vec<_> = old_plugins
548            .iter()
549            .map(|(name, plugin)| (name.clone(), plugin.path.clone()))
550            .collect();
551        drop(old_plugins);
552        for (name, path) in plugins_to_reload {
553            self.load(&name, &path)?;
554        }
555        Ok(())
556    }
557
558    async fn wait_for_drain(plugin: &LoadedPlugin, grace: Duration) -> Result<()> {
559        let deadline = tokio::time::Instant::now() + grace;
560        while plugin.call_tracker.active_calls() != 0 {
561            if tokio::time::Instant::now() >= deadline {
562                return Err(NylonRingHostError::DrainTimeout {
563                    remaining: plugin.call_tracker.active_calls(),
564                });
565            }
566            tokio::time::sleep(Duration::from_millis(1)).await;
567        }
568        Ok(())
569    }
570
571    /// Get a handle to a loaded plugin by name.
572    pub fn plugin(&self, name: &str) -> Option<PluginHandle> {
573        self.plugins
574            .get(name)
575            .map(|p| PluginHandle { plugin: p.clone() })
576    }
577
578    /// Capture lightweight host metrics without allocating the shard map.
579    pub fn metrics(&self) -> HostMetrics {
580        HostMetrics {
581            loaded_plugins: self.plugins.len(),
582            pending_requests: self.host_ctx.pending_count(),
583            state_sessions: self.host_ctx.state_count(),
584            in_flight_calls: self
585                .plugins
586                .values()
587                .map(|plugin| plugin.call_tracker.active_calls())
588                .sum(),
589        }
590    }
591
592    /// Get host extension pointer from host_ctx.
593    ///
594    /// # Safety
595    ///
596    /// The caller must ensure that `host_ctx` is a valid pointer to a `HostContext`
597    /// instance that was created by this host, or a null pointer.
598    pub unsafe fn get_host_ext(host_ctx: *mut c_void) -> *const NrHostExt {
599        if host_ctx.is_null() {
600            return std::ptr::null();
601        }
602        let ctx = unsafe { &*host_ctx.cast::<HostContext>() };
603        &ctx.host_ext as *const NrHostExt
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610
611    struct WakeFlag(Arc<std::sync::atomic::AtomicBool>);
612
613    impl std::task::Wake for WakeFlag {
614        fn wake(self: Arc<Self>) {
615            self.0.store(true, std::sync::atomic::Ordering::Release);
616        }
617
618        fn wake_by_ref(self: &Arc<Self>) {
619            self.0.store(true, std::sync::atomic::Ordering::Release);
620        }
621    }
622
623    struct ReentrantWakerState {
624        host_ctx: Arc<HostContext>,
625        clones: Arc<std::sync::atomic::AtomicUsize>,
626        drops: Arc<std::sync::atomic::AtomicUsize>,
627    }
628
629    unsafe fn clone_reentrant_waker(data: *const ()) -> std::task::RawWaker {
630        let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
631        state.host_ctx.pending_count();
632        state
633            .clones
634            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
635        let clone = state.clone();
636        let _ = Arc::into_raw(state);
637        std::task::RawWaker::new(Arc::into_raw(clone).cast(), &REENTRANT_WAKER_VTABLE)
638    }
639
640    unsafe fn wake_reentrant_waker(data: *const ()) {
641        let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
642        state.host_ctx.pending_count();
643    }
644
645    unsafe fn wake_reentrant_waker_by_ref(data: *const ()) {
646        let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
647        state.host_ctx.pending_count();
648        let _ = Arc::into_raw(state);
649    }
650
651    unsafe fn drop_reentrant_waker(data: *const ()) {
652        let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
653        state.host_ctx.pending_count();
654        state
655            .drops
656            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
657    }
658
659    static REENTRANT_WAKER_VTABLE: std::task::RawWakerVTable = std::task::RawWakerVTable::new(
660        clone_reentrant_waker,
661        wake_reentrant_waker,
662        wake_reentrant_waker_by_ref,
663        drop_reentrant_waker,
664    );
665
666    fn reentrant_waker(
667        host_ctx: Arc<HostContext>,
668        clones: Arc<std::sync::atomic::AtomicUsize>,
669        drops: Arc<std::sync::atomic::AtomicUsize>,
670    ) -> std::task::Waker {
671        let state = Arc::new(ReentrantWakerState {
672            host_ctx,
673            clones,
674            drops,
675        });
676        let raw = std::task::RawWaker::new(Arc::into_raw(state).cast(), &REENTRANT_WAKER_VTABLE);
677        unsafe { std::task::Waker::from_raw(raw) }
678    }
679
680    fn example_plugin_path() -> Option<std::path::PathBuf> {
681        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
682            .parent()?
683            .parent()?;
684        let manifest = workspace_root.join("examples/ex-nyring-plugin/Cargo.toml");
685        if !manifest.is_file() {
686            return None;
687        }
688        let status = std::process::Command::new("cargo")
689            .args(["build", "--release", "--manifest-path"])
690            .arg(&manifest)
691            .status()
692            .ok()?;
693        if !status.success() {
694            return None;
695        }
696        let filename = if cfg!(target_os = "macos") {
697            "libex_nyring_plugin.dylib"
698        } else if cfg!(target_os = "windows") {
699            "ex_nyring_plugin.dll"
700        } else {
701            "libex_nyring_plugin.so"
702        };
703        Some(workspace_root.join("target/release").join(filename))
704    }
705
706    fn c_plugin_path() -> Option<std::path::PathBuf> {
707        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
708            .parent()?
709            .parent()?;
710        let source = workspace_root.join("examples/c-plugin/plugin.c");
711        if !source.is_file() {
712            return None;
713        }
714        let extension = if cfg!(target_os = "macos") {
715            "dylib"
716        } else if cfg!(target_os = "windows") {
717            "dll"
718        } else {
719            "so"
720        };
721        let output = workspace_root
722            .join("target/c-example")
723            .join(format!("libnylon_ring_c_example.{extension}"));
724        std::fs::create_dir_all(output.parent()?).ok()?;
725        let status = std::process::Command::new("cc")
726            .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-shared"])
727            .args((!cfg!(target_os = "windows")).then_some("-fPIC"))
728            .arg(&source)
729            .arg("-o")
730            .arg(&output)
731            .status()
732            .ok()?;
733        status.success().then_some(output)
734    }
735
736    #[test]
737    fn dropping_pending_guard_unregisters_unary_request() {
738        let host = NylonRingHost::new();
739        let sid = 42;
740        context::insert_pending(
741            &host.host_ctx,
742            sid,
743            types::Pending::Unary(types::UnaryPending::waiting()),
744        );
745        host.host_ctx.set_state(sid, "key".into(), vec![1]);
746
747        drop(PendingGuard::new(&host.host_ctx, sid));
748
749        assert!(context::remove_pending(&host.host_ctx, sid).is_none());
750        assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
751    }
752
753    #[test]
754    fn unary_completion_wakes_latest_waiter_across_threads() {
755        let host = NylonRingHost::new();
756        let sid = 46;
757        context::insert_pending(
758            &host.host_ctx,
759            sid,
760            types::Pending::Unary(types::UnaryPending::waiting()),
761        );
762        host.host_ctx.set_state(sid, "key".into(), vec![1]);
763
764        let first_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
765        let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
766        let first_waker = std::task::Waker::from(Arc::new(WakeFlag(first_woken.clone())));
767        let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken.clone())));
768        let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
769
770        let mut first_context = std::task::Context::from_waker(&first_waker);
771        assert!(matches!(
772            std::future::Future::poll(future.as_mut(), &mut first_context),
773            std::task::Poll::Pending
774        ));
775        let mut second_context = std::task::Context::from_waker(&second_waker);
776        assert!(matches!(
777            std::future::Future::poll(future.as_mut(), &mut second_context),
778            std::task::Poll::Pending
779        ));
780
781        let host_ctx = host.host_ctx.clone();
782        let dispatch_status = std::thread::spawn(move || {
783            context::dispatch_pending(
784                &host_ctx,
785                sid,
786                StreamFrame {
787                    status: NrStatus::Ok,
788                    data: vec![7],
789                },
790            )
791        })
792        .join()
793        .unwrap();
794        assert_eq!(dispatch_status, NrStatus::Ok);
795        assert!(!first_woken.load(std::sync::atomic::Ordering::Acquire));
796        assert!(second_woken.load(std::sync::atomic::Ordering::Acquire));
797        assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
798        assert_eq!(host.metrics().pending_requests, 1);
799
800        assert_eq!(
801            context::dispatch_pending(
802                &host.host_ctx,
803                sid,
804                StreamFrame {
805                    status: NrStatus::Ok,
806                    data: vec![8],
807                },
808            ),
809            NrStatus::Invalid
810        );
811
812        match std::future::Future::poll(future.as_mut(), &mut second_context) {
813            std::task::Poll::Ready(Some((status, data))) => {
814                assert_eq!(status, NrStatus::Ok);
815                assert_eq!(data, vec![7]);
816            }
817            result => panic!("unexpected unary completion result: {result:?}"),
818        }
819        assert_eq!(host.metrics().pending_requests, 0);
820    }
821
822    #[test]
823    fn unary_waker_callbacks_run_outside_pending_lock() {
824        let host = NylonRingHost::new();
825        let sid = 47;
826        context::insert_pending(
827            &host.host_ctx,
828            sid,
829            types::Pending::Unary(types::UnaryPending::waiting()),
830        );
831
832        let clones = Arc::new(std::sync::atomic::AtomicUsize::new(0));
833        let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
834        let first_waker = reentrant_waker(host.host_ctx.clone(), clones.clone(), drops.clone());
835        let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
836        {
837            let mut context = std::task::Context::from_waker(&first_waker);
838            assert!(matches!(
839                std::future::Future::poll(future.as_mut(), &mut context),
840                std::task::Poll::Pending
841            ));
842        }
843        assert_eq!(clones.load(std::sync::atomic::Ordering::Relaxed), 1);
844
845        let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
846        let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken)));
847        let mut context = std::task::Context::from_waker(&second_waker);
848        assert!(matches!(
849            std::future::Future::poll(future.as_mut(), &mut context),
850            std::task::Poll::Pending
851        ));
852        assert_eq!(drops.load(std::sync::atomic::Ordering::Relaxed), 1);
853
854        drop(future);
855        context::cleanup_sid(&host.host_ctx, sid);
856    }
857
858    #[test]
859    fn dropping_stream_receiver_unregisters_stream() {
860        let host = NylonRingHost::new();
861        let sid = 43;
862        let (tx, rx) = tokio::sync::mpsc::channel(1);
863        context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
864        host.host_ctx.set_state(sid, "key".into(), vec![1]);
865
866        drop(StreamReceiver::new(rx, host.host_ctx.clone(), sid, None));
867
868        assert!(context::remove_pending(&host.host_ctx, sid).is_none());
869        assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
870    }
871
872    #[test]
873    fn fast_slot_reentry_is_rejected_and_binding_is_cleared() {
874        let mut first = types::UnaryResultSlot {
875            sid: 1,
876            result: None,
877        };
878        let mut second = types::UnaryResultSlot {
879            sid: 2,
880            result: None,
881        };
882        let binding = FastSlotBinding::bind(&mut first).unwrap();
883        assert!(matches!(
884            FastSlotBinding::bind(&mut second),
885            Err(NylonRingHostError::FastPathReentrant)
886        ));
887        drop(binding);
888        assert!(FastSlotBinding::bind(&mut second).is_ok());
889    }
890
891    #[test]
892    fn bounded_stream_reports_backpressure_and_removes_terminal_once() {
893        let host = NylonRingHost::with_stream_capacity(1);
894        let sid = 44;
895        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
896        context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
897
898        assert_eq!(
899            context::dispatch_pending(
900                &host.host_ctx,
901                sid,
902                StreamFrame {
903                    status: NrStatus::Ok,
904                    data: vec![1],
905                },
906            ),
907            NrStatus::Ok
908        );
909        assert_eq!(
910            context::dispatch_pending(
911                &host.host_ctx,
912                sid,
913                StreamFrame {
914                    status: NrStatus::Ok,
915                    data: vec![2],
916                },
917            ),
918            NrStatus::Backpressure
919        );
920        assert_eq!(rx.try_recv().unwrap().data, vec![1]);
921        assert_eq!(
922            context::dispatch_pending(
923                &host.host_ctx,
924                sid,
925                StreamFrame {
926                    status: NrStatus::StreamEnd,
927                    data: vec![],
928                },
929            ),
930            NrStatus::Ok
931        );
932        assert_eq!(host.metrics().pending_requests, 0);
933        assert_eq!(rx.try_recv().unwrap().status, NrStatus::StreamEnd);
934    }
935
936    #[test]
937    fn concurrent_terminal_frames_complete_stream_once() {
938        let host = NylonRingHost::with_stream_capacity(2);
939        let sid = 45;
940        let (tx, mut rx) = tokio::sync::mpsc::channel(2);
941        context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
942        let barrier = Arc::new(std::sync::Barrier::new(3));
943
944        let results: Vec<_> = std::thread::scope(|scope| {
945            let handles: Vec<_> = (0..2)
946                .map(|value| {
947                    let ctx = host.host_ctx.clone();
948                    let barrier = barrier.clone();
949                    scope.spawn(move || {
950                        barrier.wait();
951                        context::dispatch_pending(
952                            &ctx,
953                            sid,
954                            StreamFrame {
955                                status: NrStatus::StreamEnd,
956                                data: vec![value],
957                            },
958                        )
959                    })
960                })
961                .collect();
962            barrier.wait();
963            handles
964                .into_iter()
965                .map(|handle| handle.join().unwrap())
966                .collect()
967        });
968
969        assert_eq!(
970            results
971                .iter()
972                .filter(|&&status| status == NrStatus::Ok)
973                .count(),
974            1
975        );
976        assert_eq!(
977            results
978                .iter()
979                .filter(|&&status| status == NrStatus::Invalid)
980                .count(),
981            1
982        );
983        assert!(rx.try_recv().is_ok());
984        assert!(rx.try_recv().is_err());
985        assert_eq!(host.metrics().pending_requests, 0);
986    }
987
988    #[test]
989    fn unload_defers_library_drop_and_rejects_new_calls_on_existing_handle() {
990        let Some(path) = example_plugin_path() else {
991            return;
992        };
993        let mut host = NylonRingHost::new();
994        host.load("example", path.to_str().unwrap()).unwrap();
995        let handle = host.plugin("example").unwrap();
996        let runtime = tokio::runtime::Runtime::new().unwrap();
997        assert!(matches!(
998            runtime.block_on(handle.call_response_timeout(
999                "benchmark_without_response",
1000                b"",
1001                Duration::from_millis(1),
1002            )),
1003            Err(NylonRingHostError::Timeout)
1004        ));
1005        assert_eq!(host.metrics().pending_requests, 0);
1006
1007        let guard = handle.plugin.begin_call().unwrap();
1008        assert_eq!(host.metrics().in_flight_calls, 1);
1009
1010        host.unload("example").unwrap();
1011        assert!(matches!(
1012            runtime.block_on(handle.call("benchmark_without_response", b"")),
1013            Err(NylonRingHostError::PluginUnloaded)
1014        ));
1015        drop(guard);
1016        drop(handle);
1017    }
1018
1019    #[test]
1020    fn example_plugin_fire_and_forget_entry_returns_ok() {
1021        let Some(path) = example_plugin_path() else {
1022            return;
1023        };
1024        let mut host = NylonRingHost::new();
1025        host.load("example", path.to_str().unwrap()).unwrap();
1026        let handle = host.plugin("example").unwrap();
1027        let runtime = tokio::runtime::Runtime::new().unwrap();
1028
1029        assert!(matches!(
1030            runtime.block_on(handle.call("notify", b"fire and forget")),
1031            Ok(NrStatus::Ok)
1032        ));
1033    }
1034
1035    #[test]
1036    fn c_plugin_layout_round_trips_through_host() {
1037        let Some(path) = c_plugin_path() else {
1038            return;
1039        };
1040        let mut host = NylonRingHost::new();
1041        host.load("c-example", path.to_str().unwrap()).unwrap();
1042        let handle = host.plugin("c-example").unwrap();
1043        let runtime = tokio::runtime::Runtime::new().unwrap();
1044        let (status, response) = runtime
1045            .block_on(handle.call_response("echo", b"from-c"))
1046            .unwrap();
1047        assert_eq!(status, NrStatus::Ok);
1048        assert_eq!(response, b"from-c");
1049    }
1050}