diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/01_core.js | 25 | ||||
-rw-r--r-- | core/Cargo.toml | 1 | ||||
-rw-r--r-- | core/bindings.rs | 370 | ||||
-rw-r--r-- | core/error_codes.rs | 2 | ||||
-rw-r--r-- | core/examples/disable_ops.rs | 2 | ||||
-rw-r--r-- | core/examples/hello_world.rs | 34 | ||||
-rw-r--r-- | core/examples/http_bench_json_ops.js | 4 | ||||
-rw-r--r-- | core/examples/http_bench_json_ops.rs | 13 | ||||
-rw-r--r-- | core/examples/schedule_task.rs | 12 | ||||
-rw-r--r-- | core/extensions.rs | 9 | ||||
-rw-r--r-- | core/lib.rs | 31 | ||||
-rw-r--r-- | core/modules.rs | 41 | ||||
-rw-r--r-- | core/ops.rs | 147 | ||||
-rw-r--r-- | core/ops_builtin.rs | 94 | ||||
-rw-r--r-- | core/ops_json.rs | 166 | ||||
-rw-r--r-- | core/ops_metrics.rs | 12 | ||||
-rw-r--r-- | core/runtime.rs | 293 |
17 files changed, 456 insertions, 800 deletions
diff --git a/core/01_core.js b/core/01_core.js index b6c72e5d2..747d69241 100644 --- a/core/01_core.js +++ b/core/01_core.js @@ -16,7 +16,6 @@ ErrorCaptureStackTrace, Promise, ObjectEntries, - ObjectFreeze, ObjectFromEntries, MapPrototypeGet, MapPrototypeDelete, @@ -27,11 +26,12 @@ ObjectAssign, SymbolFor, } = window.__bootstrap.primordials; + const ops = window.Deno.core.ops; + const opIds = Object.keys(ops).reduce((a, v, i) => ({ ...a, [v]: i }), {}); // Available on start due to bindings. - const { opcallSync, opcallAsync, refOp_, unrefOp_ } = window.Deno.core; + const { refOp_, unrefOp_ } = window.Deno.core; - let opsCache = {}; const errorMap = {}; // Builtin v8 / JS errors registerErrorClass("Error", Error); @@ -110,15 +110,6 @@ return promiseRing[idx] != NO_PROMISE; } - function ops() { - return opsCache; - } - - function syncOpsCache() { - // op id 0 is a special value to retrieve the map of registered ops. - opsCache = ObjectFreeze(ObjectFromEntries(opcallSync(0))); - } - function opresolve() { for (let i = 0; i < arguments.length; i += 2) { const promiseId = arguments[i]; @@ -160,7 +151,7 @@ function opAsync(opName, arg1 = null, arg2 = null) { const promiseId = nextPromiseId++; - const maybeError = opcallAsync(opsCache[opName], promiseId, arg1, arg2); + const maybeError = ops[opName](opIds[opName], promiseId, arg1, arg2); // Handle sync error (e.g: error parsing args) if (maybeError) return unwrapOpResult(maybeError); let p = PromisePrototypeThen(setPromise(promiseId), unwrapOpResult); @@ -179,8 +170,8 @@ return p; } - function opSync(opName, arg1 = null, arg2 = null) { - return unwrapOpResult(opcallSync(opsCache[opName], arg1, arg2)); + function opSync(opName, arg1, arg2) { + return unwrapOpResult(ops[opName](opIds[opName], arg1, arg2)); } function refOp(promiseId) { @@ -228,7 +219,7 @@ function metrics() { const [aggregate, perOps] = opSync("op_metrics"); aggregate.ops = ObjectFromEntries(ArrayPrototypeMap( - ObjectEntries(opsCache), + ObjectEntries(opIds), ([opName, opId]) => [opName, perOps[opId]], )); return aggregate; @@ -257,7 +248,6 @@ const core = ObjectAssign(globalThis.Deno.core, { opAsync, opSync, - ops, close, tryClose, read, @@ -269,7 +259,6 @@ registerErrorBuilder, registerErrorClass, opresolve, - syncOpsCache, BadResource, BadResourcePrototype, Interrupted, diff --git a/core/Cargo.toml b/core/Cargo.toml index db18b025e..b8ae6bff4 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,6 +14,7 @@ path = "lib.rs" [dependencies] anyhow = "1.0.55" +deno_ops = { path = "../ops", version = "0.1.0" } futures = "0.3.21" indexmap = "1.7.0" libc = "0.2.106" diff --git a/core/bindings.rs b/core/bindings.rs index f84711abf..4e5c68675 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -1,6 +1,7 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use crate::error::is_instance_of_error; +use crate::extensions::OpPair; use crate::modules::get_module_type_from_assertions; use crate::modules::parse_import_assertions; use crate::modules::validate_import_assertions; @@ -8,21 +9,18 @@ use crate::modules::ImportAssertionsKind; use crate::modules::ModuleMap; use crate::resolve_url_or_path; use crate::JsRuntime; -use crate::Op; -use crate::OpId; -use crate::OpPayload; -use crate::OpResult; -use crate::OpTable; +use crate::OpState; use crate::PromiseId; use crate::ZeroCopyBuf; use anyhow::Error; use log::debug; -use once_cell::sync::Lazy; use serde::Deserialize; use serde::Serialize; use serde_v8::to_v8; use std::cell::RefCell; use std::option::Option; +use std::os::raw::c_void; +use std::rc::Rc; use url::Url; use v8::HandleScope; use v8::Local; @@ -31,88 +29,88 @@ use v8::SharedArrayBuffer; use v8::ValueDeserializerHelper; use v8::ValueSerializerHelper; -const UNDEFINED_OP_ID_MSG: &str = - "invalid op id: received `undefined` instead of an integer. -This error is often caused by a typo in an op name, or not calling -JsRuntime::sync_ops_cache() after JsRuntime initialization."; - -pub static EXTERNAL_REFERENCES: Lazy<v8::ExternalReferences> = - Lazy::new(|| { - v8::ExternalReferences::new(&[ - v8::ExternalReference { - function: opcall_async.map_fn_to(), - }, - v8::ExternalReference { - function: opcall_sync.map_fn_to(), - }, - v8::ExternalReference { - function: ref_op.map_fn_to(), - }, - v8::ExternalReference { - function: unref_op.map_fn_to(), - }, - v8::ExternalReference { - function: set_macrotask_callback.map_fn_to(), - }, - v8::ExternalReference { - function: set_nexttick_callback.map_fn_to(), - }, - v8::ExternalReference { - function: set_promise_reject_callback.map_fn_to(), - }, - v8::ExternalReference { - function: set_uncaught_exception_callback.map_fn_to(), - }, - v8::ExternalReference { - function: run_microtasks.map_fn_to(), - }, - v8::ExternalReference { - function: has_tick_scheduled.map_fn_to(), - }, - v8::ExternalReference { - function: set_has_tick_scheduled.map_fn_to(), - }, - v8::ExternalReference { - function: eval_context.map_fn_to(), - }, - v8::ExternalReference { - function: queue_microtask.map_fn_to(), - }, - v8::ExternalReference { - function: create_host_object.map_fn_to(), - }, - v8::ExternalReference { - function: encode.map_fn_to(), - }, - v8::ExternalReference { - function: decode.map_fn_to(), - }, - v8::ExternalReference { - function: serialize.map_fn_to(), - }, - v8::ExternalReference { - function: deserialize.map_fn_to(), - }, - v8::ExternalReference { - function: get_promise_details.map_fn_to(), - }, - v8::ExternalReference { - function: get_proxy_details.map_fn_to(), - }, - v8::ExternalReference { - function: is_proxy.map_fn_to(), - }, - v8::ExternalReference { - function: memory_usage.map_fn_to(), - }, - v8::ExternalReference { - function: call_console.map_fn_to(), - }, - v8::ExternalReference { - function: set_wasm_streaming_callback.map_fn_to(), - }, - ]) +pub fn external_references( + ops: &[OpPair], + op_state: Rc<RefCell<OpState>>, +) -> v8::ExternalReferences { + let mut refs = vec![ + v8::ExternalReference { + function: ref_op.map_fn_to(), + }, + v8::ExternalReference { + function: unref_op.map_fn_to(), + }, + v8::ExternalReference { + function: set_macrotask_callback.map_fn_to(), + }, + v8::ExternalReference { + function: set_nexttick_callback.map_fn_to(), + }, + v8::ExternalReference { + function: set_promise_reject_callback.map_fn_to(), + }, + v8::ExternalReference { + function: set_uncaught_exception_callback.map_fn_to(), + }, + v8::ExternalReference { + function: run_microtasks.map_fn_to(), + }, + v8::ExternalReference { + function: has_tick_scheduled.map_fn_to(), + }, + v8::ExternalReference { + function: set_has_tick_scheduled.map_fn_to(), + }, + v8::ExternalReference { + function: eval_context.map_fn_to(), + }, + v8::ExternalReference { + function: queue_microtask.map_fn_to(), + }, + v8::ExternalReference { + function: create_host_object.map_fn_to(), + }, + v8::ExternalReference { + function: encode.map_fn_to(), + }, + v8::ExternalReference { + function: decode.map_fn_to(), + }, + v8::ExternalReference { + function: serialize.map_fn_to(), + }, + v8::ExternalReference { + function: deserialize.map_fn_to(), + }, + v8::ExternalReference { + function: get_promise_details.map_fn_to(), + }, + v8::ExternalReference { + function: get_proxy_details.map_fn_to(), + }, + v8::ExternalReference { + function: is_proxy.map_fn_to(), + }, + v8::ExternalReference { + function: memory_usage.map_fn_to(), + }, + v8::ExternalReference { + function: call_console.map_fn_to(), + }, + v8::ExternalReference { + function: set_wasm_streaming_callback.map_fn_to(), + }, + ]; + let op_refs = ops + .iter() + .map(|(_, opref)| v8::ExternalReference { function: *opref }); + refs.extend(op_refs); + let raw_op_state = Rc::as_ptr(&op_state) as *mut c_void; + refs.push(v8::ExternalReference { + pointer: raw_op_state, }); + v8::ExternalReferences::new(&refs) +} pub fn script_origin<'a>( s: &mut v8::HandleScope<'a>, @@ -154,6 +152,9 @@ pub fn module_origin<'a>( pub fn initialize_context<'s>( scope: &mut v8::HandleScope<'s, ()>, + ops: &[OpPair], + snapshot_loaded: bool, + op_state: Rc<RefCell<OpState>>, ) -> v8::Local<'s, v8::Context> { let scope = &mut v8::EscapableHandleScope::new(scope); @@ -162,17 +163,43 @@ pub fn initialize_context<'s>( let scope = &mut v8::ContextScope::new(scope, context); - // global.Deno = { core: {} }; let deno_key = v8::String::new(scope, "Deno").unwrap(); + let core_key = v8::String::new(scope, "core").unwrap(); + let ops_key = v8::String::new(scope, "ops").unwrap(); + // Snapshot already registered `Deno.core.ops` but + // extensions may provide ops that aren't part of the snapshot. + // + // TODO(@littledivy): This is extra complexity for + // a really weird usecase. Remove this once all + // tsc ops are static at snapshot time. + if snapshot_loaded { + // Grab Deno.core.ops object + let deno_val = global.get(scope, deno_key.into()).unwrap(); + let deno_val = v8::Local::<v8::Object>::try_from(deno_val) + .expect("`Deno` not in global scope."); + let core_val = deno_val.get(scope, core_key.into()).unwrap(); + let core_val = v8::Local::<v8::Object>::try_from(core_val) + .expect("`Deno.core` not in global scope"); + let ops_val = core_val.get(scope, ops_key.into()).unwrap(); + let ops_val = v8::Local::<v8::Object>::try_from(ops_val) + .expect("`Deno.core.ops` not in global scope"); + + let raw_op_state = Rc::as_ptr(&op_state) as *const c_void; + for (name, opfn) in ops { + set_func_raw(scope, ops_val, name, *opfn, raw_op_state); + } + return scope.escape(context); + } + + // global.Deno = { core: { ops: {} } }; let deno_val = v8::Object::new(scope); global.set(scope, deno_key.into(), deno_val.into()); - let core_key = v8::String::new(scope, "core").unwrap(); let core_val = v8::Object::new(scope); deno_val.set(scope, core_key.into(), core_val.into()); + let ops_val = v8::Object::new(scope); + core_val.set(scope, ops_key.into(), ops_val.into()); // Bind functions to Deno.core.* - set_func(scope, core_val, "opcallSync", opcall_sync); - set_func(scope, core_val, "opcallAsync", opcall_async); set_func(scope, core_val, "refOp_", ref_op); set_func(scope, core_val, "unrefOp_", unref_op); set_func( @@ -227,10 +254,14 @@ pub fn initialize_context<'s>( // Direct bindings on `window`. set_func(scope, global, "queueMicrotask", queue_microtask); + // Bind functions to Deno.core.ops.* + let raw_op_state = Rc::as_ptr(&op_state) as *const c_void; + for (name, opfn) in ops { + set_func_raw(scope, ops_val, name, *opfn, raw_op_state); + } scope.escape(context) } -#[inline(always)] pub fn set_func( scope: &mut v8::HandleScope<'_>, obj: v8::Local<v8::Object>, @@ -238,8 +269,26 @@ pub fn set_func( callback: impl v8::MapFnTo<v8::FunctionCallback>, ) { let key = v8::String::new(scope, name).unwrap(); - let tmpl = v8::FunctionTemplate::new(scope, callback); - let val = tmpl.get_function(scope).unwrap(); + let val = v8::Function::new(scope, callback).unwrap(); + val.set_name(key); + obj.set(scope, key.into(), val.into()); +} + +// Register a raw v8::FunctionCallback +// with some external data. +pub fn set_func_raw( + scope: &mut v8::HandleScope<'_>, + obj: v8::Local<v8::Object>, + name: &'static str, + callback: v8::FunctionCallback, + external_data: *const c_void, +) { + let key = v8::String::new(scope, name).unwrap(); + let external = v8::External::new(scope, external_data as *mut c_void); + let val = v8::Function::builder_raw(callback) + .data(external.into()) + .build(scope) + .unwrap(); val.set_name(key); obj.set(scope, key.into(), val.into()); } @@ -460,137 +509,6 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) { } } -fn opcall_sync<'s>( - scope: &mut v8::HandleScope<'s>, - args: v8::FunctionCallbackArguments, - mut rv: v8::ReturnValue, -) { - let state_rc = JsRuntime::state(scope); - let state = state_rc.borrow_mut(); - - let op_id = match v8::Local::<v8::Integer>::try_from(args.get(0)) - .map(|l| l.value() as OpId) - .map_err(Error::from) - { - Ok(op_id) => op_id, - Err(err) => { - let msg = if args.get(0).is_undefined() { - UNDEFINED_OP_ID_MSG.to_string() - } else { - format!("invalid op id: {}", err) - }; - throw_type_error(scope, msg); - return; - } - }; - - // opcall(0) returns obj of all ops, handle as special case - if op_id == 0 { - // TODO: Serialize as HashMap when serde_v8 supports maps ... - let ops = OpTable::op_entries(state.op_state.clone()); - rv.set(to_v8(scope, ops).unwrap()); - return; - } - - // Deserializable args (may be structured args or ZeroCopyBuf) - let a = args.get(1); - let b = args.get(2); - - let payload = OpPayload { - scope, - a, - b, - op_id, - promise_id: 0, - }; - let op = OpTable::route_op(op_id, state.op_state.clone(), payload); - match op { - Op::Sync(result) => { - state.op_state.borrow().tracker.track_sync(op_id); - rv.set(result.to_v8(scope).unwrap()); - } - Op::NotFound => { - throw_type_error(scope, format!("Unknown op id: {}", op_id)); - } - // Async ops (ref or unref) - _ => { - throw_type_error( - scope, - format!("Can not call an async op [{}] with opSync()", op_id), - ); - } - } -} - -fn opcall_async<'s>( - scope: &mut v8::HandleScope<'s>, - args: v8::FunctionCallbackArguments, - mut rv: v8::ReturnValue, -) { - let state_rc = JsRuntime::state(scope); - let mut state = state_rc.borrow_mut(); - - let op_id = match v8::Local::<v8::Integer>::try_from(args.get(0)) - .map(|l| l.value() as OpId) - .map_err(Error::from) - { - Ok(op_id) => op_id, - Err(err) => { - let msg = if args.get(0).is_undefined() { - UNDEFINED_OP_ID_MSG.to_string() - } else { - format!("invalid op id: {}", err) - }; - throw_type_error(scope, msg); - return; - } - }; - - // PromiseId - let arg1 = args.get(1); - let promise_id = v8::Local::<v8::Integer>::try_from(arg1) - .map(|l| l.value() as PromiseId) - .map_err(Error::from); - // Fail if promise id invalid (not an int) - let promise_id: PromiseId = match promise_id { - Ok(promise_id) => promise_id, - Err(err) => { - throw_type_error(scope, format!("invalid promise id: {}", err)); - return; - } - }; - - // Deserializable args (may be structured args or ZeroCopyBuf) - let a = args.get(2); - let b = args.get(3); - - let payload = OpPayload { - scope, - a, - b, - op_id, - promise_id, - }; - let op = OpTable::route_op(op_id, state.op_state.clone(), payload); - match op { - Op::Sync(result) => match result { - OpResult::Ok(_) => throw_type_error( - scope, - format!("Can not call a sync op [{}] with opAsync()", op_id), - ), - OpResult::Err(_) => rv.set(result.to_v8(scope).unwrap()), - }, - Op::Async(fut) => { - state.op_state.borrow().tracker.track_async(op_id); - state.pending_ops.push(fut); - state.have_unpolled_ops = true; - } - Op::NotFound => { - throw_type_error(scope, format!("Unknown op id: {}", op_id)); - } - } -} - fn ref_op<'s>( scope: &mut v8::HandleScope<'s>, args: v8::FunctionCallbackArguments, @@ -1471,7 +1389,7 @@ fn is_proxy( rv.set(v8::Boolean::new(scope, args.get(0).is_proxy()).into()) } -fn throw_type_error(scope: &mut v8::HandleScope, message: impl AsRef<str>) { +pub fn throw_type_error(scope: &mut v8::HandleScope, message: impl AsRef<str>) { let message = v8::String::new(scope, message.as_ref()).unwrap(); let exception = v8::Exception::type_error(scope, message); scope.throw_exception(exception); diff --git a/core/error_codes.rs b/core/error_codes.rs index 43a6c4391..b34cbe639 100644 --- a/core/error_codes.rs +++ b/core/error_codes.rs @@ -1,6 +1,6 @@ use anyhow::Error; -pub(crate) fn get_error_code(err: &Error) -> Option<&'static str> { +pub fn get_error_code(err: &Error) -> Option<&'static str> { err .downcast_ref::<std::io::Error>() .map(|e| match e.raw_os_error() { diff --git a/core/examples/disable_ops.rs b/core/examples/disable_ops.rs index 0612845c8..9565d6e7f 100644 --- a/core/examples/disable_ops.rs +++ b/core/examples/disable_ops.rs @@ -9,7 +9,7 @@ use deno_core::RuntimeOptions; fn main() { let my_ext = Extension::builder() .middleware(|name, opfn| match name { - "op_print" => deno_core::void_op_sync(), + "op_print" => deno_core::void_op_sync::v8_cb(), _ => opfn, }) .build(); diff --git a/core/examples/hello_world.rs b/core/examples/hello_world.rs index 42c9779f3..bfca5447c 100644 --- a/core/examples/hello_world.rs +++ b/core/examples/hello_world.rs @@ -2,27 +2,37 @@ //! This example shows you how to define ops in Rust and then call them from //! JavaScript. -use deno_core::op_sync; +use deno_core::op; use deno_core::Extension; use deno_core::JsRuntime; +use deno_core::OpState; use deno_core::RuntimeOptions; +// This is a hack to make the `#[op]` macro work with +// deno_core examples. +// You can remove this: +use deno_core::*; + +#[op] +fn op_sum( + _state: &mut OpState, + nums: Vec<f64>, + _: (), +) -> Result<f64, deno_core::error::AnyError> { + // Sum inputs + let sum = nums.iter().fold(0.0, |a, v| a + v); + // return as a Result<f64, AnyError> + Ok(sum) +} + fn main() { // Build a deno_core::Extension providing custom ops let ext = Extension::builder() .ops(vec![ // An op for summing an array of numbers - ( - "op_sum", - // The op-layer automatically deserializes inputs - // and serializes the returned Result & value - op_sync(|_state, nums: Vec<f64>, _: ()| { - // Sum inputs - let sum = nums.iter().fold(0.0, |a, v| a + v); - // return as a Result<f64, AnyError> - Ok(sum) - }), - ), + // The op-layer automatically deserializes inputs + // and serializes the returned Result & value + op_sum::decl(), ]) .build(); diff --git a/core/examples/http_bench_json_ops.js b/core/examples/http_bench_json_ops.js index 22e1b468c..5b16776f4 100644 --- a/core/examples/http_bench_json_ops.js +++ b/core/examples/http_bench_json_ops.js @@ -11,12 +11,12 @@ const responseBuf = new Uint8Array( /** Listens on 0.0.0.0:4500, returns rid. */ function listen() { - return Deno.core.opSync("listen"); + return Deno.core.opSync("op_listen"); } /** Accepts a connection, returns rid. */ function accept(serverRid) { - return Deno.core.opAsync("accept", serverRid); + return Deno.core.opAsync("op_accept", serverRid); } async function serve(rid) { diff --git a/core/examples/http_bench_json_ops.rs b/core/examples/http_bench_json_ops.rs index 9e60df4a4..3f608eeae 100644 --- a/core/examples/http_bench_json_ops.rs +++ b/core/examples/http_bench_json_ops.rs @@ -1,5 +1,6 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use deno_core::anyhow::Error; +use deno_core::op; use deno_core::AsyncRefCell; use deno_core::AsyncResult; use deno_core::CancelHandle; @@ -17,6 +18,11 @@ use std::rc::Rc; use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; +// This is a hack to make the `#[op]` macro work with +// deno_core examples. +// You can remove this: +use deno_core::*; + struct Logger; impl log::Log for Logger { @@ -119,10 +125,7 @@ impl From<tokio::net::TcpStream> for TcpStream { fn create_js_runtime() -> JsRuntime { let ext = deno_core::Extension::builder() - .ops(vec![ - ("listen", deno_core::op_sync(op_listen)), - ("accept", deno_core::op_async(op_accept)), - ]) + .ops(vec![op_listen::decl(), op_accept::decl()]) .build(); JsRuntime::new(deno_core::RuntimeOptions { @@ -131,6 +134,7 @@ fn create_js_runtime() -> JsRuntime { }) } +#[op] fn op_listen(state: &mut OpState, _: (), _: ()) -> Result<ResourceId, Error> { log::debug!("listen"); let addr = "127.0.0.1:4544".parse::<SocketAddr>().unwrap(); @@ -141,6 +145,7 @@ fn op_listen(state: &mut OpState, _: (), _: ()) -> Result<ResourceId, Error> { Ok(rid) } +#[op] async fn op_accept( state: Rc<RefCell<OpState>>, rid: ResourceId, diff --git a/core/examples/schedule_task.rs b/core/examples/schedule_task.rs index 2f4909b4f..3ada86417 100644 --- a/core/examples/schedule_task.rs +++ b/core/examples/schedule_task.rs @@ -1,6 +1,7 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use deno_core::anyhow::Error; +use deno_core::op; use deno_core::Extension; use deno_core::JsRuntime; use deno_core::OpState; @@ -9,14 +10,16 @@ use futures::channel::mpsc; use futures::stream::StreamExt; use std::task::Poll; +// This is a hack to make the `#[op]` macro work with +// deno_core examples. +// You can remove this: +use deno_core::*; + type Task = Box<dyn FnOnce()>; fn main() { let my_ext = Extension::builder() - .ops(vec![( - "op_schedule_task", - deno_core::op_sync(op_schedule_task), - )]) + .ops(vec![op_schedule_task::decl()]) .event_loop_middleware(|state, cx| { let recv = state.borrow_mut::<mpsc::UnboundedReceiver<Task>>(); let mut ref_loop = false; @@ -58,6 +61,7 @@ fn main() { runtime.block_on(future).unwrap(); } +#[op] fn op_schedule_task(state: &mut OpState, i: u8, _: ()) -> Result<(), Error> { let tx = state.borrow_mut::<mpsc::UnboundedSender<Task>>(); tx.unbounded_send(Box::new(move || println!("Hello, world! x{}", i))) diff --git a/core/extensions.rs b/core/extensions.rs index 031cb073a..7361165f0 100644 --- a/core/extensions.rs +++ b/core/extensions.rs @@ -1,12 +1,13 @@ -use crate::OpFn; +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use crate::OpState; use anyhow::Error; use std::task::Context; pub type SourcePair = (&'static str, Box<SourceLoadFn>); pub type SourceLoadFn = dyn Fn() -> Result<String, Error>; -pub type OpPair = (&'static str, Box<OpFn>); -pub type OpMiddlewareFn = dyn Fn(&'static str, Box<OpFn>) -> Box<OpFn>; +pub type OpFnRef = v8::FunctionCallback; +pub type OpPair = (&'static str, OpFnRef); +pub type OpMiddlewareFn = dyn Fn(&'static str, OpFnRef) -> OpFnRef; pub type OpStateFn = dyn Fn(&mut OpState) -> Result<(), Error>; pub type OpEventLoopFn = dyn Fn(&mut OpState, &mut Context) -> bool; @@ -108,7 +109,7 @@ impl ExtensionBuilder { pub fn middleware<F>(&mut self, middleware_fn: F) -> &mut Self where - F: Fn(&'static str, Box<OpFn>) -> Box<OpFn> + 'static, + F: Fn(&'static str, OpFnRef) -> OpFnRef + 'static, { self.middleware = Some(Box::new(middleware_fn)); self diff --git a/core/lib.rs b/core/lib.rs index 2f46c1ffc..29cdbba01 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -13,7 +13,6 @@ mod modules; mod normalize_path; mod ops; mod ops_builtin; -mod ops_json; mod ops_metrics; mod resources; mod runtime; @@ -44,6 +43,10 @@ pub use crate::async_cell::AsyncRefCell; pub use crate::async_cell::AsyncRefFuture; pub use crate::async_cell::RcLike; pub use crate::async_cell::RcRef; +pub use crate::extensions::Extension; +pub use crate::extensions::ExtensionBuilder; +pub use crate::extensions::OpMiddlewareFn; +pub use crate::extensions::OpPair; pub use crate::flags::v8_set_flags; pub use crate::inspector::InspectorMsg; pub use crate::inspector::InspectorMsgKind; @@ -65,24 +68,21 @@ pub use crate::modules::ModuleSourceFuture; pub use crate::modules::ModuleType; pub use crate::modules::NoopModuleLoader; pub use crate::normalize_path::normalize_path; -pub use crate::ops::serialize_op_result; pub use crate::ops::Op; pub use crate::ops::OpAsyncFuture; pub use crate::ops::OpCall; +pub use crate::ops::OpError; pub use crate::ops::OpFn; pub use crate::ops::OpId; -pub use crate::ops::OpPayload; pub use crate::ops::OpResult; pub use crate::ops::OpState; -pub use crate::ops::OpTable; pub use crate::ops::PromiseId; pub use crate::ops_builtin::op_close; pub use crate::ops_builtin::op_print; pub use crate::ops_builtin::op_resources; -pub use crate::ops_json::op_async; -pub use crate::ops_json::op_sync; -pub use crate::ops_json::void_op_async; -pub use crate::ops_json::void_op_sync; +pub use crate::ops_builtin::void_op_async; +pub use crate::ops_builtin::void_op_sync; +pub use crate::ops_metrics::OpsTracker; pub use crate::resources::AsyncResult; pub use crate::resources::Resource; pub use crate::resources::ResourceId; @@ -95,16 +95,21 @@ pub use crate::runtime::JsRuntime; pub use crate::runtime::RuntimeOptions; pub use crate::runtime::SharedArrayBufferStore; pub use crate::runtime::Snapshot; -// pub use crate::runtime_modules::include_js_files!; -pub use crate::extensions::Extension; -pub use crate::extensions::ExtensionBuilder; -pub use crate::extensions::OpMiddlewareFn; -pub use crate::extensions::OpPair; +pub use deno_ops::op; pub fn v8_version() -> &'static str { v8::V8::get_version() } +/// An internal module re-exporting funcs used by the #[op] (`deno_ops`) macro +#[doc(hidden)] +pub mod _ops { + pub use super::bindings::throw_type_error; + pub use super::error_codes::get_error_code; + pub use super::ops::to_op_result; + pub use super::runtime::queue_async_op; +} + /// A helper macro that will return a call site in Rust code. Should be /// used when executing internal one-line scripts for JsRuntime lifecycle. /// diff --git a/core/modules.rs b/core/modules.rs index d4a0a86ef..7c5274193 100644 --- a/core/modules.rs +++ b/core/modules.rs @@ -1073,13 +1073,11 @@ impl ModuleMap { #[cfg(test)] mod tests { use super::*; - use crate::ops::OpCall; - use crate::serialize_op_result; + use crate::error::AnyError; use crate::Extension; use crate::JsRuntime; - use crate::Op; - use crate::OpPayload; use crate::RuntimeOptions; + use deno_ops::op; use futures::future::FutureExt; use parking_lot::Mutex; use std::fmt; @@ -1088,6 +1086,10 @@ mod tests { use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; + // deno_ops macros generate code assuming deno_core in scope. + mod deno_core { + pub use crate::*; + } // TODO(ry) Sadly FuturesUnordered requires the current task to be set. So // even though we are only using poll() in these tests and not Tokio, we must @@ -1401,20 +1403,16 @@ import "/a.js"; let loader = Rc::new(ModsLoader::default()); let resolve_count = loader.count.clone(); - let dispatch_count = Arc::new(AtomicUsize::new(0)); - let dispatch_count_ = dispatch_count.clone(); + static DISPATCH_COUNT: AtomicUsize = AtomicUsize::new(0); - let op_test = move |state, payload: OpPayload| -> Op { - dispatch_count_.fetch_add(1, Ordering::Relaxed); - let (control, _): (u8, ()) = payload.deserialize().unwrap(); + #[op] + fn op_test(_: &mut OpState, control: u8, _: ()) -> Result<u8, AnyError> { + DISPATCH_COUNT.fetch_add(1, Ordering::Relaxed); assert_eq!(control, 42); - let resp = (0, 1, serialize_op_result(Ok(43), state)); - Op::Async(OpCall::ready(resp)) - }; + Ok(43) + } - let ext = Extension::builder() - .ops(vec![("op_test", Box::new(op_test))]) - .build(); + let ext = Extension::builder().ops(vec![op_test::decl()]).build(); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![ext], @@ -1435,7 +1433,7 @@ import "/a.js"; ) .unwrap(); - assert_eq!(dispatch_count.load(Ordering::Relaxed), 0); + assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0); let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); @@ -1452,12 +1450,12 @@ import "/a.js"; import { b } from './b.js' if (b() != 'b') throw Error(); let control = 42; - Deno.core.opAsync("op_test", control); + Deno.core.opSync("op_test", control); "#, ) .unwrap(); - assert_eq!(dispatch_count.load(Ordering::Relaxed), 0); + assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0); let imports = module_map.get_requested_modules(mod_a); assert_eq!( imports, @@ -1481,14 +1479,14 @@ import "/a.js"; }; runtime.instantiate_module(mod_b).unwrap(); - assert_eq!(dispatch_count.load(Ordering::Relaxed), 0); + assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0); assert_eq!(resolve_count.load(Ordering::SeqCst), 1); runtime.instantiate_module(mod_a).unwrap(); - assert_eq!(dispatch_count.load(Ordering::Relaxed), 0); + assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0); let _ = runtime.mod_evaluate(mod_a); - assert_eq!(dispatch_count.load(Ordering::Relaxed), 1); + assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 1); } #[test] @@ -1766,7 +1764,6 @@ import "/a.js"; module_loader: Some(loader), ..Default::default() }); - runtime.sync_ops_cache(); runtime .execute_script( "file:///dyn_import3.js", diff --git a/core/ops.rs b/core/ops.rs index 50197c9be..42718b8ff 100644 --- a/core/ops.rs +++ b/core/ops.rs @@ -1,10 +1,9 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. -use crate::error::type_error; use crate::gotham_state::GothamState; -use crate::ops_metrics::OpsTracker; use crate::resources::ResourceTable; use crate::runtime::GetErrorClassFn; +use crate::OpsTracker; use anyhow::Error; use futures::future::maybe_done; use futures::future::FusedFuture; @@ -12,23 +11,19 @@ use futures::future::MaybeDone; use futures::ready; use futures::task::noop_waker; use futures::Future; -use indexmap::IndexMap; -use serde::de::DeserializeOwned; use serde::Serialize; -use std::cell::RefCell; use std::cell::UnsafeCell; -use std::iter::once; use std::ops::Deref; use std::ops::DerefMut; use std::pin::Pin; -use std::rc::Rc; use std::task::Context; use std::task::Poll; /// Wrapper around a Future, which causes that Future to be polled immediately. -/// (Background: ops are stored in a `FuturesUnordered` structure which polls +/// +/// Background: ops are stored in a `FuturesUnordered` structure which polls /// them, but without the `OpCall` wrapper this doesn't happen until the next -/// turn of the event loop, which is too late for certain ops.) +/// turn of the event loop, which is too late for certain ops. pub struct OpCall<T>(MaybeDone<Pin<Box<dyn Future<Output = T>>>>); impl<T> OpCall<T> { @@ -83,32 +78,10 @@ where pub type PromiseId = i32; pub type OpAsyncFuture = OpCall<(PromiseId, OpId, OpResult)>; -pub type OpFn = dyn Fn(Rc<RefCell<OpState>>, OpPayload) -> Op + 'static; +pub type OpFn = + fn(&mut v8::HandleScope, v8::FunctionCallbackArguments, v8::ReturnValue); pub type OpId = usize; -pub struct OpPayload<'a, 'b, 'c> { - pub(crate) scope: &'a mut v8::HandleScope<'b>, - pub(crate) a: v8::Local<'c, v8::Value>, - pub(crate) b: v8::Local<'c, v8::Value>, - pub(crate) op_id: OpId, - pub(crate) promise_id: PromiseId, -} - -impl<'a, 'b, 'c> OpPayload<'a, 'b, 'c> { - pub fn deserialize<T: DeserializeOwned, U: DeserializeOwned>( - self, - ) -> Result<(T, U), Error> { - let a: T = serde_v8::from_v8(self.scope, self.a) - .map_err(Error::from) - .map_err(|e| type_error(format!("Error parsing args: {}", e)))?; - - let b: U = serde_v8::from_v8(self.scope, self.b) - .map_err(Error::from) - .map_err(|e| type_error(format!("Error parsing args: {}", e)))?; - Ok((a, b)) - } -} - pub enum Op { Sync(OpResult), Async(OpAsyncFuture), @@ -141,39 +114,43 @@ pub struct OpError { code: Option<&'static str>, } -pub fn serialize_op_result<R: Serialize + 'static>( +impl OpError { + pub fn new(get_class: GetErrorClassFn, err: Error) -> Self { + Self { + class_name: (get_class)(&err), + message: err.to_string(), + code: crate::error_codes::get_error_code(&err), + } + } +} + +pub fn to_op_result<R: Serialize + 'static>( + get_class: GetErrorClassFn, result: Result<R, Error>, - state: Rc<RefCell<OpState>>, ) -> OpResult { match result { Ok(v) => OpResult::Ok(v.into()), - Err(err) => OpResult::Err(OpError { - class_name: (state.borrow().get_error_class_fn)(&err), - message: err.to_string(), - code: crate::error_codes::get_error_code(&err), - }), + Err(err) => OpResult::Err(OpError::new(get_class, err)), } } /// Maintains the resources and ops inside a JS runtime. pub struct OpState { pub resource_table: ResourceTable, - pub op_table: OpTable, pub get_error_class_fn: GetErrorClassFn, - pub(crate) tracker: OpsTracker, + pub tracker: OpsTracker, gotham_state: GothamState, } impl OpState { - pub(crate) fn new() -> OpState { + pub fn new(ops_count: usize) -> OpState { OpState { resource_table: Default::default(), - op_table: OpTable::default(), get_error_class_fn: &|_| "Error", + gotham_state: Default::default(), tracker: OpsTracker { - ops: UnsafeCell::new(Vec::with_capacity(256)), + ops: UnsafeCell::new(vec![Default::default(); ops_count]), }, - gotham_state: Default::default(), } } } @@ -191,81 +168,3 @@ impl DerefMut for OpState { &mut self.gotham_state } } - -/// Collection for storing registered ops. The special 'get_op_catalog' -/// op with OpId `0` is automatically added when the OpTable is created. -pub struct OpTable(IndexMap<String, Rc<OpFn>>); - -impl OpTable { - pub fn register_op<F>(&mut self, name: &str, op_fn: F) -> OpId - where - F: Fn(Rc<RefCell<OpState>>, OpPayload) -> Op + 'static, - { - let (op_id, prev) = self.0.insert_full(name.to_owned(), Rc::new(op_fn)); - assert!(prev.is_none()); - op_id - } - - pub fn op_entries(state: Rc<RefCell<OpState>>) -> Vec<(String, OpId)> { - state.borrow().op_table.0.keys().cloned().zip(0..).collect() - } - - pub fn route_op( - op_id: OpId, - state: Rc<RefCell<OpState>>, - payload: OpPayload, - ) -> Op { - let op_fn = state - .borrow() - .op_table - .0 - .get_index(op_id) - .map(|(_, op_fn)| op_fn.clone()); - match op_fn { - Some(f) => (f)(state, payload), - None => Op::NotFound, - } - } -} - -impl Default for OpTable { - fn default() -> Self { - fn dummy(_state: Rc<RefCell<OpState>>, _p: OpPayload) -> Op { - unreachable!() - } - Self(once(("ops".to_owned(), Rc::new(dummy) as _)).collect()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn op_table() { - let state = Rc::new(RefCell::new(OpState::new())); - - let foo_id; - let bar_id; - { - let op_table = &mut state.borrow_mut().op_table; - foo_id = - op_table.register_op("foo", |_, _| Op::Sync(OpResult::Ok(321.into()))); - assert_eq!(foo_id, 1); - bar_id = - op_table.register_op("bar", |_, _| Op::Sync(OpResult::Ok(123.into()))); - assert_eq!(bar_id, 2); - } - - let mut catalog_entries = OpTable::op_entries(state); - catalog_entries.sort_by(|(_, id1), (_, id2)| id1.partial_cmp(id2).unwrap()); - assert_eq!( - catalog_entries, - vec![ - ("ops".to_owned(), 0), - ("foo".to_owned(), 1), - ("bar".to_owned(), 2) - ] - ); - } -} diff --git a/core/ops_builtin.rs b/core/ops_builtin.rs index d759bc5ba..14d8c6e39 100644 --- a/core/ops_builtin.rs +++ b/core/ops_builtin.rs @@ -1,16 +1,13 @@ use crate::error::type_error; use crate::include_js_files; -use crate::op_async; -use crate::op_sync; use crate::ops_metrics::OpMetrics; use crate::resources::ResourceId; -use crate::void_op_async; -use crate::void_op_sync; use crate::Extension; use crate::OpState; use crate::Resource; use crate::ZeroCopyBuf; use anyhow::Error; +use deno_ops::op; use std::cell::RefCell; use std::io::{stderr, stdout, Write}; use std::rc::Rc; @@ -24,29 +21,40 @@ pub(crate) fn init_builtins() -> Extension { "02_error.js", )) .ops(vec![ - ("op_close", op_sync(op_close)), - ("op_try_close", op_sync(op_try_close)), - ("op_print", op_sync(op_print)), - ("op_resources", op_sync(op_resources)), - ("op_wasm_streaming_feed", op_sync(op_wasm_streaming_feed)), - ("op_wasm_streaming_abort", op_sync(op_wasm_streaming_abort)), - ( - "op_wasm_streaming_set_url", - op_sync(op_wasm_streaming_set_url), - ), - ("op_metrics", op_sync(op_metrics)), - ("op_void_sync", void_op_sync()), - ("op_void_async", void_op_async()), - // TODO(@AaronO): track IO metrics for builtin streams - ("op_read", op_async(op_read)), - ("op_write", op_async(op_write)), - ("op_shutdown", op_async(op_shutdown)), + op_close::decl(), + op_try_close::decl(), + op_print::decl(), + op_resources::decl(), + op_wasm_streaming_feed::decl(), + op_wasm_streaming_abort::decl(), + op_wasm_streaming_set_url::decl(), + op_void_sync::decl(), + op_void_async::decl(), + // // TODO(@AaronO): track IO metrics for builtin streams + op_read::decl(), + op_write::decl(), + op_shutdown::decl(), + op_metrics::decl(), ]) .build() } +#[op] +pub fn void_op_sync(_: &mut OpState, _: (), _: ()) -> Result<(), Error> { + Ok(()) +} + +pub async fn void_op_async( + _state: Rc<RefCell<OpState>>, + _: (), + _: (), +) -> Result<(), Error> { + Ok(()) +} + /// Return map of resources with id as key /// and string representation as value. +#[op] pub fn op_resources( state: &mut OpState, _: (), @@ -60,7 +68,22 @@ pub fn op_resources( Ok(serialized_resources) } +#[op] +pub fn op_void_sync(_state: &mut OpState, _: (), _: ()) -> Result<(), Error> { + Ok(()) +} + +#[op] +pub async fn op_void_async( + _state: Rc<RefCell<OpState>>, + _: (), + _: (), +) -> Result<(), Error> { + Ok(()) +} + /// Remove a resource from the resource table. +#[op] pub fn op_close( state: &mut OpState, rid: Option<ResourceId>, @@ -75,6 +98,7 @@ pub fn op_close( /// Try to remove a resource from the resource table. If there is no resource /// with the specified `rid`, this is a no-op. +#[op] pub fn op_try_close( state: &mut OpState, rid: Option<ResourceId>, @@ -87,7 +111,19 @@ pub fn op_try_close( Ok(()) } +#[op] +pub fn op_metrics( + state: &mut OpState, + _: (), + _: (), +) -> Result<(OpMetrics, Vec<OpMetrics>), Error> { + let aggregate = state.tracker.aggregate(); + let per_op = state.tracker.per_op(); + Ok((aggregate, per_op)) +} + /// Builtin utility to print to stdout/stderr +#[op] pub fn op_print( _state: &mut OpState, msg: String, @@ -119,6 +155,7 @@ impl Resource for WasmStreamingResource { } /// Feed bytes to WasmStreamingResource. +#[op] pub fn op_wasm_streaming_feed( state: &mut OpState, rid: ResourceId, @@ -133,6 +170,7 @@ pub fn op_wasm_streaming_feed( } /// Abort a WasmStreamingResource. +#[op] pub fn op_wasm_streaming_abort( state: &mut OpState, rid: ResourceId, @@ -153,6 +191,7 @@ pub fn op_wasm_streaming_abort( Ok(()) } +#[op] pub fn op_wasm_streaming_set_url( state: &mut OpState, rid: ResourceId, @@ -166,16 +205,7 @@ pub fn op_wasm_streaming_set_url( Ok(()) } -pub fn op_metrics( - state: &mut OpState, - _: (), - _: (), -) -> Result<(OpMetrics, Vec<OpMetrics>), Error> { - let aggregate = state.tracker.aggregate(); - let per_op = state.tracker.per_op(); - Ok((aggregate, per_op)) -} - +#[op] async fn op_read( state: Rc<RefCell<OpState>>, rid: ResourceId, @@ -185,6 +215,7 @@ async fn op_read( resource.read(buf).await.map(|n| n as u32) } +#[op] async fn op_write( state: Rc<RefCell<OpState>>, rid: ResourceId, @@ -194,6 +225,7 @@ async fn op_write( resource.write(buf).await.map(|n| n as u32) } +#[op] async fn op_shutdown( state: Rc<RefCell<OpState>>, rid: ResourceId, diff --git a/core/ops_json.rs b/core/ops_json.rs deleted file mode 100644 index 6d8af602f..000000000 --- a/core/ops_json.rs +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. - -use crate::ops::OpCall; -use crate::serialize_op_result; -use crate::Op; -use crate::OpFn; -use crate::OpState; -use anyhow::Error; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::cell::RefCell; -use std::future::Future; -use std::rc::Rc; - -/// A helper function that returns a sync NOP OpFn -/// -/// It's mainly intended for embedders who want to disable ops, see ./examples/disable_ops.rs -pub fn void_op_sync() -> Box<OpFn> { - op_sync(|_, _: (), _: ()| Ok(())) -} - -/// A helper function that returns an async NOP OpFn -/// -/// It's mainly intended for embedders who want to disable ops, see ./examples/disable_ops.rs -pub fn void_op_async() -> Box<OpFn> { - op_async(|_, _: (), _: ()| futures::future::ok(())) -} - -/// Creates an op that passes data synchronously using JSON. -/// -/// The provided function `op_fn` has the following parameters: -/// * `&mut OpState`: the op state, can be used to read/write resources in the runtime from an op. -/// * `V`: the deserializable value that is passed to the Rust function. -/// * `&mut [ZeroCopyBuf]`: raw bytes passed along, usually not needed if the JSON value is used. -/// -/// `op_fn` returns a serializable value, which is directly returned to JavaScript. -/// -/// When registering an op like this... -/// ```ignore -/// let mut runtime = JsRuntime::new(...); -/// runtime.register_op("hello", deno_core::op_sync(Self::hello_op)); -/// runtime.sync_ops_cache(); -/// ``` -/// -/// ...it can be invoked from JS using the provided name, for example: -/// ```js -/// let result = Deno.core.opSync("hello", args); -/// ``` -/// -/// `runtime.sync_ops_cache()` must be called after registering new ops -/// A more complete example is available in the examples directory. -pub fn op_sync<F, A, B, R>(op_fn: F) -> Box<OpFn> -where - F: Fn(&mut OpState, A, B) -> Result<R, Error> + 'static, - A: DeserializeOwned, - B: DeserializeOwned, - R: Serialize + 'static, -{ - Box::new(move |state, payload| -> Op { - let result = payload - .deserialize() - .and_then(|(a, b)| op_fn(&mut state.borrow_mut(), a, b)); - Op::Sync(serialize_op_result(result, state)) - }) -} - -/// Creates an op that passes data asynchronously using JSON. -/// -/// When this op is dispatched, the runtime doesn't exit while processing it. -/// -/// The provided function `op_fn` has the following parameters: -/// * `Rc<RefCell<OpState>`: the op state, can be used to read/write resources in the runtime from an op. -/// * `V`: the deserializable value that is passed to the Rust function. -/// * `BufVec`: raw bytes passed along, usually not needed if the JSON value is used. -/// -/// `op_fn` returns a future, whose output is a serializable value. This value will be asynchronously -/// returned to JavaScript. -/// -/// When registering an op like this... -/// ```ignore -/// let mut runtime = JsRuntime::new(...); -/// runtime.register_op("hello", deno_core::op_async(Self::hello_op)); -/// runtime.sync_ops_cache(); -/// ``` -/// -/// ...it can be invoked from JS using the provided name, for example: -/// ```js -/// let future = Deno.core.opAsync("hello", args); -/// ``` -/// -/// `runtime.sync_ops_cache()` must be called after registering new ops -/// A more complete example is available in the examples directory. -pub fn op_async<F, A, B, R, RV>(op_fn: F) -> Box<OpFn> -where - F: Fn(Rc<RefCell<OpState>>, A, B) -> R + 'static, - A: DeserializeOwned, - B: DeserializeOwned, - R: Future<Output = Result<RV, Error>> + 'static, - RV: Serialize + 'static, -{ - Box::new(move |state, payload| -> Op { - let op_id = payload.op_id; - let pid = payload.promise_id; - // Deserialize args, sync error on failure - let args = match payload.deserialize() { - Ok(args) => args, - Err(err) => { - return Op::Sync(serialize_op_result(Err::<(), Error>(err), state)) - } - }; - let (a, b) = args; - - use crate::futures::FutureExt; - let fut = op_fn(state.clone(), a, b) - .map(move |result| (pid, op_id, serialize_op_result(result, state))); - Op::Async(OpCall::eager(fut)) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn op_async_stack_trace() { - async fn op_throw( - _state: Rc<RefCell<OpState>>, - msg: Option<String>, - _: (), - ) -> Result<(), Error> { - assert_eq!(msg.unwrap(), "hello"); - Err(crate::error::generic_error("foo")) - } - - let ext = crate::Extension::builder() - .ops(vec![("op_throw", op_async(op_throw))]) - .build(); - - let mut runtime = crate::JsRuntime::new(crate::RuntimeOptions { - extensions: vec![ext], - ..Default::default() - }); - - runtime - .execute_script( - "<init>", - r#" - async function f1() { - await Deno.core.opAsync('op_throw', 'hello'); - } - - async function f2() { - await f1(); - } - - f2(); - "#, - ) - .unwrap(); - let e = runtime.run_event_loop(false).await.unwrap_err().to_string(); - println!("{}", e); - assert!(e.contains("Error: foo")); - assert!(e.contains("at async f1 (<init>:")); - assert!(e.contains("at async f2 (<init>:")); - } -} diff --git a/core/ops_metrics.rs b/core/ops_metrics.rs index 35c1faf1c..b068aa0ee 100644 --- a/core/ops_metrics.rs +++ b/core/ops_metrics.rs @@ -59,20 +59,10 @@ impl OpsTracker { unsafe { &mut *self.ops.get() } } - #[inline] - fn ensure_capacity(ops: &mut Vec<OpMetrics>, op_id: OpId) { - if op_id >= ops.len() { - ops.resize(1 + op_id, OpMetrics::default()) - } - } - #[allow(clippy::mut_from_ref)] #[inline] fn metrics_mut(&self, id: OpId) -> &mut OpMetrics { - let ops = self.ops_mut(); - // TODO(@AaronO): Pre-alloc capacity at runtime init once we forbid post-boot op registrations - Self::ensure_capacity(ops, id); - unsafe { ops.get_unchecked_mut(id) } + unsafe { self.ops_mut().get_unchecked_mut(id) } } #[inline] diff --git a/core/runtime.rs b/core/runtime.rs index 7555bc301..a4fa0c51f 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -6,6 +6,7 @@ use crate::error::generic_error; use crate::error::ErrWithV8Handle; use crate::error::JsError; use crate::extensions::OpEventLoopFn; +use crate::extensions::OpPair; use crate::inspector::JsRuntimeInspector; use crate::module_specifier::ModuleSpecifier; use crate::modules::ModuleId; @@ -16,13 +17,13 @@ use crate::modules::NoopModuleLoader; use crate::ops::*; use crate::Extension; use crate::OpMiddlewareFn; -use crate::OpPayload; use crate::OpResult; use crate::OpState; use crate::PromiseId; use anyhow::Error; use futures::channel::oneshot; use futures::future::poll_fn; +use futures::future::Future; use futures::future::FutureExt; use futures::stream::FuturesUnordered; use futures::stream::StreamExt; @@ -143,7 +144,6 @@ pub type CompiledWasmModuleStore = CrossIsolateStore<v8::CompiledWasmModule>; pub(crate) struct JsRuntimeState { pub global_context: Option<v8::Global<v8::Context>>, pub(crate) js_recv_cb: Option<v8::Global<v8::Function>>, - pub(crate) js_sync_cb: Option<v8::Global<v8::Function>>, pub(crate) js_macrotask_cbs: Vec<v8::Global<v8::Function>>, pub(crate) js_nexttick_cbs: Vec<v8::Global<v8::Function>>, pub(crate) js_promise_reject_cb: Option<v8::Global<v8::Function>>, @@ -279,17 +279,37 @@ impl JsRuntime { let has_startup_snapshot = options.startup_snapshot.is_some(); + let js_error_create_fn = options + .js_error_create_fn + .unwrap_or_else(|| Rc::new(JsError::create)); + + // Add builtins extension + options + .extensions + .insert(0, crate::ops_builtin::init_builtins()); + + let ops = Self::collect_ops(&mut options.extensions); + let mut op_state = OpState::new(ops.len()); + + if let Some(get_error_class_fn) = options.get_error_class_fn { + op_state.get_error_class_fn = get_error_class_fn; + } + + let op_state = Rc::new(RefCell::new(op_state)); + + let refs = bindings::external_references(&ops, op_state.clone()); + let refs: &'static v8::ExternalReferences = Box::leak(Box::new(refs)); let global_context; let (mut isolate, maybe_snapshot_creator) = if options.will_snapshot { // TODO(ry) Support loading snapshots before snapshotting. assert!(options.startup_snapshot.is_none()); - let mut creator = - v8::SnapshotCreator::new(Some(&bindings::EXTERNAL_REFERENCES)); + let mut creator = v8::SnapshotCreator::new(Some(refs)); let isolate = unsafe { creator.get_owned_isolate() }; let mut isolate = JsRuntime::setup_isolate(isolate); { let scope = &mut v8::HandleScope::new(&mut isolate); - let context = bindings::initialize_context(scope); + let context = + bindings::initialize_context(scope, &ops, false, op_state.clone()); global_context = v8::Global::new(scope, context); creator.set_default_context(context); } @@ -299,7 +319,7 @@ impl JsRuntime { .create_params .take() .unwrap_or_else(v8::Isolate::create_params) - .external_references(&**bindings::EXTERNAL_REFERENCES); + .external_references(&**refs); let snapshot_loaded = if let Some(snapshot) = options.startup_snapshot { params = match snapshot { Snapshot::Static(data) => params.snapshot_blob(data), @@ -315,13 +335,13 @@ impl JsRuntime { let mut isolate = JsRuntime::setup_isolate(isolate); { let scope = &mut v8::HandleScope::new(&mut isolate); - let context = if snapshot_loaded { - v8::Context::new(scope) - } else { - // If no snapshot is provided, we initialize the context with empty - // main source code and source maps. - bindings::initialize_context(scope) - }; + let context = bindings::initialize_context( + scope, + &ops, + snapshot_loaded, + op_state.clone(), + ); + global_context = v8::Global::new(scope, context); } (isolate, None) @@ -334,17 +354,6 @@ impl JsRuntime { .module_loader .unwrap_or_else(|| Rc::new(NoopModuleLoader)); - let js_error_create_fn = options - .js_error_create_fn - .unwrap_or_else(|| Rc::new(JsError::create)); - let mut op_state = OpState::new(); - - if let Some(get_error_class_fn) = options.get_error_class_fn { - op_state.get_error_class_fn = get_error_class_fn; - } - - let op_state = Rc::new(RefCell::new(op_state)); - isolate.set_slot(Rc::new(RefCell::new(JsRuntimeState { global_context: Some(global_context), pending_promise_exceptions: HashMap::new(), @@ -352,7 +361,6 @@ impl JsRuntime { pending_mod_evaluate: None, dyn_module_evaluate_idle_counter: 0, js_recv_cb: None, - js_sync_cb: None, js_macrotask_cbs: vec![], js_nexttick_cbs: vec![], js_promise_reject_cb: None, @@ -372,11 +380,6 @@ impl JsRuntime { let module_map = ModuleMap::new(loader, op_state); isolate.set_slot(Rc::new(RefCell::new(module_map))); - // Add builtins extension - options - .extensions - .insert(0, crate::ops_builtin::init_builtins()); - let mut js_runtime = Self { v8_isolate: Some(isolate), inspector: Some(inspector), @@ -394,10 +397,8 @@ impl JsRuntime { } // Init extension ops js_runtime.init_extension_ops().unwrap(); - // Init callbacks (opresolve & syncOpsCache) + // Init callbacks (opresolve) js_runtime.init_cbs(); - // Sync ops cache - js_runtime.sync_ops_cache(); js_runtime } @@ -461,34 +462,44 @@ impl JsRuntime { Ok(()) } - /// Initializes ops of provided Extensions - fn init_extension_ops(&mut self) -> Result<(), Error> { - let op_state = self.op_state(); - // Take extensions to avoid double-borrow - let mut extensions: Vec<Extension> = std::mem::take(&mut self.extensions); - + /// Collects ops from extensions & applies middleware + fn collect_ops(extensions: &mut [Extension]) -> Vec<OpPair> { // Middleware let middleware: Vec<Box<OpMiddlewareFn>> = extensions .iter_mut() .filter_map(|e| e.init_middleware()) .collect(); + // macroware wraps an opfn in all the middleware let macroware = move |name, opfn| middleware.iter().fold(opfn, |opfn, m| m(name, opfn)); - // Register ops + // Flatten ops & apply middlware + extensions + .iter_mut() + .filter_map(|e| e.init_ops()) + .flatten() + .map(|(name, opfn)| (name, macroware(name, opfn))) + .collect() + } + + /// Initializes ops of provided Extensions + fn init_extension_ops(&mut self) -> Result<(), Error> { + let op_state = self.op_state(); + // Take extensions to avoid double-borrow + let mut extensions: Vec<Extension> = std::mem::take(&mut self.extensions); + + // Setup state for e in extensions.iter_mut() { + // ops are already registered during in bindings::initialize_context(); e.init_state(&mut op_state.borrow_mut())?; - // Register each op after middlewaring it - let ops = e.init_ops().unwrap_or_default(); - for (name, opfn) in ops { - self.register_op(name, macroware(name, opfn)); - } + // Setup event-loop middleware if let Some(middleware) = e.init_event_loop_middleware() { self.event_loop_middlewares.push(middleware); } } + // Restore extensions self.extensions = extensions; @@ -511,22 +522,10 @@ impl JsRuntime { fn init_cbs(&mut self) { let mut scope = self.handle_scope(); let recv_cb = Self::grab_fn(&mut scope, "Deno.core.opresolve"); - let sync_cb = Self::grab_fn(&mut scope, "Deno.core.syncOpsCache"); // Put global handles in state let state_rc = JsRuntime::state(&scope); let mut state = state_rc.borrow_mut(); state.js_recv_cb.replace(recv_cb); - state.js_sync_cb.replace(sync_cb); - } - - /// Ensures core.js has the latest op-name to op-id mappings - pub fn sync_ops_cache(&mut self) { - let scope = &mut self.handle_scope(); - let state_rc = JsRuntime::state(scope); - let js_sync_cb_handle = state_rc.borrow().js_sync_cb.clone().unwrap(); - let js_sync_cb = js_sync_cb_handle.open(scope); - let this = v8::undefined(scope).into(); - js_sync_cb.call(scope, this, &[]); } /// Returns the runtime's op state, which can be used to maintain ops @@ -612,7 +611,6 @@ impl JsRuntime { )))); // Drop other v8::Global handles before snapshotting std::mem::take(&mut state.borrow_mut().js_recv_cb); - std::mem::take(&mut state.borrow_mut().js_sync_cb); let snapshot_creator = self.snapshot_creator.as_mut().unwrap(); let snapshot = snapshot_creator @@ -623,27 +621,6 @@ impl JsRuntime { snapshot } - /// Registers an op that can be called from JavaScript. - /// - /// The _op_ mechanism allows to expose Rust functions to the JS runtime, - /// which can be called using the provided `name`. - /// - /// This function provides byte-level bindings. To pass data via JSON, the - /// following functions can be passed as an argument for `op_fn`: - /// * [op_sync()](fn.op_sync.html) - /// * [op_async()](fn.op_async.html) - pub fn register_op<F>(&mut self, name: &str, op_fn: F) -> OpId - where - F: Fn(Rc<RefCell<OpState>>, OpPayload) -> Op + 'static, - { - Self::state(self.v8_isolate()) - .borrow_mut() - .op_state - .borrow_mut() - .op_table - .register_op(name, op_fn) - } - /// Registers a callback on the isolate when the memory limits are approached. /// Use this to prevent V8 from crashing the process when reaching the limit. /// @@ -1552,13 +1529,11 @@ impl JsRuntime { let mut state = state_rc.borrow_mut(); state.have_unpolled_ops = false; - let op_state = state.op_state.clone(); - while let Poll::Ready(Some(item)) = state.pending_ops.poll_next_unpin(cx) { let (promise_id, op_id, resp) = item; - op_state.borrow().tracker.track_async_completed(op_id); state.unrefed_ops.remove(&promise_id); + state.op_state.borrow().tracker.track_async_completed(op_id); args.push(v8::Integer::new(scope, promise_id as i32).into()); args.push(resp.to_v8(scope).unwrap()); } @@ -1654,22 +1629,37 @@ impl JsRuntime { } } +#[inline] +pub fn queue_async_op( + scope: &v8::Isolate, + op: impl Future<Output = (PromiseId, OpId, OpResult)> + 'static, +) { + let state_rc = JsRuntime::state(scope); + let mut state = state_rc.borrow_mut(); + state.pending_ops.push(OpCall::eager(op)); + state.have_unpolled_ops = true; +} + #[cfg(test)] pub mod tests { use super::*; use crate::error::custom_error; + use crate::error::AnyError; use crate::modules::ModuleSource; use crate::modules::ModuleSourceFuture; use crate::modules::ModuleType; - use crate::op_async; - use crate::op_sync; use crate::ZeroCopyBuf; + use deno_ops::op; use futures::future::lazy; use std::ops::FnOnce; use std::pin::Pin; use std::rc::Rc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; + // deno_ops macros generate code assuming deno_core in scope. + mod deno_core { + pub use crate::*; + } pub fn run_in_task<F>(f: F) where @@ -1689,26 +1679,26 @@ pub mod tests { dispatch_count: Arc<AtomicUsize>, } - fn op_test(rc_op_state: Rc<RefCell<OpState>>, payload: OpPayload) -> Op { - let rc_op_state2 = rc_op_state.clone(); - let op_state_ = rc_op_state2.borrow(); + #[op] + async fn op_test( + rc_op_state: Rc<RefCell<OpState>>, + control: u8, + buf: Option<ZeroCopyBuf>, + ) -> Result<u8, AnyError> { + let op_state_ = rc_op_state.borrow(); let test_state = op_state_.borrow::<TestState>(); test_state.dispatch_count.fetch_add(1, Ordering::Relaxed); - let (control, buf): (u8, Option<ZeroCopyBuf>) = - payload.deserialize().unwrap(); match test_state.mode { Mode::Async => { assert_eq!(control, 42); - let resp = (0, 1, serialize_op_result(Ok(43), rc_op_state)); - Op::Async(OpCall::ready(resp)) + Ok(43) } Mode::AsyncZeroCopy(has_buffer) => { assert_eq!(buf.is_some(), has_buffer); if let Some(buf) = buf { assert_eq!(buf.len(), 1); } - let resp = (0, 1, serialize_op_result(Ok(43), rc_op_state)); - Op::Async(OpCall::ready(resp)) + Ok(43) } } } @@ -1717,7 +1707,7 @@ pub mod tests { let dispatch_count = Arc::new(AtomicUsize::new(0)); let dispatch_count2 = dispatch_count.clone(); let ext = Extension::builder() - .ops(vec![("op_test", Box::new(op_test))]) + .ops(vec![op_test::decl()]) .state(move |state| { state.put(TestState { mode, @@ -2028,30 +2018,6 @@ pub mod tests { } #[test] - fn test_pre_dispatch() { - run_in_task(|cx| { - let (mut runtime, _dispatch_count) = setup(Mode::Async); - runtime - .execute_script( - "bad_op_id.js", - r#" - let thrown; - try { - Deno.core.opcallSync(100, null, null); - } catch (e) { - thrown = e; - } - assert(String(thrown) === "TypeError: Unknown op id: 100"); - "#, - ) - .unwrap(); - if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) { - unreachable!(); - } - }); - } - - #[test] fn syntax_error() { let mut runtime = JsRuntime::new(Default::default()); let src = "hocuspocus("; @@ -2095,6 +2061,7 @@ pub mod tests { #[test] fn test_error_builder() { + #[op] fn op_err(_: &mut OpState, _: (), _: ()) -> Result<(), Error> { Err(custom_error("DOMExceptionOperationError", "abc")) } @@ -2104,9 +2071,7 @@ pub mod tests { } run_in_task(|cx| { - let ext = Extension::builder() - .ops(vec![("op_err", op_sync(op_err))]) - .build(); + let ext = Extension::builder().ops(vec![op_err::decl()]).build(); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![ext], get_error_class_fn: Some(&get_error_class_name), @@ -2177,7 +2142,7 @@ pub mod tests { }); let cb_handle = runtime.v8_isolate().thread_safe_handle(); - let callback_invoke_count = Rc::new(AtomicUsize::default()); + let callback_invoke_count = Rc::new(AtomicUsize::new(0)); let inner_invoke_count = Rc::clone(&callback_invoke_count); runtime.add_near_heap_limit_callback( @@ -2221,7 +2186,7 @@ pub mod tests { }); let cb_handle = runtime.v8_isolate().thread_safe_handle(); - let callback_invoke_count_first = Rc::new(AtomicUsize::default()); + let callback_invoke_count_first = Rc::new(AtomicUsize::new(0)); let inner_invoke_count_first = Rc::clone(&callback_invoke_count_first); runtime.add_near_heap_limit_callback( move |current_limit, _initial_limit| { @@ -2230,7 +2195,7 @@ pub mod tests { }, ); - let callback_invoke_count_second = Rc::new(AtomicUsize::default()); + let callback_invoke_count_second = Rc::new(AtomicUsize::new(0)); let inner_invoke_count_second = Rc::clone(&callback_invoke_count_second); runtime.add_near_heap_limit_callback( move |current_limit, _initial_limit| { @@ -2500,6 +2465,7 @@ assertEquals(1, notify_return_value); async fn test_async_opstate_borrow() { struct InnerState(u64); + #[op] async fn op_async_borrow( op_state: Rc<RefCell<OpState>>, _: (), @@ -2519,7 +2485,7 @@ assertEquals(1, notify_return_value); } let extension = Extension::builder() - .ops(vec![("op_async_borrow", op_async(op_async_borrow))]) + .ops(vec![op_async_borrow::decl()]) .state(|state| { state.put(InnerState(42)); Ok(()) @@ -2542,6 +2508,7 @@ assertEquals(1, notify_return_value); #[tokio::test] async fn test_set_macrotask_callback_set_next_tick_callback() { + #[op] async fn op_async_sleep( _op_state: Rc<RefCell<OpState>>, _: (), @@ -2553,7 +2520,7 @@ assertEquals(1, notify_return_value); } let extension = Extension::builder() - .ops(vec![("op_async_sleep", op_async(op_async_sleep))]) + .ops(vec![op_async_sleep::decl()]) .build(); let mut runtime = JsRuntime::new(RuntimeOptions { @@ -2617,25 +2584,23 @@ assertEquals(1, notify_return_value); fn test_has_tick_scheduled() { use futures::task::ArcWake; - let macrotask = Arc::new(AtomicUsize::default()); - let macrotask_ = Arc::clone(¯otask); - - let next_tick = Arc::new(AtomicUsize::default()); - let next_tick_ = Arc::clone(&next_tick); + static MACROTASK: AtomicUsize = AtomicUsize::new(0); + static NEXT_TICK: AtomicUsize = AtomicUsize::new(0); - let op_macrotask = move |_: &mut OpState, _: (), _: ()| { - macrotask_.fetch_add(1, Ordering::Relaxed); + #[op] + fn op_macrotask(_: &mut OpState, _: (), _: ()) -> Result<(), AnyError> { + MACROTASK.fetch_add(1, Ordering::Relaxed); Ok(()) - }; + } - let op_next_tick = move |_: &mut OpState, _: (), _: ()| { - next_tick_.fetch_add(1, Ordering::Relaxed); + #[op] + fn op_next_tick(_: &mut OpState, _: (), _: ()) -> Result<(), AnyError> { + NEXT_TICK.fetch_add(1, Ordering::Relaxed); Ok(()) - }; + } let extension = Extension::builder() - .ops(vec![("op_macrotask", op_sync(op_macrotask))]) - .ops(vec![("op_next_tick", op_sync(op_next_tick))]) + .ops(vec![op_macrotask::decl(), op_next_tick::decl()]) .build(); let mut runtime = JsRuntime::new(RuntimeOptions { @@ -2670,8 +2635,8 @@ assertEquals(1, notify_return_value); let cx = &mut Context::from_waker(&waker); assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); - assert_eq!(1, macrotask.load(Ordering::Relaxed)); - assert_eq!(1, next_tick.load(Ordering::Relaxed)); + assert_eq!(1, MACROTASK.load(Ordering::Relaxed)); + assert_eq!(1, NEXT_TICK.load(Ordering::Relaxed)); assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); @@ -2756,28 +2721,34 @@ assertEquals(1, notify_return_value); #[tokio::test] async fn test_set_promise_reject_callback() { - let promise_reject = Arc::new(AtomicUsize::default()); - let promise_reject_ = Arc::clone(&promise_reject); + static PROMISE_REJECT: AtomicUsize = AtomicUsize::new(0); + static UNCAUGHT_EXCEPTION: AtomicUsize = AtomicUsize::new(0); - let uncaught_exception = Arc::new(AtomicUsize::default()); - let uncaught_exception_ = Arc::clone(&uncaught_exception); - - let op_promise_reject = move |_: &mut OpState, _: (), _: ()| { - promise_reject_.fetch_add(1, Ordering::Relaxed); + #[op] + fn op_promise_reject( + _: &mut OpState, + _: (), + _: (), + ) -> Result<(), AnyError> { + PROMISE_REJECT.fetch_add(1, Ordering::Relaxed); Ok(()) - }; + } - let op_uncaught_exception = move |_: &mut OpState, _: (), _: ()| { - uncaught_exception_.fetch_add(1, Ordering::Relaxed); + #[op] + fn op_uncaught_exception( + _: &mut OpState, + _: (), + _: (), + ) -> Result<(), AnyError> { + UNCAUGHT_EXCEPTION.fetch_add(1, Ordering::Relaxed); Ok(()) - }; + } let extension = Extension::builder() - .ops(vec![("op_promise_reject", op_sync(op_promise_reject))]) - .ops(vec![( - "op_uncaught_exception", - op_sync(op_uncaught_exception), - )]) + .ops(vec![ + op_promise_reject::decl(), + op_uncaught_exception::decl(), + ]) .build(); let mut runtime = JsRuntime::new(RuntimeOptions { @@ -2812,8 +2783,8 @@ assertEquals(1, notify_return_value); .unwrap(); runtime.run_event_loop(false).await.unwrap(); - assert_eq!(1, promise_reject.load(Ordering::Relaxed)); - assert_eq!(1, uncaught_exception.load(Ordering::Relaxed)); + assert_eq!(1, PROMISE_REJECT.load(Ordering::Relaxed)); + assert_eq!(1, UNCAUGHT_EXCEPTION.load(Ordering::Relaxed)); runtime .execute_script( @@ -2840,7 +2811,7 @@ assertEquals(1, notify_return_value); // printed to stderr. runtime.run_event_loop(false).await.unwrap(); - assert_eq!(2, promise_reject.load(Ordering::Relaxed)); - assert_eq!(2, uncaught_exception.load(Ordering::Relaxed)); + assert_eq!(2, PROMISE_REJECT.load(Ordering::Relaxed)); + assert_eq!(2, UNCAUGHT_EXCEPTION.load(Ordering::Relaxed)); } } |