Skip to main content

foldit_plugin_sdk/
export.rs

1//! Wraps a `Box<dyn Plugin>` into the exported `foldit_plugin_vtable`
2//! symbol the host dlopens.
3//!
4//! Marshalling lives in free functions here so it is unit-testable; the
5//! [`export_plugin!`](crate::export_plugin) macro only emits the `extern "C"`
6//! entry points and the static vtable.
7
8use std::collections::HashMap;
9use std::panic::{catch_unwind, AssertUnwindSafe};
10
11use crate::abi::{
12    FolditPluginAsset, FolditPluginBuffer, FolditPluginDispatchContext, FolditPluginError,
13    FolditPluginParamEntry, FolditPluginParamTag, FolditPluginResidueRef, FolditPluginStatus,
14};
15use crate::error::{PluginError, Result};
16use crate::proto::plugin as proto;
17use crate::protocol::{DispatchContext, ParamValue, PollOutcome, ResidueRef};
18
19/// Handle payload behind [`crate::abi::FolditPluginHandle`]: a thin
20/// pointer to the boxed trait object (the trait object itself is a fat
21/// pointer and cannot be cast to `*mut c_void` directly).
22pub type BoxedPlugin = Box<dyn crate::Plugin>;
23
24// ---------------------------------------------------------------------
25// Buffers / errors. Plugin allocates, host frees via the vtable.
26// ---------------------------------------------------------------------
27
28/// Hand a `Vec<u8>` to the host. Ownership transfers; the host returns it
29/// through `free_buffer`, which reconstructs the `Vec` from the recorded
30/// capacity.
31#[must_use]
32pub fn buffer_from_vec(mut v: Vec<u8>) -> FolditPluginBuffer {
33    if v.is_empty() {
34        return FolditPluginBuffer::empty();
35    }
36    let (data, len, capacity) = (v.as_mut_ptr(), v.len(), v.capacity());
37    std::mem::forget(v);
38    FolditPluginBuffer {
39        data,
40        len,
41        capacity,
42    }
43}
44
45/// Release a buffer previously produced by [`buffer_from_vec`].
46///
47/// # Safety
48/// `buf` must be null or point to a buffer this plugin allocated.
49pub unsafe fn drop_buffer(buf: *mut FolditPluginBuffer) {
50    if buf.is_null() {
51        return;
52    }
53    let b = unsafe { &mut *buf };
54    if !b.data.is_null() {
55        drop(unsafe { Vec::from_raw_parts(b.data, b.len, b.capacity) });
56    }
57    *b = FolditPluginBuffer::empty();
58}
59
60/// Release both inner buffers of an error this plugin allocated.
61///
62/// # Safety
63/// `err` must be null or point to an error this plugin allocated.
64pub unsafe fn drop_error(err: *mut FolditPluginError) {
65    if err.is_null() {
66        return;
67    }
68    let e = unsafe { &mut *err };
69    unsafe {
70        drop_buffer(&raw mut e.code);
71        drop_buffer(&raw mut e.message);
72    }
73}
74
75/// Populate `out_err` and pick the status the host should see. `Unsupported`
76/// maps to the dedicated status so the host can distinguish "plugin has no
77/// streaming" from "streaming failed".
78///
79/// # Safety
80/// `out_err` must be null or point to a writable `FolditPluginError`.
81pub unsafe fn report_error(out_err: *mut FolditPluginError, e: &PluginError) -> FolditPluginStatus {
82    match e {
83        PluginError::Unsupported => FolditPluginStatus::Unsupported,
84        PluginError::Op { code, message } => {
85            unsafe { write_error(out_err, code, message) };
86            FolditPluginStatus::Err
87        }
88        other => {
89            unsafe { write_error(out_err, "PLUGIN_ERROR", &other.to_string()) };
90            FolditPluginStatus::Err
91        }
92    }
93}
94
95/// # Safety
96/// `out_err` must be null or point to a writable `FolditPluginError`.
97pub unsafe fn write_error(out_err: *mut FolditPluginError, code: &str, message: &str) {
98    if out_err.is_null() {
99        return;
100    }
101    unsafe {
102        *out_err = FolditPluginError {
103            code: buffer_from_vec(code.as_bytes().to_vec()),
104            message: buffer_from_vec(message.as_bytes().to_vec()),
105        };
106    }
107}
108
109/// Run plugin code, converting a panic into an error rather than
110/// unwinding across the FFI boundary (which is UB).
111///
112/// # Errors
113/// Returns whatever `f` returns, or `Other("plugin panicked")`.
114pub fn guard<T>(f: impl FnOnce() -> Result<T>) -> Result<T> {
115    catch_unwind(AssertUnwindSafe(f))
116        .unwrap_or_else(|_| Err(PluginError::Other("plugin panicked".into())))
117}
118
119// ---------------------------------------------------------------------
120// Host -> plugin marshalling. All inputs are borrowed for the call only.
121// ---------------------------------------------------------------------
122
123/// # Safety
124/// `p` must be null or valid for `len` bytes.
125#[must_use]
126pub unsafe fn slice_from_raw<'a>(p: *const u8, len: usize) -> &'a [u8] {
127    if p.is_null() || len == 0 {
128        &[]
129    } else {
130        unsafe { std::slice::from_raw_parts(p, len) }
131    }
132}
133
134/// # Safety
135/// `p` must be null or valid for `len` bytes.
136unsafe fn str_from_raw<'a>(p: *const u8, len: usize) -> Option<&'a str> {
137    std::str::from_utf8(unsafe { slice_from_raw(p, len) }).ok()
138}
139
140/// # Safety
141/// `p` must be null or valid for `n` entries.
142unsafe fn residues_from_raw(p: *const FolditPluginResidueRef, n: usize) -> Vec<ResidueRef> {
143    if p.is_null() || n == 0 {
144        return Vec::new();
145    }
146    unsafe { std::slice::from_raw_parts(p, n) }
147        .iter()
148        .map(|r| ResidueRef {
149            // The C ABI widens EntityId to u64 for alignment; molex ids are u32.
150            entity_id: molex::EntityId::from_raw(u32::try_from(r.entity_id).unwrap_or_default()),
151            residue_index: r.residue_index,
152        })
153        .collect()
154}
155
156/// # Safety
157/// `ctx` must be null or point to a valid context owned by the host.
158#[must_use]
159pub unsafe fn ctx_from_c(ctx: *const FolditPluginDispatchContext) -> DispatchContext {
160    if ctx.is_null() {
161        return DispatchContext::default();
162    }
163    let c = unsafe { &*ctx };
164    DispatchContext {
165        focused_entity_id: (c.has_focused_entity == 1).then(|| {
166            molex::EntityId::from_raw(u32::try_from(c.focused_entity_id).unwrap_or_default())
167        }),
168        selection: unsafe { residues_from_raw(c.selection, c.selection_len) },
169        designable: unsafe { residues_from_raw(c.designable, c.designable_len) },
170    }
171}
172
173/// `Unspecified` entries are dropped: the ABI treats them as "skip".
174///
175/// # Safety
176/// `p` must be null or valid for `n` entries.
177#[must_use]
178pub unsafe fn params_from_c(
179    p: *const FolditPluginParamEntry,
180    n: usize,
181) -> HashMap<String, ParamValue> {
182    if p.is_null() || n == 0 {
183        return HashMap::new();
184    }
185    unsafe { std::slice::from_raw_parts(p, n) }
186        .iter()
187        .filter_map(|e| {
188            let key = unsafe { str_from_raw(e.key_data, e.key_len) }?.to_owned();
189            let v = &e.value;
190            let value = match v.tag {
191                FolditPluginParamTag::Int => ParamValue::Int(v.int_value),
192                FolditPluginParamTag::Float => ParamValue::Float(v.float_value),
193                FolditPluginParamTag::Bool => ParamValue::Bool(v.bool_value != 0),
194                FolditPluginParamTag::String => ParamValue::String(
195                    unsafe { str_from_raw(v.string_data, v.string_len) }?.to_owned(),
196                ),
197                FolditPluginParamTag::Vec3 => {
198                    ParamValue::Vec3([v.vec3_value.x, v.vec3_value.y, v.vec3_value.z])
199                }
200                FolditPluginParamTag::Unspecified => return None,
201            };
202            Some((key, value))
203        })
204        .collect()
205}
206
207/// # Safety
208/// `p` must be null or valid for `n` entries.
209#[must_use]
210pub unsafe fn assets_from_c(p: *const FolditPluginAsset, n: usize) -> Vec<proto::PuzzleAsset> {
211    if p.is_null() || n == 0 {
212        return Vec::new();
213    }
214    unsafe { std::slice::from_raw_parts(p, n) }
215        .iter()
216        .map(|a| proto::PuzzleAsset {
217            name: unsafe { str_from_raw(a.name_data, a.name_len) }
218                .unwrap_or_default()
219                .to_owned(),
220            data: unsafe { slice_from_raw(a.data, a.data_len) }.to_vec(),
221        })
222        .collect()
223}
224
225// ---------------------------------------------------------------------
226// Plugin -> host encoding.
227// ---------------------------------------------------------------------
228
229/// `poll_stream` is the one vtable slot whose payload is proto-encoded.
230#[must_use]
231pub fn poll_outcome_to_proto(outcome: PollOutcome) -> proto::PollStreamResponse {
232    use proto::poll_stream_response::Result as R;
233    let result = match outcome {
234        PollOutcome::Pending {
235            latest_assembly,
236            progress,
237            stage,
238            score,
239        } => R::Pending(proto::StreamPending {
240            latest_assembly: latest_assembly.unwrap_or_default(),
241            progress,
242            stage,
243            score,
244        }),
245        PollOutcome::Checkpoint {
246            latest_assembly,
247            progress,
248            stage,
249            score,
250        } => R::Checkpoint(proto::StreamCheckpoint {
251            latest_assembly: latest_assembly.unwrap_or_default(),
252            progress,
253            stage,
254            score,
255        }),
256        PollOutcome::Cancelled { assembly, score } => {
257            R::Cancelled(proto::StreamCancelled { assembly, score })
258        }
259        PollOutcome::Final { assembly, score } => R::Final(proto::StreamFinal { assembly, score }),
260        PollOutcome::Error {
261            code,
262            message,
263            details,
264        } => R::Error(proto::Error {
265            code,
266            message,
267            details,
268        }),
269    };
270    proto::PollStreamResponse {
271        result: Some(result),
272    }
273}
274
275/// Encode a prost message into a host-owned buffer.
276#[must_use]
277pub fn encode_to_buffer<M: prost::Message>(msg: &M) -> FolditPluginBuffer {
278    buffer_from_vec(msg.encode_to_vec())
279}
280
281/// Emit the `foldit_plugin_vtable` symbol for a plugin.
282///
283/// Takes a constructor `fn(&str) -> Result<Box<dyn Plugin>>` receiving the
284/// host's JSON config (always contains `plugin_dir`).
285///
286/// ```ignore
287/// fn new_plugin(config_json: &str) -> foldit_plugin_sdk::Result<Box<dyn Plugin>> { .. }
288/// foldit_plugin_sdk::export_plugin!(new_plugin);
289/// ```
290#[macro_export]
291macro_rules! export_plugin {
292    ($ctor:expr) => {
293        const _: () = {
294            use ::std::os::raw::{c_char, c_void};
295            use $crate::abi::*;
296            use $crate::export::*;
297
298            /// # Safety
299            /// `handle` must come from `create` and still be live.
300            unsafe fn plugin<'a>(handle: FolditPluginHandle) -> &'a dyn $crate::Plugin {
301                unsafe { &**handle.cast::<BoxedPlugin>() }
302            }
303
304            /// Run `f`, writing any error into `out_err`.
305            unsafe fn call(
306                out_err: *mut FolditPluginError,
307                f: impl FnOnce() -> $crate::Result<()>,
308            ) -> FolditPluginStatus {
309                match guard(f) {
310                    Ok(()) => FolditPluginStatus::Ok,
311                    Err(e) => unsafe { report_error(out_err, &e) },
312                }
313            }
314
315            unsafe extern "C" fn create(
316                config_json: *const c_char,
317                config_len: usize,
318            ) -> FolditPluginHandle {
319                let ctor: fn(&str) -> $crate::Result<BoxedPlugin> = $ctor;
320                let built = guard(|| {
321                    let bytes = unsafe { slice_from_raw(config_json.cast::<u8>(), config_len) };
322                    ctor(::std::str::from_utf8(bytes).unwrap_or("{}"))
323                });
324                match built {
325                    Ok(p) => {
326                        ::std::boxed::Box::into_raw(::std::boxed::Box::new(p)).cast::<c_void>()
327                    }
328                    Err(_) => ::std::ptr::null_mut(),
329                }
330            }
331
332            unsafe extern "C" fn destroy(handle: FolditPluginHandle) {
333                if handle.is_null() {
334                    return;
335                }
336                drop(unsafe { ::std::boxed::Box::from_raw(handle.cast::<BoxedPlugin>()) });
337            }
338
339            unsafe extern "C" fn register(
340                handle: FolditPluginHandle,
341                out_buf: *mut FolditPluginBuffer,
342                out_err: *mut FolditPluginError,
343            ) -> FolditPluginStatus {
344                unsafe {
345                    call(out_err, || {
346                        let reg = plugin(handle).register()?;
347                        *out_buf = encode_to_buffer(&reg);
348                        Ok(())
349                    })
350                }
351            }
352
353            #[allow(clippy::too_many_arguments)]
354            unsafe extern "C" fn init(
355                handle: FolditPluginHandle,
356                assembly: *const u8,
357                assembly_len: usize,
358                assets: *const FolditPluginAsset,
359                assets_len: usize,
360                params: *const FolditPluginParamEntry,
361                params_len: usize,
362                out_session: *mut u64,
363                out_initial_buf: *mut FolditPluginBuffer,
364                out_err: *mut FolditPluginError,
365            ) -> FolditPluginStatus {
366                unsafe {
367                    call(out_err, || {
368                        let (session, initial) = plugin(handle).init(
369                            slice_from_raw(assembly, assembly_len),
370                            &assets_from_c(assets, assets_len),
371                            &params_from_c(params, params_len),
372                        )?;
373                        *out_session = session;
374                        *out_initial_buf = buffer_from_vec(initial);
375                        Ok(())
376                    })
377                }
378            }
379
380            unsafe extern "C" fn update_assembly(
381                handle: FolditPluginHandle,
382                session: u64,
383                payload_kind: FolditPluginAssemblyPayloadKind,
384                bytes: *const u8,
385                bytes_len: usize,
386                from_gen: u64,
387                to_gen: u64,
388                out_err: *mut FolditPluginError,
389            ) -> FolditPluginStatus {
390                unsafe {
391                    call(out_err, || {
392                        let b = slice_from_raw(bytes, bytes_len);
393                        let payload = match payload_kind {
394                            FolditPluginAssemblyPayloadKind::Full => {
395                                $crate::AssemblyPayload::Full(b)
396                            }
397                            FolditPluginAssemblyPayloadKind::Delta => {
398                                $crate::AssemblyPayload::Delta(b)
399                            }
400                        };
401                        plugin(handle).update_assembly(session, payload, from_gen, to_gen)
402                    })
403                }
404            }
405
406            unsafe extern "C" fn drop_session(
407                handle: FolditPluginHandle,
408                session: u64,
409                out_err: *mut FolditPluginError,
410            ) -> FolditPluginStatus {
411                unsafe { call(out_err, || plugin(handle).drop_session(session)) }
412            }
413
414            #[allow(clippy::too_many_arguments)]
415            unsafe extern "C" fn invoke(
416                handle: FolditPluginHandle,
417                session: u64,
418                op_id: *const u8,
419                op_id_len: usize,
420                ctx: *const FolditPluginDispatchContext,
421                params: *const FolditPluginParamEntry,
422                params_len: usize,
423                out_assembly: *mut FolditPluginBuffer,
424                out_err: *mut FolditPluginError,
425            ) -> FolditPluginStatus {
426                unsafe {
427                    call(out_err, || {
428                        let op = ::std::str::from_utf8(slice_from_raw(op_id, op_id_len))
429                            .map_err(|e| $crate::PluginError::Other(e.to_string()))?;
430                        let bytes = plugin(handle).invoke(
431                            session,
432                            op,
433                            &ctx_from_c(ctx),
434                            &params_from_c(params, params_len),
435                        )?;
436                        *out_assembly = buffer_from_vec(bytes);
437                        Ok(())
438                    })
439                }
440            }
441
442            #[allow(clippy::too_many_arguments)]
443            unsafe extern "C" fn start_stream(
444                handle: FolditPluginHandle,
445                session: u64,
446                op_id: *const u8,
447                op_id_len: usize,
448                ctx: *const FolditPluginDispatchContext,
449                params: *const FolditPluginParamEntry,
450                params_len: usize,
451                request_id: u64,
452                out_err: *mut FolditPluginError,
453            ) -> FolditPluginStatus {
454                unsafe {
455                    call(out_err, || {
456                        let op = ::std::str::from_utf8(slice_from_raw(op_id, op_id_len))
457                            .map_err(|e| $crate::PluginError::Other(e.to_string()))?;
458                        plugin(handle).start_stream(
459                            session,
460                            op,
461                            &ctx_from_c(ctx),
462                            &params_from_c(params, params_len),
463                            request_id,
464                        )
465                    })
466                }
467            }
468
469            unsafe extern "C" fn poll_stream(
470                handle: FolditPluginHandle,
471                request_id: u64,
472                out_buf: *mut FolditPluginBuffer,
473                out_err: *mut FolditPluginError,
474            ) -> FolditPluginStatus {
475                unsafe {
476                    call(out_err, || {
477                        let outcome = plugin(handle).poll_stream(request_id)?;
478                        *out_buf = encode_to_buffer(&poll_outcome_to_proto(outcome));
479                        Ok(())
480                    })
481                }
482            }
483
484            unsafe extern "C" fn update_stream(
485                handle: FolditPluginHandle,
486                request_id: u64,
487                params: *const FolditPluginParamEntry,
488                params_len: usize,
489                out_err: *mut FolditPluginError,
490            ) -> FolditPluginStatus {
491                unsafe {
492                    call(out_err, || {
493                        plugin(handle).update_stream(request_id, &params_from_c(params, params_len))
494                    })
495                }
496            }
497
498            unsafe extern "C" fn cancel_stream(
499                handle: FolditPluginHandle,
500                request_id: u64,
501                out_err: *mut FolditPluginError,
502            ) -> FolditPluginStatus {
503                unsafe { call(out_err, || plugin(handle).cancel_stream(request_id)) }
504            }
505
506            #[allow(clippy::too_many_arguments)]
507            unsafe extern "C" fn query(
508                handle: FolditPluginHandle,
509                session: u64,
510                query_id: *const u8,
511                query_id_len: usize,
512                ctx: *const FolditPluginDispatchContext,
513                params: *const FolditPluginParamEntry,
514                params_len: usize,
515                assembly: *const u8,
516                assembly_len: usize,
517                out_data: *mut FolditPluginBuffer,
518                out_err: *mut FolditPluginError,
519            ) -> FolditPluginStatus {
520                unsafe {
521                    call(out_err, || {
522                        let q = ::std::str::from_utf8(slice_from_raw(query_id, query_id_len))
523                            .map_err(|e| $crate::PluginError::Other(e.to_string()))?;
524                        let bytes = plugin(handle).query(
525                            session,
526                            q,
527                            &ctx_from_c(ctx),
528                            &params_from_c(params, params_len),
529                            slice_from_raw(assembly, assembly_len),
530                        )?;
531                        *out_data = buffer_from_vec(bytes);
532                        Ok(())
533                    })
534                }
535            }
536
537            unsafe extern "C" fn free_buffer(buf: *mut FolditPluginBuffer) {
538                unsafe { drop_buffer(buf) };
539            }
540
541            unsafe extern "C" fn free_error(err: *mut FolditPluginError) {
542                unsafe { drop_error(err) };
543            }
544
545            static VTABLE: FolditPluginVtable = FolditPluginVtable {
546                abi_version: FOLDIT_PLUGIN_ABI_VERSION,
547                padding: 0,
548                create,
549                destroy,
550                register,
551                init,
552                update_assembly,
553                drop_session,
554                invoke,
555                start_stream,
556                poll_stream,
557                update_stream,
558                cancel_stream,
559                query,
560                free_buffer,
561                free_error,
562            };
563
564            #[no_mangle]
565            extern "C" fn foldit_plugin_vtable() -> *const FolditPluginVtable {
566                &raw const VTABLE
567            }
568        };
569    };
570}
571
572// `macro_rules!` bodies are only type-checked once expanded, so the macro needs
573// a real expansion site. `cfg(test)` keeps `#[no_mangle]` out of the cdylib.
574#[cfg(test)]
575mod tests {
576    use std::collections::HashMap;
577
578    use prost::Message as _;
579
580    use crate::abi::{
581        FolditPluginBuffer, FolditPluginError, FolditPluginStatus, FolditPluginVtable,
582        FOLDIT_PLUGIN_ABI_VERSION,
583    };
584    use crate::plugin::AssemblyPayload;
585    use crate::proto::plugin as proto;
586    use crate::protocol::{ParamValue, PollOutcome};
587    use crate::{Plugin, Result};
588
589    struct Dummy;
590
591    impl Plugin for Dummy {
592        fn init(
593            &self,
594            assembly_bytes: &[u8],
595            _assets: &[proto::PuzzleAsset],
596            _params: &HashMap<String, ParamValue>,
597        ) -> Result<(u64, Vec<u8>)> {
598            Ok((42, assembly_bytes.to_vec()))
599        }
600
601        fn register(&self) -> Result<proto::PluginRegistration> {
602            Ok(proto::PluginRegistration {
603                id: "dummy".into(),
604                version: "0.1.0".into(),
605                operations: vec![],
606                queries: vec![],
607            })
608        }
609
610        fn update_assembly(
611            &self,
612            _s: u64,
613            _p: AssemblyPayload<'_>,
614            _f: u64,
615            _t: u64,
616        ) -> Result<()> {
617            Ok(())
618        }
619
620        fn drop_session(&self, _s: u64) -> Result<()> {
621            Ok(())
622        }
623
624        fn poll_stream(&self, _request_id: u64) -> Result<PollOutcome> {
625            Ok(PollOutcome::Final {
626                assembly: vec![1, 2, 3],
627                score: None,
628            })
629        }
630    }
631
632    // `export_plugin!` requires a `fn(&str) -> Result<Box<dyn Plugin>>`; this
633    // one is infallible, so the wrap is the trait's shape, not a real fallible.
634    #[allow(
635        clippy::unnecessary_wraps,
636        reason = "signature is fixed by export_plugin!"
637    )]
638    fn ctor(_config_json: &str) -> Result<Box<dyn Plugin>> {
639        Ok(Box::new(Dummy))
640    }
641
642    crate::export_plugin!(ctor);
643
644    unsafe extern "C" {
645        fn foldit_plugin_vtable() -> *const FolditPluginVtable;
646    }
647
648    unsafe fn take(buf: &mut FolditPluginBuffer, vt: &FolditPluginVtable) -> Vec<u8> {
649        let bytes = unsafe { super::slice_from_raw(buf.data, buf.len) }.to_vec();
650        unsafe { (vt.free_buffer)(&raw mut *buf) };
651        bytes
652    }
653
654    #[test]
655    fn vtable_roundtrips_through_the_c_abi() {
656        let vt = unsafe { &*foldit_plugin_vtable() };
657        assert_eq!(vt.abi_version, FOLDIT_PLUGIN_ABI_VERSION);
658
659        let cfg = br#"{"plugin_dir":"/tmp"}"#;
660        let handle = unsafe { (vt.create)(cfg.as_ptr().cast(), cfg.len()) };
661        assert!(!handle.is_null(), "create returned null");
662
663        let mut err = FolditPluginError::empty();
664
665        // register -> prost-encoded PluginRegistration
666        let mut buf = FolditPluginBuffer::empty();
667        let status = unsafe { (vt.register)(handle, &raw mut buf, &raw mut err) };
668        assert!(matches!(status, FolditPluginStatus::Ok));
669        // `unwrap_or_default` rather than `unwrap`: a decode failure yields the
670        // empty registration, and the id assertion below fails on it with a
671        // clear message instead of a bare unwrap panic.
672        let reg = proto::PluginRegistration::decode(&unsafe { take(&mut buf, vt) }[..])
673            .unwrap_or_default();
674        assert_eq!(reg.id, "dummy");
675
676        // init -> session id + normalized assembly echoed back
677        let mut session = 0u64;
678        let mut initial = FolditPluginBuffer::empty();
679        let assembly = [7u8, 8, 9];
680        let status = unsafe {
681            (vt.init)(
682                handle,
683                assembly.as_ptr(),
684                assembly.len(),
685                std::ptr::null(),
686                0,
687                std::ptr::null(),
688                0,
689                &raw mut session,
690                &raw mut initial,
691                &raw mut err,
692            )
693        };
694        assert!(matches!(status, FolditPluginStatus::Ok));
695        assert_eq!(session, 42);
696        assert_eq!(unsafe { take(&mut initial, vt) }, assembly);
697
698        // poll_stream -> PollOutcome encoded as PollStreamResponse::Final
699        let mut poll = FolditPluginBuffer::empty();
700        let status = unsafe { (vt.poll_stream)(handle, 1, &raw mut poll, &raw mut err) };
701        assert!(matches!(status, FolditPluginStatus::Ok));
702        let resp = proto::PollStreamResponse::decode(&unsafe { take(&mut poll, vt) }[..])
703            .unwrap_or_default();
704        assert!(
705            matches!(
706                resp.result,
707                Some(proto::poll_stream_response::Result::Final(ref f)) if f.assembly == [1, 2, 3]
708            ),
709            "poll_stream must return Final with the echoed assembly, got {:?}",
710            resp.result
711        );
712
713        // An unimplemented method must report Unsupported, not Err.
714        let mut out = FolditPluginBuffer::empty();
715        let op = b"nope";
716        let status = unsafe {
717            (vt.invoke)(
718                handle,
719                session,
720                op.as_ptr(),
721                op.len(),
722                std::ptr::null(),
723                std::ptr::null(),
724                0,
725                &raw mut out,
726                &raw mut err,
727            )
728        };
729        assert!(matches!(status, FolditPluginStatus::Unsupported));
730
731        unsafe { (vt.destroy)(handle) };
732    }
733}