Skip to main content

rich_plugin_api/
abi.rs

1//! The runtime plugin ABI: what a native (`dylib`) or WASM plugin exports,
2//! and the one description, [`PluginAbi`], both loaders decode it into.
3//!
4//! Rust's own ABI is unstable, so nothing Rust-specific crosses the
5//! boundary: a runtime plugin exchanges UTF-8 text with its host and nothing
6//! else. What it may contribute is deliberately small ([`CapabilityKind`]):
7//!
8//! | kind | input | output |
9//! |---|---|---|
10//! | `transform` | plain text | plain text |
11//! | `highlighter` | plain text | style spans, one `START END STYLE` per line |
12//! | `fence-markup` | a Markdown fence's code | `rich` console markup |
13//! | `fence-ansi` | a Markdown fence's code | text with ANSI SGR styling |
14//!
15//! Every call also receives the width available, in cells. The host sanitizes
16//! every output before it reaches a console: terminal controls are made
17//! visible, and only `fence-ansi` keeps SGR styling, through the same
18//! sanitizer `rich view` uses.
19//!
20//! **Native plugins** export one C function, [`DYLIB_ENTRY_SYMBOL`], returning
21//! a [`PluginDescriptor`]. Write it with [`export_dylib_plugin!`](crate::export_dylib_plugin)
22//! rather than by hand. **WASM plugins** export a manifest in the text form
23//! of [`PluginAbi`] (see [`PluginAbi::to_manifest`]) and a call function; the
24//! exports are listed under [`wasm`].
25//!
26//! Versioning: [`ABI_MAJOR`] changes with any incompatible change, and a host
27//! refuses a plugin built for another major. A newer minor loads when it uses
28//! nothing the host lacks (an unknown capability kind is refused).
29
30// The C ABI needs raw pointers, `no_mangle` and `extern "C"`; everything
31// unsafe in this crate is here.
32#![allow(unsafe_code)]
33
34use std::fmt;
35use std::mem::ManuallyDrop;
36use std::panic::{catch_unwind, AssertUnwindSafe};
37
38use crate::is_valid_name;
39
40/// The ABI's major version. A host refuses a plugin with another.
41pub const ABI_MAJOR: u32 = 1;
42/// The ABI's minor version: additions a host of the same major may lack.
43pub const ABI_MINOR: u32 = 0;
44
45/// The function a native plugin exports: `extern "C" fn() -> *const PluginDescriptor`.
46pub const DYLIB_ENTRY_SYMBOL: &str = "rich_plugin_entry";
47
48/// The most capabilities one runtime plugin may declare.
49pub const MAX_CAPABILITIES: usize = 64;
50/// The longest version or description string accepted, in bytes.
51pub const MAX_FIELD_LEN: usize = 1024;
52
53/// The call succeeded; the output is the result.
54pub const STATUS_OK: u32 = 0;
55/// The call failed; the output is an error message.
56pub const STATUS_ERROR: u32 = 1;
57
58/// What a runtime plugin can contribute. The subset is chosen so that every
59/// contribution is text in and text out, which the host can check.
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
61#[non_exhaustive]
62pub enum CapabilityKind {
63    /// Plain text in, plain text out: a named `TextTransform`. Styles on the
64    /// input are not passed, and the output is unstyled.
65    Transform,
66    /// Plain text in, style spans out: a regex-style `Highlighter`. Each
67    /// output line is `START END STYLE`: byte offsets into the UTF-8 input,
68    /// on character boundaries, with `END` exclusive, and `STYLE` a `rich`
69    /// style such as `bold red`. A span the host cannot apply is skipped.
70    Highlighter,
71    /// A Markdown fence's code in, `rich` markup out (`[bold]x[/]`).
72    FenceMarkup,
73    /// A Markdown fence's code in, ANSI-styled text out. Only SGR (colour and
74    /// attribute) sequences survive the host's sanitizer.
75    FenceAnsi,
76}
77
78impl CapabilityKind {
79    /// Every kind this host understands.
80    pub const ALL: [CapabilityKind; 4] = [
81        CapabilityKind::Transform,
82        CapabilityKind::Highlighter,
83        CapabilityKind::FenceMarkup,
84        CapabilityKind::FenceAnsi,
85    ];
86
87    /// The number used in [`AbiCapability::kind`].
88    pub const fn code(self) -> u32 {
89        match self {
90            CapabilityKind::Transform => 1,
91            CapabilityKind::Highlighter => 2,
92            CapabilityKind::FenceMarkup => 3,
93            CapabilityKind::FenceAnsi => 4,
94        }
95    }
96
97    /// The kind for a [`code`](Self::code), if this host knows it.
98    pub fn from_code(code: u32) -> Option<Self> {
99        Self::ALL.into_iter().find(|kind| kind.code() == code)
100    }
101
102    /// The name used in a manifest (`fence-markup`).
103    pub const fn as_str(self) -> &'static str {
104        match self {
105            CapabilityKind::Transform => "transform",
106            CapabilityKind::Highlighter => "highlighter",
107            CapabilityKind::FenceMarkup => "fence-markup",
108            CapabilityKind::FenceAnsi => "fence-ansi",
109        }
110    }
111
112    /// The kind a manifest name spells, if this host knows it.
113    pub fn parse(name: &str) -> Option<Self> {
114        Self::ALL.into_iter().find(|kind| kind.as_str() == name)
115    }
116
117    /// Whether the output may keep ANSI styling (after sanitizing).
118    pub const fn produces_ansi(self) -> bool {
119        matches!(self, CapabilityKind::FenceAnsi)
120    }
121}
122
123impl fmt::Display for CapabilityKind {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        f.write_str(self.as_str())
126    }
127}
128
129/// One capability a runtime plugin declares.
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct AbiCapability {
132    pub kind: CapabilityKind,
133    /// The capability's name; for a fence, the language (`mermaid`).
134    pub name: String,
135}
136
137/// A runtime plugin's self-description, decoded from a native plugin's
138/// [`PluginDescriptor`] or a WASM plugin's manifest. Both loaders produce this
139/// one type, and one host adapter turns it into registry capabilities.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct PluginAbi {
142    pub abi_major: u32,
143    pub abi_minor: u32,
144    /// The plugin id: lowercase letters, digits, `-`, `_` and `.`.
145    pub name: String,
146    pub version: String,
147    pub description: String,
148    /// In declaration order; a call names a capability by its index here.
149    pub capabilities: Vec<AbiCapability>,
150}
151
152/// Why a runtime plugin's description was refused.
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub enum AbiError {
155    /// Built for another major version of this ABI.
156    Incompatible { major: u32, minor: u32 },
157    /// The description is malformed; the message says how.
158    Invalid(String),
159}
160
161impl fmt::Display for AbiError {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            AbiError::Incompatible { major, minor } => write!(
165                f,
166                "built for plugin ABI {major}.{minor}, but this host supports ABI \
167                 {ABI_MAJOR}.x; rebuild it against rs-rich-plugin-api with ABI {ABI_MAJOR}"
168            ),
169            AbiError::Invalid(message) => f.write_str(message),
170        }
171    }
172}
173
174impl std::error::Error for AbiError {}
175
176/// The first line of a manifest: `rich-plugin-abi MAJOR.MINOR`.
177const MANIFEST_MAGIC: &str = "rich-plugin-abi";
178
179impl PluginAbi {
180    /// A description for this ABI version, with no capabilities.
181    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
182        PluginAbi {
183            abi_major: ABI_MAJOR,
184            abi_minor: ABI_MINOR,
185            name: name.into(),
186            version: version.into(),
187            description: String::new(),
188            capabilities: Vec::new(),
189        }
190    }
191
192    /// Check the version and every field a host relies on.
193    pub fn validate(&self) -> Result<(), AbiError> {
194        if self.abi_major != ABI_MAJOR {
195            return Err(AbiError::Incompatible {
196                major: self.abi_major,
197                minor: self.abi_minor,
198            });
199        }
200        if !is_valid_name(&self.name) {
201            return Err(AbiError::Invalid(format!(
202                "invalid plugin name {:?}: use lowercase letters, digits, '-', '_' and '.'",
203                self.name
204            )));
205        }
206        for (field, value) in [
207            ("version", &self.version),
208            ("description", &self.description),
209        ] {
210            if value.len() > MAX_FIELD_LEN || value.chars().any(char::is_control) {
211                return Err(AbiError::Invalid(format!(
212                    "the {field} must be one line of at most {MAX_FIELD_LEN} bytes"
213                )));
214            }
215        }
216        if self.version.is_empty() {
217            return Err(AbiError::Invalid("the version is empty".into()));
218        }
219        if self.capabilities.len() > MAX_CAPABILITIES {
220            return Err(AbiError::Invalid(format!(
221                "{} capabilities declared; at most {MAX_CAPABILITIES} are allowed",
222                self.capabilities.len()
223            )));
224        }
225        for (i, capability) in self.capabilities.iter().enumerate() {
226            if !is_valid_name(&capability.name) {
227                return Err(AbiError::Invalid(format!(
228                    "invalid {} name {:?}",
229                    capability.kind, capability.name
230                )));
231            }
232            if self.capabilities[..i].contains(capability) {
233                return Err(AbiError::Invalid(format!(
234                    "{} {:?} is declared twice",
235                    capability.kind, capability.name
236                )));
237            }
238        }
239        Ok(())
240    }
241
242    /// The manifest text form, which a WASM plugin returns from
243    /// [`wasm::MANIFEST`]:
244    ///
245    /// ```text
246    /// rich-plugin-abi 1.0
247    /// name shout
248    /// version 0.1.0
249    /// description Upper-cases text
250    /// capability transform upper
251    /// capability fence-markup shout
252    /// ```
253    pub fn to_manifest(&self) -> String {
254        let mut out = format!(
255            "{MANIFEST_MAGIC} {}.{}\nname {}\nversion {}\n",
256            self.abi_major, self.abi_minor, self.name, self.version
257        );
258        if !self.description.is_empty() {
259            out.push_str(&format!("description {}\n", self.description));
260        }
261        for capability in &self.capabilities {
262            out.push_str(&format!(
263                "capability {} {}\n",
264                capability.kind, capability.name
265            ));
266        }
267        out
268    }
269
270    /// Parse and [validate](Self::validate) a manifest. Unknown keys are
271    /// ignored (a newer minor may add some); an unknown capability kind is
272    /// refused, because the host could not honour it.
273    pub fn parse_manifest(text: &str) -> Result<PluginAbi, AbiError> {
274        let mut lines = text.lines();
275        let first = lines.next().unwrap_or_default();
276        let version = first
277            .strip_prefix(MANIFEST_MAGIC)
278            .and_then(|rest| rest.strip_prefix(' '))
279            .ok_or_else(|| {
280                AbiError::Invalid(format!(
281                    "the manifest must start with `{MANIFEST_MAGIC} MAJOR.MINOR`"
282                ))
283            })?;
284        let (major, minor) = version
285            .split_once('.')
286            .and_then(|(major, minor)| Some((major.parse().ok()?, minor.parse().ok()?)))
287            .ok_or_else(|| AbiError::Invalid(format!("invalid ABI version {version:?}")))?;
288        if major != ABI_MAJOR {
289            return Err(AbiError::Incompatible { major, minor });
290        }
291        let mut abi = PluginAbi {
292            abi_major: major,
293            abi_minor: minor,
294            name: String::new(),
295            version: String::new(),
296            description: String::new(),
297            capabilities: Vec::new(),
298        };
299        for line in lines {
300            if line.trim().is_empty() {
301                continue;
302            }
303            let (key, value) = line.split_once(' ').unwrap_or((line, ""));
304            match key {
305                "name" => abi.name = value.to_string(),
306                "version" => abi.version = value.to_string(),
307                "description" => abi.description = value.to_string(),
308                "capability" => {
309                    let (kind, name) = value.split_once(' ').ok_or_else(|| {
310                        AbiError::Invalid(format!("expected `capability KIND NAME`: {line:?}"))
311                    })?;
312                    let kind = CapabilityKind::parse(kind).ok_or_else(|| {
313                        AbiError::Invalid(format!(
314                            "unknown capability kind {kind:?}; this host knows {}",
315                            CapabilityKind::ALL.map(CapabilityKind::as_str).join(", ")
316                        ))
317                    })?;
318                    abi.capabilities.push(AbiCapability {
319                        kind,
320                        name: name.to_string(),
321                    });
322                }
323                _ => {}
324            }
325        }
326        abi.validate()?;
327        Ok(abi)
328    }
329}
330
331/// The export names of a WASM plugin. A module must export all of them and
332/// import nothing:
333///
334/// - `memory`: its linear memory;
335/// - `rich_plugin_alloc(len: i32) -> i32`: room for `len` bytes of input;
336/// - `rich_plugin_manifest() -> i64`: where its manifest is, packed as
337///   `ptr << 32 | len`;
338/// - `rich_plugin_call(capability: i32, ptr: i32, len: i32, width: i32) -> i64`:
339///   run capability number `capability` (its index in the manifest) on the
340///   input at `ptr`, returning the output packed like the manifest, with bit
341///   63 set when the output is an error message instead.
342pub mod wasm {
343    pub const MEMORY: &str = "memory";
344    pub const ALLOC: &str = "rich_plugin_alloc";
345    pub const MANIFEST: &str = "rich_plugin_manifest";
346    pub const CALL: &str = "rich_plugin_call";
347    /// Set in a call's result when the output is an error message.
348    pub const ERROR_BIT: u64 = 1 << 63;
349}
350
351// ---------------------------------------------------------------------------
352// The native (C) ABI.
353// ---------------------------------------------------------------------------
354
355/// A borrowed UTF-8 string: `len` bytes at `ptr`.
356#[repr(C)]
357#[derive(Clone, Copy, Debug)]
358pub struct AbiStr {
359    pub ptr: *const u8,
360    pub len: usize,
361}
362
363impl AbiStr {
364    /// Borrow `text` for the duration of a call.
365    pub fn new(text: &str) -> Self {
366        AbiStr {
367            ptr: text.as_ptr(),
368            len: text.len(),
369        }
370    }
371
372    /// The string, if it is valid UTF-8.
373    ///
374    /// # Safety
375    ///
376    /// `ptr` must be null with `len == 0`, or point to `len` readable bytes
377    /// that outlive `'a`.
378    pub unsafe fn as_str<'a>(self) -> Result<&'a str, AbiError> {
379        if self.ptr.is_null() {
380            return if self.len == 0 {
381                Ok("")
382            } else {
383                Err(AbiError::Invalid("a null string with a length".into()))
384            };
385        }
386        // SAFETY: the caller promises `len` readable bytes at `ptr`.
387        let bytes = unsafe { std::slice::from_raw_parts(self.ptr, self.len) };
388        std::str::from_utf8(bytes).map_err(|_| AbiError::Invalid("a string is not UTF-8".into()))
389    }
390}
391
392/// One capability in a [`PluginDescriptor`]: a [`CapabilityKind::code`] and a
393/// name.
394#[repr(C)]
395#[derive(Clone, Copy, Debug)]
396pub struct AbiCapabilityEntry {
397    pub kind: u32,
398    pub name: AbiStr,
399}
400
401/// Bytes a plugin allocated and hands to the host, which gives them back
402/// through [`PluginVTable::free`] (the plugin's allocator frees them).
403#[repr(C)]
404#[derive(Debug)]
405pub struct AbiOutput {
406    pub ptr: *mut u8,
407    pub len: usize,
408    pub cap: usize,
409}
410
411impl AbiOutput {
412    /// An empty output, for the host to pass to a call.
413    pub const fn empty() -> Self {
414        AbiOutput {
415            ptr: std::ptr::null_mut(),
416            len: 0,
417            cap: 0,
418        }
419    }
420
421    /// Hand `text` over; free it with [`free_output`].
422    pub fn from_string(text: String) -> Self {
423        let mut bytes = ManuallyDrop::new(text.into_bytes());
424        AbiOutput {
425            ptr: bytes.as_mut_ptr(),
426            len: bytes.len(),
427            cap: bytes.capacity(),
428        }
429    }
430}
431
432/// Run capability `capability` (an index into the descriptor's capabilities)
433/// on `input`, with `width` cells available, writing the output to `output`
434/// and returning [`STATUS_OK`] or [`STATUS_ERROR`].
435pub type AbiCallFn = unsafe extern "C" fn(
436    capability: usize,
437    input: AbiStr,
438    width: u32,
439    output: *mut AbiOutput,
440) -> u32;
441
442/// Free an output a call returned.
443pub type AbiFreeFn = unsafe extern "C" fn(output: AbiOutput);
444
445/// The functions a native plugin provides. Each is nullable here, because a
446/// non-nullable Rust function pointer holding null is undefined behaviour
447/// before it is ever called; [`read_descriptor`] refuses a null one.
448#[repr(C)]
449#[derive(Clone, Copy, Debug)]
450pub struct PluginVTable {
451    pub call: Option<AbiCallFn>,
452    pub free: Option<AbiFreeFn>,
453}
454
455/// A [`PluginVTable`] that [`read_descriptor`] checked: neither is null.
456#[derive(Clone, Copy, Debug)]
457pub struct PluginFunctions {
458    pub call: AbiCallFn,
459    pub free: AbiFreeFn,
460}
461
462/// What [`DYLIB_ENTRY_SYMBOL`] returns. `abi_major` and `abi_minor` come first
463/// and stay first in every version, so a host can read them before trusting
464/// the rest of the layout.
465#[repr(C)]
466#[derive(Debug)]
467pub struct PluginDescriptor {
468    pub abi_major: u32,
469    pub abi_minor: u32,
470    pub name: AbiStr,
471    pub version: AbiStr,
472    pub description: AbiStr,
473    pub capabilities: *const AbiCapabilityEntry,
474    pub capability_count: usize,
475    pub vtable: PluginVTable,
476}
477
478/// Decode a descriptor a native plugin's entry point returned.
479///
480/// Reads the version first and refuses another major before looking at the
481/// rest; then checks every string and capability ([`PluginAbi::validate`]).
482///
483/// # Safety
484///
485/// `descriptor` must be null or point to a [`PluginDescriptor`] (or, for
486/// another major version, at least two `u32`s) whose strings and capability
487/// array stay valid while the plugin is loaded.
488pub unsafe fn read_descriptor(
489    descriptor: *const PluginDescriptor,
490) -> Result<(PluginAbi, PluginFunctions), AbiError> {
491    if descriptor.is_null() {
492        return Err(AbiError::Invalid(format!(
493            "{DYLIB_ENTRY_SYMBOL} returned no descriptor (it failed to initialise)"
494        )));
495    }
496    // SAFETY: every version starts with two u32s (see `PluginDescriptor`).
497    let (major, minor) = unsafe {
498        let version = descriptor.cast::<u32>();
499        (version.read(), version.add(1).read())
500    };
501    if major != ABI_MAJOR {
502        return Err(AbiError::Incompatible { major, minor });
503    }
504    // SAFETY: same major, so the layout is this one.
505    let descriptor = unsafe { &*descriptor };
506    if descriptor.capability_count > MAX_CAPABILITIES {
507        return Err(AbiError::Invalid(format!(
508            "{} capabilities declared; at most {MAX_CAPABILITIES} are allowed",
509            descriptor.capability_count
510        )));
511    }
512    let entries: &[AbiCapabilityEntry] = if descriptor.capability_count == 0 {
513        &[]
514    } else if descriptor.capabilities.is_null() {
515        return Err(AbiError::Invalid("a null capability list".into()));
516    } else {
517        // SAFETY: the caller promises the array is valid.
518        unsafe { std::slice::from_raw_parts(descriptor.capabilities, descriptor.capability_count) }
519    };
520    let text = |s: AbiStr, what: &str| -> Result<String, AbiError> {
521        if s.len > MAX_FIELD_LEN {
522            return Err(AbiError::Invalid(format!("the {what} is too long")));
523        }
524        // SAFETY: the caller promises the strings are valid.
525        unsafe { s.as_str() }.map(str::to_string)
526    };
527    let mut capabilities = Vec::with_capacity(entries.len());
528    for entry in entries {
529        let kind = CapabilityKind::from_code(entry.kind).ok_or_else(|| {
530            AbiError::Invalid(format!(
531                "unknown capability kind {}; this host knows {}",
532                entry.kind,
533                CapabilityKind::ALL.map(CapabilityKind::as_str).join(", ")
534            ))
535        })?;
536        capabilities.push(AbiCapability {
537            kind,
538            name: text(entry.name, "capability name")?,
539        });
540    }
541    let abi = PluginAbi {
542        abi_major: major,
543        abi_minor: minor,
544        name: text(descriptor.name, "name")?,
545        version: text(descriptor.version, "version")?,
546        description: text(descriptor.description, "description")?,
547        capabilities,
548    };
549    abi.validate()?;
550    let (Some(call), Some(free)) = (descriptor.vtable.call, descriptor.vtable.free) else {
551        return Err(AbiError::Invalid(
552            "a null function in the vtable (`call` and `free` are required)".into(),
553        ));
554    };
555    Ok((abi, PluginFunctions { call, free }))
556}
557
558// ---------------------------------------------------------------------------
559// The plugin side: build a native plugin without writing unsafe code.
560// ---------------------------------------------------------------------------
561
562/// One capability's implementation: the input and the width in cells, to the
563/// output or an error message.
564pub type ExportFn = fn(input: &str, width: u32) -> Result<String, String>;
565
566/// A native plugin's description and functions, for
567/// [`export_dylib_plugin!`](crate::export_dylib_plugin).
568pub struct Exports {
569    abi: PluginAbi,
570    functions: Vec<ExportFn>,
571}
572
573impl Exports {
574    /// A plugin called `name` (its id) at `version`.
575    pub fn new(name: &str, version: &str) -> Self {
576        Exports {
577            abi: PluginAbi::new(name, version),
578            functions: Vec::new(),
579        }
580    }
581
582    /// One line on what it does.
583    pub fn description(mut self, description: &str) -> Self {
584        self.abi.description = description.to_string();
585        self
586    }
587
588    /// Add a capability of any kind.
589    pub fn capability(mut self, kind: CapabilityKind, name: &str, function: ExportFn) -> Self {
590        self.abi.capabilities.push(AbiCapability {
591            kind,
592            name: name.to_string(),
593        });
594        self.functions.push(function);
595        self
596    }
597
598    /// A named text transform: plain text in and out.
599    pub fn transform(self, name: &str, function: ExportFn) -> Self {
600        self.capability(CapabilityKind::Transform, name, function)
601    }
602
603    /// A highlighter: plain text in, `START END STYLE` lines out.
604    pub fn highlighter(self, name: &str, function: ExportFn) -> Self {
605        self.capability(CapabilityKind::Highlighter, name, function)
606    }
607
608    /// A fence renderer for `language` returning `rich` markup.
609    pub fn fence_markup(self, language: &str, function: ExportFn) -> Self {
610        self.capability(CapabilityKind::FenceMarkup, language, function)
611    }
612
613    /// A fence renderer for `language` returning ANSI-styled text.
614    pub fn fence_ansi(self, language: &str, function: ExportFn) -> Self {
615        self.capability(CapabilityKind::FenceAnsi, language, function)
616    }
617
618    /// The description, as a host will decode it.
619    pub fn abi(&self) -> &PluginAbi {
620        &self.abi
621    }
622}
623
624/// An [`Exports`] laid out for the C ABI. [`export_dylib_plugin!`](crate::export_dylib_plugin)
625/// keeps one in a `static`.
626#[doc(hidden)]
627pub struct Exported {
628    exports: Exports,
629    _entries: Vec<AbiCapabilityEntry>,
630    descriptor: PluginDescriptor,
631}
632
633// SAFETY: the raw pointers in `descriptor` and `_entries` point into
634// `exports`' strings and `_entries`' buffer, which are never mutated after
635// construction and live as long as the `Exported`.
636unsafe impl Send for Exported {}
637// SAFETY: as above; shared access is read-only.
638unsafe impl Sync for Exported {}
639
640impl Exported {
641    pub fn new(exports: Exports, call: AbiCallFn) -> Self {
642        let entries: Vec<AbiCapabilityEntry> = exports
643            .abi
644            .capabilities
645            .iter()
646            .map(|capability| AbiCapabilityEntry {
647                kind: capability.kind.code(),
648                name: AbiStr::new(&capability.name),
649            })
650            .collect();
651        let descriptor = PluginDescriptor {
652            abi_major: exports.abi.abi_major,
653            abi_minor: exports.abi.abi_minor,
654            name: AbiStr::new(&exports.abi.name),
655            version: AbiStr::new(&exports.abi.version),
656            description: AbiStr::new(&exports.abi.description),
657            capabilities: entries.as_ptr(),
658            capability_count: entries.len(),
659            vtable: PluginVTable {
660                call: Some(call),
661                free: Some(free_output),
662            },
663        };
664        Exported {
665            exports,
666            _entries: entries,
667            descriptor,
668        }
669    }
670
671    pub fn descriptor(&'static self) -> *const PluginDescriptor {
672        &self.descriptor
673    }
674
675    /// The body of the generated `call` function. Never unwinds: a panic in
676    /// a capability becomes an error result.
677    ///
678    /// # Safety
679    ///
680    /// `input` must be valid for the call, and `output` null or writable.
681    pub unsafe fn call(
682        this: Option<&Exported>,
683        capability: usize,
684        input: AbiStr,
685        width: u32,
686        output: *mut AbiOutput,
687    ) -> u32 {
688        let result = catch_unwind(AssertUnwindSafe(|| {
689            let this = this.ok_or("the plugin was called before it was initialised")?;
690            let function = this
691                .exports
692                .functions
693                .get(capability)
694                .ok_or_else(|| format!("no capability number {capability}"))?;
695            // SAFETY: the caller promises `input` is valid for this call.
696            let input = unsafe { input.as_str() }.map_err(|e| e.to_string())?;
697            function(input, width)
698        }));
699        let (status, text) = match result {
700            Ok(Ok(text)) => (STATUS_OK, text),
701            Ok(Err(message)) => (STATUS_ERROR, message),
702            Err(_) => (STATUS_ERROR, "the plugin panicked".to_string()),
703        };
704        if output.is_null() {
705            return STATUS_ERROR;
706        }
707        // SAFETY: the caller promises `output` is writable.
708        unsafe { output.write(AbiOutput::from_string(text)) };
709        status
710    }
711
712    /// The body of the generated entry point: `f` makes the exports, and a
713    /// panic in it is a null descriptor instead of an abort.
714    pub fn entry(
715        slot: &'static std::sync::OnceLock<Exported>,
716        make: fn() -> Exports,
717        call: AbiCallFn,
718    ) -> *const PluginDescriptor {
719        match catch_unwind(AssertUnwindSafe(|| {
720            slot.get_or_init(|| Exported::new(make(), call))
721        })) {
722            Ok(exported) => exported.descriptor(),
723            Err(_) => std::ptr::null(),
724        }
725    }
726}
727
728/// Free an [`AbiOutput`] made by [`AbiOutput::from_string`].
729///
730/// # Safety
731///
732/// `output` must come from [`AbiOutput::from_string`] in this same binary,
733/// and be freed once.
734pub unsafe extern "C" fn free_output(output: AbiOutput) {
735    if !output.ptr.is_null() {
736        // SAFETY: made by `from_string` from a `Vec<u8>` with these parts.
737        drop(unsafe { Vec::from_raw_parts(output.ptr, output.len, output.cap) });
738    }
739}
740
741/// Export a native plugin from a `cdylib` crate.
742///
743/// The argument is a function (or non-capturing closure) returning
744/// [`Exports`](crate::abi::Exports). The macro defines the
745/// [`rich_plugin_entry`](crate::abi::DYLIB_ENTRY_SYMBOL) symbol; the crate
746/// needs `crate-type = ["cdylib"]` and no unsafe code of its own.
747///
748/// ```
749/// use rich_plugin_api::abi::Exports;
750///
751/// fn upper(input: &str, _width: u32) -> Result<String, String> {
752///     Ok(input.to_uppercase())
753/// }
754///
755/// rich_plugin_api::export_dylib_plugin!(|| {
756///     Exports::new("shout", "0.1.0")
757///         .description("Upper-cases text")
758///         .transform("upper", upper)
759/// });
760/// # fn main() {}
761/// ```
762#[macro_export]
763macro_rules! export_dylib_plugin {
764    ($exports:expr) => {
765        #[doc(hidden)]
766        static __RICH_PLUGIN_EXPORTED: ::std::sync::OnceLock<$crate::abi::Exported> =
767            ::std::sync::OnceLock::new();
768
769        #[doc(hidden)]
770        #[allow(unsafe_code)]
771        unsafe extern "C" fn __rich_plugin_call(
772            capability: usize,
773            input: $crate::abi::AbiStr,
774            width: u32,
775            output: *mut $crate::abi::AbiOutput,
776        ) -> u32 {
777            // SAFETY: the host passes a valid input and a writable output.
778            unsafe {
779                $crate::abi::Exported::call(
780                    __RICH_PLUGIN_EXPORTED.get(),
781                    capability,
782                    input,
783                    width,
784                    output,
785                )
786            }
787        }
788
789        /// The plugin's entry point, for the `rich` host.
790        #[allow(unsafe_code)]
791        #[unsafe(no_mangle)]
792        pub extern "C" fn rich_plugin_entry() -> *const $crate::abi::PluginDescriptor {
793            $crate::abi::Exported::entry(&__RICH_PLUGIN_EXPORTED, $exports, __rich_plugin_call)
794        }
795    };
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801
802    fn upper(input: &str, _: u32) -> Result<String, String> {
803        Ok(input.to_uppercase())
804    }
805
806    fn fails(_: &str, _: u32) -> Result<String, String> {
807        Err("no".into())
808    }
809
810    fn panics(_: &str, _: u32) -> Result<String, String> {
811        panic!("boom")
812    }
813
814    fn exports() -> Exports {
815        Exports::new("shout", "0.1.0")
816            .description("Upper-cases text")
817            .transform("upper", upper)
818            .fence_markup("shout", fails)
819            .highlighter("boom", panics)
820    }
821
822    crate::export_dylib_plugin!(exports);
823
824    unsafe fn call(descriptor: &PluginFunctions, capability: usize, input: &str) -> (u32, String) {
825        let mut output = AbiOutput::empty();
826        let status = unsafe { (descriptor.call)(capability, AbiStr::new(input), 80, &mut output) };
827        let text = unsafe { std::slice::from_raw_parts(output.ptr, output.len) };
828        let text = String::from_utf8(text.to_vec()).unwrap();
829        unsafe { (descriptor.free)(output) };
830        (status, text)
831    }
832
833    #[test]
834    fn a_descriptor_round_trips_through_the_c_abi() {
835        let (abi, vtable) = unsafe { read_descriptor(rich_plugin_entry()) }.unwrap();
836        assert_eq!(abi, *exports().abi());
837        assert_eq!(abi.capabilities[1].kind, CapabilityKind::FenceMarkup);
838        assert_eq!(
839            unsafe { call(&vtable, 0, "hi") },
840            (STATUS_OK, "HI".to_string())
841        );
842        assert_eq!(
843            unsafe { call(&vtable, 1, "hi") },
844            (STATUS_ERROR, "no".to_string())
845        );
846        let (status, message) = unsafe { call(&vtable, 2, "hi") };
847        assert_eq!(status, STATUS_ERROR);
848        assert!(message.contains("panicked"), "{message}");
849        let (status, message) = unsafe { call(&vtable, 9, "hi") };
850        assert_eq!(status, STATUS_ERROR);
851        assert!(message.contains("no capability"), "{message}");
852    }
853
854    #[test]
855    fn another_major_is_refused_before_the_rest_is_read() {
856        // Only the two version words are valid: reading further would be a
857        // bug the version check must prevent.
858        let words: [u32; 2] = [ABI_MAJOR + 1, 0];
859        let error = unsafe { read_descriptor(words.as_ptr().cast()) }.unwrap_err();
860        assert_eq!(
861            error,
862            AbiError::Incompatible {
863                major: ABI_MAJOR + 1,
864                minor: 0
865            }
866        );
867        assert!(error.to_string().contains("rebuild"));
868        assert!(unsafe { read_descriptor(std::ptr::null()) }.is_err());
869    }
870
871    #[test]
872    fn a_null_function_in_the_vtable_is_refused() {
873        // Accepting it would crash (or worse) on the first call.
874        for (call, free) in [(false, true), (true, false), (false, false)] {
875            let mut exported = Exported::new(exports(), __rich_plugin_call);
876            let vtable = &mut exported.descriptor.vtable;
877            if !call {
878                vtable.call = None;
879            }
880            if !free {
881                vtable.free = None;
882            }
883            let error = unsafe { read_descriptor(&exported.descriptor) }.unwrap_err();
884            assert!(error.to_string().contains("null function"), "{error}");
885        }
886    }
887
888    #[test]
889    fn a_newer_minor_loads_but_an_unknown_kind_does_not() {
890        let mut exported = Exported::new(exports(), __rich_plugin_call);
891        exported.descriptor.abi_minor = ABI_MINOR + 3;
892        let (abi, _) = unsafe { read_descriptor(&exported.descriptor) }.unwrap();
893        assert_eq!(abi.abi_minor, ABI_MINOR + 3);
894        let entries = [AbiCapabilityEntry {
895            kind: 99,
896            name: AbiStr::new("x"),
897        }];
898        exported.descriptor.capabilities = entries.as_ptr();
899        exported.descriptor.capability_count = 1;
900        let error = unsafe { read_descriptor(&exported.descriptor) }.unwrap_err();
901        assert!(error.to_string().contains("unknown capability kind 99"));
902    }
903
904    #[test]
905    fn manifests_round_trip_and_are_checked() {
906        let abi = exports().abi().clone();
907        let manifest = abi.to_manifest();
908        assert!(manifest.starts_with("rich-plugin-abi 1.0\nname shout\n"));
909        assert_eq!(PluginAbi::parse_manifest(&manifest).unwrap(), abi);
910        // Unknown keys are ignored; unknown kinds and other majors are not.
911        let extra = manifest.replace("name shout", "name shout\nhomepage x");
912        assert_eq!(PluginAbi::parse_manifest(&extra).unwrap(), abi);
913        let other = manifest.replace("rich-plugin-abi 1.0", "rich-plugin-abi 2.0");
914        assert_eq!(
915            PluginAbi::parse_manifest(&other),
916            Err(AbiError::Incompatible { major: 2, minor: 0 })
917        );
918        for bad in [
919            "",
920            "hello",
921            "rich-plugin-abi one",
922            "rich-plugin-abi 1.0\nname Bad\nversion 1",
923            "rich-plugin-abi 1.0\nname ok\n",
924            "rich-plugin-abi 1.0\nname ok\nversion 1\ncapability sing x",
925            "rich-plugin-abi 1.0\nname ok\nversion 1\ncapability transform X",
926            "rich-plugin-abi 1.0\nname ok\nversion 1\ncapability transform x\ncapability transform x",
927        ] {
928            assert!(PluginAbi::parse_manifest(bad).is_err(), "{bad:?}");
929        }
930    }
931}