You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
we should probably allow returning references/borrowed values somehow, even if their life time cannot be tracked.
Motivation
I've been using wasm-bindgen in a project in which the core functionality (including most of our application's state) lives in the Rust/Web Assembly module. This posed many challenges, because many objects are owned by the module and live in the Web Assembly memory, but their state must ultimately still be communicated to the JavaScript side for the rendering of the UI. We also use Typescript, so we want our Typescript definitions to closely match our Rust types.
Goal
After a lot of investigation and many attempts at different approaches, we decided to manually export individual functions (not structs) manipulating raw pointers (JS still sees them as numbers). We then wrote our own idiomatic Typescript wrapper around those functions, creating our own classes that wrap those number pointers to provide a type-safe and idiomatic Typescript API. This may sound like unnecessary work, since this is essentially what wasm-bindgen aims to do for you, but we did this because we couldn't figure out a way to make wasm-bindgen generate the bindings we wanted.
This is a very simple, idiomatic Rust code for a basic "to-do" style application. The way one would use it in Rust is pretty straightforward, so I'm not going to show an example here, but it is interesting to look at how one might want to use it from JavaScript.
Desired usage from JavaScript
constproject=newProject("world domination");project.add_task("embrace");project.add_task("extend");project.add_task("extinguish");console.log(`commencing ${project.name}.`);for(consttaskofproject.tasks){console.log(`it's time to ${task.name}...`);task.done=true;}
This is pretty close from what one would write if they had written the same application logic in JavaScript directly: Project would be a class, it would have a name member of type string, and an array of tasks, which themselves have a name and a done field of their own. The Typescript definitions might look something like this:
This was our goal, but wasm-bindgen makes writing code like this very difficult, as there is no way to have that project.tasks field to be generated for us. In general, it is impossible to get a reference on the JavaScript side to a Task that is owned by Project on the Rust side, even though it is possible in Rust.
Obviously, the borrow checker has no way to know that someone won't be keeping a reference to a Task after it was moved, which is why, I'm assuming, wasm-bindgen wraps objects in objects containing a raw field for the pointer: the pointer is set to null when the value is moved, resulting in an error if that object is ever used again. This is much more challenging with references, but in our specific case, we aren't keeping references for very long anywhere anyway, so this was just preventing us from reading the state of our app.
Our solution
Ultimately, our solution consists of writing our own wrapper (still using the wasm-bindgen attribute to avoid dealing with string encoding/decoding, async and error handling with Result). For the Project type, it would look like this:
/// Constructor#[wasm_bindgen]pubfnproject_new(name:String) -> *mutProject{Box::into_raw(Box::new(Project::new(name)))}/// Destructor#[wasm_bindgen]pubfnproject_free(project:*mutProject){if !project.is_null(){unsafe{Box::from_raw(project)};}}/// Returns the length of the `tasks` vector.#[wasm_bindgen]pubfnproject_tasks_len(project:*constProject) -> usize{unsafe{&*project }.tasks().len()}/// Returns a specific task by index.#[wasm_bindgen]pubfnproject_tasks_get(project:*mutProject,index:usize) -> *mutTask{&mutunsafe{&mut*project }.tasks_mut()[index]}/// Adds a new task.#[wasm_bindgen]pubfnproject_add_task(project:*mutProject,name:String){unsafe{&mut*project }.add_task(name);}
classProject{raw: number;privateconstructor(raw: number){this.raw=raw;}staticwrap(raw: number): Project{returnnewProject(raw);}// The constructor becomes a static method (sad 😔) because we need to have a way to "wrap" referencesstaticnew(name: string): Project{returnnewProject(project_new(name));}free(){project_free(this.raw);this.raw=0;}getname(){returnproject_name(this.raw);}setname(value: string){project_set_name(this.raw,value);}readonlytasks=array_proxy((index)=>Task.wrap(project_tasks_get(this.raw,index)),()=>project_tasks_len(this.raw));add_task(name: string){project_add_task(name);}}
...where array_proxy is a utility we have that uses Proxy to create an object that implements an ArrayLike and Iterable interface over some collection. Borrowed values are wrapped in an instance of their corresponding JavaScript class (demonstrated here with Task.wrap).
Conclusion
This worked beautifully for us, and enabled us to keep our core logic in idiomatic Rust while still providing a similar Typescript interface. We've seen that it makes manipulating objects living in WASM memory from JavaScript significantly easier, as we are able to map pretty much any Rust method signature into an equivalent function that converts references into raw pointers (effectively discarding any static "lifetime" information, but I'm pretty sure this is just part of FFIs anyway). One area of improvement here would be to differentiates mutable from immutable references using Typescript, which does not sound very complicated, but we haven't explored that just yet.
Any thoughts on this? Do you think this is something that should be integrated to wasm-bindgen? Or something provided by a separate crate that would work on-top of it?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
TL/DR
we should probably allow returning references/borrowed values somehow, even if their life time cannot be tracked.
Motivation
I've been using
wasm-bindgen
in a project in which the core functionality (including most of our application's state) lives in the Rust/Web Assembly module. This posed many challenges, because many objects are owned by the module and live in the Web Assembly memory, but their state must ultimately still be communicated to the JavaScript side for the rendering of the UI. We also use Typescript, so we want our Typescript definitions to closely match our Rust types.Goal
After a lot of investigation and many attempts at different approaches, we decided to manually export individual functions (not
struct
s) manipulating raw pointers (JS still sees them asnumber
s). We then wrote our own idiomatic Typescript wrapper around those functions, creating our ownclass
es that wrap thosenumber
pointers to provide a type-safe and idiomatic Typescript API. This may sound like unnecessary work, since this is essentially whatwasm-bindgen
aims to do for you, but we did this because we couldn't figure out a way to makewasm-bindgen
generate the bindings we wanted.Rust logic
Take the following example:
This is a very simple, idiomatic Rust code for a basic "to-do" style application. The way one would use it in Rust is pretty straightforward, so I'm not going to show an example here, but it is interesting to look at how one might want to use it from JavaScript.
Desired usage from JavaScript
This is pretty close from what one would write if they had written the same application logic in JavaScript directly:
Project
would be a class, it would have aname
member of typestring
, and an array oftasks
, which themselves have aname
and adone
field of their own. The Typescript definitions might look something like this:The problem
This was our goal, but
wasm-bindgen
makes writing code like this very difficult, as there is no way to have thatproject.tasks
field to be generated for us. In general, it is impossible to get a reference on the JavaScript side to aTask
that is owned byProject
on the Rust side, even though it is possible in Rust.Obviously, the borrow checker has no way to know that someone won't be keeping a reference to a
Task
after it was moved, which is why, I'm assuming,wasm-bindgen
wraps objects in objects containing araw
field for the pointer: the pointer is set to null when the value is moved, resulting in an error if that object is ever used again. This is much more challenging with references, but in our specific case, we aren't keeping references for very long anywhere anyway, so this was just preventing us from reading the state of our app.Our solution
Ultimately, our solution consists of writing our own wrapper (still using the
wasm-bindgen
attribute to avoid dealing with string encoding/decoding,async
and error handling withResult
). For theProject
type, it would look like this:...where
array_proxy
is a utility we have that usesProxy
to create an object that implements anArrayLike
andIterable
interface over some collection. Borrowed values are wrapped in an instance of their corresponding JavaScript class (demonstrated here withTask.wrap
).Conclusion
This worked beautifully for us, and enabled us to keep our core logic in idiomatic Rust while still providing a similar Typescript interface. We've seen that it makes manipulating objects living in WASM memory from JavaScript significantly easier, as we are able to map pretty much any Rust method signature into an equivalent function that converts references into raw pointers (effectively discarding any static "lifetime" information, but I'm pretty sure this is just part of FFIs anyway). One area of improvement here would be to differentiates mutable from immutable references using Typescript, which does not sound very complicated, but we haven't explored that just yet.
Any thoughts on this? Do you think this is something that should be integrated to
wasm-bindgen
? Or something provided by a separate crate that would work on-top of it?Beta Was this translation helpful? Give feedback.
All reactions