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