nylon_ring/plugin.rs
1//! Trait-based plugin API, inspired by
2//! [Pingora's `ProxyHttp`/`CTX` design](https://github.com/cloudflare/pingora/blob/main/docs/user_guide/ctx.md):
3//! implement [`Plugin`] on a struct and export it with [`export_plugin!`](crate::export_plugin) —
4//! no raw pointers, no `unsafe` handlers, panics contained at the ABI
5//! boundary.
6//!
7//! Two kinds of state, exactly like Pingora:
8//!
9//! - **Per-call state** lives in [`Plugin::Ctx`], created fresh by
10//! [`Plugin::new_ctx`] for every incoming call and dropped when the call
11//! ends.
12//! - **Cross-call state** lives on the plugin struct itself: one instance
13//! serves every call concurrently, so plain fields with `Atomic*`,
14//! `Mutex`, or anything `Sync` work like Pingora's service struct.
15//!
16//! ```no_run
17//! use nylon_ring::{export_plugin, NrStatus, Plugin, Reply, Session};
18//! use std::sync::atomic::{AtomicU64, Ordering};
19//!
20//! struct Demo {
21//! // cross-call state, shared by every call
22//! calls: AtomicU64,
23//! }
24//!
25//! struct DemoCtx {
26//! // per-call state (Pingora-style CTX)
27//! uppercase: bool,
28//! }
29//!
30//! impl Plugin for Demo {
31//! type Ctx = DemoCtx;
32//! const ENTRIES: &'static [&'static str] = &["echo", "shout"];
33//!
34//! fn new() -> Self {
35//! Demo { calls: AtomicU64::new(0) }
36//! }
37//!
38//! fn new_ctx(&self) -> DemoCtx {
39//! DemoCtx { uppercase: false }
40//! }
41//!
42//! fn on_call(&self, session: &mut Session<'_>, ctx: &mut DemoCtx) -> Reply {
43//! self.calls.fetch_add(1, Ordering::Relaxed);
44//! match session.entry() {
45//! "echo" => Reply::Bytes(session.payload().to_vec()),
46//! "shout" => {
47//! ctx.uppercase = true;
48//! let text = String::from_utf8_lossy(session.payload()).to_string();
49//! Reply::Text(if ctx.uppercase { text.to_uppercase() } else { text })
50//! }
51//! _ => Reply::Fail(NrStatus::Invalid),
52//! }
53//! }
54//! }
55//!
56//! export_plugin!(Demo);
57//! ```
58//!
59//! Entries listed in [`Plugin::ASYNC_ENTRIES`] dispatch to
60//! [`Plugin::on_async_call`] instead — a native `async` handler (no
61//! `async_trait` crate needed). The ABI callback returns immediately and
62//! the reply is delivered when the future completes, through the host's
63//! cross-thread async path. Futures run via [`Plugin::spawn_async`]:
64//! dependency-free thread-per-call by default, or override it with
65//! `tokio::spawn` (or any executor) for real workloads.
66//!
67//! The raw `define_plugin!` macro remains available for handlers that need
68//! full control over the ABI (owned responses, buffer leases); the safe
69//! host-call helpers in this module ([`send_result`], [`send_result_owned`],
70//! [`acquire_result_buffer`], [`commit_result_buffer`]) can be mixed into
71//! either style.
72
73use crate::{
74 NR_ENTRY_UNKNOWN, NrBufferLease, NrBytes, NrHostVTable, NrOwnedBytes, NrStatus, NrStr, NrVec,
75};
76use std::ffi::c_void;
77use std::future::Future;
78use std::sync::atomic::{AtomicPtr, Ordering};
79
80static HOST_CTX: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
81static HOST_VTABLE: AtomicPtr<NrHostVTable> = AtomicPtr::new(std::ptr::null_mut());
82
83fn host() -> Option<(*mut c_void, &'static NrHostVTable)> {
84 let ctx = HOST_CTX.load(Ordering::Acquire);
85 let vtable = HOST_VTABLE.load(Ordering::Acquire);
86 if ctx.is_null() || vtable.is_null() {
87 return None;
88 }
89 // SAFETY: the host guarantees the vtable outlives the loaded plugin,
90 // and init stored a non-null pointer to it.
91 Some((ctx, unsafe { &*vtable }))
92}
93
94/// Sends a response or stream frame for `sid` through the host's
95/// `send_result`. Borrowed payloads (`owned = 0`) are copied by the host
96/// before this returns. Reports `Invalid` when the plugin has not been
97/// initialized.
98pub fn send_result(sid: u64, status: NrStatus, data: NrVec<u8>) -> NrStatus {
99 let Some((ctx, vtable)) = host() else {
100 return NrStatus::Invalid;
101 };
102 // SAFETY: ctx/vtable come from the host's init call and stay valid for
103 // the lifetime of the loaded plugin.
104 unsafe { (vtable.send_result)(ctx, sid, status, data) }
105}
106
107/// Sends a response the host consumes without copying; the host takes
108/// ownership and calls the payload's release exactly once.
109pub fn send_result_owned(sid: u64, status: NrStatus, data: NrOwnedBytes) -> NrStatus {
110 let Some((ctx, vtable)) = host() else {
111 return NrStatus::Invalid;
112 };
113 // SAFETY: as in `send_result`.
114 unsafe { (vtable.send_result_owned)(ctx, sid, status, data) }
115}
116
117/// Leases a host-owned response buffer for `sid`; check
118/// [`NrBufferLease::is_failed`] before writing.
119pub fn acquire_result_buffer(sid: u64, capacity: u64) -> NrBufferLease {
120 let Some((ctx, vtable)) = host() else {
121 return NrBufferLease::failed();
122 };
123 // SAFETY: as in `send_result`.
124 unsafe { (vtable.acquire_result_buffer)(ctx, sid, capacity) }
125}
126
127/// Commits a leased buffer as the response for `sid`.
128pub fn commit_result_buffer(
129 sid: u64,
130 status: NrStatus,
131 token: u64,
132 initialized_len: u64,
133) -> NrStatus {
134 let Some((ctx, vtable)) = host() else {
135 return NrStatus::Invalid;
136 };
137 // SAFETY: as in `send_result`.
138 unsafe { (vtable.commit_result_buffer)(ctx, sid, status, token, initialized_len) }
139}
140
141/// One incoming call: the entry it targets, its payload, and the session id
142/// for manual (streaming) replies.
143pub struct Session<'a> {
144 sid: u64,
145 entry_id: u32,
146 entry: &'static str,
147 payload: &'a [u8],
148}
149
150impl Session<'_> {
151 /// The session id the host assigned to this call.
152 pub fn sid(&self) -> u64 {
153 self.sid
154 }
155
156 /// The entry name this call targets.
157 pub fn entry(&self) -> &'static str {
158 self.entry
159 }
160
161 /// The entry's dispatch id (its index in [`Plugin::ENTRIES`]).
162 pub fn entry_id(&self) -> u32 {
163 self.entry_id
164 }
165
166 /// The request payload.
167 pub fn payload(&self) -> &[u8] {
168 self.payload
169 }
170
171 /// Sends one stream data frame; finish with [`Session::end_stream`]
172 /// and return [`Reply::None`] from the handler.
173 pub fn send_frame(&mut self, data: Vec<u8>) -> NrStatus {
174 send_result(self.sid, NrStatus::Ok, NrVec::from_vec(data))
175 }
176
177 /// Ends a stream with a final frame.
178 pub fn end_stream(&mut self, data: Vec<u8>) -> NrStatus {
179 send_result(self.sid, NrStatus::StreamEnd, NrVec::from_vec(data))
180 }
181}
182
183/// What a [`Plugin::on_call`] handler answers with.
184pub enum Reply {
185 /// Respond with these bytes; ownership transfers to the host zero-copy.
186 Bytes(Vec<u8>),
187 /// Respond with this text; ownership transfers to the host zero-copy.
188 Text(String),
189 /// Respond zero-copy from a static buffer — nothing to allocate or free.
190 Static(&'static [u8]),
191 /// Send no response: fire-and-forget entries, or streams that already
192 /// finished through [`Session::end_stream`].
193 None,
194 /// Fail the call with this status.
195 Fail(NrStatus),
196}
197
198/// One incoming async call: like [`Session`] but self-contained — the
199/// payload is copied out of host memory so the future may outlive the
200/// ABI callback that created it.
201pub struct AsyncSession {
202 sid: u64,
203 entry_id: u32,
204 entry: &'static str,
205 payload: Vec<u8>,
206}
207
208impl AsyncSession {
209 /// The session id the host assigned to this call.
210 pub fn sid(&self) -> u64 {
211 self.sid
212 }
213
214 /// The entry name this call targets.
215 pub fn entry(&self) -> &'static str {
216 self.entry
217 }
218
219 /// The entry's dispatch id (its index in the combined
220 /// [`Plugin::ENTRIES`] + [`Plugin::ASYNC_ENTRIES`] list).
221 pub fn entry_id(&self) -> u32 {
222 self.entry_id
223 }
224
225 /// The request payload.
226 pub fn payload(&self) -> &[u8] {
227 &self.payload
228 }
229
230 /// Takes the payload without copying it again.
231 pub fn into_payload(self) -> Vec<u8> {
232 self.payload
233 }
234
235 /// Sends one stream data frame; safe from any thread.
236 pub fn send_frame(&self, data: Vec<u8>) -> NrStatus {
237 send_result(self.sid, NrStatus::Ok, NrVec::from_vec(data))
238 }
239
240 /// Ends a stream with a final frame.
241 pub fn end_stream(&self, data: Vec<u8>) -> NrStatus {
242 send_result(self.sid, NrStatus::StreamEnd, NrVec::from_vec(data))
243 }
244}
245
246/// A boxed future handed to [`Plugin::spawn_async`]; drive it to completion
247/// to deliver the call's reply.
248pub type AsyncTask = std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
249
250/// A nylon-ring plugin. Implement this and export it with
251/// [`export_plugin!`](crate::export_plugin); see the [module docs](self) for the state model.
252pub trait Plugin: Send + Sync + 'static {
253 /// Per-call state, created fresh for every incoming call and dropped
254 /// when the call ends. Use `()` when no per-call state is needed.
255 /// (`'static` so async handlers can carry it across await points.)
256 type Ctx: Send + 'static;
257
258 /// Entry names dispatched to [`Plugin::on_call`]. An entry's dispatch
259 /// id is its index here, so appending keeps existing ids stable.
260 const ENTRIES: &'static [&'static str];
261
262 /// Entry names dispatched to [`Plugin::on_async_call`]. Their dispatch
263 /// ids continue after [`Plugin::ENTRIES`] (a name in both lists
264 /// dispatches synchronously).
265 const ASYNC_ENTRIES: &'static [&'static str] = &[];
266
267 /// Constructs the plugin instance; runs once when the host initializes
268 /// the library.
269 fn new() -> Self
270 where
271 Self: Sized;
272
273 /// Creates the per-call context.
274 fn new_ctx(&self) -> Self::Ctx;
275
276 /// Handles one call. Runs on a host thread; may be called concurrently
277 /// from many threads, which is why it takes `&self`.
278 fn on_call(&self, session: &mut Session<'_>, ctx: &mut Self::Ctx) -> Reply;
279
280 /// Handles one [`ASYNC_ENTRIES`](Plugin::ASYNC_ENTRIES) call. The ABI
281 /// callback returns `Ok` immediately; the reply is delivered whenever
282 /// the returned future completes, from whatever thread runs it — the
283 /// host's standard async path supports this deferred, cross-thread
284 /// reply natively. Implement with plain `async fn` syntax; no
285 /// `async_trait` crate is needed.
286 ///
287 /// [`Reply::Fail`] is sent to the host as a result (so an awaiting
288 /// caller fails instead of hanging); [`Reply::None`] sends nothing.
289 fn on_async_call(
290 &self,
291 session: AsyncSession,
292 ctx: Self::Ctx,
293 ) -> impl Future<Output = Reply> + Send {
294 let _ = (session, ctx);
295 async { Reply::Fail(NrStatus::Unsupported) }
296 }
297
298 /// Runs an async handler's task. Only futures that are still pending
299 /// after their first poll arrive here — ready-on-first-poll handlers
300 /// deliver inline on the host thread and never touch the executor.
301 /// The default drives the task on a dedicated thread with a
302 /// park/unpark waker — correct, dependency-free, and fine for
303 /// occasional calls. Override with a real executor for throughput,
304 /// e.g. `tokio::spawn(task);` on a runtime the plugin owns (stop it in
305 /// [`Plugin::on_shutdown`]).
306 fn spawn_async(&self, task: AsyncTask) {
307 std::thread::spawn(move || block_on(task));
308 }
309
310 /// Data the host pushes into a plugin-consumed stream.
311 fn on_stream_data(&self, _sid: u64, _data: &[u8]) -> NrStatus {
312 NrStatus::Unsupported
313 }
314
315 /// The host closed a plugin-consumed stream.
316 fn on_stream_close(&self, _sid: u64) -> NrStatus {
317 NrStatus::Unsupported
318 }
319
320 /// Runs when the host shuts the plugin down; stop worker threads here.
321 fn on_shutdown(&self) {}
322}
323
324/// Minimal executor for the default [`Plugin::spawn_async`]: parks the
325/// thread between polls.
326fn block_on(mut task: AsyncTask) {
327 struct ThreadWaker(std::thread::Thread);
328 impl std::task::Wake for ThreadWaker {
329 fn wake(self: std::sync::Arc<Self>) {
330 self.0.unpark();
331 }
332 }
333 let waker = std::task::Waker::from(std::sync::Arc::new(ThreadWaker(std::thread::current())));
334 let mut cx = std::task::Context::from_waker(&waker);
335 while task.as_mut().poll(&mut cx).is_pending() {
336 std::thread::park();
337 }
338}
339
340/// ABI glue behind [`export_plugin!`](crate::export_plugin); not part of the public API.
341#[doc(hidden)]
342pub mod __glue {
343 use super::*;
344 use std::panic::{AssertUnwindSafe, catch_unwind};
345 use std::sync::OnceLock;
346
347 pub fn init<P: Plugin>(
348 instance: &'static OnceLock<P>,
349 host_ctx: *mut c_void,
350 host_vtable: *const NrHostVTable,
351 ) -> NrStatus {
352 catch_unwind(AssertUnwindSafe(|| {
353 if host_ctx.is_null() || host_vtable.is_null() {
354 return NrStatus::Invalid;
355 }
356 HOST_CTX.store(host_ctx, Ordering::Release);
357 HOST_VTABLE.store(host_vtable.cast_mut(), Ordering::Release);
358 instance.get_or_init(P::new);
359 NrStatus::Ok
360 }))
361 .unwrap_or(NrStatus::Panic)
362 }
363
364 pub fn shutdown<P: Plugin>(instance: &'static OnceLock<P>) {
365 let _ = catch_unwind(AssertUnwindSafe(|| {
366 if let Some(plugin) = instance.get() {
367 plugin.on_shutdown();
368 }
369 HOST_VTABLE.store(std::ptr::null_mut(), Ordering::Release);
370 HOST_CTX.store(std::ptr::null_mut(), Ordering::Release);
371 }));
372 }
373
374 /// Dispatch id in the combined `ENTRIES` + `ASYNC_ENTRIES` space.
375 fn entry_index<P: Plugin>(entry_bytes: &[u8]) -> Option<u32> {
376 if let Some(id) = P::ENTRIES
377 .iter()
378 .position(|name| name.as_bytes() == entry_bytes)
379 {
380 return Some(id as u32);
381 }
382 P::ASYNC_ENTRIES
383 .iter()
384 .position(|name| name.as_bytes() == entry_bytes)
385 .map(|index| (P::ENTRIES.len() + index) as u32)
386 }
387
388 pub fn resolve_entry<P: Plugin>(entry: NrStr) -> u32 {
389 catch_unwind(AssertUnwindSafe(|| {
390 // SAFETY: the host keeps the entry name valid for this call.
391 let entry_bytes = match unsafe { entry.as_bytes() } {
392 Ok(bytes) => bytes,
393 Err(_) => return NR_ENTRY_UNKNOWN,
394 };
395 entry_index::<P>(entry_bytes).unwrap_or(NR_ENTRY_UNKNOWN)
396 }))
397 .unwrap_or(NR_ENTRY_UNKNOWN)
398 }
399
400 /// # Safety
401 /// `entry` and `payload` must be valid for the duration of this call,
402 /// which the host guarantees for vtable callbacks.
403 pub unsafe fn handle<P: Plugin>(
404 instance: &'static OnceLock<P>,
405 entry: NrStr,
406 sid: u64,
407 payload: NrBytes,
408 ) -> NrStatus {
409 catch_unwind(AssertUnwindSafe(|| {
410 // SAFETY: per this function's contract.
411 let entry_bytes = match unsafe { entry.as_bytes() } {
412 Ok(bytes) => bytes,
413 Err(_) => return NrStatus::Invalid,
414 };
415 let Some(id) = entry_index::<P>(entry_bytes) else {
416 return NrStatus::Invalid;
417 };
418 // SAFETY: per this function's contract.
419 unsafe { dispatch::<P>(instance, id, sid, payload) }
420 }))
421 .unwrap_or(NrStatus::Panic)
422 }
423
424 /// # Safety
425 /// As for [`handle`].
426 pub unsafe fn handle_by_id<P: Plugin>(
427 instance: &'static OnceLock<P>,
428 id: u32,
429 sid: u64,
430 payload: NrBytes,
431 ) -> NrStatus {
432 catch_unwind(AssertUnwindSafe(|| {
433 if id as usize >= P::ENTRIES.len() + P::ASYNC_ENTRIES.len() {
434 return NrStatus::Invalid;
435 }
436 // SAFETY: per this function's contract.
437 unsafe { dispatch::<P>(instance, id, sid, payload) }
438 }))
439 .unwrap_or(NrStatus::Panic)
440 }
441
442 /// Sends a completed reply for `sid`; shared by the sync and async
443 /// paths (the sync path returns the resulting status to the host).
444 fn deliver(sid: u64, reply: Reply) -> NrStatus {
445 match reply {
446 Reply::Bytes(data) => send_result(sid, NrStatus::Ok, NrVec::from_vec(data)),
447 Reply::Text(text) => send_result(sid, NrStatus::Ok, NrVec::from_string(text)),
448 Reply::Static(data) => {
449 send_result_owned(sid, NrStatus::Ok, NrOwnedBytes::from_static(data))
450 }
451 Reply::None => NrStatus::Ok,
452 Reply::Fail(status) => status,
453 }
454 }
455
456 /// # Safety
457 /// As for [`handle`].
458 unsafe fn dispatch<P: Plugin>(
459 instance: &'static OnceLock<P>,
460 id: u32,
461 sid: u64,
462 payload: NrBytes,
463 ) -> NrStatus {
464 let Some(plugin) = instance.get() else {
465 return NrStatus::Invalid;
466 };
467 // SAFETY: per this function's contract.
468 let payload = match unsafe { payload.as_slice() } {
469 Ok(payload) => payload,
470 Err(_) => return NrStatus::Invalid,
471 };
472 let sync_count = P::ENTRIES.len() as u32;
473 if id < sync_count {
474 let mut session = Session {
475 sid,
476 entry_id: id,
477 entry: P::ENTRIES[id as usize],
478 payload,
479 };
480 let mut ctx = plugin.new_ctx();
481 return deliver(sid, plugin.on_call(&mut session, &mut ctx));
482 }
483
484 // Async entry: copy the payload out of host memory, hand the future
485 // to the plugin's executor, and report Ok now — the host's standard
486 // async path accepts the reply later from any thread.
487 let session = AsyncSession {
488 sid,
489 entry_id: id,
490 entry: P::ASYNC_ENTRIES[(id - sync_count) as usize],
491 payload: payload.to_vec(),
492 };
493 let ctx = plugin.new_ctx();
494 let handler = plugin.on_async_call(session, ctx);
495 let mut task: AsyncTask = Box::pin(async move {
496 match handler.await {
497 // A failed async call must still answer the awaiting host.
498 Reply::Fail(status) => {
499 let _ = send_result(sid, status, NrVec::from_vec(Vec::new()));
500 }
501 reply => {
502 let _ = deliver(sid, reply);
503 }
504 }
505 });
506 // Fast path: a future that is ready on its first poll (no real
507 // await) delivers inline on the host thread and never touches the
508 // executor. A Pending future moves to spawn_async, whose first real
509 // poll re-registers a live waker per the Future contract.
510 let mut poll_ctx = std::task::Context::from_waker(std::task::Waker::noop());
511 if task.as_mut().poll(&mut poll_ctx).is_ready() {
512 return NrStatus::Ok;
513 }
514 plugin.spawn_async(task);
515 NrStatus::Ok
516 }
517
518 /// # Safety
519 /// As for [`handle`].
520 pub unsafe fn stream_data<P: Plugin>(
521 instance: &'static OnceLock<P>,
522 sid: u64,
523 data: NrBytes,
524 ) -> NrStatus {
525 catch_unwind(AssertUnwindSafe(|| {
526 let Some(plugin) = instance.get() else {
527 return NrStatus::Invalid;
528 };
529 // SAFETY: per this function's contract.
530 let data = match unsafe { data.as_slice() } {
531 Ok(data) => data,
532 Err(_) => return NrStatus::Invalid,
533 };
534 plugin.on_stream_data(sid, data)
535 }))
536 .unwrap_or(NrStatus::Panic)
537 }
538
539 pub fn stream_close<P: Plugin>(instance: &'static OnceLock<P>, sid: u64) -> NrStatus {
540 catch_unwind(AssertUnwindSafe(|| {
541 let Some(plugin) = instance.get() else {
542 return NrStatus::Invalid;
543 };
544 plugin.on_stream_close(sid)
545 }))
546 .unwrap_or(NrStatus::Panic)
547 }
548}
549
550/// Exports a [`Plugin`] implementation as this cdylib's nylon-ring entry
551/// point. Use exactly once per plugin crate; see the [module docs](self)
552/// for a complete example.
553#[macro_export]
554macro_rules! export_plugin {
555 ($ty:ty) => {
556 static NY_PLUGIN_INSTANCE: std::sync::OnceLock<$ty> = std::sync::OnceLock::new();
557
558 static PLUGIN_VTABLE: $crate::NrPluginVTable = $crate::NrPluginVTable {
559 init: Some(ny_plugin_init_wrapper),
560 handle: Some(ny_plugin_handle_wrapper),
561 shutdown: Some(ny_plugin_shutdown_wrapper),
562 stream_data: Some(ny_plugin_stream_data_wrapper),
563 stream_close: Some(ny_plugin_stream_close_wrapper),
564 resolve_entry: Some(ny_plugin_resolve_entry_wrapper),
565 handle_by_id: Some(ny_plugin_handle_by_id_wrapper),
566 };
567
568 static PLUGIN_INFO: $crate::NrPluginInfo = $crate::NrPluginInfo {
569 abi_version: $crate::ABI_VERSION,
570 struct_size: std::mem::size_of::<$crate::NrPluginInfo>() as u32,
571 name: $crate::NrStr::from_static(env!("CARGO_PKG_NAME")),
572 version: $crate::NrStr::from_static(env!("CARGO_PKG_VERSION")),
573 plugin_ctx: std::ptr::null_mut(),
574 vtable: &PLUGIN_VTABLE,
575 };
576
577 #[unsafe(no_mangle)]
578 pub extern "C" fn nylon_ring_get_plugin() -> *const $crate::NrPluginInfo {
579 &PLUGIN_INFO
580 }
581
582 unsafe extern "C" fn ny_plugin_init_wrapper(
583 host_ctx: *mut std::ffi::c_void,
584 host_vtable: *const $crate::NrHostVTable,
585 ) -> $crate::NrStatus {
586 $crate::plugin::__glue::init::<$ty>(&NY_PLUGIN_INSTANCE, host_ctx, host_vtable)
587 }
588
589 unsafe extern "C" fn ny_plugin_shutdown_wrapper() {
590 $crate::plugin::__glue::shutdown::<$ty>(&NY_PLUGIN_INSTANCE);
591 }
592
593 unsafe extern "C" fn ny_plugin_handle_wrapper(
594 entry: $crate::NrStr,
595 sid: u64,
596 payload: $crate::NrBytes,
597 ) -> $crate::NrStatus {
598 // SAFETY: the host keeps entry/payload valid for this callback.
599 unsafe {
600 $crate::plugin::__glue::handle::<$ty>(&NY_PLUGIN_INSTANCE, entry, sid, payload)
601 }
602 }
603
604 unsafe extern "C" fn ny_plugin_handle_by_id_wrapper(
605 id: u32,
606 sid: u64,
607 payload: $crate::NrBytes,
608 ) -> $crate::NrStatus {
609 // SAFETY: the host keeps the payload valid for this callback.
610 unsafe {
611 $crate::plugin::__glue::handle_by_id::<$ty>(&NY_PLUGIN_INSTANCE, id, sid, payload)
612 }
613 }
614
615 unsafe extern "C" fn ny_plugin_resolve_entry_wrapper(entry: $crate::NrStr) -> u32 {
616 $crate::plugin::__glue::resolve_entry::<$ty>(entry)
617 }
618
619 unsafe extern "C" fn ny_plugin_stream_data_wrapper(
620 sid: u64,
621 data: $crate::NrBytes,
622 ) -> $crate::NrStatus {
623 // SAFETY: the host keeps the data valid for this callback.
624 unsafe { $crate::plugin::__glue::stream_data::<$ty>(&NY_PLUGIN_INSTANCE, sid, data) }
625 }
626
627 unsafe extern "C" fn ny_plugin_stream_close_wrapper(sid: u64) -> $crate::NrStatus {
628 $crate::plugin::__glue::stream_close::<$ty>(&NY_PLUGIN_INSTANCE, sid)
629 }
630 };
631}