-
Notifications
You must be signed in to change notification settings - Fork 9
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
JsBox RFC #33
JsBox RFC #33
Conversation
text/0000-jsbox.md
Outdated
} | ||
``` | ||
|
||
The instance of `Pool` can be returned to Javascript using a `JsBox`. The `Pool` won't be dropped until the `JsBox` is garbage collected. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
text/0000-jsbox.md
Outdated
} | ||
``` | ||
|
||
`JsBox` can be passed back and forth between Javascript and Rust like any other Js value. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Example of boilerplate required to use a Neon Class as a thin wrapper: | ||
|
||
```rust | ||
pub struct Person { |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
text/0000-jsbox.md
Outdated
} | ||
``` | ||
|
||
Data may only be borrowed immutability. However, `RefCell` can be used to introduce interior mutability with dynamic borrow checking rules: |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
text/0000-jsbox.md
Outdated
|
||
fn set_pool_size(mut cx: FunctionContext) -> JsResult<JsUndefined> { | ||
let size = cx.argument::<JsNumber>(1)?.value() as u32; | ||
let mut pool = cx.argument::<JsPool<RefCell<Pool>>>(0)?; |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
text/0000-jsbox.md
Outdated
|
||
#### `Send + 'static` | ||
|
||
`JsBox` are passed across threads and therefore must be `Send`. It is *not* necessary for `JsBox` to be `Sync` because they may only be borrowed on the main thread. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
text/0000-jsbox.md
Outdated
|
||
*Note*: Passing an external created by another library, potentially even another neon library, is _undefined behavior_. | ||
|
||
Progress is being made to add a [tagging feature](https://github.com/nodejs/node/pull/28237) to more safely unwrap externals. It should be incorporated into future designs; however, it is not included in this proposal. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
|
||
##### Rust | ||
|
||
```rust |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
} | ||
``` | ||
|
||
This approach has several significant drawbacks: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very compelling! Seeing the contrast and drawbacks really helps justify the design you ended up with.
|
||
* `JsExternal`. Matches the name in N-API documentation (external reference); however, `cx.external(v)` might be confusing. | ||
* `JsRef`. Also, matches N-API but, does not match Rust naming conventions. | ||
* `JsCell`. Only appropriate for the `RefCell` approach. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to consider a high-level convenience type, something like type JsRefCell<T> = JsBox<JsRefCell<T>>;
, for the common case of interior mutability? This would make a common case more concise, e.g. in your Pool
example above, it would change from
let mut pool = cx.argument::<JsBox<RefCell<Pool>>>(0)?;
to
let mut pool = cx.argument::<JsRefCell<Pool>>(0)?;
It would also create an opportunity to document the use case of interior mutability directly in the API docs, making the idiom a little more discoverable.
Maybe not necessary for this RFC, but we could leave a note in "unresolved questions" about future convenience APIs and mention the idea there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a section about future extension that might include this. In my opinion, this is above and beyond this RFC since in addition to the type alias we would want other helper methods (e.g., cx.ref_cell(..)
)
|
||
* `Buffer` can be mutably borrowed multiple times | ||
* `Lock` does not prevent overlapping memory regions | ||
* Multiple [`Lock`](https://docs.rs/neon/0.4.0/neon/context/struct.Lock.html) can be created allowing aliased mutable borrows |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still kind of shocked about this soundness hole. I could've sworn I checked for it and panic.
Conceptually, I still find the idea of being able to use RAII and Rust mutability rules to lock the main JS thread to be pretty compelling. But I like how you've at least decoupled that idea from the idea of safely exposing Rust data owned by the JS engine back to Rust.
I wonder if we might still have a place for the Lock
API just as a way to have self-documenting critical sections. OTOH, it might end up being a cute idea that's not particularly useful anymore. Curious what you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the Lock
API fits here because nothing is being locked. I don't understand what you mean by RAII to lock the main js thread. The existing API doesn't take any action to lock the the VM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dherman I thought about this some more and it finally clicked. I think the Lock
guard API only makes sense for accessing data from Buffer
.
When accessing a Buffer
, Neon needs to uphold Rusts guarantees on ownership and mutability of the slice. However, since the data is not owned by Rust, there may exist references unknown to Rust.
For example, imagine a scenario where a user borrows a &[u8]
to the Buffer
. According to Rust invariants, this data will not change during the lifetime of the borrow. However, if JavaScript is invoked, e.g. by calling a JsFunction
, the data could be mutated, violating the invariant.
The Lock
API solves this by holding an exclusive reference to Context
. The Lock
statically prevents calling any Neon methods that could potentially mutate the slice.
This problem does not exist for JsBox
. The data is owned by Rust and can't be mutated by pure JavaScript. At best, the user could invoke JavaScript that calls another Neon method. However, that Neon method would still be bound by Rusts dynamic borrow checking (RefCell
). It's simpler, more ergonomic, and no less safe to return shared references without a Lock/Guard on JsBox
.
Lastly, there is an edge case where the Lock
API might not uphold invariants. If the Buffer
was created as an external buffer (memory created and managed externally), it's possible for other non-Neon native code to mutate these types of buffers. Neon could use n-api
methods for detecting externals and refuse to lend them. Additionally, neon could provide unsafe fn borrow_unchecked
methods for reading data from externals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a couple of reads but I think you've really nailed the difference:
- Rust data (
JsBox
) can be protected by Rust'sCell
invariants, so we don't need to worry about reborrows via VM re-entrancy; - C/C++ data (
JsArrayBuffer
) can't be protected by Rust'sCell
invariants, so we do need to worry about reborrows via VM re-entrancy.
So if I understand you, you're saying the Lock
API makes sense to keep, but specifically and only for typed arrays.
On the one hand, it's nice that we can borrow a JsBox
more ergonomically. On the other, the asymmetry is a little frustrating. It's a shame there's no way (AFAIK) to dynamically borrow an ArrayBuffer
, which we could then use to make this safe without having to lock the JS VM. You can permanently detach its backing buffer, but that doesn't help this use case; we'd need a temporary, reversible version of detaching.
The
Lock
statically prevents calling any Neon methods that could potentially mutate the slice.
Exactly! This is definitely subtle (if ingenious—thank Niko for the idea), and is probably worth some better explanation in docs, assuming we keep the API.
If we only end up needing it for ArrayBuffer
and not JsBox
, I wonder if we want a less general name than Borrow
, especially since JsBox
doesn't use it. Maybe something that makes more explicit that it's a stop-the-world borrow, so the connection to Lock
is clearer?
Lastly, there is an edge case where the
Lock
API might not uphold invariants. If theBuffer
was created as an external buffer (memory created and managed externally), it's possible for other non-Neon native code to mutate these types of buffers. Neon could use n-api methods for detecting externals and refuse to lend them. Additionally, neon could provideunsafe fn borrow_unchecked
methods for reading data from externals.
Good catch. That makes sense to me.
So, some of this stuff is out of scope for this RFC, but probably worth summarizing the issues in the "open questions" section so we can capture them for subsequent work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dherman I like the clear naming of borrow
for the API. However, we could move it from neon::borrow
to neon::binary
to clarify the relationship to JsArrayBuffer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this idea!
text/0000-jsbox.md
Outdated
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
- Should we implement this for the legacy backend? We likely will not since this is a brand new API. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
text/0000-jsbox.md
Outdated
[unresolved]: #unresolved-questions | ||
|
||
- Should we implement this for the legacy backend? We likely will not since this is a brand new API. | ||
- Is it acceptable to let `JsBox::new` panic since it only fails when throwing or shutting down? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, these are both highly rare cases, which is the spirit of panic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently the RFC defines the APIs as returning a JsResult
, but that's inconsistent with other methods. I think that should be changed to return an unwrapped JsBox<_>
.
If we ever decide in the future that we want this potential failure exposed directly, it would be best done all at once for consitency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel comfortable making all the APIs return an unwrapped JsBox<_>
. NeonResult
is meant to represent whether the operation caused a JS exception. Failing because it was already in a throwing state is basically a programmer error that we happen not to be able to enforce through the type system and therefore a panic; the VM shutting down is extremely rare and unrelated to triggering a JS exception.
text/0000-jsbox.md
Outdated
|
||
The `JsBox` API is not entirely safe. It relies on the user only passing N-API externals created within that Neon library. Passing an external created by another library, potentially even another neon library, is _undefined behavior_. | ||
|
||
It is *not* undefined behavior to attempt a value that is not an external as a `JsBox`. This will fail predictably. It **is** undefined behavior to downcast a non-`JsBox` externally because the pointer will be treated as if it is one. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This RFC has entered the final comment period. |
Future ExpansionBackgroundWhile exploring the Possible Solution
APIpub trait Finalize {
fn finalize(self, cx: FinalizeContext);
}
impl<T: Send + 'static> JsBox<T> {
pub fn with_finalizer<'a, C>(cx: &mut C, value: T) -> Handle<'a, JsBox<T>>
where
C: Context<'a>,
T: Finalize;
} An additional Following idiomatic patterns establish elsewhere, impl<T> Drop for Persistent<T> {
fn drop(&mut self) {
panic!("Persistent must be manually dropped.");
}
} AlternativeAn alternative design is to provide the impl<T: Send + 'static> JsBox<T> {
pub fn with_finalizer<'a, C>(
cx: &mut C,
value: T,
finalizer: fn(FinalizerContext, T),
) -> Handle<'a, JsBox<T>>
where
C: Context<'a>,
} The advantage of the trait approach is that it co-locates the finalizer with the data. The disadvantage is that Neon would not prevent a user from defining a trait, but failing to use the In my opinion the Examplestruct MyServer {
server: Server,
// Wrapping the `Persistent` in a `ManuallyDrop` provides two advantages:
// 1. Immediately signals that something in this struct needs special attention
// 2. If a manual `drop` is missed, it causes a leak instead of a `panic`
callback: ManuallyDrop<Persistent<JsFunction>>,
}
impl Finalize for MyServer {
fn finalize(self, cx: FinalizeContext) {
self.callback.drop(&mut cx);
}
}
fn create_server(cx: FunctionContext) -> JsResult<JsBox<MyServer>> {
let callback = cx.argument::<JsFunction>(0)?.persistent(&mut cx);
let server = MyServer {
server: Server::new(),
callback: ManuallyDrop::new(callback),
};
Ok(JsBox::with_finalizer(server))
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look good to me! Just one FIXME to update.
Merged in 951e625 |
JsBox
is a smart pointer to data created in Rust and managed by the V8 garbage collector.JsBox
are a basic building block for higher level APIs like neon classes.Rendered RFC
Work in progress implementation