#![allow(unsafe_code)]
use std::fmt;
use std::mem::ManuallyDrop;
use std::panic::{catch_unwind, AssertUnwindSafe};
use crate::is_valid_name;
pub const ABI_MAJOR: u32 = 1;
pub const ABI_MINOR: u32 = 0;
pub const DYLIB_ENTRY_SYMBOL: &str = "rich_plugin_entry";
pub const MAX_CAPABILITIES: usize = 64;
pub const MAX_FIELD_LEN: usize = 1024;
pub const STATUS_OK: u32 = 0;
pub const STATUS_ERROR: u32 = 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum CapabilityKind {
Transform,
Highlighter,
FenceMarkup,
FenceAnsi,
}
impl CapabilityKind {
pub const ALL: [CapabilityKind; 4] = [
CapabilityKind::Transform,
CapabilityKind::Highlighter,
CapabilityKind::FenceMarkup,
CapabilityKind::FenceAnsi,
];
pub const fn code(self) -> u32 {
match self {
CapabilityKind::Transform => 1,
CapabilityKind::Highlighter => 2,
CapabilityKind::FenceMarkup => 3,
CapabilityKind::FenceAnsi => 4,
}
}
pub fn from_code(code: u32) -> Option<Self> {
Self::ALL.into_iter().find(|kind| kind.code() == code)
}
pub const fn as_str(self) -> &'static str {
match self {
CapabilityKind::Transform => "transform",
CapabilityKind::Highlighter => "highlighter",
CapabilityKind::FenceMarkup => "fence-markup",
CapabilityKind::FenceAnsi => "fence-ansi",
}
}
pub fn parse(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|kind| kind.as_str() == name)
}
pub const fn produces_ansi(self) -> bool {
matches!(self, CapabilityKind::FenceAnsi)
}
}
impl fmt::Display for CapabilityKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AbiCapability {
pub kind: CapabilityKind,
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginAbi {
pub abi_major: u32,
pub abi_minor: u32,
pub name: String,
pub version: String,
pub description: String,
pub capabilities: Vec<AbiCapability>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AbiError {
Incompatible { major: u32, minor: u32 },
Invalid(String),
}
impl fmt::Display for AbiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AbiError::Incompatible { major, minor } => write!(
f,
"built for plugin ABI {major}.{minor}, but this host supports ABI \
{ABI_MAJOR}.x; rebuild it against rs-rich-plugin-api with ABI {ABI_MAJOR}"
),
AbiError::Invalid(message) => f.write_str(message),
}
}
}
impl std::error::Error for AbiError {}
const MANIFEST_MAGIC: &str = "rich-plugin-abi";
impl PluginAbi {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
PluginAbi {
abi_major: ABI_MAJOR,
abi_minor: ABI_MINOR,
name: name.into(),
version: version.into(),
description: String::new(),
capabilities: Vec::new(),
}
}
pub fn validate(&self) -> Result<(), AbiError> {
if self.abi_major != ABI_MAJOR {
return Err(AbiError::Incompatible {
major: self.abi_major,
minor: self.abi_minor,
});
}
if !is_valid_name(&self.name) {
return Err(AbiError::Invalid(format!(
"invalid plugin name {:?}: use lowercase letters, digits, '-', '_' and '.'",
self.name
)));
}
for (field, value) in [
("version", &self.version),
("description", &self.description),
] {
if value.len() > MAX_FIELD_LEN || value.chars().any(char::is_control) {
return Err(AbiError::Invalid(format!(
"the {field} must be one line of at most {MAX_FIELD_LEN} bytes"
)));
}
}
if self.version.is_empty() {
return Err(AbiError::Invalid("the version is empty".into()));
}
if self.capabilities.len() > MAX_CAPABILITIES {
return Err(AbiError::Invalid(format!(
"{} capabilities declared; at most {MAX_CAPABILITIES} are allowed",
self.capabilities.len()
)));
}
for (i, capability) in self.capabilities.iter().enumerate() {
if !is_valid_name(&capability.name) {
return Err(AbiError::Invalid(format!(
"invalid {} name {:?}",
capability.kind, capability.name
)));
}
if self.capabilities[..i].contains(capability) {
return Err(AbiError::Invalid(format!(
"{} {:?} is declared twice",
capability.kind, capability.name
)));
}
}
Ok(())
}
pub fn to_manifest(&self) -> String {
let mut out = format!(
"{MANIFEST_MAGIC} {}.{}\nname {}\nversion {}\n",
self.abi_major, self.abi_minor, self.name, self.version
);
if !self.description.is_empty() {
out.push_str(&format!("description {}\n", self.description));
}
for capability in &self.capabilities {
out.push_str(&format!(
"capability {} {}\n",
capability.kind, capability.name
));
}
out
}
pub fn parse_manifest(text: &str) -> Result<PluginAbi, AbiError> {
let mut lines = text.lines();
let first = lines.next().unwrap_or_default();
let version = first
.strip_prefix(MANIFEST_MAGIC)
.and_then(|rest| rest.strip_prefix(' '))
.ok_or_else(|| {
AbiError::Invalid(format!(
"the manifest must start with `{MANIFEST_MAGIC} MAJOR.MINOR`"
))
})?;
let (major, minor) = version
.split_once('.')
.and_then(|(major, minor)| Some((major.parse().ok()?, minor.parse().ok()?)))
.ok_or_else(|| AbiError::Invalid(format!("invalid ABI version {version:?}")))?;
if major != ABI_MAJOR {
return Err(AbiError::Incompatible { major, minor });
}
let mut abi = PluginAbi {
abi_major: major,
abi_minor: minor,
name: String::new(),
version: String::new(),
description: String::new(),
capabilities: Vec::new(),
};
for line in lines {
if line.trim().is_empty() {
continue;
}
let (key, value) = line.split_once(' ').unwrap_or((line, ""));
match key {
"name" => abi.name = value.to_string(),
"version" => abi.version = value.to_string(),
"description" => abi.description = value.to_string(),
"capability" => {
let (kind, name) = value.split_once(' ').ok_or_else(|| {
AbiError::Invalid(format!("expected `capability KIND NAME`: {line:?}"))
})?;
let kind = CapabilityKind::parse(kind).ok_or_else(|| {
AbiError::Invalid(format!(
"unknown capability kind {kind:?}; this host knows {}",
CapabilityKind::ALL.map(CapabilityKind::as_str).join(", ")
))
})?;
abi.capabilities.push(AbiCapability {
kind,
name: name.to_string(),
});
}
_ => {}
}
}
abi.validate()?;
Ok(abi)
}
}
pub mod wasm {
pub const MEMORY: &str = "memory";
pub const ALLOC: &str = "rich_plugin_alloc";
pub const MANIFEST: &str = "rich_plugin_manifest";
pub const CALL: &str = "rich_plugin_call";
pub const ERROR_BIT: u64 = 1 << 63;
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct AbiStr {
pub ptr: *const u8,
pub len: usize,
}
impl AbiStr {
pub fn new(text: &str) -> Self {
AbiStr {
ptr: text.as_ptr(),
len: text.len(),
}
}
pub unsafe fn as_str<'a>(self) -> Result<&'a str, AbiError> {
if self.ptr.is_null() {
return if self.len == 0 {
Ok("")
} else {
Err(AbiError::Invalid("a null string with a length".into()))
};
}
let bytes = unsafe { std::slice::from_raw_parts(self.ptr, self.len) };
std::str::from_utf8(bytes).map_err(|_| AbiError::Invalid("a string is not UTF-8".into()))
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct AbiCapabilityEntry {
pub kind: u32,
pub name: AbiStr,
}
#[repr(C)]
#[derive(Debug)]
pub struct AbiOutput {
pub ptr: *mut u8,
pub len: usize,
pub cap: usize,
}
impl AbiOutput {
pub const fn empty() -> Self {
AbiOutput {
ptr: std::ptr::null_mut(),
len: 0,
cap: 0,
}
}
pub fn from_string(text: String) -> Self {
let mut bytes = ManuallyDrop::new(text.into_bytes());
AbiOutput {
ptr: bytes.as_mut_ptr(),
len: bytes.len(),
cap: bytes.capacity(),
}
}
}
pub type AbiCallFn = unsafe extern "C" fn(
capability: usize,
input: AbiStr,
width: u32,
output: *mut AbiOutput,
) -> u32;
pub type AbiFreeFn = unsafe extern "C" fn(output: AbiOutput);
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct PluginVTable {
pub call: Option<AbiCallFn>,
pub free: Option<AbiFreeFn>,
}
#[derive(Clone, Copy, Debug)]
pub struct PluginFunctions {
pub call: AbiCallFn,
pub free: AbiFreeFn,
}
#[repr(C)]
#[derive(Debug)]
pub struct PluginDescriptor {
pub abi_major: u32,
pub abi_minor: u32,
pub name: AbiStr,
pub version: AbiStr,
pub description: AbiStr,
pub capabilities: *const AbiCapabilityEntry,
pub capability_count: usize,
pub vtable: PluginVTable,
}
pub unsafe fn read_descriptor(
descriptor: *const PluginDescriptor,
) -> Result<(PluginAbi, PluginFunctions), AbiError> {
if descriptor.is_null() {
return Err(AbiError::Invalid(format!(
"{DYLIB_ENTRY_SYMBOL} returned no descriptor (it failed to initialise)"
)));
}
let (major, minor) = unsafe {
let version = descriptor.cast::<u32>();
(version.read(), version.add(1).read())
};
if major != ABI_MAJOR {
return Err(AbiError::Incompatible { major, minor });
}
let descriptor = unsafe { &*descriptor };
if descriptor.capability_count > MAX_CAPABILITIES {
return Err(AbiError::Invalid(format!(
"{} capabilities declared; at most {MAX_CAPABILITIES} are allowed",
descriptor.capability_count
)));
}
let entries: &[AbiCapabilityEntry] = if descriptor.capability_count == 0 {
&[]
} else if descriptor.capabilities.is_null() {
return Err(AbiError::Invalid("a null capability list".into()));
} else {
unsafe { std::slice::from_raw_parts(descriptor.capabilities, descriptor.capability_count) }
};
let text = |s: AbiStr, what: &str| -> Result<String, AbiError> {
if s.len > MAX_FIELD_LEN {
return Err(AbiError::Invalid(format!("the {what} is too long")));
}
unsafe { s.as_str() }.map(str::to_string)
};
let mut capabilities = Vec::with_capacity(entries.len());
for entry in entries {
let kind = CapabilityKind::from_code(entry.kind).ok_or_else(|| {
AbiError::Invalid(format!(
"unknown capability kind {}; this host knows {}",
entry.kind,
CapabilityKind::ALL.map(CapabilityKind::as_str).join(", ")
))
})?;
capabilities.push(AbiCapability {
kind,
name: text(entry.name, "capability name")?,
});
}
let abi = PluginAbi {
abi_major: major,
abi_minor: minor,
name: text(descriptor.name, "name")?,
version: text(descriptor.version, "version")?,
description: text(descriptor.description, "description")?,
capabilities,
};
abi.validate()?;
let (Some(call), Some(free)) = (descriptor.vtable.call, descriptor.vtable.free) else {
return Err(AbiError::Invalid(
"a null function in the vtable (`call` and `free` are required)".into(),
));
};
Ok((abi, PluginFunctions { call, free }))
}
pub type ExportFn = fn(input: &str, width: u32) -> Result<String, String>;
pub struct Exports {
abi: PluginAbi,
functions: Vec<ExportFn>,
}
impl Exports {
pub fn new(name: &str, version: &str) -> Self {
Exports {
abi: PluginAbi::new(name, version),
functions: Vec::new(),
}
}
pub fn description(mut self, description: &str) -> Self {
self.abi.description = description.to_string();
self
}
pub fn capability(mut self, kind: CapabilityKind, name: &str, function: ExportFn) -> Self {
self.abi.capabilities.push(AbiCapability {
kind,
name: name.to_string(),
});
self.functions.push(function);
self
}
pub fn transform(self, name: &str, function: ExportFn) -> Self {
self.capability(CapabilityKind::Transform, name, function)
}
pub fn highlighter(self, name: &str, function: ExportFn) -> Self {
self.capability(CapabilityKind::Highlighter, name, function)
}
pub fn fence_markup(self, language: &str, function: ExportFn) -> Self {
self.capability(CapabilityKind::FenceMarkup, language, function)
}
pub fn fence_ansi(self, language: &str, function: ExportFn) -> Self {
self.capability(CapabilityKind::FenceAnsi, language, function)
}
pub fn abi(&self) -> &PluginAbi {
&self.abi
}
}
#[doc(hidden)]
pub struct Exported {
exports: Exports,
_entries: Vec<AbiCapabilityEntry>,
descriptor: PluginDescriptor,
}
unsafe impl Send for Exported {}
unsafe impl Sync for Exported {}
impl Exported {
pub fn new(exports: Exports, call: AbiCallFn) -> Self {
let entries: Vec<AbiCapabilityEntry> = exports
.abi
.capabilities
.iter()
.map(|capability| AbiCapabilityEntry {
kind: capability.kind.code(),
name: AbiStr::new(&capability.name),
})
.collect();
let descriptor = PluginDescriptor {
abi_major: exports.abi.abi_major,
abi_minor: exports.abi.abi_minor,
name: AbiStr::new(&exports.abi.name),
version: AbiStr::new(&exports.abi.version),
description: AbiStr::new(&exports.abi.description),
capabilities: entries.as_ptr(),
capability_count: entries.len(),
vtable: PluginVTable {
call: Some(call),
free: Some(free_output),
},
};
Exported {
exports,
_entries: entries,
descriptor,
}
}
pub fn descriptor(&'static self) -> *const PluginDescriptor {
&self.descriptor
}
pub unsafe fn call(
this: Option<&Exported>,
capability: usize,
input: AbiStr,
width: u32,
output: *mut AbiOutput,
) -> u32 {
let result = catch_unwind(AssertUnwindSafe(|| {
let this = this.ok_or("the plugin was called before it was initialised")?;
let function = this
.exports
.functions
.get(capability)
.ok_or_else(|| format!("no capability number {capability}"))?;
let input = unsafe { input.as_str() }.map_err(|e| e.to_string())?;
function(input, width)
}));
let (status, text) = match result {
Ok(Ok(text)) => (STATUS_OK, text),
Ok(Err(message)) => (STATUS_ERROR, message),
Err(_) => (STATUS_ERROR, "the plugin panicked".to_string()),
};
if output.is_null() {
return STATUS_ERROR;
}
unsafe { output.write(AbiOutput::from_string(text)) };
status
}
pub fn entry(
slot: &'static std::sync::OnceLock<Exported>,
make: fn() -> Exports,
call: AbiCallFn,
) -> *const PluginDescriptor {
match catch_unwind(AssertUnwindSafe(|| {
slot.get_or_init(|| Exported::new(make(), call))
})) {
Ok(exported) => exported.descriptor(),
Err(_) => std::ptr::null(),
}
}
}
pub unsafe extern "C" fn free_output(output: AbiOutput) {
if !output.ptr.is_null() {
drop(unsafe { Vec::from_raw_parts(output.ptr, output.len, output.cap) });
}
}
#[macro_export]
macro_rules! export_dylib_plugin {
($exports:expr) => {
#[doc(hidden)]
static __RICH_PLUGIN_EXPORTED: ::std::sync::OnceLock<$crate::abi::Exported> =
::std::sync::OnceLock::new();
#[doc(hidden)]
#[allow(unsafe_code)]
unsafe extern "C" fn __rich_plugin_call(
capability: usize,
input: $crate::abi::AbiStr,
width: u32,
output: *mut $crate::abi::AbiOutput,
) -> u32 {
unsafe {
$crate::abi::Exported::call(
__RICH_PLUGIN_EXPORTED.get(),
capability,
input,
width,
output,
)
}
}
#[allow(unsafe_code)]
#[unsafe(no_mangle)]
pub extern "C" fn rich_plugin_entry() -> *const $crate::abi::PluginDescriptor {
$crate::abi::Exported::entry(&__RICH_PLUGIN_EXPORTED, $exports, __rich_plugin_call)
}
};
}
#[cfg(test)]
mod tests {
use super::*;
fn upper(input: &str, _: u32) -> Result<String, String> {
Ok(input.to_uppercase())
}
fn fails(_: &str, _: u32) -> Result<String, String> {
Err("no".into())
}
fn panics(_: &str, _: u32) -> Result<String, String> {
panic!("boom")
}
fn exports() -> Exports {
Exports::new("shout", "0.1.0")
.description("Upper-cases text")
.transform("upper", upper)
.fence_markup("shout", fails)
.highlighter("boom", panics)
}
crate::export_dylib_plugin!(exports);
unsafe fn call(descriptor: &PluginFunctions, capability: usize, input: &str) -> (u32, String) {
let mut output = AbiOutput::empty();
let status = unsafe { (descriptor.call)(capability, AbiStr::new(input), 80, &mut output) };
let text = unsafe { std::slice::from_raw_parts(output.ptr, output.len) };
let text = String::from_utf8(text.to_vec()).unwrap();
unsafe { (descriptor.free)(output) };
(status, text)
}
#[test]
fn a_descriptor_round_trips_through_the_c_abi() {
let (abi, vtable) = unsafe { read_descriptor(rich_plugin_entry()) }.unwrap();
assert_eq!(abi, *exports().abi());
assert_eq!(abi.capabilities[1].kind, CapabilityKind::FenceMarkup);
assert_eq!(
unsafe { call(&vtable, 0, "hi") },
(STATUS_OK, "HI".to_string())
);
assert_eq!(
unsafe { call(&vtable, 1, "hi") },
(STATUS_ERROR, "no".to_string())
);
let (status, message) = unsafe { call(&vtable, 2, "hi") };
assert_eq!(status, STATUS_ERROR);
assert!(message.contains("panicked"), "{message}");
let (status, message) = unsafe { call(&vtable, 9, "hi") };
assert_eq!(status, STATUS_ERROR);
assert!(message.contains("no capability"), "{message}");
}
#[test]
fn another_major_is_refused_before_the_rest_is_read() {
let words: [u32; 2] = [ABI_MAJOR + 1, 0];
let error = unsafe { read_descriptor(words.as_ptr().cast()) }.unwrap_err();
assert_eq!(
error,
AbiError::Incompatible {
major: ABI_MAJOR + 1,
minor: 0
}
);
assert!(error.to_string().contains("rebuild"));
assert!(unsafe { read_descriptor(std::ptr::null()) }.is_err());
}
#[test]
fn a_null_function_in_the_vtable_is_refused() {
for (call, free) in [(false, true), (true, false), (false, false)] {
let mut exported = Exported::new(exports(), __rich_plugin_call);
let vtable = &mut exported.descriptor.vtable;
if !call {
vtable.call = None;
}
if !free {
vtable.free = None;
}
let error = unsafe { read_descriptor(&exported.descriptor) }.unwrap_err();
assert!(error.to_string().contains("null function"), "{error}");
}
}
#[test]
fn a_newer_minor_loads_but_an_unknown_kind_does_not() {
let mut exported = Exported::new(exports(), __rich_plugin_call);
exported.descriptor.abi_minor = ABI_MINOR + 3;
let (abi, _) = unsafe { read_descriptor(&exported.descriptor) }.unwrap();
assert_eq!(abi.abi_minor, ABI_MINOR + 3);
let entries = [AbiCapabilityEntry {
kind: 99,
name: AbiStr::new("x"),
}];
exported.descriptor.capabilities = entries.as_ptr();
exported.descriptor.capability_count = 1;
let error = unsafe { read_descriptor(&exported.descriptor) }.unwrap_err();
assert!(error.to_string().contains("unknown capability kind 99"));
}
#[test]
fn manifests_round_trip_and_are_checked() {
let abi = exports().abi().clone();
let manifest = abi.to_manifest();
assert!(manifest.starts_with("rich-plugin-abi 1.0\nname shout\n"));
assert_eq!(PluginAbi::parse_manifest(&manifest).unwrap(), abi);
let extra = manifest.replace("name shout", "name shout\nhomepage x");
assert_eq!(PluginAbi::parse_manifest(&extra).unwrap(), abi);
let other = manifest.replace("rich-plugin-abi 1.0", "rich-plugin-abi 2.0");
assert_eq!(
PluginAbi::parse_manifest(&other),
Err(AbiError::Incompatible { major: 2, minor: 0 })
);
for bad in [
"",
"hello",
"rich-plugin-abi one",
"rich-plugin-abi 1.0\nname Bad\nversion 1",
"rich-plugin-abi 1.0\nname ok\n",
"rich-plugin-abi 1.0\nname ok\nversion 1\ncapability sing x",
"rich-plugin-abi 1.0\nname ok\nversion 1\ncapability transform X",
"rich-plugin-abi 1.0\nname ok\nversion 1\ncapability transform x\ncapability transform x",
] {
assert!(PluginAbi::parse_manifest(bad).is_err(), "{bad:?}");
}
}
}