Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model driven api #885

Merged
merged 14 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions crates/fj-host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use std::{
thread,
};

use fj::abi;
use notify::Watcher as _;
use thiserror::Error;

Expand Down Expand Up @@ -131,8 +132,24 @@ impl Model {
// https://github.com/hannobraun/Fornjot/issues/71
let shape = unsafe {
let lib = libloading::Library::new(&self.lib_path)?;
let model: libloading::Symbol<ModelFn> = lib.get(b"model")?;
model(arguments)
let init: libloading::Symbol<abi::InitFunction> =
lib.get(abi::INIT_FUNCTION_NAME.as_bytes())?;

let mut host = Host {
args: arguments,
model: None,
};

match init(&mut abi::Host::from(&mut host)) {
abi::ffi_safe::Result::Ok(_metadata) => {}
abi::ffi_safe::Result::Err(e) => {
return Err(Error::InitializeModel(e.into()));
}
}

let model = host.model.take().ok_or(Error::NoModelRegistered)?;

model.shape(&host).map_err(Error::Shape)?
};

Ok(shape)
Expand Down Expand Up @@ -363,6 +380,19 @@ pub enum Error {
#[error("Error loading model from dynamic library")]
LibLoading(#[from] libloading::Error),

/// Initializing a model failed.
#[error("Unable to initialize the model")]
InitializeModel(#[source] Box<dyn std::error::Error + Send + Sync>),

/// The user forgot to register a model when calling
/// [`fj::register_model!()`].
#[error("No model was registered")]
NoModelRegistered,

/// An error was returned from [`fj::Model::shape()`].
#[error("Unable to determine the model's geometry")]
Shape(#[source] Box<dyn std::error::Error + Send + Sync>),

/// Error while watching the model code for changes
#[error("Error watching model for changes")]
Notify(#[from] notify::Error),
Expand Down Expand Up @@ -390,4 +420,19 @@ pub enum Error {
},
}

type ModelFn = unsafe extern "C" fn(args: &Parameters) -> fj::Shape;
struct Host<'a> {
args: &'a Parameters,
model: Option<Box<dyn fj::Model>>,
}

impl<'a> fj::Host for Host<'a> {
fn register_boxed_model(&mut self, model: Box<dyn fj::Model>) {
self.model = Some(model);
}
}

impl<'a> fj::Context for Host<'a> {
fn get_argument(&self, name: &str) -> Option<&str> {
self.args.get(name).map(|s| s.as_str())
}
}
185 changes: 185 additions & 0 deletions crates/fj-proc/src/expand.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};

use crate::parse::{
ArgumentMetadata, Constraint, ConstraintKind, ExtractedArgument,
GeometryFunction, Initializer, Metadata, Model,
};

impl Initializer {
fn register(&self) -> TokenStream {
quote! {
const _: () = {
fj::register_model!(|host| {
fj::HostExt::register_model(host, Model);

Ok(
fj::Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
.with_short_description(env!("CARGO_PKG_DESCRIPTION"))
.with_homepage(env!("CARGO_PKG_HOMEPAGE"))
.with_repository(env!("CARGO_PKG_REPOSITORY"))
.with_license(env!("CARGO_PKG_LICENSE")),
)
});
};
}
}
}

impl ToTokens for Initializer {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Initializer { model } = self;

tokens.extend(self.register());
model.to_tokens(tokens);
}
}

impl Model {
fn definition(&self) -> TokenStream {
quote! { struct Model; }
}

fn trait_implementation(&self) -> TokenStream {
let Model { metadata, geometry } = self;

quote! {
impl fj::Model for Model {
#metadata
#geometry
}
}
}
}

impl ToTokens for Model {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(self.definition());
tokens.extend(self.trait_implementation());
}
}

impl ToTokens for Metadata {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Metadata { name, arguments } = self;

tokens.extend(quote! {
fn metadata(&self) -> fj::ModelMetadata {
fj::ModelMetadata::new(#name)
#( .with_argument(#arguments) )*
}
});
}
}

impl ToTokens for ArgumentMetadata {
fn to_tokens(&self, tokens: &mut TokenStream) {
let ArgumentMetadata {
name,
default_value,
} = self;

tokens.extend(quote! { fj::ArgumentMetadata::new(#name) });

if let Some(default_value) = default_value {
tokens.extend(quote! {
.with_default_value(stringify!(#default_value))
});
}
}
}

impl ToTokens for GeometryFunction {
fn to_tokens(&self, tokens: &mut TokenStream) {
let GeometryFunction {
geometry_function,
arguments,
constraints,
fallible,
} = self;

let argument_names = arguments.iter().map(|a| &a.ident);

let invocation = quote! {
#geometry_function(#( #argument_names ),*)
};
let invocation = if *fallible {
quote! { #invocation.map(fj::Shape::from).map_err(Into::into) }
} else {
quote! { Ok(#invocation.into()) }
};

tokens.extend(quote! {
fn shape(
&self,
ctx: &dyn fj::Context,
) -> Result<fj::Shape, Box<dyn std::error::Error + Send + Sync>> {
#( #arguments )*
#( #constraints )*
#invocation
}
});
}
}

impl ToTokens for ExtractedArgument {
fn to_tokens(&self, tokens: &mut TokenStream) {
let ExtractedArgument {
ident,
ty,
default_value,
} = self;

let name = ident.to_string();
let t = match default_value {
Some(default) => quote! {
let #ident: #ty = match ctx.get_argument(#name) {
Some(value) => value.parse()?,
None => #default
};
},
None => {
let error_message = format!("Expected {name}");
quote! {
let #ident: #ty = match ctx.get_argument(#name) {
Some(value) => value.parse()?,
None => return Err(#error_message.into()),
};
}
}
};

tokens.extend(t);
}
}

impl ToTokens for Constraint {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Constraint { target, expr, kind } = self;

let operator = match kind {
ConstraintKind::Max => quote!(<=),
ConstraintKind::Min => quote!(>=),
};
let predicate = quote! { #target #operator #expr };
// Note: this will cause `expr` to be evaluated twice. Predicates should
// be pure functions, so in theory this shouldn't be an issue.
let error_message = quote! {
format!(
"Expected {} {} {} (i.e. {} {} {})",
stringify!(#target),
stringify!(#operator),
stringify!(#expr),
#target,
stringify!(#operator),
#expr,
)
};

tokens.extend(quote! {
if !(#predicate) {
return Err(#error_message.into());
}
});
}
}
Loading