-
-
Notifications
You must be signed in to change notification settings - Fork 119
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
Switch to model-driven model system #804
Comments
This looks great! As a thought, I like the idea of defining the standard interface as a C header then implementing that as the host API, as opposed to Rust |
Thanks for the feedback, @Protryon!
I'm open to that, but let me make a counter-point, in the interest of coming up with the best possible solution. The That means in theory, we could generate a C header file from the Rust code, instead of the other way around. This would mean we can keep using In practice, I don't know if a tool that can generate C headers from |
Ah, addendum to my previous post: My unspoken assumption is, that if we define this |
Quoting my comment on #71 about the overall architecture:
|
Just to mirror my comment from #71, I'd like to thank you for posting this, @Michael-F-Bryan, and say that I'm fully on board with what you're suggesting. A |
I've put together a proof-of-concept for how we could set up the host-model interface. There are two main differences between my interface and the existing interface,
Here is my version of the cuboid model: // TODO: replace this with a custom attribute.
fj_plugins::register_plugin!(|host| {
let _span = tracing::info_span!("init").entered();
host.register_model(Cuboid);
tracing::info!("Registered cuboid");
Ok(
PluginMetadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
.set_short_description(env!("CARGO_PKG_DESCRIPTION"))
.set_repository(env!("CARGO_PKG_REPOSITORY"))
.set_homepage(env!("CARGO_PKG_HOMEPAGE"))
.set_license(env!("CARGO_PKG_LICENSE"))
.set_description(include_str!("../README.md")),
)
});
#[derive(Debug, Clone, PartialEq)]
pub struct Cuboid;
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<fj::Shape, fj_plugins::Error> {
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");
let rectangle = fj::Sketch::from_points(vec![
[-x / 2., -y / 2.],
[x / 2., -y / 2.],
[x / 2., y / 2.],
[-x / 2., y / 2.],
])
.with_color([100, 255, 0, 200]);
let cuboid = fj::Sweep::from_path(rectangle.into(), [0., 0., z]);
Ok(cuboid.into())
}
} To help iterate on design, I've added a polyfill to that repo so you can compile the model to a |
Thank you, @Michael-F-Bryan! Looks promising. I'll take a closer look next week. |
Hey, @Michael-F-Bryan, I've finally had a chance to take a closer look. I'm feeling a bit overwhelmed with all the details, but I'm pretty sure I understand what's going one from a high level. I think this looks great! The question I'm asking myself though, is what is the next step? A proof of concept is good, but a pull request that I can merge would be much better! The one concern I have about this, is that the approach taken here is a bit too... professional. We don't have a large user base that expects their models to still work in the next release. So I don't think we need a shim to keep them working, for example. What we do need are specific steps to get improvements like model metadata and WASM support merged. We can keep iterating on the design within the repository. |
Yeah, there's definitely a lot going on in that repo, so I'll try to summarize the main differences from how Fornjot works today. First, each plugin1 has some sort of initialization function where it is given a // models/cuboid/src/lib.rs
fj_plugins::register_plugin!(|host| {
host.register_model(Cuboid::default());
Ok(
PluginMetadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
// everything after this is optional
.set_short_description(env!("CARGO_PKG_DESCRIPTION"))
.set_repository(env!("CARGO_PKG_REPOSITORY"))
.set_homepage(env!("CARGO_PKG_HOMEPAGE"))
.set_license(env!("CARGO_PKG_LICENSE"))
.set_description(include_str!("../README.md")),
)
}); The other difference is that a model is an object implementing the // crates/plugins/src/model.rs
/// A model.
pub trait Model: Send + Sync {
/// Calculate this model's concrete geometry.
fn shape(&self, ctx: &dyn Context) -> Result<fj::Shape, Error>;
/// Get metadata for the model.
fn metadata(&self) -> ModelMetadata;
} The In theory, it is trival for the The // crates/plugins/src/context.rs
pub trait Context {
/// The arguments dictionary associated with this [`Context`].
fn arguments(&self) -> &HashMap<String, String>;
} Note: The decision to use trait objects instead of concrete types is deliberate - it lets model authors mock out Fornjot's APIs during testing and also leaves space for alternate hosts (imagine a CLI tool that inspects a The The actual guest-host interface is described by the The Another thing you might have noticed is that there's a lot of
To be honest, the shim was less for backwards compatibility and more so I could load WebAssembly models while using a stock
To integrate this into Fornjot, we would need to:
There is one concern I have with merging such a PR, though... The While they would like to publish That doesn't mean we can't introduce this new API design now, though! I've tried to structure things so that Footnotes
|
Thank you for the summary, @Michael-F-Bryan! Very helpful. First, a note on nomenclature:
I think "plugin" is fine for the purpose of this discussion, but I'd like to avoid that word as a user-facing concept, for the following reasons:
I suggest staying with model, but saying that a model exports a shape, which can be a solid (3D) or a sketch (2D). In the future, a model will be able to export multiple shapes. In the farther future, models will be able to export other types of artifacts (like assemblies, G-Code, simulation results, ...).
I don't have a strong opinion on this, but I think fallible registration is fine. We're probably going to need it eventually anyway.
Good to know about the UB! 😄 We should probably catch all model panics and convert them into errors before they cross the FFI boundary. I've made a note to open an issue about that later.
It might be best to take it one step at a time, and just adapt
Sounds interesting!
Could
Sounds good 👍
I think that's going to be an essential capability going forward. The immediate use case being model parameters (which we already have an open issue and draft PR for), but all that other metadata will also be useful down the line.
Maybe I'm wrong, but I think it could be fine to extend the scope of the WIT-generated API a bit. The kernel certainly shouldn't deal with that, but I don't see an immediate reason why the
Sounds good! I was expressing concern that you were putting more work into some aspects of this prototype than would be necessary to get something merged. But I'm certainly not against you implementing whatever infrastructure helps you in your work!
Given the state of
If we decide that we want to go for web support before
Sounds like the way to go! Let's get the new API design in (which would address this issue) and figure out what to do about WebAssembly from there. |
This has been addressed in #885. Closing! |
Fornjot's current model system is what I've decided to call "host-driven". The host is in control and just calls into the model function, passing it everything it needs (i.e. the parameters). The model function then creates the model and returns it. The host then goes on to do with that whatever it needs to.
This works well enough for now, but I believe it won't hold up in the future:
I believe that there is a better approach that can support all those use cases more simply:
init
/main
function without parameters.init
/main
function.#[repr(C)]
/extern "C"
, so it works with over the FFI boundary required by the dynamic library, but also to provide flexibility for future uses (WASM, other languages, etc.).init
/main
function and some of the API calls (e.g. for loading parameters) are generated by the proc macro.fj
crate provides a convenient and idiomatic Rust wrapper, for the model code to call.Credit goes to @Protryon, who has made me aware of those two different approaches and provided crucial input over in the Matrix channel.
Implementation
Here are my thoughts on how an implementation of this issue could go:
fj-host-api
) that defines (but doesn't implement) the host API. The whole crate would basically be anextern "C" { ... }
block with a number of function definitions inside. See below for my thoughts on the specific API.fj-host
and implement the API there.fj
. Because the API is defined in a lightweight crate without the implementation, this shouldn't negatively affect the compile times of models.fj-proc
to generate theinit
/main
function and calls to the API for loading parameters.fj-host
to call the newinit
/main
function instead.fj-proc
to no longer generate the old and now unused model function.Here's what I imagine the host API could look like:
That's just an initial sketch, of course. I'm sure I'm missing something, and I'm sure there are better ways to do it.
The text was updated successfully, but these errors were encountered: