diff --git a/crates/plugins/src/abi/native.rs b/crates/plugins/src/abi/native.rs index 2885775b..89bbc3cb 100644 --- a/crates/plugins/src/abi/native.rs +++ b/crates/plugins/src/abi/native.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use once_cell::sync::Lazy; -use crate::{abi::fornjot_plugin_init, ModelConstructor}; +use crate::{abi::fornjot_plugin_init, Model}; static HOST: Lazy = Lazy::new(|| { let mut host = Host::default(); @@ -33,30 +33,21 @@ static HOST: Lazy = Lazy::new(|| { #[no_mangle] pub extern "C" fn model(args: &HashMap) -> fj::Shape { let ctx = crate::abi::Context(args); - let model = (HOST.constructor)(&ctx).expect("Unable to initialize the model"); + let model = HOST + .models + .first() + .expect("No models were registered inside the register_plugin!() initializer"); - model.shape() + model.shape(&ctx).expect("Unable to generate the shape") } +#[derive(Default)] struct Host { - constructor: ModelConstructor, -} - -impl Default for Host { - fn default() -> Self { - Self { - constructor: Box::new(|_| { - Err( - "No model registered. Did you forget to call the register_plugin!() macro?" - .into(), - ) - }), - } - } + models: Vec>, } impl crate::Host for Host { - fn register_model_constructor(&mut self, constructor: ModelConstructor) { - self.constructor = constructor; + fn register_boxed_model(&mut self, model: Box) { + self.models.push(model); } } diff --git a/crates/plugins/src/abi/wasm.rs b/crates/plugins/src/abi/wasm.rs index 275872b3..99ff49a7 100644 --- a/crates/plugins/src/abi/wasm.rs +++ b/crates/plugins/src/abi/wasm.rs @@ -1,8 +1,9 @@ -use crate::{ModelConstructor, PluginMetadata}; +use crate::{ArgumentMetadata, Context, ModelMetadata, PluginMetadata}; use chrono::{DateTime, NaiveDateTime, Utc}; use std::{ fmt, io::{self, Write}, + sync::Arc, }; use tracing_subscriber::fmt::{format::Writer, time::FormatTime}; use wit_bindgen_rust::Handle; @@ -24,36 +25,20 @@ impl guest::Guest for Guest { let metadata = unsafe { crate::abi::fornjot_plugin_init(&mut host)? }; - let Host { constructor } = host; - let model_constructor = match constructor { - Some(c) => c, - None => { - let err = crate::Error::from( - "No model registered. Did you forget to call the register_plugin!() macro?", - ); - return Err(err.into()); - } - }; + let Host { models } = host; - Ok(Handle::new(Plugin { - model_constructor, - metadata, - })) + Ok(Handle::new(Plugin { models, metadata })) } } pub struct Plugin { - model_constructor: ModelConstructor, + models: Vec>, metadata: PluginMetadata, } impl guest::Plugin for Plugin { - fn load_model(&self, args: Vec<(String, String)>) -> Result, guest::Error> { - let args = args.into_iter().collect(); - let ctx = crate::abi::Context(&args); - let model = (self.model_constructor)(&ctx)?; - - Ok(Handle::new(Model(model))) + fn models(&self) -> Vec> { + self.models.clone() } fn metadata(&self) -> guest::PluginMetadata { @@ -63,20 +48,28 @@ impl guest::Plugin for Plugin { #[derive(Default)] struct Host { - constructor: Option, + models: Vec>, } impl crate::Host for Host { - fn register_model_constructor(&mut self, constructor: ModelConstructor) { - self.constructor = Some(constructor); + fn register_boxed_model(&mut self, model: Box) { + let model = Model(Arc::from(model)); + self.models.push(Handle::new(model)); } } -pub struct Model(Box); +pub struct Model(Arc); impl guest::Model for Model { - fn shape(&self) -> guest::Shape { - self.0.shape().into() + fn metadata(&self) -> guest::ModelMetadata { + self.0.metadata().into() + } + + fn shape(&self, args: Vec<(String, String)>) -> Result { + let args = args.into_iter().collect(); + let ctx = crate::abi::Context(&args); + + self.0.shape(&ctx).map(Into::into).map_err(Into::into) } } @@ -229,3 +222,33 @@ impl From for guest::PluginMetadata { } } } + +impl From for guest::ModelMetadata { + fn from(m: ModelMetadata) -> guest::ModelMetadata { + let ModelMetadata { + name, + description, + arguments, + } = m; + guest::ModelMetadata { + name, + description, + arguments: arguments.into_iter().map(Into::into).collect(), + } + } +} + +impl From for guest::ArgumentMetadata { + fn from(m: ArgumentMetadata) -> guest::ArgumentMetadata { + let ArgumentMetadata { + name, + description, + default_value, + } = m; + guest::ArgumentMetadata { + name, + description, + default_value, + } + } +} diff --git a/crates/plugins/src/host.rs b/crates/plugins/src/host.rs index 70606938..dbb2bcef 100644 --- a/crates/plugins/src/host.rs +++ b/crates/plugins/src/host.rs @@ -1,8 +1,4 @@ -use crate::{model::ModelFromContext, Error, Model}; - -/// A type-erased function that is called to construct a [`Model`]. -pub type ModelConstructor = - Box Result, Error> + Send + Sync>; +use crate::Model; /// An abstract interface to the Fornjot host. pub trait Host { @@ -11,37 +7,34 @@ pub trait Host { /// This is mainly for more advanced use cases (e.g. when you need to close /// over extra state to load the model). For simpler models, you probably /// want to use [`HostExt::register_model()`] instead. - fn register_model_constructor(&mut self, constructor: ModelConstructor); + fn register_boxed_model(&mut self, model: Box); } impl Host for &'_ mut H { - fn register_model_constructor(&mut self, constructor: ModelConstructor) { - (*self).register_model_constructor(constructor); + fn register_boxed_model(&mut self, model: Box) { + (*self).register_boxed_model(model); } } impl Host for Box { - fn register_model_constructor(&mut self, constructor: ModelConstructor) { - (**self).register_model_constructor(constructor); + fn register_boxed_model(&mut self, model: Box) { + (**self).register_boxed_model(model); } } /// Extension methods to augment the [`Host`] API. pub trait HostExt { /// Register a model with the Fornjot runtime. - fn register_model(&mut self) + fn register_model(&mut self, model: M) where - M: Model + ModelFromContext + 'static; + M: Model + 'static; } impl HostExt for H { - fn register_model(&mut self) + fn register_model(&mut self, model: M) where - M: Model + ModelFromContext + 'static, + M: Model + 'static, { - self.register_model_constructor(Box::new(|ctx| { - let model = M::from_context(ctx)?; - Ok(Box::new(model)) - })) + self.register_boxed_model(Box::new(model)); } } diff --git a/crates/plugins/src/lib.rs b/crates/plugins/src/lib.rs index 45854818..e1801724 100644 --- a/crates/plugins/src/lib.rs +++ b/crates/plugins/src/lib.rs @@ -3,26 +3,32 @@ //! The typical workflow is to first define a [`Model`] type. This comes with //! two main methods, //! -//! 1. Use the [`ModelFromContext`] trait to make the model loadable from the -//! host context, and -//! 2. Calculate the model's shape with the [`Model`] trait +//! 1. Getting metadata about the model, and +//! 2. Calculating the model's geometry using arguments from the host context //! //! ```rust -//! use fj_plugins::{Model, Context, Error, ModelFromContext}; +//! use fj_plugins::{Model, Context, ContextExt, Error, ModelMetadata, ArgumentMetadata}; +//! use fj::{Shape, Circle, Sketch, syntax::*}; //! -//! struct MyModel; +//! struct Cylinder; //! -//! impl ModelFromContext for MyModel { -//! fn from_context(ctx: &dyn Context) -> Result -//! where -//! Self: Sized, -//! { -//! todo!("Load arguments from the context and initialize the model"); +//! impl Model for Cylinder { +//! fn metadata(&self) -> ModelMetadata { +//! ModelMetadata::new("Cylinder") +//! .with_argument("radius") +//! .with_argument( +//! ArgumentMetadata::new("height") +//! .with_default_value("10.0"), +//! ) //! } -//! } //! -//! impl Model for MyModel { -//! fn shape(&self) -> fj::Shape { todo!("Calcualte the model's geometry") } +//! fn shape(&self, ctx: &dyn Context) -> Result +//! { +//! let radius = ctx.parse_argument("radius")?; +//! let height = ctx.parse_optional_argument("height")?.unwrap_or(10.0); +//! let circle = Circle::from_radius(radius); +//! Ok(Sketch::from_circle(circle).sweep([height, 0.0, 0.0]).into()) +//! } //! } //! ``` //! @@ -32,22 +38,20 @@ //! //! ```rust //! use fj_plugins::{Host, HostExt, PluginMetadata}; -//! # use fj_plugins::{Model, Context, Error, ModelFromContext}; +//! # use fj_plugins::{Model, Context, Error, ModelMetadata}; //! //! fj_plugins::register_plugin!(|host: &mut dyn Host| { -//! host.register_model::(); +//! host.register_model(Cylinder); //! //! Ok(PluginMetadata::new( //! env!("CARGO_PKG_NAME"), //! env!("CARGO_PKG_VERSION"), //! )) //! }); -//! # struct MyModel; -//! # impl Model for MyModel { -//! # fn shape(&self) -> fj::Shape { todo!("Calcualte the model's geometry") } -//! # } -//! # impl ModelFromContext for MyModel { -//! # fn from_context(ctx: &dyn Context) -> Result where Self: Sized { todo!(); } +//! # struct Cylinder; +//! # impl Model for Cylinder { +//! # fn metadata(&self) -> ModelMetadata { unimplemented!() } +//! # fn shape(&self, ctx: &dyn Context) -> Result { unimplemented!() } //! # } //! ``` @@ -63,9 +67,9 @@ mod metadata; mod model; pub use crate::{ - host::{Host, HostExt, ModelConstructor}, - metadata::PluginMetadata, - model::{Context, ContextExt, MissingArgument, Model, ModelFromContext}, + host::{Host, HostExt}, + metadata::{ArgumentMetadata, ModelMetadata, PluginMetadata}, + model::{Context, ContextExt, MissingArgument, Model}, }; /// The common error type used by this crate. diff --git a/crates/plugins/src/metadata.rs b/crates/plugins/src/metadata.rs index 40a9edd7..da0f186c 100644 --- a/crates/plugins/src/metadata.rs +++ b/crates/plugins/src/metadata.rs @@ -86,3 +86,67 @@ impl PluginMetadata { } } } + +#[derive(Debug, Clone, PartialEq)] +pub struct ModelMetadata { + pub name: String, + pub description: Option, + pub arguments: Vec, +} + +impl ModelMetadata { + pub fn new(name: impl Into) -> Self { + ModelMetadata { + name: name.into(), + description: None, + arguments: Vec::new(), + } + } + + pub fn with_description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn with_argument(mut self, arg: impl Into) -> Self { + self.arguments.push(arg.into()); + self + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ArgumentMetadata { + /// The name used to refer to this argument. + pub name: String, + /// A short description of this argument that could be shown to the user + /// in something like a tooltip. + pub description: Option, + /// Something that could be used as a default if no value was provided. + pub default_value: Option, +} + +impl ArgumentMetadata { + pub fn new(name: impl Into) -> Self { + ArgumentMetadata { + name: name.into(), + description: None, + default_value: None, + } + } + + pub fn with_description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn with_default_value(mut self, default_value: impl Into) -> Self { + self.default_value = Some(default_value.into()); + self + } +} + +impl From<&str> for ArgumentMetadata { + fn from(name: &str) -> Self { + ArgumentMetadata::new(name) + } +} diff --git a/crates/plugins/src/model.rs b/crates/plugins/src/model.rs index 8dd43ea1..323c383a 100644 --- a/crates/plugins/src/model.rs +++ b/crates/plugins/src/model.rs @@ -1,18 +1,14 @@ use std::{collections::HashMap, str::FromStr}; -use crate::Error; +use crate::{Error, ModelMetadata}; /// A model. -pub trait Model { +pub trait Model: Send + Sync { /// Calculate this model's concrete geometry. - fn shape(&self) -> fj::Shape; -} + fn shape(&self, ctx: &dyn Context) -> Result; -/// A [`Model`] that can be loaded purely from the [`Context`]. -pub trait ModelFromContext: Sized { - /// Try to initialize this [`Model`] using contextual information it has - /// been provided. - fn from_context(ctx: &dyn Context) -> Result; + /// Get metadata for the model. + fn metadata(&self) -> ModelMetadata; } /// Contextual information passed to a [`Model`] when it is being initialized. diff --git a/crates/wasm-shim/src/lib.rs b/crates/wasm-shim/src/lib.rs index 8f0b1527..30579477 100644 --- a/crates/wasm-shim/src/lib.rs +++ b/crates/wasm-shim/src/lib.rs @@ -7,8 +7,8 @@ use std::{ time::SystemTime, }; -use anyhow::Context; -use fj_plugins::{Model, PluginMetadata}; +use anyhow::Context as _; +use fj_plugins::{ArgumentMetadata, Context, HostExt, Model, ModelMetadata, PluginMetadata}; use wasmtime::{Engine, Linker, Module, Store}; wit_bindgen_wasmtime::import!("../../wit-files/guest.wit"); @@ -44,38 +44,20 @@ fn init(host: &mut dyn fj_plugins::Host) -> Result = ctx - .arguments() - .iter() - .map(|(k, v)| (k.as_str(), v.as_str())) - .collect(); - - // HACK: It looks like wit-bindgen will unconditionally try to - // deallocate zero-length arrays, which corrupts the allocator because - // zero-length arrays point to garbage (null + align_of). To avoid - // this, we make sure there's at least 1 argument in the list. - args.push(("", "")); - - let model = guest - .plugin_load_model(&mut *store.lock().unwrap(), &plugin, &args) - .context("Calling into WebAssembly triggered a trap")? - .context("Unable to load the model")?; - - Ok(Box::new(WebAssemblyModel { + for model in guest.plugin_models(&mut *store.lock().unwrap(), &plugin)? { + let model = WebAssemblyModel { guest: Arc::clone(&guest), store: Arc::clone(&store), model, - })) - })); + }; + host.register_model(model); + } Ok(PluginMetadata::new( env!("CARGO_PKG_NAME"), @@ -90,17 +72,44 @@ struct WebAssemblyModel { } impl Model for WebAssemblyModel { - fn shape(&self) -> fj::Shape { + fn metadata(&self) -> fj_plugins::ModelMetadata { + let WebAssemblyModel { + guest, + store, + model, + } = self; + + guest + .model_metadata(&mut *store.lock().unwrap(), model) + .expect("Call to WebAssembly failed") + .into() + } + + fn shape(&self, ctx: &dyn Context) -> Result { let WebAssemblyModel { guest, store, model, } = self; let mut store = store.lock().unwrap(); + + let mut args: Vec<_> = ctx + .arguments() + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect(); + + // HACK: It looks like wit-bindgen will unconditionally try to + // deallocate zero-length arrays, which corrupts the allocator because + // zero-length arrays point to garbage (null + align_of). To avoid + // this, we make sure there's at least 1 argument in the list. + args.push(("", "")); + guest - .model_shape(&mut *store, model) + .model_shape(&mut *store, model, &args) .expect("Calling the model's shape function raised a trap") - .into() + .map(Into::into) + .map_err(Into::into) } } @@ -202,3 +211,33 @@ impl From for Vec<[f64; 2]> { p.points.into_iter().map(|(x, y)| [x, y]).collect() } } + +impl From for ModelMetadata { + fn from(m: guest::ModelMetadata) -> ModelMetadata { + let guest::ModelMetadata { + name, + description, + arguments, + } = m; + ModelMetadata { + name, + description, + arguments: arguments.into_iter().map(Into::into).collect(), + } + } +} + +impl From for ArgumentMetadata { + fn from(m: guest::ArgumentMetadata) -> ArgumentMetadata { + let guest::ArgumentMetadata { + name, + description, + default_value, + } = m; + ArgumentMetadata { + name, + description, + default_value, + } + } +} diff --git a/models/cuboid/src/lib.rs b/models/cuboid/src/lib.rs index 0eccb954..ecf4f8da 100644 --- a/models/cuboid/src/lib.rs +++ b/models/cuboid/src/lib.rs @@ -1,10 +1,12 @@ -use fj_plugins::{Context, ContextExt, HostExt, Model, ModelFromContext, PluginMetadata}; +use fj_plugins::{ + ArgumentMetadata, Context, ContextExt, HostExt, Model, ModelMetadata, PluginMetadata, +}; // TODO: replace this with a custom attribute. fj_plugins::register_plugin!(|host| { let _span = tracing::info_span!("init").entered(); - host.register_model::(); + host.register_model(Cuboid); tracing::info!("Registered cuboid"); Ok( @@ -18,31 +20,23 @@ fj_plugins::register_plugin!(|host| { }); #[derive(Debug, Clone, PartialEq)] -pub struct Cuboid { - x: f64, - y: f64, - z: f64, -} +pub struct Cuboid; -impl ModelFromContext for Cuboid { - fn from_context(ctx: &dyn Context) -> Result - where - Self: Sized, - { +impl Model for Cuboid { + fn metadata(&self) -> ModelMetadata { + ModelMetadata::new("Cuboid") + .with_argument(ArgumentMetadata::new("x").with_default_value("3.0")) + .with_argument(ArgumentMetadata::new("y").with_default_value("2.0")) + .with_argument(ArgumentMetadata::new("z").with_default_value("1.0")) + } + + #[tracing::instrument(skip_all)] + fn shape(&self, ctx: &dyn Context) -> Result { let x: f64 = ctx.parse_optional_argument("x")?.unwrap_or(3.0); let y: f64 = ctx.parse_optional_argument("y")?.unwrap_or(2.0); let z: f64 = ctx.parse_optional_argument("z")?.unwrap_or(1.0); tracing::debug!(x, y, z, "Creating a cuboid model"); - Ok(Cuboid { x, y, z }) - } -} - -impl Model for Cuboid { - #[tracing::instrument] - fn shape(&self) -> fj::Shape { - let Cuboid { x, y, z } = *self; - let rectangle = fj::Sketch::from_points(vec![ [-x / 2., -y / 2.], [x / 2., -y / 2.], @@ -53,6 +47,6 @@ impl Model for Cuboid { let cuboid = fj::Sweep::from_path(rectangle.into(), [0., 0., z]); - cuboid.into() + Ok(cuboid.into()) } } diff --git a/wit-files/guest.wit b/wit-files/guest.wit index 86c0c6dc..9f1e5fda 100644 --- a/wit-files/guest.wit +++ b/wit-files/guest.wit @@ -1,7 +1,20 @@ use { arguments, shape, error } from common +record argument-metadata { + name: string, + description: option, + default-value: option, +} + +record model-metadata { + name: string, + description: option, + arguments: list, +} + resource model { - shape: func() -> shape + metadata: func() -> model-metadata + shape: func(args: arguments) -> expected } record plugin-metadata { @@ -16,7 +29,7 @@ record plugin-metadata { resource plugin { metadata: func() -> plugin-metadata - load-model: func(args: arguments) -> expected + models: func() -> list } init: func() -> expected