Skip to content

Commit eacf9d9

Browse files
authored
Merge pull request #28 from dherman/exception-safe-event-handlers
RFC: Exception-safe event handlers
2 parents 5cc7824 + 7cfef08 commit eacf9d9

File tree

1 file changed

+142
-0
lines changed

1 file changed

+142
-0
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
- Feature Name: exception_safe_event_handlers
2+
- Start Date: 2020-01-31
3+
- RFC PR:
4+
- Neon Issue:
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
[RFC 25](https://github.com/neon-bindings/rfcs/blob/master/text/0025-event-handler.md) introduced a powerful new API for transmitting a JavaScript event handler to other Rust threads so that they can asynchronously signal events back to the main JavaScript thread. This RFC proposes a modification to the API to make the event handler's callback protocol both **simpler to understand** and **safe for handling JavaScript exceptions** that can occur while preparing the call the callback.
10+
11+
# Motivation
12+
[motivation]: #motivation
13+
14+
[RFC 25](https://github.com/neon-bindings/rfcs/blob/master/text/0025-event-handler.md) introduced a powerful new API for transmitting a JavaScript event handler to other Rust threads so that they can asynchronously signal events back to the main JavaScript thread.
15+
16+
However, in that design, the Rust code that prepares the results to send to the event handler has no way to manage operations that can trigger JavaScript exceptions. This shows up even in the simple examples in that RFC, which are forced to use `.unwrap()` to deal with `NeonResult` values:
17+
18+
```rust
19+
let handler = EventHandler::new(...);
20+
21+
handler.schedule(move, |cx, this, f| {
22+
let buf = cx.buffer(1024).unwrap(); // panics on exception!
23+
...
24+
});
25+
```
26+
27+
For the high-level `schedule()` API, this RFC proposes changing the Rust closure to return a `JsResult` and using the standard, classic Node callback protocol of sending an error value as the first argument (or `null`) and a success value as the second argument (or `null`).
28+
29+
For the low-level `schedule_with()` API, this RFC proposes only the small change of a `NeonResult<()>` output type, to allow the Rust callback to propagate uncaught JavaScript exceptions to the Node top-level. However, in another RFC we could propose a `try_catch()` API to allow defensive code to handle exceptions.
30+
31+
# Guide-level explanation
32+
[guide-level-explanation]: #guide-level-explanation
33+
34+
The `neon::event::EventHandler` type is clone-able and can be sent across threads.
35+
36+
The user provides a function and a `this` context to the struct constructors.
37+
38+
An `EventHandler` contains methods for scheduling events to be fired on the main JavaScript thread. Each event scheduling method takes a Rust callback we refer to as the **_event launcher_**, which runs on the JavaScript thread in its own turn of the event loop, and whose job it is to pass the event information to the event handler.
39+
40+
## Scheduling Events
41+
42+
The `schedule` method is the usual way to schedule an event on the JavaScript thread. Scheduling is "_fire and forget_," meaning that it sends the event to the main thread and immediately returns `()`.
43+
44+
The event launcher takes a `neon::Context` and computes the result, which will automatically be passed to the event handler by Neon.
45+
46+
Following Node convention, a successful result will be passed as the second argument to the handler, with `null` as the first argument; conversely, if the event launcher throws a JavaScript exception, the exception is passed as the first argument to the handler with `null` as the second argument.
47+
48+
Example for providing the current progress of a background operation:
49+
50+
```rust
51+
let mut this = cx.this();
52+
let cb = cx.argument::<JsFunction>(0)?;
53+
let handler = EventHandler::new(cb);
54+
// or: = EventHandler::bind(this, cb);
55+
thread::spawn(move || {
56+
for i in 0..100 {
57+
// do some work ....
58+
thread::sleep(Duration::from_millis(40));
59+
// schedule a call into javascript
60+
handler.schedule(move |cx| {
61+
// successful result to be passed to the event handler
62+
Ok(cx.number(i))
63+
}
64+
}
65+
});
66+
```
67+
68+
Here the `EventHandler` "captures" `this` and `cb` and calls the closure from the JavaScript thread with the context (`cx`). The successful result value produced by the closure is then passed as the second argument of the event handler callback, with `null` passed as the first argument, indicating no error occurred.
69+
70+
*Note:* The closure is sent to the main JavaScript thread so every captured value will be moved.
71+
72+
This approach should be familiar to Rust programmers as it is the same as `std::thread::spawn` uses.
73+
74+
## Low-level API
75+
76+
For cases where you need more control, Neon offers a lower-level primitive, the `schedule_with` method. This method also takes an event launcher, but Neon does not automatically call the event handler. Instead, the event launcher receives a `neon::Context`, the `this` context, and the event handler function object, and is given total control over whether and how to call the event handler.
77+
78+
If the event launcher throws a JavaScript exception, it behaves like an uncaught exception in the Node event loop.
79+
80+
Example for providing the current progress of a background operation:
81+
82+
```rust
83+
let mut this = cx.this();
84+
let cb = cx.argument::<JsFunction>(0)?;
85+
let handler = EventHandler::new(cb);
86+
// or: = EventHandler::bind(this, cb);
87+
thread::spawn(move || {
88+
for i in 0..100 {
89+
// do some work ....
90+
thread::sleep(Duration::from_millis(40));
91+
// schedule a call into javascript
92+
handler.schedule_with(move |cx, this, cb| {
93+
// call the event handler callback
94+
let args = vec![
95+
cx.null().upcast(),
96+
cx.number(i).upcast()
97+
];
98+
cb.call(args)?;
99+
Ok(())
100+
}
101+
}
102+
});
103+
```
104+
105+
# Reference-level explanation
106+
[reference-level-explanation]: #reference-level-explanation
107+
108+
```rust
109+
struct EventHandler {
110+
111+
pub fn new<'a, C: Context<'a>, T: Value>(cx: &C, this: Handle<T>, callback: Handle<JsFunction>) -> Self;
112+
113+
pub fn schedule<T, F>(&self, arg_cb: F)
114+
where T: Value,
115+
F: for<'a> FnOnce(&mut EventContext<'a>) -> JsResult<'a, T>,
116+
F: Send + 'static;
117+
118+
pub fn schedule_with<F>(&self, arg_cb: F)
119+
where F: FnOnce(&mut EventContext, Handle<JsValue>, Handle<JsFunction>) -> NeonResult<()>,
120+
F: Send + 'static;
121+
122+
}
123+
```
124+
125+
# Drawbacks
126+
[drawbacks]: #drawbacks
127+
128+
The type signatures are perhaps a bit more complex. But we should lean on the API docs and examples as the way to explain the API, not the type signatures.
129+
130+
# Rationale and alternatives
131+
[alternatives]: #alternatives
132+
133+
Currently the `neon::event` module is protected by a feature flag, and can't be made generally available until we resolve the problem that it has no way to handle JavaScript exceptions.
134+
135+
We should separately propose a `try_catch()` API for wrapping computations that might throw JavaScript exceptions in a closure and converting the result of the computation into a `Result`. We could leave the `neon::event` API as-is and just tell people to use that. But the high-level API would be less ergonomic, since you'd commonly have to wrap everything in a `try_catch`. Also it seems better to decouple the two APIs and release `neon::event` without having to design and implement a complete `try_catch` solution.
136+
137+
Another benefit of `try_catch()` would be to avoid C++ in the implementation of this API. But that's not a blocker, just a way to eventually streamline the implementation.
138+
139+
# Unresolved questions
140+
[unresolved]: #unresolved-questions
141+
142+
None

0 commit comments

Comments
 (0)