|
| 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