-
-
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 model system to WASM #71
Comments
Hi @hannobraun 👋 Something that has gone really well at work is using wit-bindgen for declaring the interface between the host and WebAssembly plugin. The idea is you write a The end result is you just need to implement a trait or two and browse your API docs to see what functions are available. We've already got hosts for loading WebAssembly plugins in the browser or using One thing to be aware of is that (by design) WebAssembly can't reference memory from the host, so any data you give plugins access to will need to be copied in. This could get expensive if you are giving plugins direct access to meshes. One solution would be to provide high-level objects that do the expensive manipulation for the plugin. |
Hey @Michael-F-Bryan, great to see you around here! Thank you for the comment! This is very useful to me. I had this vague notion that WASM interface types exist, but hadn't looked into it yet. I expect that this information will save me quite a bit of research time, once I'm ready to start working on this.
That is good to know, thanks. I don't expect that models will have access to meshes, as those are purely for output in Fornjot (modulo #97, but I'm working on that). I do want models to have access to b-rep primitives (#262), but unless the model is emulating some missing CAD kernel feature by creating lots of small edges or faces, I don't expect that to be much data. We'll see how it shakes out. |
Allright. This took me a while, but I've been toying with with trying to port just the I'll admit I'm no expert on wasm-bindgen or wit, but as far as I can tell using wit-bindgen requires that we have an interface that is callable through the wasm runtime. which leaves the issue of generating that library in the first place. The current interface doesn't work in WASM as it uses Rust's loaded enums, which don't seem to be trivial to make portable (which is probably why wasm-bindgen doesn't support making bindings to them) without severely losing API ergonomics.
#[derive(Clone, Debug)]
pub enum Shape2d {
Circle,
Sketch,
Difference,
}
#[repr(C)]
#[derive(Debug)]
#[cfg_attr(target_family = "wasm", wasm_bindgen)]
pub struct ShapeHandle2d<'d> {
/// The primitive shape being referenced
prim_type: &'static Shape2d,
/// Reference to the memory of the shape
/// Gets transmuted to
data: RawShapeHandle2d<'d>,
/// Length of memory referenced
length: usize,
}
/// A raw pointer to data which represents one of the shapes in `Shape2d`
#[derive(Debug)]
struct RawShapeHandle2d<'handle> {
data: &'handle [u8],
}
impl<'handle> RawShapeHandle2d<'handle> {
/// Transmute to a a `Circle` without checking validity
#[inline]
unsafe fn transmute_circle(self) -> Circle {
const F64_LEN: usize = std::mem::size_of::<f64>();
unsafe {
Circle::new(
std::mem::transmute(&self.data[..F64_LEN]),
std::mem::transmute(&self.data[F64_LEN..]),
)
}
}
/// Transmute to a `Difference2d` without checking validity
#[inline]
pub unsafe fn transmute_difference2d(
self,
) -> Difference2d<'static, 'static> {
const HANDLE_LEN: usize = std::mem::size_of::<ShapeHandle2d>();
unsafe {
Difference2d::from_shapes_unchecked((
std::mem::transmute(&self.data[..HANDLE_LEN]),
std::mem::transmute(&self.data[HANDLE_LEN + 1..HANDLE_LEN * 2]),
))
}
}
/// Transmute to a `Sketch` without checking valifity
#[inline]
pub unsafe fn transmute_sketch(self) -> Sketch {
const PTR_LEN: usize = std::mem::size_of::<*mut [f64; 2]>();
const USIZE_LEN: usize = std::mem::size_of::<usize>();
const ATOMIC_USIZE_LEN: usize =
std::mem::size_of::<atomic::AtomicUsize>();
const COLOR_LEN: usize = std::mem::size_of::<[u8; 4]>();
const LENGTH_OFF: usize = PTR_LEN + 1;
const CAPACITY_OFF: usize = LENGTH_OFF + USIZE_LEN + 1;
const RC_OFF: usize = CAPACITY_OFF + USIZE_LEN + 1;
const COLOR_OFF: usize = RC_OFF + ATOMIC_USIZE_LEN + 1;
unsafe {
Sketch::new(
std::mem::transmute(&self.data[..PTR_LEN]),
std::mem::transmute(
&self.data[LENGTH_OFF..LENGTH_OFF + USIZE_LEN],
),
std::mem::transmute(
&self.data[CAPACITY_OFF..CAPACITY_OFF + USIZE_LEN],
),
std::mem::transmute(
&self.data[RC_OFF..RC_OFF + ATOMIC_USIZE_LEN],
),
std::mem::transmute(&self.data[COLOR_OFF..]),
)
}
}
} (I'm aware of the MANY bugs with this implementation, I just wanted something that would compile and demonstrate my approach. I have never written code this low level before 😧) This "creates" a very low level private implementation that deals with pointers and memory offsets and a higher level implementation that deals in shapes, but still somewhat resembles the current API. I think I could then create the existing API on top of this for people building as an rlib. ** It seems that we're either going to have to maintain .wit assemblies or change the interface so much that wasm-bindgen would support it anyway. ** This whole experiments makes me think a hypothetical distribution platform for end users would be better off distributing the models as source instead and embedding a compiler in end user applications if API ergonomics are a higher priority than portability. This is a tough nut to crack for me and I'm struggling to do it, but it's improving my knowledge is expanding quite satisfyingly while doing so, so I'd be happy to keep going with this approach, but it seems like a nightmare. |
Thank you looking into this, @freylint! I agree that this is a nightmare. Seems unmaintainable 😄 I don't have time right now to look into this myself, unfortunately, so all I can do is pose a few questions:
I don't expect you to answer these, just trying to contribute to the discussion. Looks like |
I've worked with some complex WASM integrations professionally. I don't know if I'll have the time to integrate with stuff here, but the approach I'd roll with, especially to support arbitrary languages:
What this might look like, project wise:
|
Thank you for this information, @Protryon! Do you have any experience with or opinion on It sounds to me like WIT might be a good solution for this kind of thing in the future. I do think that the approach you present here sounds perfectly reasonable, but also somewhat labor-intensive. Both in terms of implementation and ongoing maintenance. I'm wondering how the C-based approach compares to WIT, and how both fit into the larger context of WebAssembly bindings.
|
The C-based approach is indeed more labor intensive than a drag-and-drop solution at first glance. With the lack of maturity and broader language support, I would think they are about the same with some trade-offs in outcome. |
I've done some limited experimentation with
Yeah, that sounds about right. For now, I'm open to all the approaches mentioned so far. Maybe the C-based approach warrants some more exploration. I'm not up-to-date on Rust/C FFI and what tools are available these days. Could be, that my mental model is outdated, and it's not as much work as I fear. |
We've been having some discussions in the Matrix channel lately, and reflecting on those, I've changed my opinion about this issue. I previously saw a WASM-based model system as a mid-term priority, and something I might have wanted to work on myself in the not too distant future. While I still think that this is likely the right direction long-term, I no longer see this issue as a priority. Before I explain my reasoning, let me recap the benefits that I see in a WASM-based model system:
I think we can ignore security for now. Fornjot doesn't even have the most basic modeling features implemented. As long as it isn't really useful to anybody, I don't think we have to worry about people publishing malicious Fornjot models. Likewise, as long as we don't have a foundation of useful modeling features, it makes no sense to expose what little we have to multiple languages. Multi-language support is a nice-to-have for (potentially much) later. That leaves browser support, which is still something that I'd personally like to see. However, I think I had developed a bit of tunnel vision in that regard. WASM-based modules aren't necessary for browser support. For that, we can also use an approach where models are regular Rust applications that use Fornjot as a library, and compile that whole thing to WASM. This wouldn't be a practical model for the general case, due to long compile times (I've tried). But it would work for deploying your finished model to a website. I'm going to write about that in other issues, as it would be beside the point here. So where does that leave us? Here's the current situation, from my perspective:
That last item doesn't mean I won't merge anything that takes Fornjot into this direction, but I don't want anyone to feel like they're wasting their time either. If in doubt, feel free to ask here, or the Matrix channel, or Discussions. As always, feedback is appreciated! |
As mentioned on Matrix, I think focusing on the CAD features and general workflow is the right decision here. The reality is that we've already got a model system. Sure, it may not be how things are done later on, but for now it works well enough that other parts of the project can make process. Changing how models and the host interact is a fundamental part of the project's architecture (#804), whereas switching from extern "C" to WebAssembly is "just" an implementation detail. Furthermore, we've already got nice abstractions in place that will help with the transition process. Regarding the WebAssembly model implementation I'd like to share my experiences doing a very similar thing for work. For context, we make an application that lets you define and execute data processing pipelines, possibly including ML models, where each operation ("processing block") is some sort of plugin (in this case, WebAssembly modules). We've gone through roughly three different implementations for this plugin system and I learned loads in the process. Initially, each pipeline was a Rust crate that we generated from some intermediate representation, where each processing block was a Rust crate added to For the second and third implementations, each processing block is now its own WebAssembly module and it communicated with the host using I like the This is probably more relevant to #804, but one suggestion is to give model developers a "seam" they can use during testing instead of APIs that talk to the host directly. It's hard to explain in words, so here's some code. In our second iteration, we made the mistake of creating free functions for talking with the host (getting arguments, etc.) and as a result it became hard to write unit tests for those interactions. Writing tests that transitively call these If you want to see an example of what this looks like, we define the host-guest interface using WIT files and have a repo containing all the "processing block" implementations we provide. The For future reference, Rust-style enums are a first-class citizen in WIT. Just keep in mind that C-style enums are declared with the Some things to keep in mind while iterating on the model API:
|
Thank you for posting this, @Michael-F-Bryan! Very valuable information. I'd like to say that I'm fully on board with what you wrote here, and want to mirror some of the comments I made in the Matrix discussion:
Thanks for the note. I think WASI could be very beneficial for some Fornjot models, as it would allow model authors a lot of flexibility doing things that Fornjot doesn't support yet. Based on some uses of OpenSCAD I've seen in the wild, I assume this will become relevant sooner or later. For an initial implementation, I think |
I've done some research & experimentation with with different Wasm runtimes:
Protocols / binding generators:
So... I think the best combination for Fornjot (and maybe also MoonZoon) is currently Wasmer + wai-bindgen. Both Wasmer browser support and wai-bindgen + WIT definitions are a bit unstable but they make the plugin system development much easier and we'll be able to support other languages. I saw at least Python and Javascript support in the Wai repo - both could be useful for "real-time" Fornjot demos and modelling. I've tried to use Wasmer inside a MoonZoon app with both hand-written code and a bridge generated by wai-bindgen and related macros from a *.wit file. I was successful, but I had to fork some Wasmer-related repos to make it work.
@Michael-F-Bryan I would be glad for your opinions or let me know where/if I should create a PR or something like that. Once Wasmer fully supports Rust-in-browser then I would like to try compile Fornjot to Wasm module to find out which dependencies aren't Wasm-compatible or if there are too large dependencies. Another potential problem is parallelization. The only way to support multithreading in a browser is to leverage Web Workers and don't use blocking calls at all (only async functions or callbacks can be used). There are some Rust multithreading libraries trying to abstract out Web Workers and SharedArrayBuffer and other things (that are finally supported by browsers again and some of them usable at least on a nightly Rust). However, when I was testing some of them months/years ago I wasn't very successful with integrating them. I hope it's better situation now but I would recommend to implement some async-related stuff like #1327 so we have something to test while we'll be testing Fornjot in the browser environment. |
@MartinKavik If you have issues with Wasmer, it would be great to file them on the wasmer repo, so we can track them. However, I've tested (1) with [dependencies]
wasmer = { git = "https://github.com/wasmerio/wasmer" }
wasmer-wasi = { git = "https://github.com/wasmerio/wasmer" }
wasmer-vfs = { git = "https://github.com/wasmerio/wasmer" } and [dependencies]
wasmer = "3"
wasmer-wasi = "3"
wasmer-vfs = "3" ... and it does work? I'm not sure what the submodule errors you're getting look like. We did have submodule errors about a month ago, but they should have been removed on the latest master. |
Thank you for that thorough overview, @MartinKavik! And thanks for pitching in, @fschutt. Nice to see you here!
My assumption so far was that we'll have different code paths for running inside or outside of a browser. Basically, I assumed we'd have Wasmtime or Wasmer as a replacement for the current But if Wasmer can handle both scenarios, all the better!
Sounds reasonable! Please note, this issue is about migrating the model system to WASM. That's a part of browser support, but it doesn't need to include browser support in the initial version. Of course, we wouldn't want to take an approach that would make adding browser support later any harder than necessary. Just saying, we don't need to solve all the problems at once. (Also see #816.)
Most of Fornjot already compiles to WebAssembly, and this is part of the CI build. Notable exceptions include Footnotes
|
@fschutt :
I'd love to do it now but the end of the month is coming up so I have to do some paid work as well=> I'll try to look at it during the following weeks.
I've written "Submodules in the Wasmer (?) repo are broken" with a question mark because I've forked ~4 Wasmer-related repos and don't remember where the problem appeared. I think Wasmer + WIT is the only reasonable choice on the browser now and the testing is quite easy for me because I can just add Fornjot into a basic MoonZoon app as a dependency and run the command So as the next step I'll try to push the Wasmer fixes a bit once I find some time and then experiment with Fornjot compilation in the browser Wasm to have an idea how much work would be needed to make it compatible. |
Sounds great, @MartinKavik. Thank you! |
I'd be happy to help contribute to this feature. I'm only a hobbyist programmer, but I've been around for a couple years. I'd need mentorship / direction to really be able to tackle this problem. |
Thank you, @freylint! I'm happy to help, but my own knowledge is mostly limited to what others have posted here. If I were to tackle this myself, I'd just try out the approach that @MartinKavik just suggested. Maybe @MartinKavik has some input for you? |
I've just merged an example with Wasmer to the MoonZoon repo so we have a testable example. You can run & test the example this way:
wasm_component_moonzoon_demo.mp4Once we resolve all todos marked If you encounter some obstacles related to the MoonZoon, just join MoonZoon Discord and write me. |
Extism, a WASM-based universal plugin system, was announced today: https://extism.org/blog/announcing-extism/ It seems to be based on Wasmtime and has a browser runtime. I haven't looked into it more deeply than that, but it does sound interesting! Being a plugin system and not just a WASM runtime, it could probide useful stuff that Fornjot needs. On the other hand, it might force us to do things in a way we don't like. Hard to say without taking a closer look, but it might be worth doing so. |
https://discord.com/channels/1011124058408112148/1011124061100843020/1043580405514784870 |
Oh, so the browser runtime is not the same as the Rust runtime? Doesn't seem suitable to our purposes then. |
A Wasm host (e.g. Rust in a browser) is basically an edge-case from the point of view of these plugin systems, unfortunately. Javascript is meant by browser runtime in almost all cases. I was able to run only Wasmi and modified Wasmer in a Wasm host. |
Hello! It would then look something like this, where ShapeData is what would be returned instead of Shape from the shape function: pub type ShapeHandle = u32;
pub struct ShapeData {
pub shapes2d: Vec<Shape2d>,
pub shapes: Vec<Shape>,
}
pub struct Group {
pub a: ShapeHandle,
pub b: ShapeHandle,
} Which would be this in WAI:
What do you think about that @hannobraun? Or do you have another approach? |
Thank you for your work, @silesmo! I haven't worked with WAI or WIT before, so I can't provide much guidance on that side. But in general, I can say that your proposal looks reasonable. One thing that comes to mind, is whether it's a good idea to have a single As a general approach, I suggest to just make it work and worry about the details later. Having WASM support will be a big step in the right direction, and once all of that exists and is merged, we will be in a better position to figure out better usage patterns. |
Random note: Could perhaps be interesting to look at how Envoy added extensibility through WASM: https://tetrate.io/blog/wasm-modules-and-envoy-extensibility-explained-part-1/ |
Okay, I will go ahead with this then! I have one handle for shape and one for shape2d I just included one as part of the example. |
Sounds great, @silesmo. Looking forward to the PR! |
Hello again @hannobraun. Life got a bit in the way last week so didn't get the time to finish up the PR. By the looks of it I won't have the time until Tuesday next week. Just wanted to keep you in the loop. Sorry for the delay! |
Thanks for letting me know, @silesmo. Take your time! |
This subject came up over in #1569. The benefits and reasoning behind this design are explained here. I'm going to start on that experimental prototype. I won't be implementing it for WASM at this time (you all seem to know more about that than I do anyway) but I do think I'll rework the current dynamic library system to pass a pointer to a block of memory with the serialized format contained within it. |
This issue is no longer applicable. See A New Direction for context. Thanks everyone, for all the input here! All remaining Fornjot components already compile to WebAssembly and it should be possible to use them as libraries in WASM. The remaining parts of this issue are no longer in scope for the project. |
So far, we've been using dynamically linked libraries to load models into the host application. That works well enough for desktop platforms, but to run Fornjot on the web, with arbitrary models, it is necessary to compile models to WebAssembly and load that into the host application.
If at all possible, WebAssembly should be used for desktop platforms too, so we don't have to maintain two parallel systems. That has the additional advantage of providing sandboxing of models, improving security (the current system is basically unsound by design). Something like Wasmtime or Wasmer could be used, to embed WebAssembly support into the host application for desktop platforms.
The text was updated successfully, but these errors were encountered: