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