Skip to main content

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}