[DONE] Interior mutability for Wasm structs #603
Replies: 4 comments 9 replies
-
I think the overhead of I think a reasonable compromise would be to force all interaction with the fields of the wrapped
gets replaced with
and we introduce a de-facto setter as well:
Using this is not 100% idiomatic JS, but it should also not be too annoying. Alternatively (or in addition to the above possible solution) we could introduce some more methods on
note that the delegate crate can help with reducing the amount of boiler plate code needed for this. With this alternative one would then have to replace JS code like Of course we want our JS bindings to be as ergonomic and natural as possible, but at the same time we have to consider the value of chasing 100 % idiomatic JS against for example spending the time on improving the functionality offered by this library (which of course also benefits JS users). |
Beta Was this translation helpful? Give feedback.
-
I suppose the fundamental limitation is that of JS not being able to encode immutable references. I don't think the overhead of I like the general solution, but as you pointed out, it's difficult to say where we should stop To address the root of the problem, we could implement two versions of each struct: One that is mutable and has all methods and one that is immutable and only has methods that take My preferred solution would be to keep the current version of the bindings. I guess one question is also if this is a frequent problem or an exception. Those who use the bindings as intended won't report problems to us, so we may have a skewed perception? If we have to make a change I think your suggestion is good. For the specific problem of the |
Beta Was this translation helpful? Give feedback.
-
Thank you very much for the considered feedback and alternatives presented. As an outcome of this discussion I would propose we:
Overall, this should help reduce silent "accidentally-mutating-a-copy" bugs. I acknowledge the possible performance impact, however I believe the API improvements and reduction in potential user bugs to be worth it. If anyone wants to microbenchmark the effects on runtime and memory in Wasm I would still be interested but as was mentioned, such cases where performance is really important should prefer the Rust library. Any further comments, alternatives, or disagreements are still very welcome. Edit: opened PR #607. |
Beta Was this translation helpful? Give feedback.
-
Update: there are concerns around possible memory leaks with adding more Parts of this can still be implemented, however, like removing See: #607 (comment) |
Beta Was this translation helpful? Give feedback.
-
Description
Explore replacing newtype wrappers with
Rc<RefCell<T>>
for interior mutability of struct fields in the Wasm bindings.Motivation
A common problem we experience in the Wasm bindings is being unable to return mutable references. This leads to us returning clones of fields, often opaquely and unexpectedly to developers.
The way we have addressed this in
IotaDocument
/WasmDocument
, where we need to update sub-fields in its metadata (such asupdated
andpreviousMessageId
), is to duplicate those fields with a prefix on theDocument
itself. I.e. havemetadataUpdated
,metadataPreviousMessageId
etc. defined onWasmDocument
which internally access and mutate the corresponding fields in the metadata.We could instead duplicate struct field definitions in their Wasm counterparts and use
Rc<RefCell<T>>
for interior mutability without cloning data. The problem with this is that Rust interfaces expect the entire struct not just its fields, which would require cloning the data and re-constructing the expected struct.Example
In Rust,
ResolvedIotaDocument
contains anIotaDocument
with some extra fields.In Wasm, the newtype
WasmResolvedDocument
wrapsResolvedIotaDocument
.Accessing the inner document in Wasm requires either cloning it or consuming the
WasmResolvedDocument
:Developers will often want to access the inner document and end up writing code that "looks correct" but actually clones the data with each access, and then mutate the copy rather than the original. This leads to serious yet subtle bugs that are difficult to identify without knowing the inner workings of the bindings.
Real (abridged) example of this in the wild:
This code runs without errors, but publishes an unchanged version of the document. Why? Because each time
resolved.document
is called, we return a clone of the inner document. Every mutation in the above example (insertMethod()
,signSelf()
, settingmetadataPreviousMessageId
, settingmetadataUpdated
) therefore operates on an independent copy of the document.The correct version of this code would be:
Without knowing this "trick", it is incredibly easy to use and mutate clones of fields unknowingly and get confused when updates do not appear to work.
Suggestion
We could change the definition of
WasmResolvedDocument
andWasmDocument
in the bindings to allow returning "clones" of fields which internally hold a smart pointer to the actual data.E.g.
We could try go further and apply this pattern to
WasmDocument
to eliminate themetadataPreviousMessageId
etc. workaround. However, we would encounter difficulties since we would lose access to methods onIotaDocument
. I.e. anyIotaDocument
method which takesself
/&self
/&mut self
would be unusuable fromWasmDocument
since we would have destructured it into its fields, and would need to clone them and reconstruct anIotaDocument
for each method call.We even have this problem with the proposed
WasmResolvedDocument
sinceclient.resolveDiffHistory()
expects aResolvedIotaDocument
. So in the Wasm bindings we need to reconstruct aResolvedIotaDocument
from its fields inWasmResolvedDocument
, which involves cloning the document out of theRc<RefCell>
since we cannot safely take ownership of it again (though we do this anyway since we have to clone variables passed to async promises).This pattern is therefore only suitable for data-only structures that have no or few associated methods.
Open questions
Rc<RefCell>
worth it?Rc<RefCell>
when compiled in the bindings so we avoid having to clone its fields, but that seems like an unacceptable amount of work required and performance overhead in the Rust library, if possible at all.Beta Was this translation helpful? Give feedback.
All reactions