From 2b19d8c83ca9f511d73527544d4ea2c5fc46a57d Mon Sep 17 00:00:00 2001 From: "K.J. Valencik" Date: Wed, 9 Mar 2022 12:20:29 -0500 Subject: [PATCH] feat(neon): Implement Futures feature Implements https://github.com/neon-bindings/rfcs/pull/46 --- .cargo/config.toml | 2 +- Cargo.toml | 13 +++- package-lock.json | 20 ++++++ src/event/channel.rs | 92 +++++++++++++++++++++--- src/result/mod.rs | 10 +++ src/types/mod.rs | 3 + src/types/promise.rs | 136 +++++++++++++++++++++++++++++++++++- test/futures/Cargo.toml | 20 ++++++ test/futures/README.md | 3 + test/futures/lib/channel.js | 26 +++++++ test/futures/lib/promise.js | 21 ++++++ test/futures/lib/util.js | 19 +++++ test/futures/package.json | 15 ++++ test/futures/src/lib.rs | 89 +++++++++++++++++++++++ test/napi/src/js/objects.rs | 4 +- 15 files changed, 457 insertions(+), 16 deletions(-) create mode 100644 test/futures/Cargo.toml create mode 100644 test/futures/README.md create mode 100644 test/futures/lib/channel.js create mode 100644 test/futures/lib/promise.js create mode 100644 test/futures/lib/util.js create mode 100644 test/futures/package.json create mode 100644 test/futures/src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index e0cbbad59..55fa16b90 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,5 +6,5 @@ check-legacy = "check --all-targets --no-default-features -p neon -p neon-runtim clippy-legacy = "clippy --all-targets --no-default-features -p neon -p neon-runtime -p neon-build -p neon-macros -p tests -p static_tests --features event-handler-api,proc-macros,legacy-runtime -- -A clippy::missing_safety_doc" clippy-napi = "clippy --all-targets --no-default-features -p neon -p neon-runtime -p neon-build -p neon-macros -p electron-tests -p napi-tests --features napi-experimental -- -A clippy::missing_safety_doc" neon-test = "test -p neon -p neon-runtime -p neon-build -p neon-macros -p electron-tests -p napi-tests --no-default-features --features=napi-experimental" -neon-doc = "rustdoc --no-default-features --features=napi-experimental -- --cfg docsrs" +neon-doc = "rustdoc --no-default-features --features=futures,napi-experimental -- --cfg docsrs" neon-doc-test = "test --doc --no-default-features --features=napi-experimental" diff --git a/Cargo.toml b/Cargo.toml index c75c09c2c..80917e4bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,12 @@ smallvec = "1.4.2" neon-runtime = { version = "=0.10.0", path = "crates/neon-runtime" } neon-macros = { version = "=0.10.0", path = "crates/neon-macros", optional = true } +[dependencies.futures-channel] +version = "0.3" +default-features = false +features = ["alloc"] +optional = true + [features] default = ["legacy-runtime"] @@ -72,12 +78,16 @@ docs-only = ["neon-runtime/docs-only"] # DEPRECATED: Will be removed with `legacy-runtime` since it is enabled by default in Node-API backend. proc-macros = ["neon-macros"] +# Experimental Rust Futures API +# https://github.com/neon-bindings/rfcs/pull/46 +futures = ["futures-channel"] + [package.metadata.docs.rs] no-default-features = true rustdoc-args = ["--cfg", "docsrs"] features = [ + "futures", "napi-experimental", - "proc-macros", ] [workspace] @@ -89,5 +99,6 @@ members = [ "test/static", "test/electron", "test/dynamic/native", + "test/futures", "test/napi" ] diff --git a/package-lock.json b/package-lock.json index 3a82ef6a2..dd9165bca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3887,6 +3887,10 @@ "resolved": "test/cli", "link": true }, + "node_modules/neon-futures-test": { + "resolved": "test/futures", + "link": true + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -6077,6 +6081,15 @@ "playwright": "^1.17.1" } }, + "test/futures": { + "version": "0.1.0", + "hasInstallScript": true, + "license": "MIT", + "devDependencies": { + "cargo-cp-artifact": "^0.1.0", + "mocha": "^9.1.0" + } + }, "test/napi": { "name": "napi-tests", "version": "0.1.0", @@ -9446,6 +9459,13 @@ } } }, + "neon-futures-test": { + "version": "file:test/futures", + "requires": { + "cargo-cp-artifact": "^0.1.0", + "mocha": "^9.1.0" + } + }, "node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", diff --git a/src/event/channel.rs b/src/event/channel.rs index a9f7636bc..72f77e342 100644 --- a/src/event/channel.rs +++ b/src/event/channel.rs @@ -1,11 +1,31 @@ use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{mpsc, Arc}; +use std::sync::Arc; use neon_runtime::raw::Env; use neon_runtime::tsfn::ThreadsafeFunction; use crate::context::{Context, TaskContext}; -use crate::result::NeonResult; +use crate::result::{NeonResult, ResultExt}; + +#[cfg(feature = "futures")] +use { + futures_channel::oneshot, + std::future::Future, + std::pin::Pin, + std::task::{self, Poll}, +}; + +#[cfg(not(feature = "futures"))] +// Synchronous oneshot channel API compatible with `futures-channel` +mod oneshot { + use std::sync::mpsc; + + pub(super) use std::sync::mpsc::Receiver; + + pub(super) fn channel() -> (mpsc::SyncSender, mpsc::Receiver) { + mpsc::sync_channel(1) + } +} type Callback = Box; @@ -121,7 +141,7 @@ impl Channel { T: Send + 'static, F: FnOnce(TaskContext) -> NeonResult + Send + 'static, { - let (tx, rx) = mpsc::sync_channel(1); + let (tx, rx) = oneshot::channel(); let callback = Box::new(move |env| { let env = unsafe { std::mem::transmute(env) }; @@ -215,16 +235,56 @@ impl Drop for Channel { /// thread with [`Channel::send`]. pub struct JoinHandle { // `Err` is always `Throw`, but `Throw` cannot be sent across threads - rx: mpsc::Receiver>, + rx: oneshot::Receiver>, } impl JoinHandle { /// Waits for the associated closure to finish executing /// /// If the closure panics or throws an exception, `Err` is returned + /// + /// **Warning**: This should not be called from the JavaScript main thread. + /// If it is called from the JavaScript main thread, it will _deadlock_. + #[cfg(any(not(feature = "futures"), docsrs))] + #[cfg_attr(docsrs, doc(cfg(not(feature = "futures"))))] pub fn join(self) -> Result { - self.rx - .recv() + #[cfg(feature = "futures")] + { + unimplemented!("`JoinHandle::join` is not implemented with the `futures` feature") + } + + #[cfg(not(feature = "futures"))] + JoinError::map_res(self.rx.recv()) + } +} + +#[cfg(feature = "futures")] +#[cfg_attr(docsrs, doc(cfg(feature = "futures")))] +impl Future for JoinHandle { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context) -> Poll { + JoinError::map_poll(&mut self.rx, cx) + } +} + +impl JoinError { + #[cfg(feature = "futures")] + // Helper for writing a `Future` implementation by wrapping a `Future` and + // mapping to `Result` + pub(crate) fn map_poll( + f: &mut (impl Future, E>> + Unpin), + cx: &mut task::Context, + ) -> Poll> { + match Pin::new(f).poll(cx) { + Poll::Ready(result) => Poll::Ready(Self::map_res(result)), + Poll::Pending => Poll::Pending, + } + } + + // Helper for mapping a nested `Result` from joining to a `Result` + pub(crate) fn map_res(res: Result, E>) -> Result { + res // If the sending side dropped without sending, it must have panicked .map_err(|_| JoinError(JoinErrorType::Panic))? // If the closure returned `Err`, a JavaScript exception was thrown @@ -237,6 +297,15 @@ impl JoinHandle { /// or threw an exception. pub struct JoinError(JoinErrorType); +impl JoinError { + fn as_str(&self) -> &str { + match &self.0 { + JoinErrorType::Panic => "Closure panicked before returning", + JoinErrorType::Throw => "Closure threw an exception", + } + } +} + #[derive(Debug)] enum JoinErrorType { Panic, @@ -245,15 +314,18 @@ enum JoinErrorType { impl std::fmt::Display for JoinError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { - JoinErrorType::Panic => f.write_str("Closure panicked before returning"), - JoinErrorType::Throw => f.write_str("Closure threw an exception"), - } + f.write_str(self.as_str()) } } impl std::error::Error for JoinError {} +impl ResultExt for Result { + fn or_throw<'a, C: Context<'a>>(self, cx: &mut C) -> NeonResult { + self.or_else(|err| cx.throw_error(err.as_str())) + } +} + /// Error indicating that a closure was unable to be scheduled to execute on the event loop. /// /// The most likely cause of a failure is that Node is shutting down. This may occur if the diff --git a/src/result/mod.rs b/src/result/mod.rs index 2d6b9f519..70d9f1469 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -79,3 +79,13 @@ pub trait JsResultExt<'a, V: Value> { pub trait ResultExt { fn or_throw<'a, C: Context<'a>>(self, cx: &mut C) -> NeonResult; } + +impl<'a, 'b, T, E> ResultExt> for Result, Handle<'b, E>> +where + T: Value, + E: Value, +{ + fn or_throw<'cx, C: Context<'cx>>(self, cx: &mut C) -> JsResult<'a, T> { + self.or_else(|err| cx.throw(err)) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 9771909f1..6692f9f3c 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -122,6 +122,9 @@ pub use self::buffer::types::{JsArrayBuffer, JsBuffer, JsTypedArray}; #[cfg(feature = "napi-5")] pub use self::date::{DateError, DateErrorKind, JsDate}; pub use self::error::JsError; +#[cfg(all(feature = "napi-5", feature = "futures"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "napi-5", feature = "futures"))))] +pub use self::promise::JsFuture; #[cfg(feature = "napi-1")] pub use self::promise::{Deferred, JsPromise}; diff --git a/src/types/promise.rs b/src/types/promise.rs index 8be41f647..a7719f356 100644 --- a/src/types/promise.rs +++ b/src/types/promise.rs @@ -1,5 +1,5 @@ use std::ptr; -#[cfg(feature = "napi-6")] +#[cfg(any(feature = "napi-6", all(feature = "napi-5", feature = "futures")))] use std::sync::Arc; use neon_runtime::no_panic::FailureBoundary; @@ -7,7 +7,10 @@ use neon_runtime::no_panic::FailureBoundary; use neon_runtime::tsfn::ThreadsafeFunction; use neon_runtime::{napi, raw}; -use crate::context::{internal::Env, Context, TaskContext}; +#[cfg(feature = "napi-4")] +use crate::context::TaskContext; +use crate::context::{internal::Env, Context}; +#[cfg(feature = "napi-4")] use crate::event::{Channel, JoinHandle, SendError}; use crate::handle::{internal::TransparentNoCopyWrapper, Managed}; #[cfg(feature = "napi-6")] @@ -15,6 +18,19 @@ use crate::lifecycle::{DropData, InstanceData}; use crate::result::JsResult; use crate::types::{private::ValueInternal, Handle, Object, Value}; +#[cfg(all(feature = "napi-5", feature = "futures"))] +use { + crate::context::internal::ContextInternal, + crate::event::JoinError, + crate::result::NeonResult, + crate::types::{JsFunction, JsValue}, + futures_channel::oneshot, + std::future::Future, + std::pin::Pin, + std::sync::Mutex, + std::task::{self, Poll}, +}; + const BOUNDARY: FailureBoundary = FailureBoundary { both: "A panic and exception occurred while resolving a `neon::types::Deferred`", exception: "An exception occurred while resolving a `neon::types::Deferred`", @@ -39,6 +55,97 @@ impl JsPromise { (deferred, Handle::new_internal(JsPromise(promise))) } + + /// Creates a new `Promise` immediately resolved with the given value. If the value is a + /// `Promise` or a then-able, it will be flattened. + /// + /// `JsPromise::resolve` is useful to ensure a value that might not be a `Promise` or + /// might not be a native promise is converted to a `Promise` before use. + pub fn resolve<'a, C: Context<'a>, T: Value>(cx: &mut C, value: Handle) -> Handle<'a, Self> { + let (deferred, promise) = cx.promise(); + deferred.resolve(cx, value); + promise + } + + /// Creates a nwe `Promise` immediately rejected with the given error. + pub fn reject<'a, C: Context<'a>, E: Value>(cx: &mut C, err: Handle) -> Handle<'a, Self> { + let (deferred, promise) = cx.promise(); + deferred.reject(cx, err); + promise + } + + #[cfg(all(feature = "napi-5", feature = "futures"))] + #[cfg_attr(docsrs, doc(cfg(all(feature = "napi-5", feature = "futures"))))] + /// Creates a [`Future`](std::future::Future) that can be awaited to receive the result of a + /// JavaScript `Promise`. + /// + /// A callback must be provided that maps a `Result` representing the resolution or rejection of + /// the `Promise` and returns a value as the `Future` output. + /// + /// _Note_: Unlike `Future`, `Promise` are eagerly evaluated and so are `JsFuture`. + pub fn to_future<'a, O, C, F>(&self, cx: &mut C, f: F) -> NeonResult> + where + O: Send + 'static, + C: Context<'a>, + F: FnOnce(TaskContext, Result, Handle>) -> NeonResult + + Send + + 'static, + { + let then = self.get::(cx, "then")?; + let catch = self.get::(cx, "catch")?; + + let (tx, rx) = oneshot::channel(); + let take_state = { + // Note: If this becomes a bottleneck, `unsafe` could be used to avoid it. + // The promise spec guarantees that it will only be used once. + let state = Arc::new(Mutex::new(Some((f, tx)))); + + move || { + state + .lock() + .ok() + .and_then(|mut lock| lock.take()) + // This should never happen because `self` is a native `Promise` + // and settling multiple times is a violation of the spec. + .expect("Attempted to settle JsFuture multiple times") + } + }; + + let resolve = JsFunction::new(cx, { + let take_state = take_state.clone(); + + move |mut cx| { + let (f, tx) = take_state(); + let v = cx.argument::(0)?; + + TaskContext::with_context(cx.env(), move |cx| { + // Error indicates that the `Future` has already dropped; ignore + let _ = tx.send(f(cx, Ok(v)).map_err(|_| ())); + }); + + Ok(cx.undefined()) + } + })?; + + let reject = JsFunction::new(cx, { + move |mut cx| { + let (f, tx) = take_state(); + let v = cx.argument::(0)?; + + TaskContext::with_context(cx.env(), move |cx| { + // Error indicates that the `Future` has already dropped; ignore + let _ = tx.send(f(cx, Err(v)).map_err(|_| ())); + }); + + Ok(cx.undefined()) + } + })?; + + then.exec(cx, Handle::new_internal(Self(self.0)), [resolve.upcast()])?; + catch.exec(cx, Handle::new_internal(Self(self.0)), [reject.upcast()])?; + + Ok(JsFuture { rx }) + } } unsafe impl TransparentNoCopyWrapper for JsPromise { @@ -110,6 +217,8 @@ impl Deferred { } } + #[cfg(feature = "napi-4")] + #[cfg_attr(docsrs, doc(cfg(feature = "napi-4")))] /// Settle the [`JsPromise`] by sending a closure across a [`Channel`][`crate::event::Channel`] /// to be executed on the main JavaScript thread. /// @@ -132,6 +241,8 @@ impl Deferred { }) } + #[cfg(feature = "napi-4")] + #[cfg_attr(docsrs, doc(cfg(feature = "napi-4")))] /// Settle the [`JsPromise`] by sending a closure across a [`Channel`][crate::event::Channel] /// to be executed on the main JavaScript thread. /// @@ -224,3 +335,24 @@ impl Drop for Deferred { } } } + +#[cfg(all(feature = "napi-5", feature = "futures"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "napi-5", feature = "futures"))))] +/// A [`Future`](std::future::Future) created from a [`JsPromise`]. +/// +/// Unlike typical `Future`, `JsFuture` are eagerly executed because they +/// are backed by a `Promise`. +pub struct JsFuture { + // `Err` is always `Throw`, but `Throw` cannot be sent across threads + rx: oneshot::Receiver>, +} + +#[cfg(all(feature = "napi-5", feature = "futures"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "napi-5", feature = "futures"))))] +impl Future for JsFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context) -> Poll { + JoinError::map_poll(&mut self.rx, cx) + } +} diff --git a/test/futures/Cargo.toml b/test/futures/Cargo.toml new file mode 100644 index 000000000..cd852f73d --- /dev/null +++ b/test/futures/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "neon-futures-test" +version = "0.1.0" +authors = ["The Neon Community "] +license = "MIT" +exclude = ["artifacts.json", "index.node"] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +once_cell = "1" +tokio = { version = "1", features = ["rt-multi-thread"] } + +[dependencies.neon] +version = "*" +path = "../.." +default-features = false +features = ["futures", "napi-6"] diff --git a/test/futures/README.md b/test/futures/README.md new file mode 100644 index 000000000..67add7d18 --- /dev/null +++ b/test/futures/README.md @@ -0,0 +1,3 @@ +# napi + +Acceptance test suite for Neon with N-API backend diff --git a/test/futures/lib/channel.js b/test/futures/lib/channel.js new file mode 100644 index 000000000..f538917c8 --- /dev/null +++ b/test/futures/lib/channel.js @@ -0,0 +1,26 @@ +const assert = require("assert"); + +const { lazyAsyncAdd } = require(".."); +const { assertRejects } = require("./util"); + +describe("Channel", () => { + it("should be able to await channel result", async () => { + const sum = await lazyAsyncAdd( + () => 1, + () => 2 + ); + + assert.strictEqual(sum, 3); + }); + + it("exceptions should be handled", async () => { + await assertRejects(async () => { + await lazyAsyncAdd( + () => 1, + () => { + throw new Error("Failed to get Y"); + } + ); + }, /exception/i); + }); +}); diff --git a/test/futures/lib/promise.js b/test/futures/lib/promise.js new file mode 100644 index 000000000..6a12d1edc --- /dev/null +++ b/test/futures/lib/promise.js @@ -0,0 +1,21 @@ +const assert = require("assert"); + +const { lazyAsyncSum } = require(".."); +const { assertRejects } = require("./util"); + +describe("JsFuture", () => { + it("should be able to convert a promise to a future", async () => { + const nums = new Float64Array([1, 2, 3, 4]); + const sum = await lazyAsyncSum(async () => nums); + + assert.strictEqual(sum, 10); + }); + + it("should catch promise rejection", async () => { + await assertRejects(async () => { + await lazyAsyncSum(async () => { + throw new Error("Oh, no!"); + }); + }, /exception/i); + }); +}); diff --git a/test/futures/lib/util.js b/test/futures/lib/util.js new file mode 100644 index 000000000..9f90df794 --- /dev/null +++ b/test/futures/lib/util.js @@ -0,0 +1,19 @@ +const assert = require("assert"); + +async function assertRejects(f, ...args) { + try { + await f(); + } catch (err) { + assert.throws(() => { + throw err; + }, ...args); + + return; + } + + assert.throws(() => {}, ...args); +} + +module.exports = { + assertRejects, +}; diff --git a/test/futures/package.json b/test/futures/package.json new file mode 100644 index 000000000..93cb108f0 --- /dev/null +++ b/test/futures/package.json @@ -0,0 +1,15 @@ +{ + "name": "neon-futures-test", + "version": "0.1.0", + "description": "Acceptance test suite for Neon with futures feature", + "author": "The Neon Community", + "license": "MIT", + "scripts": { + "install": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", + "test": "mocha --recursive lib" + }, + "devDependencies": { + "cargo-cp-artifact": "^0.1.0", + "mocha": "^9.1.0" + } +} diff --git a/test/futures/src/lib.rs b/test/futures/src/lib.rs new file mode 100644 index 000000000..b292fcbf1 --- /dev/null +++ b/test/futures/src/lib.rs @@ -0,0 +1,89 @@ +use neon::prelude::*; +use neon::types::buffer::TypedArray; +use once_cell::sync::OnceCell; +use tokio::runtime::Runtime; + +fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> { + static RUNTIME: OnceCell = OnceCell::new(); + + RUNTIME + .get_or_try_init(|| Runtime::new()) + .or_else(|err| cx.throw_error(&err.to_string())) +} + +// Accepts two functions that take no parameters and return numbers. +// Resolves with the sum of the two numbers. +// Purpose: Test the `Future` implementation on `JoinHandle` +fn lazy_async_add(mut cx: FunctionContext) -> JsResult { + let get_x = cx.argument::(0)?.root(&mut cx); + let get_y = cx.argument::(1)?.root(&mut cx); + let channel = cx.channel(); + let runtime = runtime(&mut cx)?; + let (deferred, promise) = cx.promise(); + + runtime.spawn(async move { + let result = channel + .send(move |mut cx| { + let get_x = get_x.into_inner(&mut cx); + let get_y = get_y.into_inner(&mut cx); + + let x: Handle = get_x.call_with(&cx).apply(&mut cx)?; + let y: Handle = get_y.call_with(&cx).apply(&mut cx)?; + + Ok((x.value(&mut cx), y.value(&mut cx))) + }) + .await + .map(|(x, y)| x + y); + + deferred.settle_with(&channel, move |mut cx| { + let result = result.or_throw(&mut cx)?; + + Ok(cx.number(result)) + }) + }); + + Ok(promise) +} + +// Accepts a function that returns a `Promise`. +// Resolves with the sum of all numbers. +// Purpose: Test `JsPromise::to_future`. +fn lazy_async_sum(mut cx: FunctionContext) -> JsResult { + let nums = cx + .argument::(0)? + .call_with(&cx) + .apply::(&mut cx)? + .to_future(&mut cx, |mut cx, nums| { + let nums = nums + .or_throw(&mut cx)? + .downcast_or_throw::, _>(&mut cx)? + .as_slice(&mut cx) + .to_vec(); + + Ok(nums) + })?; + + let (deferred, promise) = cx.promise(); + let channel = cx.channel(); + let runtime = runtime(&mut cx)?; + + runtime.spawn(async move { + let result = nums.await.map(|nums| nums.into_iter().sum::()); + + deferred.settle_with(&channel, move |mut cx| { + let result = result.or_throw(&mut cx)?; + + Ok(cx.number(result)) + }) + }); + + Ok(promise) +} + +#[neon::main] +fn main(mut cx: ModuleContext) -> NeonResult<()> { + cx.export_function("lazyAsyncAdd", lazy_async_add)?; + cx.export_function("lazyAsyncSum", lazy_async_sum)?; + + Ok(()) +} diff --git a/test/napi/src/js/objects.rs b/test/napi/src/js/objects.rs index f94db5ae2..df4bf7aa1 100644 --- a/test/napi/src/js/objects.rs +++ b/test/napi/src/js/objects.rs @@ -1,5 +1,5 @@ use neon::prelude::*; -use neon::types::buffer::TypedArray; +use neon::types::buffer::{BorrowError, TypedArray}; pub fn return_js_global_object(mut cx: FunctionContext) -> JsResult { Ok(cx.global()) @@ -108,7 +108,7 @@ pub fn read_u8_typed_array(mut cx: FunctionContext) -> JsResult { pub fn copy_typed_array(mut cx: FunctionContext) -> JsResult { let source = cx.argument::>(0)?; let mut dest = cx.argument::>(1)?; - let mut run = || { + let mut run = || -> Result<_, BorrowError> { let lock = cx.lock(); let source = source.try_borrow(&lock)?; let mut dest = dest.try_borrow_mut(&lock)?;