1mod call_tracker;
9mod callbacks;
10mod context;
11mod error;
12mod extensions;
13#[cfg(test)]
14mod loom_tests;
15mod sid;
16mod stream_channel;
17mod types;
18
19use call_tracker::CallTracker;
20use callbacks::{
21 acquire_result_buffer_callback, commit_result_buffer_callback, get_state_callback,
22 send_result_owned_callback, send_result_vec_callback, set_state_callback,
23};
24use context::{CURRENT_UNARY_RESULT, HostContext};
25use libloading::{Library, Symbol};
26use nylon_ring::{ABI_VERSION, NrBytes, NrHostExt, NrHostVTable, NrPluginInfo, NrStr};
27use sid::next_sid;
28use std::collections::HashMap;
29use std::ffi::c_void;
30use std::sync::Arc;
31use std::time::Duration;
32
33pub use error::NylonRingHostError;
34pub use extensions::Extensions;
35pub use nylon_ring::NrStatus;
36pub use types::{ResponseBytes, Result, StreamFrame, StreamReceiver};
37
38pub const DEFAULT_STREAM_CAPACITY: usize = 64;
40
41#[derive(Debug, Copy, Clone, PartialEq, Eq)]
43pub struct HostMetrics {
44 pub loaded_plugins: usize,
45 pub pending_requests: usize,
46 pub state_sessions: usize,
47 pub in_flight_calls: usize,
48}
49
50struct PendingGuard<'a> {
51 host_ctx: &'a HostContext,
52 sid: u64,
53 armed: bool,
54}
55
56impl<'a> PendingGuard<'a> {
57 fn new(host_ctx: &'a HostContext, sid: u64) -> Self {
58 Self {
59 host_ctx,
60 sid,
61 armed: true,
62 }
63 }
64
65 fn disarm(&mut self) {
66 self.armed = false;
67 }
68}
69
70impl Drop for PendingGuard<'_> {
71 fn drop(&mut self) {
72 if self.armed {
73 context::cleanup_sid(self.host_ctx, self.sid);
74 }
75 }
76}
77
78struct FastSlotBinding;
79
80impl FastSlotBinding {
81 fn bind(slot: &mut types::UnaryResultSlot) -> Result<Self> {
82 let bound = CURRENT_UNARY_RESULT.with(|cell| {
83 if !cell.get().is_null() {
84 return false;
85 }
86 cell.set(slot as *mut _);
87 true
88 });
89 if bound {
90 Ok(Self)
91 } else {
92 Err(NylonRingHostError::FastPathReentrant)
93 }
94 }
95}
96
97impl Drop for FastSlotBinding {
98 fn drop(&mut self) {
99 CURRENT_UNARY_RESULT.with(|cell| cell.set(std::ptr::null_mut()));
100 }
101}
102
103struct StreamSlotBinding;
107
108impl StreamSlotBinding {
109 fn bind(slot: &mut context::StreamFrameSlot) -> Option<Self> {
110 let bound = context::CURRENT_STREAM_FRAME.with(|cell| {
111 if !cell.get().is_null() {
112 return false;
113 }
114 cell.set(slot as *mut _);
115 true
116 });
117 bound.then_some(Self)
118 }
119}
120
121impl Drop for StreamSlotBinding {
122 fn drop(&mut self) {
123 context::CURRENT_STREAM_FRAME.with(|cell| cell.set(std::ptr::null_mut()));
124 }
125}
126
127#[derive(Copy, Clone)]
130struct PluginDispatch {
131 handle: Option<unsafe extern "C" fn(NrStr, u64, NrBytes) -> NrStatus>,
132 shutdown: Option<unsafe extern "C" fn()>,
133 stream_data: Option<unsafe extern "C" fn(u64, NrBytes) -> NrStatus>,
134 stream_close: Option<unsafe extern "C" fn(u64) -> NrStatus>,
135 resolve_entry: Option<unsafe extern "C" fn(NrStr) -> u32>,
136 handle_by_id: Option<unsafe extern "C" fn(u32, u64, NrBytes) -> NrStatus>,
137}
138
139impl PluginDispatch {
140 fn from_vtable(vtable: &nylon_ring::NrPluginVTable) -> Self {
141 Self {
142 handle: vtable.handle,
143 shutdown: vtable.shutdown,
144 stream_data: vtable.stream_data,
145 stream_close: vtable.stream_close,
146 resolve_entry: vtable.resolve_entry,
147 handle_by_id: vtable.handle_by_id,
148 }
149 }
150}
151
152pub struct LoadedPlugin {
154 _lib: std::mem::ManuallyDrop<Library>,
155 dispatch: PluginDispatch,
156 host_ctx: Arc<HostContext>,
157 path: String,
158 call_tracker: CallTracker,
159 pinned: bool,
165}
166
167const PINNED_SHARD: usize = usize::MAX;
170
171static RETIRED_PLUGINS: std::sync::Mutex<Vec<Arc<LoadedPlugin>>> =
180 std::sync::Mutex::new(Vec::new());
181
182fn retire_plugin(plugin: Arc<LoadedPlugin>) {
187 plugin.stop_accepting_calls();
188 RETIRED_PLUGINS.lock().unwrap().push(plugin);
189 sweep_retired_plugins();
192}
193
194fn sweep_retired_plugins() {
205 std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
206 let drained: Vec<Arc<LoadedPlugin>> = {
207 let mut retired = RETIRED_PLUGINS.lock().unwrap();
208 let (drained, live) = std::mem::take(&mut *retired)
209 .into_iter()
210 .partition(|plugin| plugin.call_tracker.active_calls() == 0);
211 *retired = live;
212 drained
213 };
214 drop(drained);
217}
218
219impl LoadedPlugin {
220 fn begin_call(&self) -> Result<BorrowedPluginCallGuard<'_>> {
221 if self.pinned {
222 return Ok(BorrowedPluginCallGuard {
225 tracker: &self.call_tracker,
226 shard: PINNED_SHARD,
227 });
228 }
229 let shard = self
230 .call_tracker
231 .try_begin()
232 .ok_or(NylonRingHostError::PluginUnloaded)?;
233 Ok(BorrowedPluginCallGuard {
234 tracker: &self.call_tracker,
235 shard,
236 })
237 }
238
239 fn begin_owned_call(self: &Arc<Self>) -> Result<PluginCallGuard> {
240 let shard = self
241 .call_tracker
242 .try_begin()
243 .ok_or(NylonRingHostError::PluginUnloaded)?;
244 Ok(PluginCallGuard {
245 plugin: Arc::as_ptr(self),
246 shard,
247 })
248 }
249
250 fn stop_accepting_calls(&self) {
251 self.call_tracker.stop();
252 }
253}
254
255struct BorrowedPluginCallGuard<'a> {
256 tracker: &'a CallTracker,
257 shard: usize,
258}
259
260impl Drop for BorrowedPluginCallGuard<'_> {
261 fn drop(&mut self) {
262 if self.shard != PINNED_SHARD && self.tracker.finish(self.shard) {
263 sweep_retired_plugins();
264 }
265 }
266}
267
268pub(crate) struct PluginCallGuard {
278 plugin: *const LoadedPlugin,
279 shard: usize,
280}
281
282unsafe impl Send for PluginCallGuard {}
286unsafe impl Sync for PluginCallGuard {}
287
288impl PluginCallGuard {
289 pub(crate) fn plugin(&self) -> &LoadedPlugin {
290 unsafe { &*self.plugin }
293 }
294}
295
296impl Drop for PluginCallGuard {
297 fn drop(&mut self) {
298 if self.plugin().call_tracker.finish(self.shard) {
301 sweep_retired_plugins();
302 }
303 }
304}
305
306impl Drop for LoadedPlugin {
307 fn drop(&mut self) {
308 if self.pinned {
309 return;
313 }
314 if let Some(shutdown_fn) = self.dispatch.shutdown {
315 unsafe {
316 shutdown_fn();
317 }
318 }
319 unsafe { std::mem::ManuallyDrop::drop(&mut self._lib) };
322 }
323}
324
325#[derive(Clone)]
327pub struct PluginHandle {
328 plugin: Arc<LoadedPlugin>,
329}
330
331impl PluginHandle {
332 fn status_error(status: NrStatus) -> NylonRingHostError {
333 if status == NrStatus::Panic {
334 NylonRingHostError::PluginPanicked
335 } else {
336 NylonRingHostError::PluginHandleFailed(status)
337 }
338 }
339
340 pub async fn call_response(&self, entry: &str, payload: &[u8]) -> Result<(NrStatus, Vec<u8>)> {
342 let _call_guard = self.plugin.begin_call()?;
343 let sid = next_sid();
344
345 context::insert_pending(
346 &self.plugin.host_ctx,
347 sid,
348 types::Pending::Unary(types::UnaryPending::waiting()),
349 );
350 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
351
352 let mut slot = types::UnaryResultSlot {
357 sid,
358 result: None,
359 lease: None,
360 };
361 let binding = FastSlotBinding::bind(&mut slot).ok();
362
363 let payload_bytes = NrBytes::from_slice(payload);
364 let handle_raw_fn = match self.plugin.dispatch.handle {
365 Some(f) => f,
366 None => return Err(NylonRingHostError::MissingRequiredFunctions),
367 };
368
369 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
370
371 drop(binding);
372 if let Some(lease) = slot.lease.take() {
376 self.plugin.host_ctx.park_orphan_lease(lease);
377 }
378
379 if status != NrStatus::Ok {
380 return Err(Self::status_error(status));
381 }
382
383 if let Some((status, data)) = slot.result.take() {
384 return Ok((status, data));
386 }
387
388 match context::wait_for_unary(&self.plugin.host_ctx, sid).await {
389 Some((status, payload)) => {
390 pending_guard.disarm();
391 Ok((status, payload.into_vec()))
394 }
395 None => Err(NylonRingHostError::PluginUnloaded),
396 }
397 }
398
399 pub async fn call_response_bytes(
407 &self,
408 entry: &str,
409 payload: &[u8],
410 ) -> Result<(NrStatus, ResponseBytes)> {
411 let call_guard = self.plugin.begin_owned_call()?;
412 let sid = next_sid();
413
414 context::insert_pending(
415 &self.plugin.host_ctx,
416 sid,
417 types::Pending::Unary(types::UnaryPending::waiting()),
418 );
419 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
420
421 let payload_bytes = NrBytes::from_slice(payload);
422 let handle_raw_fn = match self.plugin.dispatch.handle {
423 Some(f) => f,
424 None => return Err(NylonRingHostError::MissingRequiredFunctions),
425 };
426
427 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
428
429 if status != NrStatus::Ok {
430 return Err(Self::status_error(status));
431 }
432
433 match context::wait_for_unary(&self.plugin.host_ctx, sid).await {
434 Some((status, payload)) => {
435 pending_guard.disarm();
436 let guard =
439 matches!(payload, types::ResponsePayload::Foreign(_)).then_some(call_guard);
440 Ok((status, ResponseBytes::new(payload, guard)))
441 }
442 None => Err(NylonRingHostError::PluginUnloaded),
443 }
444 }
445
446 pub async fn call_response_timeout(
453 &self,
454 entry: &str,
455 payload: &[u8],
456 timeout: std::time::Duration,
457 ) -> Result<(NrStatus, Vec<u8>)> {
458 let _call_guard = self.plugin.begin_call()?;
459 let sid = next_sid();
460
461 context::insert_pending(
462 &self.plugin.host_ctx,
463 sid,
464 types::Pending::Unary(types::UnaryPending::waiting()),
465 );
466 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
467
468 let mut slot = types::UnaryResultSlot {
470 sid,
471 result: None,
472 lease: None,
473 };
474 let binding = FastSlotBinding::bind(&mut slot).ok();
475
476 let payload_bytes = NrBytes::from_slice(payload);
477 let handle_raw_fn = match self.plugin.dispatch.handle {
478 Some(f) => f,
479 None => return Err(NylonRingHostError::MissingRequiredFunctions),
480 };
481
482 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
483
484 drop(binding);
485 if let Some(lease) = slot.lease.take() {
486 self.plugin.host_ctx.park_orphan_lease(lease);
487 }
488
489 if status != NrStatus::Ok {
490 return Err(Self::status_error(status));
491 }
492
493 if let Some((status, data)) = slot.result.take() {
494 return Ok((status, data));
495 }
496
497 match tokio::time::timeout(timeout, context::wait_for_unary(&self.plugin.host_ctx, sid))
498 .await
499 {
500 Ok(Some((status, payload))) => {
501 pending_guard.disarm();
502 Ok((status, payload.into_vec()))
503 }
504 Ok(None) => Err(NylonRingHostError::PluginUnloaded),
505 Err(_) => Err(NylonRingHostError::Timeout),
506 }
507 }
508
509 pub async fn call_response_fast(
511 &self,
512 entry: &str,
513 payload: &[u8],
514 ) -> Result<(NrStatus, Vec<u8>)> {
515 let _call_guard = self.plugin.begin_call()?;
516 let sid = next_sid();
517 let mut slot = types::UnaryResultSlot {
518 sid,
519 result: None,
520 lease: None,
521 };
522
523 let binding = FastSlotBinding::bind(&mut slot)?;
524
525 let payload_bytes = NrBytes::from_slice(payload);
526
527 let handle_raw_fn = match self.plugin.dispatch.handle {
528 Some(f) => f,
529 None => return Err(NylonRingHostError::MissingRequiredFunctions),
530 };
531
532 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
533
534 drop(binding);
535 if let Some(lease) = slot.lease.take() {
539 self.plugin.host_ctx.park_orphan_lease(lease);
540 }
541 self.plugin.host_ctx.remove_state(sid);
542
543 if status != NrStatus::Ok {
544 return Err(Self::status_error(status));
545 }
546
547 match slot.result {
548 Some((st, data)) => Ok((st, data)),
549 None => Err(NylonRingHostError::MissingSynchronousResponse),
550 }
551 }
552
553 pub async fn call(&self, entry: &str, payload: &[u8]) -> Result<NrStatus> {
555 let _call_guard = self.plugin.begin_call()?;
556 let sid = next_sid();
558
559 let payload_bytes = NrBytes::from_slice(payload);
560 let handle_raw_fn = match self.plugin.dispatch.handle {
561 Some(f) => f,
562 None => {
563 return Err(NylonRingHostError::MissingRequiredFunctions);
564 }
565 };
566
567 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
568
569 self.plugin.host_ctx.remove_state(sid);
572
573 if status != NrStatus::Ok {
574 return Err(Self::status_error(status));
575 }
576 Ok(status)
577 }
578
579 pub async fn call_stream(&self, entry: &str, payload: &[u8]) -> Result<(u64, StreamReceiver)> {
581 let call_guard = self.plugin.begin_owned_call()?;
582 let sid = next_sid();
583
584 let (tx, rx) = stream_channel::acquire(self.plugin.host_ctx.stream_capacity());
585
586 let mut frame_slot = context::StreamFrameSlot {
589 sid,
590 chan: tx.channel(),
591 terminal_seen: false,
592 };
593
594 context::insert_pending(&self.plugin.host_ctx, sid, types::Pending::Stream(tx));
596
597 let payload_bytes = NrBytes::from_slice(payload);
598
599 let handle_raw_fn = match self.plugin.dispatch.handle {
600 Some(f) => f,
601 None => {
602 context::cleanup_sid(&self.plugin.host_ctx, sid);
603 return Err(NylonRingHostError::MissingRequiredFunctions);
604 }
605 };
606
607 let binding = StreamSlotBinding::bind(&mut frame_slot);
608 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
609 drop(binding);
610
611 if frame_slot.terminal_seen {
612 context::cleanup_sid(&self.plugin.host_ctx, sid);
615 }
616
617 if status != NrStatus::Ok {
618 context::cleanup_sid(&self.plugin.host_ctx, sid);
619 return Err(Self::status_error(status));
620 }
621
622 Ok((sid, StreamReceiver::new(rx, None, sid, Some(call_guard))))
623 }
624
625 pub fn send_stream_data(&self, sid: u64, data: &[u8]) -> Result<NrStatus> {
627 let stream_data_fn = match self.plugin.dispatch.stream_data {
628 Some(f) => f,
629 None => return Err(NylonRingHostError::MissingRequiredFunctions),
630 };
631 let payload = NrBytes::from_slice(data);
632 Ok(unsafe { stream_data_fn(sid, payload) })
633 }
634
635 pub fn close_stream(&self, sid: u64) -> Result<NrStatus> {
637 let stream_close_fn = match self.plugin.dispatch.stream_close {
638 Some(f) => f,
639 None => return Err(NylonRingHostError::MissingRequiredFunctions),
640 };
641 Ok(unsafe { stream_close_fn(sid) })
642 }
643
644 pub fn entry(&self, entry: &str) -> Result<PluginEntry> {
654 let (Some(resolve_fn), Some(_)) = (
655 self.plugin.dispatch.resolve_entry,
656 self.plugin.dispatch.handle_by_id,
657 ) else {
658 return Err(NylonRingHostError::EntryDispatchUnsupported);
659 };
660 let _call_guard = self.plugin.begin_call()?;
662 let id = unsafe { resolve_fn(NrStr::new(entry)) };
663 if id == nylon_ring::NR_ENTRY_UNKNOWN {
664 return Err(NylonRingHostError::EntryNotFound(entry.to_string()));
665 }
666 Ok(PluginEntry {
667 plugin: self.plugin.clone(),
668 id,
669 })
670 }
671}
672
673#[derive(Clone)]
677pub struct PluginEntry {
678 plugin: Arc<LoadedPlugin>,
679 id: u32,
680}
681
682impl PluginEntry {
683 fn handle_by_id_fn(&self) -> Result<unsafe extern "C" fn(u32, u64, NrBytes) -> NrStatus> {
684 self.plugin
685 .dispatch
686 .handle_by_id
687 .ok_or(NylonRingHostError::EntryDispatchUnsupported)
688 }
689
690 pub async fn call(&self, payload: &[u8]) -> Result<NrStatus> {
692 let _call_guard = self.plugin.begin_call()?;
693 let sid = next_sid();
694
695 let handle_by_id_fn = self.handle_by_id_fn()?;
696 let status = unsafe { handle_by_id_fn(self.id, sid, NrBytes::from_slice(payload)) };
697
698 self.plugin.host_ctx.remove_state(sid);
699
700 if status != NrStatus::Ok {
701 return Err(PluginHandle::status_error(status));
702 }
703 Ok(status)
704 }
705
706 pub async fn call_response(&self, payload: &[u8]) -> Result<(NrStatus, Vec<u8>)> {
708 let _call_guard = self.plugin.begin_call()?;
709 let sid = next_sid();
710
711 context::insert_pending(
712 &self.plugin.host_ctx,
713 sid,
714 types::Pending::Unary(types::UnaryPending::waiting()),
715 );
716 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
717
718 let mut slot = types::UnaryResultSlot {
720 sid,
721 result: None,
722 lease: None,
723 };
724 let binding = FastSlotBinding::bind(&mut slot).ok();
725
726 let handle_by_id_fn = self.handle_by_id_fn()?;
727 let status = unsafe { handle_by_id_fn(self.id, sid, NrBytes::from_slice(payload)) };
728
729 drop(binding);
730 if let Some(lease) = slot.lease.take() {
731 self.plugin.host_ctx.park_orphan_lease(lease);
732 }
733
734 if status != NrStatus::Ok {
735 return Err(PluginHandle::status_error(status));
736 }
737
738 if let Some((status, data)) = slot.result.take() {
739 return Ok((status, data));
740 }
741
742 match context::wait_for_unary(&self.plugin.host_ctx, sid).await {
743 Some((status, payload)) => {
744 pending_guard.disarm();
745 Ok((status, payload.into_vec()))
746 }
747 None => Err(NylonRingHostError::PluginUnloaded),
748 }
749 }
750
751 pub async fn call_response_fast(&self, payload: &[u8]) -> Result<(NrStatus, Vec<u8>)> {
753 let _call_guard = self.plugin.begin_call()?;
754 let sid = next_sid();
755 let mut slot = types::UnaryResultSlot {
756 sid,
757 result: None,
758 lease: None,
759 };
760
761 let binding = FastSlotBinding::bind(&mut slot)?;
762
763 let handle_by_id_fn = self.handle_by_id_fn()?;
764 let status = unsafe { handle_by_id_fn(self.id, sid, NrBytes::from_slice(payload)) };
765
766 drop(binding);
767 if let Some(lease) = slot.lease.take() {
768 self.plugin.host_ctx.park_orphan_lease(lease);
769 }
770 self.plugin.host_ctx.remove_state(sid);
771
772 if status != NrStatus::Ok {
773 return Err(PluginHandle::status_error(status));
774 }
775
776 match slot.result {
777 Some((st, data)) => Ok((st, data)),
778 None => Err(NylonRingHostError::MissingSynchronousResponse),
779 }
780 }
781
782 pub async fn call_stream(&self, payload: &[u8]) -> Result<(u64, StreamReceiver)> {
784 let call_guard = self.plugin.begin_owned_call()?;
785 let sid = next_sid();
786
787 let (tx, rx) = stream_channel::acquire(self.plugin.host_ctx.stream_capacity());
788
789 let mut frame_slot = context::StreamFrameSlot {
791 sid,
792 chan: tx.channel(),
793 terminal_seen: false,
794 };
795
796 context::insert_pending(&self.plugin.host_ctx, sid, types::Pending::Stream(tx));
797
798 let handle_by_id_fn = match self.handle_by_id_fn() {
799 Ok(f) => f,
800 Err(error) => {
801 context::cleanup_sid(&self.plugin.host_ctx, sid);
802 return Err(error);
803 }
804 };
805 let binding = StreamSlotBinding::bind(&mut frame_slot);
806 let status = unsafe { handle_by_id_fn(self.id, sid, NrBytes::from_slice(payload)) };
807 drop(binding);
808
809 if frame_slot.terminal_seen {
810 context::cleanup_sid(&self.plugin.host_ctx, sid);
811 }
812
813 if status != NrStatus::Ok {
814 context::cleanup_sid(&self.plugin.host_ctx, sid);
815 return Err(PluginHandle::status_error(status));
816 }
817
818 Ok((sid, StreamReceiver::new(rx, None, sid, Some(call_guard))))
819 }
820}
821
822pub struct NylonRingHost {
824 plugins: HashMap<String, Arc<LoadedPlugin>>,
825 host_ctx: Arc<HostContext>,
826 host_vtable: Box<NrHostVTable>,
827}
828
829impl Default for NylonRingHost {
830 fn default() -> Self {
831 Self::new()
832 }
833}
834
835impl Drop for NylonRingHost {
836 fn drop(&mut self) {
837 for (_, plugin) in self.plugins.drain() {
841 retire_plugin(plugin);
842 }
843 }
844}
845
846impl NylonRingHost {
847 pub fn new() -> Self {
849 Self::with_stream_capacity(DEFAULT_STREAM_CAPACITY)
850 }
851
852 pub fn with_stream_capacity(stream_capacity: usize) -> Self {
854 assert!(
855 stream_capacity > 0,
856 "stream capacity must be greater than zero"
857 );
858 let host_ctx = Arc::new(HostContext::new(
859 NrHostExt {
860 set_state: set_state_callback,
861 get_state: get_state_callback,
862 },
863 stream_capacity,
864 ));
865
866 let host_vtable = Box::new(NrHostVTable {
867 send_result: send_result_vec_callback,
868 send_result_owned: send_result_owned_callback,
869 acquire_result_buffer: acquire_result_buffer_callback,
870 commit_result_buffer: commit_result_buffer_callback,
871 });
872
873 Self {
874 plugins: HashMap::new(),
875 host_ctx,
876 host_vtable,
877 }
878 }
879
880 pub fn load(&mut self, name: &str, path: &str) -> Result<()> {
885 self.load_inner(name, path, false)
886 }
887
888 pub fn load_pinned(&mut self, name: &str, path: &str) -> Result<()> {
897 self.load_inner(name, path, true)
898 }
899
900 fn load_inner(&mut self, name: &str, path: &str, pinned: bool) -> Result<()> {
901 if let Some(existing) = self.plugins.get(name)
902 && existing.pinned
903 {
904 return Err(NylonRingHostError::PluginPinned(name.to_owned()));
905 }
906 unsafe {
907 let lib = Library::new(path).map_err(NylonRingHostError::FailedToLoadLibrary)?;
908
909 let get_plugin: Symbol<extern "C" fn() -> *const NrPluginInfo> =
910 lib.get(b"nylon_ring_get_plugin\0").map_err(|_| {
911 NylonRingHostError::MissingSymbol("nylon_ring_get_plugin".to_string())
912 })?;
913 let info_ptr = get_plugin();
914 Self::validate_plugin_info::<NrPluginInfo>(info_ptr.cast(), ABI_VERSION)?;
915 let info = &*info_ptr;
916 if info.vtable.is_null() {
917 return Err(NylonRingHostError::NullPluginVTable);
918 }
919 let plugin_vtable = &*info.vtable;
920 if plugin_vtable.init.is_none() || plugin_vtable.handle.is_none() {
921 return Err(NylonRingHostError::MissingRequiredFunctions);
922 }
923 if let Some(init_fn) = plugin_vtable.init {
924 let status = init_fn(
925 Arc::as_ptr(&self.host_ctx) as *mut c_void,
926 &*self.host_vtable,
927 );
928 if status != NrStatus::Ok {
929 return Err(NylonRingHostError::PluginInitFailed(status));
930 }
931 }
932 let dispatch = PluginDispatch::from_vtable(plugin_vtable);
933
934 let loaded = LoadedPlugin {
935 _lib: std::mem::ManuallyDrop::new(lib),
936 dispatch,
937 host_ctx: self.host_ctx.clone(),
938 path: path.to_string(),
939 call_tracker: CallTracker::new(),
940 pinned,
941 };
942
943 if let Some(previous) = self.plugins.insert(name.to_string(), Arc::new(loaded)) {
944 retire_plugin(previous);
945 }
946 Ok(())
947 }
948 }
949
950 unsafe fn validate_plugin_info<T>(info_ptr: *const u32, expected_version: u32) -> Result<()> {
958 if info_ptr.is_null() {
959 return Err(NylonRingHostError::NullPluginInfo);
960 }
961 let abi_version = unsafe { std::ptr::read_unaligned(info_ptr) };
962 let struct_size = unsafe { std::ptr::read_unaligned(info_ptr.add(1)) };
963 if abi_version != expected_version {
964 return Err(NylonRingHostError::IncompatibleAbiVersion {
965 expected: expected_version,
966 actual: abi_version,
967 });
968 }
969 let expected_size = std::mem::size_of::<T>() as u32;
970 if struct_size < expected_size {
971 return Err(NylonRingHostError::IncompatiblePluginInfoSize {
972 expected: expected_size,
973 actual: struct_size,
974 });
975 }
976 Ok(())
977 }
978
979 pub fn unload(&mut self, name: &str) -> Result<()> {
981 self.refuse_pinned(name)?;
982 let plugin = self
983 .plugins
984 .remove(name)
985 .ok_or_else(|| NylonRingHostError::PluginNotFound(name.to_owned()))?;
986 retire_plugin(plugin);
987 Ok(())
988 }
989
990 fn refuse_pinned(&self, name: &str) -> Result<()> {
993 if let Some(plugin) = self.plugins.get(name)
994 && plugin.pinned
995 {
996 return Err(NylonRingHostError::PluginPinned(name.to_owned()));
997 }
998 Ok(())
999 }
1000
1001 pub async fn unload_with_grace(&mut self, name: &str, grace: Duration) -> Result<()> {
1003 self.refuse_pinned(name)?;
1004 let plugin = self
1005 .plugins
1006 .remove(name)
1007 .ok_or_else(|| NylonRingHostError::PluginNotFound(name.to_owned()))?;
1008 retire_plugin(plugin.clone());
1011 Self::wait_for_drain(&plugin, grace).await
1012 }
1013
1014 pub fn reload(&mut self) -> Result<()> {
1016 if let Some(name) = self
1017 .plugins
1018 .iter()
1019 .find_map(|(name, plugin)| plugin.pinned.then(|| name.clone()))
1020 {
1021 return Err(NylonRingHostError::PluginPinned(name));
1022 }
1023 let active_calls: usize = self
1024 .plugins
1025 .values()
1026 .map(|plugin| plugin.call_tracker.active_calls())
1027 .sum();
1028 if active_calls != 0 {
1029 return Err(NylonRingHostError::PluginBusy { active_calls });
1030 }
1031 let mut plugins_to_reload = Vec::new();
1032 for (name, plugin) in &self.plugins {
1033 plugins_to_reload.push((name.clone(), plugin.path.clone()));
1034 }
1035
1036 for (name, path) in plugins_to_reload {
1039 self.load(&name, &path)?;
1040 }
1041
1042 Ok(())
1043 }
1044
1045 pub async fn reload_with_grace(&mut self, grace: Duration) -> Result<()> {
1047 if let Some(name) = self
1048 .plugins
1049 .iter()
1050 .find_map(|(name, plugin)| plugin.pinned.then(|| name.clone()))
1051 {
1052 return Err(NylonRingHostError::PluginPinned(name));
1053 }
1054 let old_plugins: Vec<_> = self
1055 .plugins
1056 .drain()
1057 .map(|(name, plugin)| {
1058 retire_plugin(plugin.clone());
1059 (name, plugin)
1060 })
1061 .collect();
1062 for (_, plugin) in &old_plugins {
1063 Self::wait_for_drain(plugin, grace).await?;
1064 }
1065 let plugins_to_reload: Vec<_> = old_plugins
1066 .iter()
1067 .map(|(name, plugin)| (name.clone(), plugin.path.clone()))
1068 .collect();
1069 drop(old_plugins);
1070 for (name, path) in plugins_to_reload {
1071 self.load(&name, &path)?;
1072 }
1073 Ok(())
1074 }
1075
1076 async fn wait_for_drain(plugin: &LoadedPlugin, grace: Duration) -> Result<()> {
1077 let deadline = tokio::time::Instant::now() + grace;
1078 while plugin.call_tracker.active_calls() != 0 {
1079 if tokio::time::Instant::now() >= deadline {
1080 return Err(NylonRingHostError::DrainTimeout {
1081 remaining: plugin.call_tracker.active_calls(),
1082 });
1083 }
1084 tokio::time::sleep(Duration::from_millis(1)).await;
1085 }
1086 Ok(())
1087 }
1088
1089 pub fn plugin(&self, name: &str) -> Option<PluginHandle> {
1091 self.plugins
1092 .get(name)
1093 .map(|p| PluginHandle { plugin: p.clone() })
1094 }
1095
1096 pub fn metrics(&self) -> HostMetrics {
1098 HostMetrics {
1099 loaded_plugins: self.plugins.len(),
1100 pending_requests: self.host_ctx.pending_count(),
1101 state_sessions: self.host_ctx.state_count(),
1102 in_flight_calls: self
1103 .plugins
1104 .values()
1105 .map(|plugin| plugin.call_tracker.active_calls())
1106 .sum(),
1107 }
1108 }
1109
1110 pub unsafe fn get_host_ext(host_ctx: *mut c_void) -> *const NrHostExt {
1117 if host_ctx.is_null() {
1118 return std::ptr::null();
1119 }
1120 let ctx = unsafe { &*host_ctx.cast::<HostContext>() };
1121 &ctx.host_ext as *const NrHostExt
1122 }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127 use super::*;
1128
1129 struct WakeFlag(Arc<std::sync::atomic::AtomicBool>);
1130
1131 impl std::task::Wake for WakeFlag {
1132 fn wake(self: Arc<Self>) {
1133 self.0.store(true, std::sync::atomic::Ordering::Release);
1134 }
1135
1136 fn wake_by_ref(self: &Arc<Self>) {
1137 self.0.store(true, std::sync::atomic::Ordering::Release);
1138 }
1139 }
1140
1141 struct ReentrantWakerState {
1142 host_ctx: Arc<HostContext>,
1143 clones: Arc<std::sync::atomic::AtomicUsize>,
1144 drops: Arc<std::sync::atomic::AtomicUsize>,
1145 }
1146
1147 unsafe fn clone_reentrant_waker(data: *const ()) -> std::task::RawWaker {
1148 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
1149 state.host_ctx.pending_count();
1150 state
1151 .clones
1152 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1153 let clone = state.clone();
1154 let _ = Arc::into_raw(state);
1155 std::task::RawWaker::new(Arc::into_raw(clone).cast(), &REENTRANT_WAKER_VTABLE)
1156 }
1157
1158 unsafe fn wake_reentrant_waker(data: *const ()) {
1159 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
1160 state.host_ctx.pending_count();
1161 }
1162
1163 unsafe fn wake_reentrant_waker_by_ref(data: *const ()) {
1164 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
1165 state.host_ctx.pending_count();
1166 let _ = Arc::into_raw(state);
1167 }
1168
1169 unsafe fn drop_reentrant_waker(data: *const ()) {
1170 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
1171 state.host_ctx.pending_count();
1172 state
1173 .drops
1174 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1175 }
1176
1177 static REENTRANT_WAKER_VTABLE: std::task::RawWakerVTable = std::task::RawWakerVTable::new(
1178 clone_reentrant_waker,
1179 wake_reentrant_waker,
1180 wake_reentrant_waker_by_ref,
1181 drop_reentrant_waker,
1182 );
1183
1184 fn reentrant_waker(
1185 host_ctx: Arc<HostContext>,
1186 clones: Arc<std::sync::atomic::AtomicUsize>,
1187 drops: Arc<std::sync::atomic::AtomicUsize>,
1188 ) -> std::task::Waker {
1189 let state = Arc::new(ReentrantWakerState {
1190 host_ctx,
1191 clones,
1192 drops,
1193 });
1194 let raw = std::task::RawWaker::new(Arc::into_raw(state).cast(), &REENTRANT_WAKER_VTABLE);
1195 unsafe { std::task::Waker::from_raw(raw) }
1196 }
1197
1198 fn plugin_test_lock() -> std::sync::MutexGuard<'static, ()> {
1204 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1205 LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
1206 }
1207
1208 fn example_plugin_path() -> Option<std::path::PathBuf> {
1209 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1210 .parent()?
1211 .parent()?;
1212 let manifest = workspace_root.join("examples/ex-nyring-plugin/Cargo.toml");
1213 if !manifest.is_file() {
1214 return None;
1215 }
1216 let status = std::process::Command::new("cargo")
1217 .args(["build", "--release", "--manifest-path"])
1218 .arg(&manifest)
1219 .status()
1220 .ok()?;
1221 if !status.success() {
1222 return None;
1223 }
1224 let filename = if cfg!(target_os = "macos") {
1225 "libex_nyring_plugin.dylib"
1226 } else if cfg!(target_os = "windows") {
1227 "ex_nyring_plugin.dll"
1228 } else {
1229 "libex_nyring_plugin.so"
1230 };
1231 Some(workspace_root.join("target/release").join(filename))
1232 }
1233
1234 fn c_plugin_path() -> Option<std::path::PathBuf> {
1235 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1236 .parent()?
1237 .parent()?;
1238 let source = workspace_root.join("examples/c-plugin/plugin.c");
1239 if !source.is_file() {
1240 return None;
1241 }
1242 let extension = if cfg!(target_os = "macos") {
1243 "dylib"
1244 } else if cfg!(target_os = "windows") {
1245 "dll"
1246 } else {
1247 "so"
1248 };
1249 let output = workspace_root
1250 .join("target/c-example")
1251 .join(format!("libnylon_ring_c_example.{extension}"));
1252 std::fs::create_dir_all(output.parent()?).ok()?;
1253 let status = std::process::Command::new("cc")
1254 .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-shared"])
1255 .args((!cfg!(target_os = "windows")).then_some("-fPIC"))
1256 .arg(&source)
1257 .arg("-o")
1258 .arg(&output)
1259 .status()
1260 .ok()?;
1261 status.success().then_some(output)
1262 }
1263
1264 #[test]
1265 fn dropping_pending_guard_unregisters_unary_request() {
1266 let host = NylonRingHost::new();
1267 let sid = 42;
1268 context::insert_pending(
1269 &host.host_ctx,
1270 sid,
1271 types::Pending::Unary(types::UnaryPending::waiting()),
1272 );
1273 host.host_ctx.set_state(sid, "key".into(), vec![1]);
1274
1275 drop(PendingGuard::new(&host.host_ctx, sid));
1276
1277 assert!(context::remove_pending(&host.host_ctx, sid).is_none());
1278 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
1279 }
1280
1281 #[test]
1282 fn unary_completion_wakes_latest_waiter_across_threads() {
1283 let host = NylonRingHost::new();
1284 let sid = 46;
1285 context::insert_pending(
1286 &host.host_ctx,
1287 sid,
1288 types::Pending::Unary(types::UnaryPending::waiting()),
1289 );
1290 host.host_ctx.set_state(sid, "key".into(), vec![1]);
1291
1292 let first_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
1293 let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
1294 let first_waker = std::task::Waker::from(Arc::new(WakeFlag(first_woken.clone())));
1295 let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken.clone())));
1296 let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
1297
1298 let mut first_context = std::task::Context::from_waker(&first_waker);
1299 assert!(matches!(
1300 std::future::Future::poll(future.as_mut(), &mut first_context),
1301 std::task::Poll::Pending
1302 ));
1303 let mut second_context = std::task::Context::from_waker(&second_waker);
1304 assert!(matches!(
1305 std::future::Future::poll(future.as_mut(), &mut second_context),
1306 std::task::Poll::Pending
1307 ));
1308
1309 let host_ctx = host.host_ctx.clone();
1310 let dispatch_status = std::thread::spawn(move || {
1311 context::dispatch_pending(
1312 &host_ctx,
1313 sid,
1314 NrStatus::Ok,
1315 types::ResponsePayload::Owned(vec![7]),
1316 )
1317 })
1318 .join()
1319 .unwrap();
1320 assert_eq!(dispatch_status, NrStatus::Ok);
1321 assert!(!first_woken.load(std::sync::atomic::Ordering::Acquire));
1322 assert!(second_woken.load(std::sync::atomic::Ordering::Acquire));
1323 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
1324 assert_eq!(host.metrics().pending_requests, 1);
1325
1326 assert_eq!(
1327 context::dispatch_pending(
1328 &host.host_ctx,
1329 sid,
1330 NrStatus::Ok,
1331 types::ResponsePayload::Owned(vec![8]),
1332 ),
1333 NrStatus::Invalid
1334 );
1335
1336 match std::future::Future::poll(future.as_mut(), &mut second_context) {
1337 std::task::Poll::Ready(Some((status, data))) => {
1338 assert_eq!(status, NrStatus::Ok);
1339 assert_eq!(data.as_slice(), [7]);
1340 }
1341 result => panic!("unexpected unary completion result: {result:?}"),
1342 }
1343 assert_eq!(host.metrics().pending_requests, 0);
1344 }
1345
1346 #[test]
1347 fn unary_waker_callbacks_run_outside_pending_lock() {
1348 let host = NylonRingHost::new();
1349 let sid = 47;
1350 context::insert_pending(
1351 &host.host_ctx,
1352 sid,
1353 types::Pending::Unary(types::UnaryPending::waiting()),
1354 );
1355
1356 let clones = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1357 let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1358 let first_waker = reentrant_waker(host.host_ctx.clone(), clones.clone(), drops.clone());
1359 let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
1360 {
1361 let mut context = std::task::Context::from_waker(&first_waker);
1362 assert!(matches!(
1363 std::future::Future::poll(future.as_mut(), &mut context),
1364 std::task::Poll::Pending
1365 ));
1366 }
1367 assert_eq!(clones.load(std::sync::atomic::Ordering::Relaxed), 1);
1368
1369 let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
1370 let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken)));
1371 let mut context = std::task::Context::from_waker(&second_waker);
1372 assert!(matches!(
1373 std::future::Future::poll(future.as_mut(), &mut context),
1374 std::task::Poll::Pending
1375 ));
1376 assert_eq!(drops.load(std::sync::atomic::Ordering::Relaxed), 1);
1377
1378 drop(future);
1379 context::cleanup_sid(&host.host_ctx, sid);
1380 }
1381
1382 #[test]
1383 fn dropping_stream_receiver_unregisters_stream() {
1384 let host = NylonRingHost::new();
1385 let sid = 43;
1386 let (tx, rx) = stream_channel::acquire(1);
1387 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
1388 host.host_ctx.set_state(sid, "key".into(), vec![1]);
1389
1390 drop(StreamReceiver::new(
1391 rx,
1392 Some(host.host_ctx.clone()),
1393 sid,
1394 None,
1395 ));
1396
1397 assert!(context::remove_pending(&host.host_ctx, sid).is_none());
1398 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
1399 }
1400
1401 #[test]
1402 fn fast_slot_reentry_is_rejected_and_binding_is_cleared() {
1403 let mut first = types::UnaryResultSlot {
1404 sid: 1,
1405 result: None,
1406 lease: None,
1407 };
1408 let mut second = types::UnaryResultSlot {
1409 sid: 2,
1410 result: None,
1411 lease: None,
1412 };
1413 let binding = FastSlotBinding::bind(&mut first).unwrap();
1414 assert!(matches!(
1415 FastSlotBinding::bind(&mut second),
1416 Err(NylonRingHostError::FastPathReentrant)
1417 ));
1418 drop(binding);
1419 assert!(FastSlotBinding::bind(&mut second).is_ok());
1420 }
1421
1422 #[test]
1423 fn bounded_stream_reports_backpressure_and_removes_terminal_once() {
1424 let host = NylonRingHost::with_stream_capacity(1);
1425 let sid = 44;
1426 let (tx, mut rx) = stream_channel::acquire(1);
1427 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
1428
1429 assert_eq!(
1430 context::dispatch_pending(
1431 &host.host_ctx,
1432 sid,
1433 NrStatus::Ok,
1434 types::ResponsePayload::Owned(vec![1]),
1435 ),
1436 NrStatus::Ok
1437 );
1438 assert_eq!(
1439 context::dispatch_pending(
1440 &host.host_ctx,
1441 sid,
1442 NrStatus::Ok,
1443 types::ResponsePayload::Owned(vec![2]),
1444 ),
1445 NrStatus::Backpressure
1446 );
1447 assert_eq!(rx.try_recv().unwrap().data, vec![1]);
1448 assert_eq!(
1449 context::dispatch_pending(
1450 &host.host_ctx,
1451 sid,
1452 NrStatus::StreamEnd,
1453 types::ResponsePayload::Owned(vec![]),
1454 ),
1455 NrStatus::Ok
1456 );
1457 assert_eq!(host.metrics().pending_requests, 0);
1458 assert_eq!(rx.try_recv().unwrap().status, NrStatus::StreamEnd);
1459 }
1460
1461 #[test]
1462 fn concurrent_terminal_frames_complete_stream_once() {
1463 let host = NylonRingHost::with_stream_capacity(2);
1464 let sid = 45;
1465 let (tx, mut rx) = stream_channel::acquire(2);
1466 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
1467 let barrier = Arc::new(std::sync::Barrier::new(3));
1468
1469 let results: Vec<_> = std::thread::scope(|scope| {
1470 let handles: Vec<_> = (0..2)
1471 .map(|value| {
1472 let ctx = host.host_ctx.clone();
1473 let barrier = barrier.clone();
1474 scope.spawn(move || {
1475 barrier.wait();
1476 context::dispatch_pending(
1477 &ctx,
1478 sid,
1479 NrStatus::StreamEnd,
1480 types::ResponsePayload::Owned(vec![value]),
1481 )
1482 })
1483 })
1484 .collect();
1485 barrier.wait();
1486 handles
1487 .into_iter()
1488 .map(|handle| handle.join().unwrap())
1489 .collect()
1490 });
1491
1492 assert_eq!(
1493 results
1494 .iter()
1495 .filter(|&&status| status == NrStatus::Ok)
1496 .count(),
1497 1
1498 );
1499 assert_eq!(
1500 results
1501 .iter()
1502 .filter(|&&status| status == NrStatus::Invalid)
1503 .count(),
1504 1
1505 );
1506 assert!(rx.try_recv().is_some());
1507 assert!(rx.try_recv().is_none());
1508 assert_eq!(host.metrics().pending_requests, 0);
1509 }
1510
1511 #[test]
1512 fn unload_defers_library_drop_and_rejects_new_calls_on_existing_handle() {
1513 let _plugin_lock = plugin_test_lock();
1514 let Some(path) = example_plugin_path() else {
1515 return;
1516 };
1517 let mut host = NylonRingHost::new();
1518 host.load("example", path.to_str().unwrap()).unwrap();
1519 let handle = host.plugin("example").unwrap();
1520 let runtime = tokio::runtime::Runtime::new().unwrap();
1521 assert!(matches!(
1522 runtime.block_on(handle.call_response_timeout(
1523 "benchmark_without_response",
1524 b"",
1525 Duration::from_millis(1),
1526 )),
1527 Err(NylonRingHostError::Timeout)
1528 ));
1529 assert_eq!(host.metrics().pending_requests, 0);
1530
1531 let guard = handle.plugin.begin_call().unwrap();
1532 assert_eq!(host.metrics().in_flight_calls, 1);
1533
1534 host.unload("example").unwrap();
1535 assert!(matches!(
1536 runtime.block_on(handle.call("benchmark_without_response", b"")),
1537 Err(NylonRingHostError::PluginUnloaded)
1538 ));
1539 drop(guard);
1540 drop(handle);
1541 }
1542
1543 #[test]
1544 fn stream_receiver_outlives_unload_and_drains_frames() {
1545 let _plugin_lock = plugin_test_lock();
1546 let Some(path) = example_plugin_path() else {
1547 return;
1548 };
1549 let mut host = NylonRingHost::new();
1550 host.load("example", path.to_str().unwrap()).unwrap();
1551 let handle = host.plugin("example").unwrap();
1552 let runtime = tokio::runtime::Runtime::new().unwrap();
1553
1554 let (_sid, mut receiver) = runtime
1555 .block_on(handle.call_stream("benchmark_stream", b""))
1556 .unwrap();
1557 host.unload("example").unwrap();
1558 drop(handle);
1559
1560 let frames = runtime.block_on(async {
1563 let mut frames = 0u32;
1564 while receiver.recv().await.is_some() {
1565 frames += 1;
1566 }
1567 frames
1568 });
1569 assert_eq!(frames, 9);
1570 drop(receiver);
1571 }
1572
1573 #[test]
1574 fn stream_receiver_outlives_host_drop() {
1575 let _plugin_lock = plugin_test_lock();
1576 let Some(path) = example_plugin_path() else {
1577 return;
1578 };
1579 let mut host = NylonRingHost::new();
1580 host.load("example", path.to_str().unwrap()).unwrap();
1581 let handle = host.plugin("example").unwrap();
1582 let runtime = tokio::runtime::Runtime::new().unwrap();
1583
1584 let (_sid, mut receiver) = runtime
1585 .block_on(handle.call_stream("benchmark_stream", b""))
1586 .unwrap();
1587 drop(host);
1588 drop(handle);
1589
1590 let frames = runtime.block_on(async {
1591 let mut frames = 0u32;
1592 while receiver.recv().await.is_some() {
1593 frames += 1;
1594 }
1595 frames
1596 });
1597 assert_eq!(frames, 9);
1598 }
1599
1600 #[test]
1601 fn owned_response_round_trips_and_releases_exactly_once() {
1602 let _plugin_lock = plugin_test_lock();
1603 let Some(path) = example_plugin_path() else {
1604 return;
1605 };
1606 let mut host = NylonRingHost::new();
1607 host.load("example", path.to_str().unwrap()).unwrap();
1608 let handle = host.plugin("example").unwrap();
1609 let runtime = tokio::runtime::Runtime::new().unwrap();
1610
1611 async fn release_count(handle: &PluginHandle) -> u64 {
1612 let (status, data) = handle
1613 .call_response("owned_release_count", b"")
1614 .await
1615 .unwrap();
1616 assert_eq!(status, NrStatus::Ok);
1617 u64::from_le_bytes(data.try_into().unwrap())
1618 }
1619
1620 runtime.block_on(async {
1621 let (status, data) = handle
1623 .call_response_bytes("benchmark_owned", &[0u8; 128])
1624 .await
1625 .unwrap();
1626 assert_eq!(status, NrStatus::Ok);
1627 assert_eq!(data.len(), 128);
1628 assert!(data.iter().all(|&byte| byte == 42));
1629
1630 let before = release_count(&handle).await;
1631
1632 let (status, held) = handle
1634 .call_response_bytes("echo_owned", b"held")
1635 .await
1636 .unwrap();
1637 assert_eq!(status, NrStatus::Ok);
1638 assert_eq!(&*held, b"held");
1639 assert_eq!(release_count(&handle).await, before);
1640 drop(held);
1641 assert_eq!(release_count(&handle).await, before + 1);
1642
1643 let (status, data) = handle.call_response("echo_owned", b"copied").await.unwrap();
1645 assert_eq!(status, NrStatus::Ok);
1646 assert_eq!(data, b"copied");
1647 assert_eq!(release_count(&handle).await, before + 2);
1648
1649 let (_, view) = handle
1651 .call_response_bytes("echo_owned", b"vec")
1652 .await
1653 .unwrap();
1654 assert_eq!(view.into_vec(), b"vec");
1655 assert_eq!(release_count(&handle).await, before + 3);
1656
1657 let (status, data) = handle
1659 .call_response_fast("echo_owned", b"fast")
1660 .await
1661 .unwrap();
1662 assert_eq!(status, NrStatus::Ok);
1663 assert_eq!(data, b"fast");
1664 assert_eq!(release_count(&handle).await, before + 4);
1665 });
1666 }
1667
1668 #[test]
1669 fn owned_response_keeps_plugin_alive_after_unload() {
1670 let _plugin_lock = plugin_test_lock();
1671 let Some(path) = example_plugin_path() else {
1672 return;
1673 };
1674 let mut host = NylonRingHost::new();
1675 host.load("example", path.to_str().unwrap()).unwrap();
1676 let handle = host.plugin("example").unwrap();
1677 let runtime = tokio::runtime::Runtime::new().unwrap();
1678
1679 let (status, response) = runtime
1680 .block_on(handle.call_response_bytes("benchmark_owned", &[0u8; 64]))
1681 .unwrap();
1682 assert_eq!(status, NrStatus::Ok);
1683
1684 host.unload("example").unwrap();
1687 drop(handle);
1688 drop(host);
1689 assert_eq!(response.len(), 64);
1690 assert!(response.iter().all(|&byte| byte == 42));
1691 drop(response);
1692 }
1693
1694 #[test]
1695 fn lease_round_trips_and_rejects_misuse() {
1696 let _plugin_lock = plugin_test_lock();
1697 let Some(path) = example_plugin_path() else {
1698 return;
1699 };
1700 let mut host = NylonRingHost::new();
1701 host.load("example", path.to_str().unwrap()).unwrap();
1702 let handle = host.plugin("example").unwrap();
1703 let runtime = tokio::runtime::Runtime::new().unwrap();
1704
1705 runtime.block_on(async {
1706 let (status, data) = handle
1708 .call_response("echo_lease", b"through the lease")
1709 .await
1710 .unwrap();
1711 assert_eq!(status, NrStatus::Ok);
1712 assert_eq!(data, b"through the lease");
1713
1714 let (status, data) = handle.call_response("echo_lease", b"").await.unwrap();
1716 assert_eq!(status, NrStatus::Ok);
1717 assert!(data.is_empty());
1718
1719 let (status, data) = handle
1721 .call_response_fast("echo_lease", b"fast lease")
1722 .await
1723 .unwrap();
1724 assert_eq!(status, NrStatus::Ok);
1725 assert_eq!(data, b"fast lease");
1726
1727 let (status, data) = handle
1731 .call_response("lease_misuse", b"contract")
1732 .await
1733 .unwrap();
1734 assert_eq!(status, NrStatus::Ok);
1735 assert_eq!(data, b"contract");
1736 });
1737
1738 assert_eq!(host.host_ctx.orphaned_lease_count(), 0);
1740 assert_eq!(host.host_ctx.pending_count(), 0);
1741 }
1742
1743 #[test]
1744 fn abandoned_leases_are_parked_not_freed() {
1745 let _plugin_lock = plugin_test_lock();
1746 let Some(path) = example_plugin_path() else {
1747 return;
1748 };
1749 let mut host = NylonRingHost::new();
1750 host.load("example", path.to_str().unwrap()).unwrap();
1751 let handle = host.plugin("example").unwrap();
1752 let runtime = tokio::runtime::Runtime::new().unwrap();
1753
1754 runtime.block_on(async {
1755 let result = handle
1759 .call_response_timeout("lease_abandon", b"", std::time::Duration::from_millis(50))
1760 .await;
1761 assert!(matches!(result, Err(NylonRingHostError::Timeout)));
1762 assert_eq!(host.host_ctx.orphaned_lease_count(), 1);
1763 assert_eq!(host.host_ctx.pending_count(), 0);
1764
1765 let result = handle.call_response_fast("lease_abandon", b"").await;
1767 assert!(matches!(
1768 result,
1769 Err(NylonRingHostError::MissingSynchronousResponse)
1770 ));
1771 assert_eq!(host.host_ctx.orphaned_lease_count(), 2);
1772 });
1773 }
1774
1775 #[test]
1776 fn example_plugin_fire_and_forget_entry_returns_ok() {
1777 let _plugin_lock = plugin_test_lock();
1778 let Some(path) = example_plugin_path() else {
1779 return;
1780 };
1781 let mut host = NylonRingHost::new();
1782 host.load("example", path.to_str().unwrap()).unwrap();
1783 let handle = host.plugin("example").unwrap();
1784 let runtime = tokio::runtime::Runtime::new().unwrap();
1785
1786 assert!(matches!(
1787 runtime.block_on(handle.call("notify", b"fire and forget")),
1788 Ok(NrStatus::Ok)
1789 ));
1790 }
1791
1792 #[test]
1793 fn c_plugin_layout_round_trips_through_host() {
1794 let _plugin_lock = plugin_test_lock();
1795 let Some(path) = c_plugin_path() else {
1796 return;
1797 };
1798 let mut host = NylonRingHost::new();
1799 host.load("c-example", path.to_str().unwrap()).unwrap();
1800 let handle = host.plugin("c-example").unwrap();
1801 let runtime = tokio::runtime::Runtime::new().unwrap();
1802 let (status, response) = runtime
1803 .block_on(handle.call_response("echo", b"from-c"))
1804 .unwrap();
1805 assert_eq!(status, NrStatus::Ok);
1806 assert_eq!(response, b"from-c");
1807
1808 assert!(matches!(
1811 handle.entry("echo"),
1812 Err(NylonRingHostError::EntryDispatchUnsupported)
1813 ));
1814 }
1815
1816 #[test]
1817 fn entry_id_dispatch_round_trips_all_call_shapes() {
1818 let _plugin_lock = plugin_test_lock();
1819 let Some(path) = example_plugin_path() else {
1820 return;
1821 };
1822 let mut host = NylonRingHost::new();
1823 host.load("example", path.to_str().unwrap()).unwrap();
1824 let handle = host.plugin("example").unwrap();
1825 let runtime = tokio::runtime::Runtime::new().unwrap();
1826
1827 assert!(matches!(
1828 handle.entry("no_such_entry"),
1829 Err(NylonRingHostError::EntryNotFound(_))
1830 ));
1831
1832 let echo = handle.entry("benchmark").unwrap();
1833 let notify = handle.entry("benchmark_without_response").unwrap();
1834 let stream = handle.entry("benchmark_stream").unwrap();
1835
1836 runtime.block_on(async {
1837 let (status, data) = echo.call_response(b"by-id").await.unwrap();
1838 assert_eq!(status, NrStatus::Ok);
1839 assert_eq!(data, b"by-id");
1840
1841 let (status, data) = echo.call_response_fast(b"fast-by-id").await.unwrap();
1842 assert_eq!(status, NrStatus::Ok);
1843 assert_eq!(data, b"fast-by-id");
1844
1845 assert_eq!(notify.call(b"").await.unwrap(), NrStatus::Ok);
1846
1847 let (_sid, mut receiver) = stream.call_stream(b"").await.unwrap();
1848 let mut frames = 0u32;
1849 while receiver.recv().await.is_some() {
1850 frames += 1;
1851 }
1852 assert_eq!(frames, 9);
1853 });
1854 }
1855
1856 #[test]
1857 fn pinned_plugin_calls_and_refuses_unload() {
1858 let _guard = plugin_test_lock();
1859 let Some(path) = example_plugin_path() else {
1860 return;
1861 };
1862 let runtime = tokio::runtime::Builder::new_current_thread()
1863 .enable_time()
1864 .build()
1865 .unwrap();
1866 let mut host = NylonRingHost::new();
1867 host.load_pinned("pinned", path.to_str().unwrap()).unwrap();
1868 let handle = host.plugin("pinned").unwrap();
1869 runtime.block_on(async {
1870 let (status, data) = handle
1871 .call_response_fast("benchmark", b"pin")
1872 .await
1873 .unwrap();
1874 assert_eq!(status, NrStatus::Ok);
1875 assert_eq!(data, b"pin");
1876 let (status, data) = handle.call_response("benchmark", b"pin2").await.unwrap();
1877 assert_eq!(status, NrStatus::Ok);
1878 assert_eq!(data, b"pin2");
1879 let (_sid, mut receiver) = handle.call_stream("stream", b"").await.unwrap();
1881 let mut frames = 0u32;
1882 while receiver.recv().await.is_some() {
1883 frames += 1;
1884 }
1885 assert!(frames > 0);
1886 });
1887 assert_eq!(handle.plugin.call_tracker.active_calls(), 0);
1888 assert!(matches!(
1889 host.unload("pinned"),
1890 Err(NylonRingHostError::PluginPinned(_))
1891 ));
1892 assert!(matches!(
1893 host.reload(),
1894 Err(NylonRingHostError::PluginPinned(_))
1895 ));
1896 assert!(matches!(
1897 host.load("pinned", path.to_str().unwrap()),
1898 Err(NylonRingHostError::PluginPinned(_))
1899 ));
1900 runtime.block_on(async {
1901 assert!(matches!(
1902 host.unload_with_grace("pinned", Duration::from_millis(10))
1903 .await,
1904 Err(NylonRingHostError::PluginPinned(_))
1905 ));
1906 assert!(matches!(
1907 host.reload_with_grace(Duration::from_millis(10)).await,
1908 Err(NylonRingHostError::PluginPinned(_))
1909 ));
1910 });
1911 }
1912
1913 #[test]
1916 #[ignore = "manual probe; needs --release for meaningful numbers"]
1917 fn cycle_budget_probe() {
1918 use std::future::Future;
1919 use std::hint::black_box;
1920 use std::task::{Context, Poll, Waker};
1921
1922 let _guard = plugin_test_lock();
1923 let path = example_plugin_path().expect("example plugin must build");
1924 let mut host = NylonRingHost::new();
1925 host.load("bench", path.to_str().unwrap()).unwrap();
1926 let handle = host.plugin("bench").unwrap();
1927 let fire_entry = handle.entry("benchmark_without_response").unwrap();
1928 let fast_entry = handle.entry("benchmark").unwrap();
1929
1930 const N: u64 = 20_000_000;
1931
1932 fn time_ns(iters: u64, mut f: impl FnMut()) -> f64 {
1933 for _ in 0..iters / 10 {
1934 f();
1935 }
1936 let start = std::time::Instant::now();
1937 for _ in 0..iters {
1938 f();
1939 }
1940 start.elapsed().as_nanos() as f64 / iters as f64
1941 }
1942
1943 let calibrate = |iters: u64| -> f64 {
1949 #[cfg_attr(not(target_arch = "aarch64"), allow(unused_mut))]
1952 let mut x: u64 = black_box(0);
1953 let start = std::time::Instant::now();
1954 #[cfg(target_arch = "aarch64")]
1955 unsafe {
1956 let mut n = iters;
1957 std::arch::asm!(
1958 "2:",
1959 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
1960 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
1961 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
1962 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
1963 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
1964 "add {x}, {x}, #1",
1965 "subs {n}, {n}, #1",
1966 "b.ne 2b",
1967 x = inout(reg) x,
1968 n = inout(reg) n,
1969 );
1970 black_box(n);
1971 }
1972 black_box(x);
1973 start.elapsed().as_nanos() as f64 / iters as f64
1974 };
1975 calibrate(200_000_000); let calib_ns = calibrate(100_000_000).min(calibrate(100_000_000));
1977 let ghz = 16.0 / calib_ns;
1978 println!("clock calibration: {calib_ns:.2} ns / 16 adds -> {ghz:.3} GHz");
1979
1980 let cyc = |ns: f64| ns * ghz;
1981 let report = |name: &str, ns: f64| {
1982 println!("{name:<28} {ns:>7.2} ns {:>6.1} cycles", cyc(ns));
1983 };
1984
1985 let plugin = &fire_entry.plugin;
1986 let handle_by_id_fn = plugin.dispatch.handle_by_id.unwrap();
1987 let fire_id = fire_entry.id;
1988
1989 report(
1990 "component: tracker begin+fin",
1991 time_ns(N, || {
1992 let shard = plugin.call_tracker.try_begin().unwrap();
1993 let _ = black_box(plugin.call_tracker.finish(shard));
1994 }),
1995 );
1996
1997 #[repr(align(128))]
2001 struct PaddedCounter(std::sync::atomic::AtomicUsize);
2002 use std::sync::atomic::Ordering as AtomOrd;
2003 let word = PaddedCounter(std::sync::atomic::AtomicUsize::new(0));
2004 report(
2005 "atomics: casa loop + ldaddl",
2006 time_ns(N, || {
2007 let _ = black_box(
2008 word.0
2009 .fetch_update(AtomOrd::Acquire, AtomOrd::Relaxed, |v| Some(v + 1)),
2010 );
2011 black_box(word.0.fetch_sub(1, AtomOrd::Release));
2012 }),
2013 );
2014 report(
2015 "atomics: relaxed cas + sub",
2016 time_ns(N, || {
2017 let _ = black_box(
2018 word.0
2019 .fetch_update(AtomOrd::Relaxed, AtomOrd::Relaxed, |v| Some(v + 1)),
2020 );
2021 black_box(word.0.fetch_sub(1, AtomOrd::Relaxed));
2022 }),
2023 );
2024 report(
2025 "atomics: ldadda + ldaddl",
2026 time_ns(N, || {
2027 black_box(word.0.fetch_add(1, AtomOrd::Acquire));
2028 black_box(word.0.fetch_sub(1, AtomOrd::Release));
2029 }),
2030 );
2031 report(
2032 "atomics: single ldadda",
2033 time_ns(N, || {
2034 black_box(word.0.fetch_add(1, AtomOrd::Acquire));
2035 }),
2036 );
2037 report(
2038 "atomics: relaxed cas + ldaddl",
2039 time_ns(N, || {
2040 let _ = black_box(
2041 word.0
2042 .fetch_update(AtomOrd::Relaxed, AtomOrd::Relaxed, |v| Some(v + 1)),
2043 );
2044 black_box(word.0.fetch_sub(1, AtomOrd::Release));
2045 }),
2046 );
2047
2048 {
2052 let tracker = std::sync::Arc::new(crate::call_tracker::CallTracker::new());
2053 let per_thread: u64 = 4_000_000;
2054 let start = std::time::Instant::now();
2055 let threads: Vec<_> = (0..8)
2056 .map(|_| {
2057 let tracker = tracker.clone();
2058 std::thread::spawn(move || {
2059 for _ in 0..per_thread {
2060 let shard = tracker.try_begin().unwrap();
2061 let _ = black_box(tracker.finish(shard));
2062 }
2063 })
2064 })
2065 .collect();
2066 for t in threads {
2067 t.join().unwrap();
2068 }
2069 let ns = start.elapsed().as_nanos() as f64 / per_thread as f64;
2070 report("tracker pair, 8 threads", ns);
2071 }
2072 report(
2073 "component: next_sid",
2074 time_ns(N, || {
2075 black_box(sid::next_sid());
2076 }),
2077 );
2078 report(
2079 "component: remove_state",
2080 time_ns(N, || {
2081 plugin.host_ctx.remove_state(black_box(1));
2082 }),
2083 );
2084 report(
2085 "raw FFI handle_by_id (fire)",
2086 time_ns(N, || {
2087 let status = unsafe {
2088 handle_by_id_fn(black_box(fire_id), 1, NrBytes::from_slice(black_box(b"")))
2089 };
2090 assert_eq!(status, NrStatus::Ok);
2091 }),
2092 );
2093
2094 let mut cx = Context::from_waker(Waker::noop());
2095 macro_rules! poll_ready {
2096 ($fut:expr) => {{
2097 let fut = $fut;
2098 let mut fut = std::pin::pin!(fut);
2099 match fut.as_mut().poll(&mut cx) {
2100 Poll::Ready(result) => result.unwrap(),
2101 Poll::Pending => panic!("probe future must complete synchronously"),
2102 }
2103 }};
2104 }
2105
2106 report(
2107 "full fire (entry id)",
2108 time_ns(N, || {
2109 poll_ready!(fire_entry.call(black_box(b"")));
2110 }),
2111 );
2112 report(
2113 "full fire (by name)",
2114 time_ns(N, || {
2115 poll_ready!(handle.call("benchmark_without_response", black_box(b"")));
2116 }),
2117 );
2118 report(
2119 "full fast (entry id)",
2120 time_ns(N, || {
2121 black_box(poll_ready!(fast_entry.call_response_fast(black_box(b""))));
2122 }),
2123 );
2124 report(
2125 "full fast (by name)",
2126 time_ns(N, || {
2127 black_box(poll_ready!(
2128 handle.call_response_fast("benchmark", black_box(b""))
2129 ));
2130 }),
2131 );
2132 report(
2133 "full unary (entry id)",
2134 time_ns(N, || {
2135 black_box(poll_ready!(fast_entry.call_response(black_box(b""))));
2136 }),
2137 );
2138 report(
2139 "full unary (by name)",
2140 time_ns(N, || {
2141 black_box(poll_ready!(
2142 handle.call_response("benchmark", black_box(b""))
2143 ));
2144 }),
2145 );
2146 }
2147}