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 workspace_root() -> Option<&'static std::path::Path> {
1235 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1236 .parent()?
1237 .parent()
1238 }
1239
1240 fn dynlib_extension() -> &'static str {
1241 if cfg!(target_os = "macos") {
1242 "dylib"
1243 } else if cfg!(target_os = "windows") {
1244 "dll"
1245 } else {
1246 "so"
1247 }
1248 }
1249
1250 fn build_foreign_plugin(
1255 out_dir: &str,
1256 lib_stem: &str,
1257 build: impl FnOnce(&std::path::Path, &std::path::Path) -> std::process::Command,
1258 ) -> Option<std::path::PathBuf> {
1259 let workspace_root = workspace_root()?;
1260 let output = workspace_root
1261 .join(out_dir)
1262 .join(format!("lib{lib_stem}.{}", dynlib_extension()));
1263 std::fs::create_dir_all(output.parent()?).ok()?;
1264 let Ok(status) = build(workspace_root, &output).status() else {
1265 return None; };
1267 assert!(status.success(), "{lib_stem} failed to compile");
1268 Some(output)
1269 }
1270
1271 fn c_plugin_path() -> Option<std::path::PathBuf> {
1272 build_foreign_plugin(
1273 "target/c-example",
1274 "nylon_ring_c_example",
1275 |root, output| {
1276 let mut command = std::process::Command::new("cc");
1277 command
1278 .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-shared"])
1279 .args((!cfg!(target_os = "windows")).then_some("-fPIC"))
1280 .arg(root.join("examples/c-plugin/plugin.c"))
1281 .arg("-o")
1282 .arg(output);
1283 command
1284 },
1285 )
1286 }
1287
1288 fn cpp_plugin_path() -> Option<std::path::PathBuf> {
1289 build_foreign_plugin(
1290 "target/cpp-example",
1291 "nylon_ring_cpp_example",
1292 |root, output| {
1293 let mut command = std::process::Command::new("c++");
1294 command
1295 .args(["-std=c++17", "-Wall", "-Wextra", "-Werror", "-shared"])
1296 .args((!cfg!(target_os = "windows")).then_some("-fPIC"))
1297 .arg(root.join("examples/cpp-plugin/plugin.cpp"))
1298 .arg("-o")
1299 .arg(output);
1300 command
1301 },
1302 )
1303 }
1304
1305 fn go_plugin_path() -> Option<std::path::PathBuf> {
1306 build_foreign_plugin(
1307 "target/go-example",
1308 "nylon_ring_go_example",
1309 |root, output| {
1310 let mut command = std::process::Command::new("go");
1311 command
1312 .args(["build", "-buildmode=c-shared", "-o"])
1313 .arg(output)
1314 .arg(".")
1315 .current_dir(root.join("examples/go-plugin"));
1316 command
1317 },
1318 )
1319 }
1320
1321 fn zig_plugin_path() -> Option<std::path::PathBuf> {
1322 build_foreign_plugin(
1323 "target/zig-example",
1324 "nylon_ring_zig_example",
1325 |root, output| {
1326 let mut command = std::process::Command::new("zig");
1327 command
1328 .args(["build-lib", "-dynamic", "-O", "ReleaseFast"])
1329 .arg(format!("-femit-bin={}", output.display()))
1330 .arg(root.join("examples/zig-plugin/plugin.zig"));
1331 command
1332 },
1333 )
1334 }
1335
1336 fn trait_plugin_path() -> Option<std::path::PathBuf> {
1337 let workspace_root = workspace_root()?;
1338 let manifest = workspace_root.join("examples/ex-nyring-trait-plugin/Cargo.toml");
1339 if !manifest.is_file() {
1340 return None;
1341 }
1342 let status = std::process::Command::new("cargo")
1343 .args(["build", "--release", "--manifest-path"])
1344 .arg(&manifest)
1345 .status()
1346 .ok()?;
1347 assert!(status.success(), "trait example plugin failed to compile");
1348 let filename = if cfg!(target_os = "windows") {
1349 "ex_nyring_trait_plugin.dll".to_string()
1350 } else {
1351 format!("libex_nyring_trait_plugin.{}", dynlib_extension())
1352 };
1353 Some(workspace_root.join("target/release").join(filename))
1354 }
1355
1356 #[test]
1357 fn dropping_pending_guard_unregisters_unary_request() {
1358 let host = NylonRingHost::new();
1359 let sid = 42;
1360 context::insert_pending(
1361 &host.host_ctx,
1362 sid,
1363 types::Pending::Unary(types::UnaryPending::waiting()),
1364 );
1365 host.host_ctx.set_state(sid, "key".into(), vec![1]);
1366
1367 drop(PendingGuard::new(&host.host_ctx, sid));
1368
1369 assert!(context::remove_pending(&host.host_ctx, sid).is_none());
1370 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
1371 }
1372
1373 #[test]
1374 fn unary_completion_wakes_latest_waiter_across_threads() {
1375 let host = NylonRingHost::new();
1376 let sid = 46;
1377 context::insert_pending(
1378 &host.host_ctx,
1379 sid,
1380 types::Pending::Unary(types::UnaryPending::waiting()),
1381 );
1382 host.host_ctx.set_state(sid, "key".into(), vec![1]);
1383
1384 let first_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
1385 let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
1386 let first_waker = std::task::Waker::from(Arc::new(WakeFlag(first_woken.clone())));
1387 let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken.clone())));
1388 let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
1389
1390 let mut first_context = std::task::Context::from_waker(&first_waker);
1391 assert!(matches!(
1392 std::future::Future::poll(future.as_mut(), &mut first_context),
1393 std::task::Poll::Pending
1394 ));
1395 let mut second_context = std::task::Context::from_waker(&second_waker);
1396 assert!(matches!(
1397 std::future::Future::poll(future.as_mut(), &mut second_context),
1398 std::task::Poll::Pending
1399 ));
1400
1401 let host_ctx = host.host_ctx.clone();
1402 let dispatch_status = std::thread::spawn(move || {
1403 context::dispatch_pending(
1404 &host_ctx,
1405 sid,
1406 NrStatus::Ok,
1407 types::ResponsePayload::Owned(vec![7]),
1408 )
1409 })
1410 .join()
1411 .unwrap();
1412 assert_eq!(dispatch_status, NrStatus::Ok);
1413 assert!(!first_woken.load(std::sync::atomic::Ordering::Acquire));
1414 assert!(second_woken.load(std::sync::atomic::Ordering::Acquire));
1415 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
1416 assert_eq!(host.metrics().pending_requests, 1);
1417
1418 assert_eq!(
1419 context::dispatch_pending(
1420 &host.host_ctx,
1421 sid,
1422 NrStatus::Ok,
1423 types::ResponsePayload::Owned(vec![8]),
1424 ),
1425 NrStatus::Invalid
1426 );
1427
1428 match std::future::Future::poll(future.as_mut(), &mut second_context) {
1429 std::task::Poll::Ready(Some((status, data))) => {
1430 assert_eq!(status, NrStatus::Ok);
1431 assert_eq!(data.as_slice(), [7]);
1432 }
1433 result => panic!("unexpected unary completion result: {result:?}"),
1434 }
1435 assert_eq!(host.metrics().pending_requests, 0);
1436 }
1437
1438 #[test]
1439 fn unary_waker_callbacks_run_outside_pending_lock() {
1440 let host = NylonRingHost::new();
1441 let sid = 47;
1442 context::insert_pending(
1443 &host.host_ctx,
1444 sid,
1445 types::Pending::Unary(types::UnaryPending::waiting()),
1446 );
1447
1448 let clones = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1449 let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1450 let first_waker = reentrant_waker(host.host_ctx.clone(), clones.clone(), drops.clone());
1451 let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
1452 {
1453 let mut context = std::task::Context::from_waker(&first_waker);
1454 assert!(matches!(
1455 std::future::Future::poll(future.as_mut(), &mut context),
1456 std::task::Poll::Pending
1457 ));
1458 }
1459 assert_eq!(clones.load(std::sync::atomic::Ordering::Relaxed), 1);
1460
1461 let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
1462 let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken)));
1463 let mut context = std::task::Context::from_waker(&second_waker);
1464 assert!(matches!(
1465 std::future::Future::poll(future.as_mut(), &mut context),
1466 std::task::Poll::Pending
1467 ));
1468 assert_eq!(drops.load(std::sync::atomic::Ordering::Relaxed), 1);
1469
1470 drop(future);
1471 context::cleanup_sid(&host.host_ctx, sid);
1472 }
1473
1474 #[test]
1475 fn dropping_stream_receiver_unregisters_stream() {
1476 let host = NylonRingHost::new();
1477 let sid = 43;
1478 let (tx, rx) = stream_channel::acquire(1);
1479 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
1480 host.host_ctx.set_state(sid, "key".into(), vec![1]);
1481
1482 drop(StreamReceiver::new(
1483 rx,
1484 Some(host.host_ctx.clone()),
1485 sid,
1486 None,
1487 ));
1488
1489 assert!(context::remove_pending(&host.host_ctx, sid).is_none());
1490 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
1491 }
1492
1493 #[test]
1494 fn fast_slot_reentry_is_rejected_and_binding_is_cleared() {
1495 let mut first = types::UnaryResultSlot {
1496 sid: 1,
1497 result: None,
1498 lease: None,
1499 };
1500 let mut second = types::UnaryResultSlot {
1501 sid: 2,
1502 result: None,
1503 lease: None,
1504 };
1505 let binding = FastSlotBinding::bind(&mut first).unwrap();
1506 assert!(matches!(
1507 FastSlotBinding::bind(&mut second),
1508 Err(NylonRingHostError::FastPathReentrant)
1509 ));
1510 drop(binding);
1511 assert!(FastSlotBinding::bind(&mut second).is_ok());
1512 }
1513
1514 #[test]
1515 fn bounded_stream_reports_backpressure_and_removes_terminal_once() {
1516 let host = NylonRingHost::with_stream_capacity(1);
1517 let sid = 44;
1518 let (tx, mut rx) = stream_channel::acquire(1);
1519 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
1520
1521 assert_eq!(
1522 context::dispatch_pending(
1523 &host.host_ctx,
1524 sid,
1525 NrStatus::Ok,
1526 types::ResponsePayload::Owned(vec![1]),
1527 ),
1528 NrStatus::Ok
1529 );
1530 assert_eq!(
1531 context::dispatch_pending(
1532 &host.host_ctx,
1533 sid,
1534 NrStatus::Ok,
1535 types::ResponsePayload::Owned(vec![2]),
1536 ),
1537 NrStatus::Backpressure
1538 );
1539 assert_eq!(rx.try_recv().unwrap().data, vec![1]);
1540 assert_eq!(
1541 context::dispatch_pending(
1542 &host.host_ctx,
1543 sid,
1544 NrStatus::StreamEnd,
1545 types::ResponsePayload::Owned(vec![]),
1546 ),
1547 NrStatus::Ok
1548 );
1549 assert_eq!(host.metrics().pending_requests, 0);
1550 assert_eq!(rx.try_recv().unwrap().status, NrStatus::StreamEnd);
1551 }
1552
1553 #[test]
1554 fn concurrent_terminal_frames_complete_stream_once() {
1555 let host = NylonRingHost::with_stream_capacity(2);
1556 let sid = 45;
1557 let (tx, mut rx) = stream_channel::acquire(2);
1558 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
1559 let barrier = Arc::new(std::sync::Barrier::new(3));
1560
1561 let results: Vec<_> = std::thread::scope(|scope| {
1562 let handles: Vec<_> = (0..2)
1563 .map(|value| {
1564 let ctx = host.host_ctx.clone();
1565 let barrier = barrier.clone();
1566 scope.spawn(move || {
1567 barrier.wait();
1568 context::dispatch_pending(
1569 &ctx,
1570 sid,
1571 NrStatus::StreamEnd,
1572 types::ResponsePayload::Owned(vec![value]),
1573 )
1574 })
1575 })
1576 .collect();
1577 barrier.wait();
1578 handles
1579 .into_iter()
1580 .map(|handle| handle.join().unwrap())
1581 .collect()
1582 });
1583
1584 assert_eq!(
1585 results
1586 .iter()
1587 .filter(|&&status| status == NrStatus::Ok)
1588 .count(),
1589 1
1590 );
1591 assert_eq!(
1592 results
1593 .iter()
1594 .filter(|&&status| status == NrStatus::Invalid)
1595 .count(),
1596 1
1597 );
1598 assert!(rx.try_recv().is_some());
1599 assert!(rx.try_recv().is_none());
1600 assert_eq!(host.metrics().pending_requests, 0);
1601 }
1602
1603 #[test]
1604 fn unload_defers_library_drop_and_rejects_new_calls_on_existing_handle() {
1605 let _plugin_lock = plugin_test_lock();
1606 let Some(path) = example_plugin_path() else {
1607 return;
1608 };
1609 let mut host = NylonRingHost::new();
1610 host.load("example", path.to_str().unwrap()).unwrap();
1611 let handle = host.plugin("example").unwrap();
1612 let runtime = tokio::runtime::Runtime::new().unwrap();
1613 assert!(matches!(
1614 runtime.block_on(handle.call_response_timeout(
1615 "benchmark_without_response",
1616 b"",
1617 Duration::from_millis(1),
1618 )),
1619 Err(NylonRingHostError::Timeout)
1620 ));
1621 assert_eq!(host.metrics().pending_requests, 0);
1622
1623 let guard = handle.plugin.begin_call().unwrap();
1624 assert_eq!(host.metrics().in_flight_calls, 1);
1625
1626 host.unload("example").unwrap();
1627 assert!(matches!(
1628 runtime.block_on(handle.call("benchmark_without_response", b"")),
1629 Err(NylonRingHostError::PluginUnloaded)
1630 ));
1631 drop(guard);
1632 drop(handle);
1633 }
1634
1635 #[test]
1636 fn stream_receiver_outlives_unload_and_drains_frames() {
1637 let _plugin_lock = plugin_test_lock();
1638 let Some(path) = example_plugin_path() else {
1639 return;
1640 };
1641 let mut host = NylonRingHost::new();
1642 host.load("example", path.to_str().unwrap()).unwrap();
1643 let handle = host.plugin("example").unwrap();
1644 let runtime = tokio::runtime::Runtime::new().unwrap();
1645
1646 let (_sid, mut receiver) = runtime
1647 .block_on(handle.call_stream("benchmark_stream", b""))
1648 .unwrap();
1649 host.unload("example").unwrap();
1650 drop(handle);
1651
1652 let frames = runtime.block_on(async {
1655 let mut frames = 0u32;
1656 while receiver.recv().await.is_some() {
1657 frames += 1;
1658 }
1659 frames
1660 });
1661 assert_eq!(frames, 9);
1662 drop(receiver);
1663 }
1664
1665 #[test]
1666 fn stream_receiver_outlives_host_drop() {
1667 let _plugin_lock = plugin_test_lock();
1668 let Some(path) = example_plugin_path() else {
1669 return;
1670 };
1671 let mut host = NylonRingHost::new();
1672 host.load("example", path.to_str().unwrap()).unwrap();
1673 let handle = host.plugin("example").unwrap();
1674 let runtime = tokio::runtime::Runtime::new().unwrap();
1675
1676 let (_sid, mut receiver) = runtime
1677 .block_on(handle.call_stream("benchmark_stream", b""))
1678 .unwrap();
1679 drop(host);
1680 drop(handle);
1681
1682 let frames = runtime.block_on(async {
1683 let mut frames = 0u32;
1684 while receiver.recv().await.is_some() {
1685 frames += 1;
1686 }
1687 frames
1688 });
1689 assert_eq!(frames, 9);
1690 }
1691
1692 #[test]
1693 fn owned_response_round_trips_and_releases_exactly_once() {
1694 let _plugin_lock = plugin_test_lock();
1695 let Some(path) = example_plugin_path() else {
1696 return;
1697 };
1698 let mut host = NylonRingHost::new();
1699 host.load("example", path.to_str().unwrap()).unwrap();
1700 let handle = host.plugin("example").unwrap();
1701 let runtime = tokio::runtime::Runtime::new().unwrap();
1702
1703 async fn release_count(handle: &PluginHandle) -> u64 {
1704 let (status, data) = handle
1705 .call_response("owned_release_count", b"")
1706 .await
1707 .unwrap();
1708 assert_eq!(status, NrStatus::Ok);
1709 u64::from_le_bytes(data.try_into().unwrap())
1710 }
1711
1712 runtime.block_on(async {
1713 let (status, data) = handle
1715 .call_response_bytes("benchmark_owned", &[0u8; 128])
1716 .await
1717 .unwrap();
1718 assert_eq!(status, NrStatus::Ok);
1719 assert_eq!(data.len(), 128);
1720 assert!(data.iter().all(|&byte| byte == 42));
1721
1722 let before = release_count(&handle).await;
1723
1724 let (status, held) = handle
1726 .call_response_bytes("echo_owned", b"held")
1727 .await
1728 .unwrap();
1729 assert_eq!(status, NrStatus::Ok);
1730 assert_eq!(&*held, b"held");
1731 assert_eq!(release_count(&handle).await, before);
1732 drop(held);
1733 assert_eq!(release_count(&handle).await, before + 1);
1734
1735 let (status, data) = handle.call_response("echo_owned", b"copied").await.unwrap();
1737 assert_eq!(status, NrStatus::Ok);
1738 assert_eq!(data, b"copied");
1739 assert_eq!(release_count(&handle).await, before + 2);
1740
1741 let (_, view) = handle
1743 .call_response_bytes("echo_owned", b"vec")
1744 .await
1745 .unwrap();
1746 assert_eq!(view.into_vec(), b"vec");
1747 assert_eq!(release_count(&handle).await, before + 3);
1748
1749 let (status, data) = handle
1751 .call_response_fast("echo_owned", b"fast")
1752 .await
1753 .unwrap();
1754 assert_eq!(status, NrStatus::Ok);
1755 assert_eq!(data, b"fast");
1756 assert_eq!(release_count(&handle).await, before + 4);
1757 });
1758 }
1759
1760 #[test]
1761 fn owned_response_keeps_plugin_alive_after_unload() {
1762 let _plugin_lock = plugin_test_lock();
1763 let Some(path) = example_plugin_path() else {
1764 return;
1765 };
1766 let mut host = NylonRingHost::new();
1767 host.load("example", path.to_str().unwrap()).unwrap();
1768 let handle = host.plugin("example").unwrap();
1769 let runtime = tokio::runtime::Runtime::new().unwrap();
1770
1771 let (status, response) = runtime
1772 .block_on(handle.call_response_bytes("benchmark_owned", &[0u8; 64]))
1773 .unwrap();
1774 assert_eq!(status, NrStatus::Ok);
1775
1776 host.unload("example").unwrap();
1779 drop(handle);
1780 drop(host);
1781 assert_eq!(response.len(), 64);
1782 assert!(response.iter().all(|&byte| byte == 42));
1783 drop(response);
1784 }
1785
1786 #[test]
1787 fn lease_round_trips_and_rejects_misuse() {
1788 let _plugin_lock = plugin_test_lock();
1789 let Some(path) = example_plugin_path() else {
1790 return;
1791 };
1792 let mut host = NylonRingHost::new();
1793 host.load("example", path.to_str().unwrap()).unwrap();
1794 let handle = host.plugin("example").unwrap();
1795 let runtime = tokio::runtime::Runtime::new().unwrap();
1796
1797 runtime.block_on(async {
1798 let (status, data) = handle
1800 .call_response("echo_lease", b"through the lease")
1801 .await
1802 .unwrap();
1803 assert_eq!(status, NrStatus::Ok);
1804 assert_eq!(data, b"through the lease");
1805
1806 let (status, data) = handle.call_response("echo_lease", b"").await.unwrap();
1808 assert_eq!(status, NrStatus::Ok);
1809 assert!(data.is_empty());
1810
1811 let (status, data) = handle
1813 .call_response_fast("echo_lease", b"fast lease")
1814 .await
1815 .unwrap();
1816 assert_eq!(status, NrStatus::Ok);
1817 assert_eq!(data, b"fast lease");
1818
1819 let (status, data) = handle
1823 .call_response("lease_misuse", b"contract")
1824 .await
1825 .unwrap();
1826 assert_eq!(status, NrStatus::Ok);
1827 assert_eq!(data, b"contract");
1828 });
1829
1830 assert_eq!(host.host_ctx.orphaned_lease_count(), 0);
1832 assert_eq!(host.host_ctx.pending_count(), 0);
1833 }
1834
1835 #[test]
1836 fn abandoned_leases_are_parked_not_freed() {
1837 let _plugin_lock = plugin_test_lock();
1838 let Some(path) = example_plugin_path() else {
1839 return;
1840 };
1841 let mut host = NylonRingHost::new();
1842 host.load("example", path.to_str().unwrap()).unwrap();
1843 let handle = host.plugin("example").unwrap();
1844 let runtime = tokio::runtime::Runtime::new().unwrap();
1845
1846 runtime.block_on(async {
1847 let result = handle
1851 .call_response_timeout("lease_abandon", b"", std::time::Duration::from_millis(50))
1852 .await;
1853 assert!(matches!(result, Err(NylonRingHostError::Timeout)));
1854 assert_eq!(host.host_ctx.orphaned_lease_count(), 1);
1855 assert_eq!(host.host_ctx.pending_count(), 0);
1856
1857 let result = handle.call_response_fast("lease_abandon", b"").await;
1859 assert!(matches!(
1860 result,
1861 Err(NylonRingHostError::MissingSynchronousResponse)
1862 ));
1863 assert_eq!(host.host_ctx.orphaned_lease_count(), 2);
1864 });
1865 }
1866
1867 #[test]
1868 fn example_plugin_fire_and_forget_entry_returns_ok() {
1869 let _plugin_lock = plugin_test_lock();
1870 let Some(path) = example_plugin_path() else {
1871 return;
1872 };
1873 let mut host = NylonRingHost::new();
1874 host.load("example", path.to_str().unwrap()).unwrap();
1875 let handle = host.plugin("example").unwrap();
1876 let runtime = tokio::runtime::Runtime::new().unwrap();
1877
1878 assert!(matches!(
1879 runtime.block_on(handle.call("notify", b"fire and forget")),
1880 Ok(NrStatus::Ok)
1881 ));
1882 }
1883
1884 #[test]
1885 fn c_plugin_layout_round_trips_through_host() {
1886 let _plugin_lock = plugin_test_lock();
1887 let Some(path) = c_plugin_path() else {
1888 return;
1889 };
1890 let mut host = NylonRingHost::new();
1891 host.load("c-example", path.to_str().unwrap()).unwrap();
1892 let handle = host.plugin("c-example").unwrap();
1893 let runtime = tokio::runtime::Runtime::new().unwrap();
1894 let (status, response) = runtime
1895 .block_on(handle.call_response("echo", b"from-c"))
1896 .unwrap();
1897 assert_eq!(status, NrStatus::Ok);
1898 assert_eq!(response, b"from-c");
1899
1900 assert!(matches!(
1903 handle.entry("echo"),
1904 Err(NylonRingHostError::EntryDispatchUnsupported)
1905 ));
1906 }
1907
1908 #[test]
1909 fn cpp_plugin_layout_round_trips_through_host() {
1910 let _plugin_lock = plugin_test_lock();
1911 let Some(path) = cpp_plugin_path() else {
1912 return;
1913 };
1914 let mut host = NylonRingHost::new();
1915 host.load("cpp-example", path.to_str().unwrap()).unwrap();
1916 let handle = host.plugin("cpp-example").unwrap();
1917 let runtime = tokio::runtime::Runtime::new().unwrap();
1918 let (status, response) = runtime
1919 .block_on(handle.call_response("echo", b"from-cpp"))
1920 .unwrap();
1921 assert_eq!(status, NrStatus::Ok);
1922 assert_eq!(response, b"from-cpp");
1923 }
1924
1925 #[test]
1926 fn go_plugin_round_trips_through_pinned_host() {
1927 let _plugin_lock = plugin_test_lock();
1928 let Some(path) = go_plugin_path() else {
1929 return;
1930 };
1931 let mut host = NylonRingHost::new();
1935 host.load_pinned("go-example", path.to_str().unwrap())
1936 .unwrap();
1937 let handle = host.plugin("go-example").unwrap();
1938 let runtime = tokio::runtime::Runtime::new().unwrap();
1939 let (status, response) = runtime
1940 .block_on(handle.call_response("echo", b"from-go"))
1941 .unwrap();
1942 assert_eq!(status, NrStatus::Ok);
1943 assert_eq!(response, b"from-go");
1944 }
1945
1946 #[test]
1947 fn zig_plugin_layout_round_trips_through_host() {
1948 let _plugin_lock = plugin_test_lock();
1949 let Some(path) = zig_plugin_path() else {
1950 return;
1951 };
1952 let mut host = NylonRingHost::new();
1953 host.load("zig-example", path.to_str().unwrap()).unwrap();
1954 let handle = host.plugin("zig-example").unwrap();
1955 let runtime = tokio::runtime::Runtime::new().unwrap();
1956 let (status, response) = runtime
1957 .block_on(handle.call_response("echo", b"from-zig"))
1958 .unwrap();
1959 assert_eq!(status, NrStatus::Ok);
1960 assert_eq!(response, b"from-zig");
1961 }
1962
1963 #[test]
1964 fn trait_plugin_round_trips_through_host() {
1965 let _plugin_lock = plugin_test_lock();
1966 let Some(path) = trait_plugin_path() else {
1967 return;
1968 };
1969 let mut host = NylonRingHost::new();
1970 host.load("trait-example", path.to_str().unwrap()).unwrap();
1971 let handle = host.plugin("trait-example").unwrap();
1972 let runtime = tokio::runtime::Runtime::new().unwrap();
1973
1974 let (status, response) = runtime
1976 .block_on(handle.call_response("echo", b"hi"))
1977 .unwrap();
1978 assert_eq!(status, NrStatus::Ok);
1979 assert_eq!(response, b"hi");
1980
1981 let (status, response) = runtime
1983 .block_on(handle.call_response("shout", b"hello"))
1984 .unwrap();
1985 assert_eq!(status, NrStatus::Ok);
1986 assert_eq!(response, b"HELLO");
1987
1988 let status = runtime.block_on(handle.call("notify", b"ping")).unwrap();
1990 assert_eq!(status, NrStatus::Ok);
1991
1992 let (status, response) = runtime
1995 .block_on(handle.call_response("count", b""))
1996 .unwrap();
1997 assert_eq!(status, NrStatus::Ok);
1998 assert_eq!(response, 4u64.to_le_bytes());
1999
2000 let echo = handle.entry("echo").unwrap();
2002 let (status, response) = runtime.block_on(echo.call_response(b"by-id")).unwrap();
2003 assert_eq!(status, NrStatus::Ok);
2004 assert_eq!(response, b"by-id");
2005 assert!(matches!(
2006 handle.entry("missing"),
2007 Err(NylonRingHostError::EntryNotFound(_))
2008 ));
2009
2010 let (status, response) = runtime
2013 .block_on(handle.call_response("async_echo", b"later"))
2014 .unwrap();
2015 assert_eq!(status, NrStatus::Ok);
2016 assert_eq!(response, b"later");
2017
2018 let (status, response) = runtime
2019 .block_on(handle.call_response("async_delay", b"after-sleep"))
2020 .unwrap();
2021 assert_eq!(status, NrStatus::Ok);
2022 assert_eq!(response, b"after-sleep");
2023
2024 let async_echo = handle.entry("async_echo").unwrap();
2026 let (status, response) = runtime
2027 .block_on(async_echo.call_response(b"by-id"))
2028 .unwrap();
2029 assert_eq!(status, NrStatus::Ok);
2030 assert_eq!(response, b"by-id");
2031
2032 let (_sid, mut rx) = runtime
2034 .block_on(handle.call_stream("stream", b"start"))
2035 .unwrap();
2036 let mut frames = Vec::new();
2037 runtime.block_on(async {
2038 while let Some(frame) = rx.recv().await {
2039 let terminal = frame.status == NrStatus::StreamEnd;
2040 frames.push((frame.status, frame.data));
2041 if terminal {
2042 break;
2043 }
2044 }
2045 });
2046 assert_eq!(frames.len(), 4);
2047 assert_eq!(frames[0], (NrStatus::Ok, b"frame 1".to_vec()));
2048 assert_eq!(frames[3], (NrStatus::StreamEnd, b"done".to_vec()));
2049 }
2050
2051 #[test]
2052 fn entry_id_dispatch_round_trips_all_call_shapes() {
2053 let _plugin_lock = plugin_test_lock();
2054 let Some(path) = example_plugin_path() else {
2055 return;
2056 };
2057 let mut host = NylonRingHost::new();
2058 host.load("example", path.to_str().unwrap()).unwrap();
2059 let handle = host.plugin("example").unwrap();
2060 let runtime = tokio::runtime::Runtime::new().unwrap();
2061
2062 assert!(matches!(
2063 handle.entry("no_such_entry"),
2064 Err(NylonRingHostError::EntryNotFound(_))
2065 ));
2066
2067 let echo = handle.entry("benchmark").unwrap();
2068 let notify = handle.entry("benchmark_without_response").unwrap();
2069 let stream = handle.entry("benchmark_stream").unwrap();
2070
2071 runtime.block_on(async {
2072 let (status, data) = echo.call_response(b"by-id").await.unwrap();
2073 assert_eq!(status, NrStatus::Ok);
2074 assert_eq!(data, b"by-id");
2075
2076 let (status, data) = echo.call_response_fast(b"fast-by-id").await.unwrap();
2077 assert_eq!(status, NrStatus::Ok);
2078 assert_eq!(data, b"fast-by-id");
2079
2080 assert_eq!(notify.call(b"").await.unwrap(), NrStatus::Ok);
2081
2082 let (_sid, mut receiver) = stream.call_stream(b"").await.unwrap();
2083 let mut frames = 0u32;
2084 while receiver.recv().await.is_some() {
2085 frames += 1;
2086 }
2087 assert_eq!(frames, 9);
2088 });
2089 }
2090
2091 #[test]
2092 fn pinned_plugin_calls_and_refuses_unload() {
2093 let _guard = plugin_test_lock();
2094 let Some(path) = example_plugin_path() else {
2095 return;
2096 };
2097 let runtime = tokio::runtime::Builder::new_current_thread()
2098 .enable_time()
2099 .build()
2100 .unwrap();
2101 let mut host = NylonRingHost::new();
2102 host.load_pinned("pinned", path.to_str().unwrap()).unwrap();
2103 let handle = host.plugin("pinned").unwrap();
2104 runtime.block_on(async {
2105 let (status, data) = handle
2106 .call_response_fast("benchmark", b"pin")
2107 .await
2108 .unwrap();
2109 assert_eq!(status, NrStatus::Ok);
2110 assert_eq!(data, b"pin");
2111 let (status, data) = handle.call_response("benchmark", b"pin2").await.unwrap();
2112 assert_eq!(status, NrStatus::Ok);
2113 assert_eq!(data, b"pin2");
2114 let (_sid, mut receiver) = handle.call_stream("stream", b"").await.unwrap();
2116 let mut frames = 0u32;
2117 while receiver.recv().await.is_some() {
2118 frames += 1;
2119 }
2120 assert!(frames > 0);
2121 });
2122 assert_eq!(handle.plugin.call_tracker.active_calls(), 0);
2123 assert!(matches!(
2124 host.unload("pinned"),
2125 Err(NylonRingHostError::PluginPinned(_))
2126 ));
2127 assert!(matches!(
2128 host.reload(),
2129 Err(NylonRingHostError::PluginPinned(_))
2130 ));
2131 assert!(matches!(
2132 host.load("pinned", path.to_str().unwrap()),
2133 Err(NylonRingHostError::PluginPinned(_))
2134 ));
2135 runtime.block_on(async {
2136 assert!(matches!(
2137 host.unload_with_grace("pinned", Duration::from_millis(10))
2138 .await,
2139 Err(NylonRingHostError::PluginPinned(_))
2140 ));
2141 assert!(matches!(
2142 host.reload_with_grace(Duration::from_millis(10)).await,
2143 Err(NylonRingHostError::PluginPinned(_))
2144 ));
2145 });
2146 }
2147
2148 #[test]
2151 #[ignore = "manual probe; needs --release for meaningful numbers"]
2152 fn cycle_budget_probe() {
2153 use std::future::Future;
2154 use std::hint::black_box;
2155 use std::task::{Context, Poll, Waker};
2156
2157 let _guard = plugin_test_lock();
2158 let path = example_plugin_path().expect("example plugin must build");
2159 let mut host = NylonRingHost::new();
2160 host.load("bench", path.to_str().unwrap()).unwrap();
2161 let handle = host.plugin("bench").unwrap();
2162 let fire_entry = handle.entry("benchmark_without_response").unwrap();
2163 let fast_entry = handle.entry("benchmark").unwrap();
2164
2165 const N: u64 = 20_000_000;
2166
2167 fn time_ns(iters: u64, mut f: impl FnMut()) -> f64 {
2168 for _ in 0..iters / 10 {
2169 f();
2170 }
2171 let start = std::time::Instant::now();
2172 for _ in 0..iters {
2173 f();
2174 }
2175 start.elapsed().as_nanos() as f64 / iters as f64
2176 }
2177
2178 let calibrate = |iters: u64| -> f64 {
2184 #[cfg_attr(not(target_arch = "aarch64"), allow(unused_mut))]
2187 let mut x: u64 = black_box(0);
2188 let start = std::time::Instant::now();
2189 #[cfg(target_arch = "aarch64")]
2190 unsafe {
2191 let mut n = iters;
2192 std::arch::asm!(
2193 "2:",
2194 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
2195 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
2196 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
2197 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
2198 "add {x}, {x}, #1", "add {x}, {x}, #1", "add {x}, {x}, #1",
2199 "add {x}, {x}, #1",
2200 "subs {n}, {n}, #1",
2201 "b.ne 2b",
2202 x = inout(reg) x,
2203 n = inout(reg) n,
2204 );
2205 black_box(n);
2206 }
2207 black_box(x);
2208 start.elapsed().as_nanos() as f64 / iters as f64
2209 };
2210 calibrate(200_000_000); let calib_ns = calibrate(100_000_000).min(calibrate(100_000_000));
2212 let ghz = 16.0 / calib_ns;
2213 println!("clock calibration: {calib_ns:.2} ns / 16 adds -> {ghz:.3} GHz");
2214
2215 let cyc = |ns: f64| ns * ghz;
2216 let report = |name: &str, ns: f64| {
2217 println!("{name:<28} {ns:>7.2} ns {:>6.1} cycles", cyc(ns));
2218 };
2219
2220 let plugin = &fire_entry.plugin;
2221 let handle_by_id_fn = plugin.dispatch.handle_by_id.unwrap();
2222 let fire_id = fire_entry.id;
2223
2224 report(
2225 "component: tracker begin+fin",
2226 time_ns(N, || {
2227 let shard = plugin.call_tracker.try_begin().unwrap();
2228 let _ = black_box(plugin.call_tracker.finish(shard));
2229 }),
2230 );
2231
2232 #[repr(align(128))]
2236 struct PaddedCounter(std::sync::atomic::AtomicUsize);
2237 use std::sync::atomic::Ordering as AtomOrd;
2238 let word = PaddedCounter(std::sync::atomic::AtomicUsize::new(0));
2239 report(
2240 "atomics: casa loop + ldaddl",
2241 time_ns(N, || {
2242 let _ = black_box(
2243 word.0
2244 .fetch_update(AtomOrd::Acquire, AtomOrd::Relaxed, |v| Some(v + 1)),
2245 );
2246 black_box(word.0.fetch_sub(1, AtomOrd::Release));
2247 }),
2248 );
2249 report(
2250 "atomics: relaxed cas + sub",
2251 time_ns(N, || {
2252 let _ = black_box(
2253 word.0
2254 .fetch_update(AtomOrd::Relaxed, AtomOrd::Relaxed, |v| Some(v + 1)),
2255 );
2256 black_box(word.0.fetch_sub(1, AtomOrd::Relaxed));
2257 }),
2258 );
2259 report(
2260 "atomics: ldadda + ldaddl",
2261 time_ns(N, || {
2262 black_box(word.0.fetch_add(1, AtomOrd::Acquire));
2263 black_box(word.0.fetch_sub(1, AtomOrd::Release));
2264 }),
2265 );
2266 report(
2267 "atomics: single ldadda",
2268 time_ns(N, || {
2269 black_box(word.0.fetch_add(1, AtomOrd::Acquire));
2270 }),
2271 );
2272 report(
2273 "atomics: relaxed cas + ldaddl",
2274 time_ns(N, || {
2275 let _ = black_box(
2276 word.0
2277 .fetch_update(AtomOrd::Relaxed, AtomOrd::Relaxed, |v| Some(v + 1)),
2278 );
2279 black_box(word.0.fetch_sub(1, AtomOrd::Release));
2280 }),
2281 );
2282
2283 {
2287 let tracker = std::sync::Arc::new(crate::call_tracker::CallTracker::new());
2288 let per_thread: u64 = 4_000_000;
2289 let start = std::time::Instant::now();
2290 let threads: Vec<_> = (0..8)
2291 .map(|_| {
2292 let tracker = tracker.clone();
2293 std::thread::spawn(move || {
2294 for _ in 0..per_thread {
2295 let shard = tracker.try_begin().unwrap();
2296 let _ = black_box(tracker.finish(shard));
2297 }
2298 })
2299 })
2300 .collect();
2301 for t in threads {
2302 t.join().unwrap();
2303 }
2304 let ns = start.elapsed().as_nanos() as f64 / per_thread as f64;
2305 report("tracker pair, 8 threads", ns);
2306 }
2307 report(
2308 "component: next_sid",
2309 time_ns(N, || {
2310 black_box(sid::next_sid());
2311 }),
2312 );
2313 report(
2314 "component: remove_state",
2315 time_ns(N, || {
2316 plugin.host_ctx.remove_state(black_box(1));
2317 }),
2318 );
2319 report(
2320 "raw FFI handle_by_id (fire)",
2321 time_ns(N, || {
2322 let status = unsafe {
2323 handle_by_id_fn(black_box(fire_id), 1, NrBytes::from_slice(black_box(b"")))
2324 };
2325 assert_eq!(status, NrStatus::Ok);
2326 }),
2327 );
2328
2329 let mut cx = Context::from_waker(Waker::noop());
2330 macro_rules! poll_ready {
2331 ($fut:expr) => {{
2332 let fut = $fut;
2333 let mut fut = std::pin::pin!(fut);
2334 match fut.as_mut().poll(&mut cx) {
2335 Poll::Ready(result) => result.unwrap(),
2336 Poll::Pending => panic!("probe future must complete synchronously"),
2337 }
2338 }};
2339 }
2340
2341 report(
2342 "full fire (entry id)",
2343 time_ns(N, || {
2344 poll_ready!(fire_entry.call(black_box(b"")));
2345 }),
2346 );
2347 report(
2348 "full fire (by name)",
2349 time_ns(N, || {
2350 poll_ready!(handle.call("benchmark_without_response", black_box(b"")));
2351 }),
2352 );
2353 report(
2354 "full fast (entry id)",
2355 time_ns(N, || {
2356 black_box(poll_ready!(fast_entry.call_response_fast(black_box(b""))));
2357 }),
2358 );
2359 report(
2360 "full fast (by name)",
2361 time_ns(N, || {
2362 black_box(poll_ready!(
2363 handle.call_response_fast("benchmark", black_box(b""))
2364 ));
2365 }),
2366 );
2367 report(
2368 "full unary (entry id)",
2369 time_ns(N, || {
2370 black_box(poll_ready!(fast_entry.call_response(black_box(b""))));
2371 }),
2372 );
2373 report(
2374 "full unary (by name)",
2375 time_ns(N, || {
2376 black_box(poll_ready!(
2377 handle.call_response("benchmark", black_box(b""))
2378 ));
2379 }),
2380 );
2381 }
2382}