Files
photocraft/crates/plugins/src/lib.rs
T
7a5c1930b3 Sandboxed WebAssembly plug-ins: photocraft-plugins (wasmi), plugin.* commands, Filter › Plug-ins (#67)
* photocraft-plugins: sandboxed WebAssembly filter plug-ins (wasmi host, ABI v1)

A new L4 crate runs plug-ins in wasmi (pure Rust, MIT/Apache) with no host
imports, a fuel budget metered in slices with a wall-clock deadline, a memory
cap, a recursion cap and wasmi's strict module limits. Every band gets a fresh
instance; traps, budgets, bad manifests and bad pointers are all Errors.
Pixels go over as interleaved normalised f32 in tile-aligned bands, in
parallel on native, blended by the selection. Includes the example Invert
plug-in (no_std Rust, examples/plugins/invert-rs) built into a test fixture,
hostile-module tests, and docs/plugins.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: plugin.* commands (install, list, run, remove, reload)

plugin.run filters the active pixel layer, a targeted channel or a smart
object (recorded as a smart filter) as one undo step; Preferences >
Plug-ins gains Use Additional Plug-ins Folder, loaded when prefs apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* UI: Filter > Plug-ins lists installed plug-ins with generated dialogs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Plug-ins: bands never outgrow the memory cap; regenerate parity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bench: example plug-in Invert vs built-in Invert on 24 MP

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:08:28 -04:00

56 lines
2.2 KiB
Rust

//! Sandboxed WebAssembly plug-ins for PhotoCraft.
//!
//! A plug-in is a WebAssembly module that implements the PhotoCraft plug-in ABI (v1, documented
//! in `docs/plugins.md`). It runs in [`wasmi`], a pure-Rust interpreter, with:
//!
//! - **no host imports**: no WASI, file system, network, clock or randomness. A module that
//! imports anything is rejected;
//! - an **instruction budget** (fuel) per call, metered in slices so a wall-clock deadline is
//! also enforced (native builds), so infinite loops end with an error;
//! - a **linear-memory cap**, a **recursion-depth cap** and wasmi's strict module limits;
//! - a **fresh instance per band**, so no state leaks between calls.
//!
//! Every failure (malformed module, trap, exhausted budget, bad output) is an [`Error`]; nothing
//! a module does can panic or hang the host.
//!
//! Pixels are handed over as interleaved, normalised `f32` samples in the document's own colour
//! model (bit-depth and colour-mode agnostic), in horizontal bands so the copy stays bounded.
//! [`Plugin::apply`] runs a filter on a [`photocraft_raster::Surface`], blending by the selection.
//! The process-wide [`registry`] holds the installed plug-ins.
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod apply;
pub mod manifest;
pub mod registry;
mod runtime;
pub use apply::format_code;
pub use manifest::{Area, Kind, Manifest, ParamSpec};
pub use runtime::{ABI_VERSION, Limits, Plugin};
/// A plug-in error. Every way a module can misbehave ends here, never in a panic.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("not a valid plug-in module: {0}")]
Module(String),
#[error("plug-in ABI: {0}")]
Abi(String),
#[error("plug-in manifest: {0}")]
Manifest(String),
#[error("plug-in trapped: {0}")]
Trap(String),
#[error("plug-in exceeded its {0}")]
Limit(String),
#[error("plug-in parameters: {0}")]
Params(String),
#[error("plug-in failed: {0}")]
Failed(String),
#[error("no plug-in {0:?} is installed")]
NotFound(String),
#[error("{0}")]
Io(String),
}
pub type Result<T> = std::result::Result<T, Error>;