diff --git a/Cargo.lock b/Cargo.lock index 68149dba4..b48bed5a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,7 +500,6 @@ dependencies = [ "deno_core", "deno_core_testing", "fastwebsockets", - "futures", "http", "http-body-util", "hyper", diff --git a/core/cppgc.rs b/core/cppgc.rs index 4327e3f7b..af7a05dde 100644 --- a/core/cppgc.rs +++ b/core/cppgc.rs @@ -36,12 +36,38 @@ pub fn make_cppgc_object<'a, T: GarbageCollected + 'static>( t: T, ) -> v8::Local<'a, v8::Object> { let state = JsRuntime::state_from(scope); - let templ = - v8::Local::new(scope, state.cppgc_template.borrow().as_ref().unwrap()); - let func = templ.get_function(scope).unwrap(); - let obj = func.new_instance(scope, &[]).unwrap(); + let opstate = state.op_state.borrow(); + + // To initialize object wraps correctly, we store the function + // template in OpState with `T`'s TypeId as the key when binding + // because it'll be pretty annoying to propogate `T` generic everywhere. + // + // Here we try to retrive a function template for `T`, falling back to + // the default cppgc template. + let id = TypeId::of::(); + let obj = if let Some(templ) = + opstate.try_borrow_untyped::>(id) + { + let templ = v8::Local::new(scope, templ); + let inst = templ.instance_template(scope); + inst.new_instance(scope).unwrap() + } else { + let templ = + v8::Local::new(scope, state.cppgc_template.borrow().as_ref().unwrap()); + let func = templ.get_function(scope).unwrap(); + func.new_instance(scope, &[]).unwrap() + }; + + wrap_object(scope, obj, t) +} - let heap = scope.get_cpp_heap().unwrap(); +// Wrap an API object (eg: `args.This()`) +pub fn wrap_object<'a, T: GarbageCollected + 'static>( + isolate: &mut v8::Isolate, + obj: v8::Local<'a, v8::Object>, + t: T, +) -> v8::Local<'a, v8::Object> { + let heap = isolate.get_cpp_heap().unwrap(); let member = unsafe { v8::cppgc::make_garbage_collected( @@ -54,9 +80,8 @@ pub fn make_cppgc_object<'a, T: GarbageCollected + 'static>( }; unsafe { - v8::Object::wrap::>(scope, obj, &member); + v8::Object::wrap::>(isolate, obj, &member); } - obj } @@ -94,7 +119,6 @@ pub fn try_unwrap_cppgc_object<'sc, T: GarbageCollected + 'static>( let Ok(obj): Result, _> = val.try_into() else { return None; }; - if !obj.is_api_wrapper() { return None; } diff --git a/core/extension_set.rs b/core/extension_set.rs index 9f111f574..ac331d0bb 100644 --- a/core/extension_set.rs +++ b/core/extension_set.rs @@ -11,6 +11,7 @@ use crate::extensions::GlobalTemplateMiddlewareFn; use crate::extensions::OpMiddlewareFn; use crate::modules::ModuleName; use crate::ops::OpCtx; +use crate::ops::OpMethodCtx; use crate::runtime::ExtensionTranspiler; use crate::runtime::JsRuntimeState; use crate::runtime::OpDriverImpl; @@ -22,6 +23,7 @@ use crate::OpDecl; use crate::OpMetricsFactoryFn; use crate::OpState; use crate::SourceMapData; +use crate::_ops::OpMethodDecl; /// Contribute to the `OpState` from each extension. pub fn setup_op_state(op_state: &mut OpState, extensions: &mut [Extension]) { @@ -37,7 +39,7 @@ pub fn setup_op_state(op_state: &mut OpState, extensions: &mut [Extension]) { pub fn init_ops( deno_core_ops: &'static [OpDecl], extensions: &mut [Extension], -) -> Vec { +) -> (Vec, Vec) { // In debug build verify there that inter-Extension dependencies // are setup correctly. #[cfg(debug_assertions)] @@ -49,6 +51,12 @@ pub fn init_ops( .fold(0, |ext_ops_count, count| count + ext_ops_count); let mut ops = Vec::with_capacity(no_of_ops + deno_core_ops.len()); + let no_of_methods = extensions + .iter() + .map(|e| e.method_op_count()) + .fold(0, |ext_ops_count, count| count + ext_ops_count); + let mut op_methods = Vec::with_capacity(no_of_methods); + // Collect all middlewares - deno_core extension must not have a middleware! let middlewares: Vec> = extensions .iter_mut() @@ -76,13 +84,18 @@ pub fn init_ops( ..macroware(*ext_op) }); } + + let ext_method_ops = ext.init_method_ops(); + for ext_op in ext_method_ops { + op_methods.push(*ext_op); + } } // In debug build verify there are no duplicate ops. #[cfg(debug_assertions)] check_no_duplicate_op_names(&ops); - ops + (ops, op_methods) } /// This functions panics if any of the extensions is missing its dependencies. @@ -121,22 +134,24 @@ fn check_no_duplicate_op_names(ops: &[OpDecl]) { pub fn create_op_ctxs( op_decls: Vec, + op_method_decls: Vec, op_metrics_factory_fn: Option, op_driver: Rc, op_state: Rc>, runtime_state: Rc, get_error_class_fn: GetErrorClassFn, -) -> Box<[OpCtx]> { +) -> (Box<[OpCtx]>, Box<[OpMethodCtx]>) { let op_count = op_decls.len(); let mut op_ctxs = Vec::with_capacity(op_count); + let mut op_method_ctxs = Vec::with_capacity(op_method_decls.len()); let runtime_state_ptr = runtime_state.as_ref() as *const _; - for (index, decl) in op_decls.into_iter().enumerate() { + let create_ctx = |index, decl| { let metrics_fn = op_metrics_factory_fn .as_ref() .and_then(|f| (f)(index as _, op_count, &decl)); - let op_ctx = OpCtx::new( + OpCtx::new( index as _, std::ptr::null_mut(), op_driver.clone(), @@ -145,12 +160,37 @@ pub fn create_op_ctxs( runtime_state_ptr, get_error_class_fn, metrics_fn, - ); + ) + }; - op_ctxs.push(op_ctx); + for (index, decl) in op_decls.into_iter().enumerate() { + op_ctxs.push(create_ctx(index, decl)); + } + + for (index, mut decl) in op_method_decls.into_iter().enumerate() { + decl.constructor.name = decl.name.0; + decl.constructor.name_fast = decl.name.1; + + op_method_ctxs.push(OpMethodCtx { + id: (decl.id)(), + constructor: create_ctx(index, decl.constructor), + methods: decl + .methods + .iter() + .map(|method_decl| create_ctx(index, *method_decl)) + .collect(), + static_methods: decl + .static_methods + .iter() + .map(|method_decl| create_ctx(index, *method_decl)) + .collect(), + }); } - op_ctxs.into_boxed_slice() + ( + op_ctxs.into_boxed_slice(), + op_method_ctxs.into_boxed_slice(), + ) } pub fn get_middlewares_and_external_refs( diff --git a/core/extensions.rs b/core/extensions.rs index 2454185b7..a282d3fb9 100644 --- a/core/extensions.rs +++ b/core/extensions.rs @@ -176,10 +176,22 @@ const NOOP_FN: CFunction = CFunction::new( &CFunctionInfo::new(Type::Void.scalar(), &[], Int64Representation::Number), ); +// Declaration for object wrappers. +#[derive(Clone, Copy)] +pub struct OpMethodDecl { + // TypeId::of::() is unstable-nightly in const context so + // we store the fn pointer instead. + pub id: fn() -> std::any::TypeId, + pub name: (&'static str, FastStaticString), + pub constructor: OpDecl, + pub methods: &'static [OpDecl], + pub static_methods: &'static [OpDecl], +} + #[derive(Clone, Copy)] pub struct OpDecl { pub name: &'static str, - pub(crate) name_fast: FastStaticString, + pub name_fast: FastStaticString, pub is_async: bool, pub is_reentrant: bool, pub arg_count: u8, @@ -384,6 +396,7 @@ macro_rules! extension { $(, bounds = [ $( $bound:path : $bound_type:ident ),+ ] )? $(, ops_fn = $ops_symbol:ident $( < $ops_param:ident > )? )? $(, ops = [ $( $(#[$m:meta])* $( $op:ident )::+ $( < $( $op_param:ident ),* > )? ),+ $(,)? ] )? + $(, objects = [ $( $object:ident )::+ ] )? $(, esm_entry_point = $esm_entry_point:expr )? $(, esm = [ $($esm:tt)* ] )? $(, lazy_loaded_esm = [ $($lazy_loaded_esm:tt)* ] )? @@ -447,6 +460,9 @@ macro_rules! extension { $( #[ $m ] )* $( $op )::+ $( :: < $($op_param),* > )? () }),+)?]), + objects: ::std::borrow::Cow::Borrowed(&[ + $( $( $object )::+::DECL, )* + ]), external_references: ::std::borrow::Cow::Borrowed(&[ $( $external_reference ),* ]), global_template_middleware: ::std::option::Option::None, global_object_middleware: ::std::option::Option::None, @@ -580,6 +596,7 @@ pub struct Extension { pub lazy_loaded_esm_files: Cow<'static, [ExtensionFileSource]>, pub esm_entry_point: Option<&'static str>, pub ops: Cow<'static, [OpDecl]>, + pub objects: Cow<'static, [OpMethodDecl]>, pub external_references: Cow<'static, [v8::ExternalReference<'static>]>, pub global_template_middleware: Option, pub global_object_middleware: Option, @@ -603,6 +620,7 @@ impl Extension { lazy_loaded_esm_files: Cow::Borrowed(&[]), esm_entry_point: None, ops: self.ops.clone(), + objects: self.objects.clone(), external_references: self.external_references.clone(), global_template_middleware: self.global_template_middleware, global_object_middleware: self.global_object_middleware, @@ -621,6 +639,7 @@ impl Default for Extension { lazy_loaded_esm_files: Cow::Borrowed(&[]), esm_entry_point: None, ops: Cow::Borrowed(&[]), + objects: Cow::Borrowed(&[]), external_references: Cow::Borrowed(&[]), global_template_middleware: None, global_object_middleware: None, @@ -675,6 +694,10 @@ impl Extension { self.ops.len() } + pub fn method_op_count(&self) -> usize { + self.objects.len() + } + /// Called at JsRuntime startup to initialize ops in the isolate. pub fn init_ops(&mut self) -> &[OpDecl] { if !self.enabled { @@ -685,6 +708,11 @@ impl Extension { self.ops.as_ref() } + /// Called at JsRuntime startup to initialize method ops in the isolate. + pub fn init_method_ops(&self) -> &[OpMethodDecl] { + self.objects.as_ref() + } + /// Allows setting up the initial op-state of an isolate at startup. pub fn take_state(&mut self, state: &mut OpState) { if let Some(op_fn) = self.op_state_fn.take() { diff --git a/core/gotham_state.rs b/core/gotham_state.rs index fad5c3f99..b7fb834f3 100644 --- a/core/gotham_state.rs +++ b/core/gotham_state.rs @@ -22,6 +22,11 @@ impl GothamState { self.data.insert(type_id, Box::new(t)); } + // For internal use. + pub(crate) fn put_untyped(&mut self, t: TypeId, v: T) { + self.data.insert(t, Box::new(v)); + } + /// Determines if the current value exists in `GothamState` storage. pub fn has(&self) -> bool { let type_id = TypeId::of::(); @@ -34,6 +39,11 @@ impl GothamState { self.data.get(&type_id).and_then(|b| b.downcast_ref()) } + // For internal use. + pub(crate) fn try_borrow_untyped(&self, t: TypeId) -> Option<&T> { + self.data.get(&t).and_then(|b| b.downcast_ref()) + } + /// Borrows a value from the `GothamState` storage. pub fn borrow(&self) -> &T { self.try_borrow().unwrap_or_else(|| missing::()) diff --git a/core/lib.rs b/core/lib.rs index dbd510d00..8a3069ed7 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -177,6 +177,7 @@ pub mod _ops { pub use super::error_codes::get_error_code; pub use super::extensions::Op; pub use super::extensions::OpDecl; + pub use super::extensions::OpMethodDecl; #[cfg(debug_assertions)] pub use super::ops::reentrancy_check; pub use super::ops::OpCtx; diff --git a/core/ops.rs b/core/ops.rs index 6a8878f1a..aa2a7e2c7 100644 --- a/core/ops.rs +++ b/core/ops.rs @@ -70,6 +70,18 @@ impl OpMetadata { } } +/// Per-object contexts for members. +pub struct OpMethodCtx { + /// TypeId of the wrapped type + pub id: std::any::TypeId, + /// Op context for the constructor + pub constructor: OpCtx, + /// Per-op context for the methods + pub methods: Vec, + /// Per-op context for the static methods + pub static_methods: Vec, +} + /// Per-op context. /// // Note: We don't worry too much about the size of this struct because it's allocated once per realm, and is diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs index 2b1615601..1351815cf 100644 --- a/core/runtime/bindings.rs +++ b/core/runtime/bindings.rs @@ -1,8 +1,10 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use anyhow::Context; +use std::cell::RefCell; use std::mem::MaybeUninit; use std::os::raw::c_void; use std::path::PathBuf; +use std::rc::Rc; use url::Url; use v8::MapFnTo; @@ -23,15 +25,18 @@ use crate::modules::synthetic_module_evaluation_steps; use crate::modules::ImportAttributesKind; use crate::modules::ModuleMap; use crate::ops::OpCtx; +use crate::ops::OpMethodCtx; use crate::runtime::InitMode; use crate::runtime::JsRealm; use crate::FastStaticString; use crate::FastString; use crate::JsRuntime; use crate::ModuleType; +use crate::OpState; pub(crate) fn create_external_references( ops: &[OpCtx], + op_method_ctxs: &[OpMethodCtx], additional_references: &[v8::ExternalReference], sources: &[v8::OneByteConst], ops_in_snapshot: usize, @@ -44,6 +49,7 @@ pub(crate) fn create_external_references( + (ops.len() * 4) + additional_references.len() + sources.len() + + op_method_ctxs.len() + 18, // for callsite_fns ); @@ -83,6 +89,16 @@ pub(crate) fn create_external_references( }); } + for ctx in op_method_ctxs { + references.extend_from_slice(&ctx.constructor.external_references()); + for method in &ctx.methods { + references.extend_from_slice(&method.external_references()); + } + for method in &ctx.static_methods { + references.extend_from_slice(&method.external_references()); + } + } + references.extend_from_slice(additional_references); for ctx in &ops[..ops_in_snapshot] { @@ -327,8 +343,10 @@ pub(crate) fn initialize_primordials_and_infra( /// Set up JavaScript bindings for ops. pub(crate) fn initialize_deno_core_ops_bindings<'s>( scope: &mut v8::HandleScope<'s>, + op_state: Rc>, context: v8::Local<'s, v8::Context>, op_ctxs: &[OpCtx], + op_method_ctxs: &[OpMethodCtx], ) { let global = context.global(scope); @@ -363,15 +381,41 @@ pub(crate) fn initialize_deno_core_ops_bindings<'s>( deno_core_ops_obj.set(scope, key.into(), op_fn.into()); } + + for op_method_ctx in op_method_ctxs { + let tmpl = op_ctx_template(scope, &op_method_ctx.constructor); + let prototype = tmpl.prototype_template(scope); + let key = op_method_ctx.constructor.decl.name_fast.v8_string(scope); + + for method in op_method_ctx.methods.iter() { + let op_fn = op_ctx_template(scope, method); + let method_key = method.decl.name_fast.v8_string(scope); + prototype.set(method_key.into(), op_fn.into()); + } + + for method in op_method_ctx.static_methods.iter() { + let op_fn = op_ctx_template(scope, method); + let method_key = method.decl.name_fast.v8_string(scope); + tmpl.set(method_key.into(), op_fn.into()); + } + + let op_fn = tmpl.get_function(scope).unwrap(); + op_fn.set_name(key); + deno_core_ops_obj.set(scope, key.into(), op_fn.into()); + + let id = op_method_ctx.id; + op_state + .borrow_mut() + .put_untyped(id, v8::Global::new(scope, tmpl)); + } } -fn op_ctx_function<'s>( +pub(crate) fn op_ctx_template<'s>( scope: &mut v8::HandleScope<'s>, op_ctx: &OpCtx, -) -> v8::Local<'s, v8::Function> { +) -> v8::Local<'s, v8::FunctionTemplate> { let op_ctx_ptr = op_ctx as *const OpCtx as *const c_void; let external = v8::External::new(scope, op_ctx_ptr as *mut c_void); - let v8name = op_ctx.decl.name_fast.v8_string(scope); let (slow_fn, fast_fn) = if op_ctx.metrics_enabled() { ( @@ -398,6 +442,15 @@ fn op_ctx_function<'s>( builder.build(scope) }; + template +} + +fn op_ctx_function<'s>( + scope: &mut v8::HandleScope<'s>, + op_ctx: &OpCtx, +) -> v8::Local<'s, v8::Function> { + let v8name = op_ctx.decl.name_fast.v8_string(scope); + let template = op_ctx_template(scope, op_ctx); let v8fn = template.get_function(scope).unwrap(); v8fn.set_name(v8name); v8fn @@ -820,10 +873,11 @@ where /// to a JavaScript function that executes and op. pub fn create_exports_for_ops_virtual_module<'s>( op_ctxs: &[OpCtx], + op_method_ctxs: &[OpMethodCtx], scope: &mut v8::HandleScope<'s>, global: v8::Local, ) -> Vec<(FastStaticString, v8::Local<'s, v8::Value>)> { - let mut exports = Vec::with_capacity(op_ctxs.len()); + let mut exports = Vec::with_capacity(op_ctxs.len() + op_method_ctxs.len()); let deno_obj = get(scope, global, DENO, "Deno"); let deno_core_obj = get(scope, deno_obj, CORE, "Deno.core"); @@ -852,5 +906,25 @@ pub fn create_exports_for_ops_virtual_module<'s>( exports.push((op_ctx.decl.name_fast, op_fn.into())); } + for ctx in op_method_ctxs { + let tmpl = op_ctx_template(scope, &ctx.constructor); + let prototype = tmpl.prototype_template(scope); + + for method in ctx.methods.iter() { + let op_fn = op_ctx_template(scope, method); + let method_key = method.decl.name_fast.v8_string(scope); + prototype.set(method_key.into(), op_fn.into()); + } + + for method in ctx.static_methods.iter() { + let op_fn = op_ctx_template(scope, method); + let method_key = method.decl.name_fast.v8_string(scope); + tmpl.set(method_key.into(), op_fn.into()); + } + + let op_fn = tmpl.get_function(scope).unwrap(); + exports.push((ctx.constructor.decl.name_fast, op_fn.into())); + } + exports } diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs index 998c8f60e..b4d3aa6b6 100644 --- a/core/runtime/jsrealm.rs +++ b/core/runtime/jsrealm.rs @@ -13,6 +13,7 @@ use crate::modules::ModuleMap; use crate::modules::ModuleName; use crate::ops::ExternalOpsTracker; use crate::ops::OpCtx; +use crate::ops::OpMethodCtx; use crate::stats::RuntimeActivityTraces; use crate::tasks::V8TaskSpawnerFactory; use crate::web_timeout::WebTimers; @@ -67,6 +68,7 @@ pub struct ContextState { // We don't explicitly re-read this prop but need the slice to live alongside // the context pub(crate) op_ctxs: Box<[OpCtx]>, + pub(crate) op_method_ctxs: Box<[OpMethodCtx]>, pub(crate) isolate: Option<*mut v8::Isolate>, pub(crate) exception_state: Rc, pub(crate) has_next_tick_scheduled: Cell, @@ -80,6 +82,7 @@ impl ContextState { isolate_ptr: *mut v8::Isolate, get_error_class_fn: GetErrorClassFn, op_ctxs: Box<[OpCtx]>, + op_method_ctxs: Box<[OpMethodCtx]>, external_ops_tracker: ExternalOpsTracker, ) -> Self { Self { @@ -92,6 +95,7 @@ impl ContextState { wasm_instantiate_fn: Default::default(), activity_traces: Default::default(), op_ctxs, + op_method_ctxs, pending_ops: op_driver, task_spawner_factory: Default::default(), timers: Default::default(), diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs index 945abadcd..0603177ff 100644 --- a/core/runtime/jsruntime.rs +++ b/core/runtime/jsruntime.rs @@ -859,15 +859,16 @@ impl JsRuntime { // ...now we're moving on to ops; set them up, create `OpCtx` for each op // and get ready to actually create V8 isolate... - let op_decls = + let (op_decls, op_method_decls) = extension_set::init_ops(crate::ops_builtin::BUILTIN_OPS, &mut extensions); let op_driver = Rc::new(OpDriverImpl::default()); let op_metrics_factory_fn = options.op_metrics_factory_fn.take(); let get_error_class_fn = options.get_error_class_fn.unwrap_or(&|_| "Error"); - let mut op_ctxs = extension_set::create_op_ctxs( + let (mut op_ctxs, mut op_method_ctxs) = extension_set::create_op_ctxs( op_decls, + op_method_decls, op_metrics_factory_fn, op_driver.clone(), op_state.clone(), @@ -915,6 +916,7 @@ impl JsRuntime { isolate_allocations.external_refs = Some(Box::new(bindings::create_external_references( &op_ctxs, + &op_method_ctxs, &additional_references, &isolate_allocations.externalized_sources, ops_in_snapshot, @@ -949,6 +951,13 @@ impl JsRuntime { for op_ctx in op_ctxs.iter_mut() { op_ctx.isolate = isolate_ptr; } + for op_method_ctx in op_method_ctxs.iter_mut() { + op_method_ctx.constructor.isolate = isolate_ptr; + for op in &mut op_method_ctx.methods { + op.isolate = isolate_ptr; + } + } + op_state.borrow_mut().put(isolate_ptr); // ...once ops and isolate are set up, we can create a `ContextState`... @@ -957,6 +966,7 @@ impl JsRuntime { isolate_ptr, options.get_error_class_fn.unwrap_or(&|_| "Error"), op_ctxs, + op_method_ctxs, op_state.borrow().external_ops_tracker.clone(), )); @@ -1023,8 +1033,10 @@ impl JsRuntime { if init_mode.needs_ops_bindings() { bindings::initialize_deno_core_ops_bindings( scope, + op_state, context, &context_state.op_ctxs, + &context_state.op_method_ctxs, ); } @@ -1238,6 +1250,7 @@ impl JsRuntime { let global = context_local.global(scope); let synthetic_module_exports = create_exports_for_ops_virtual_module( &context_state.op_ctxs, + &context_state.op_method_ctxs, scope, global, ); diff --git a/dcore/Cargo.toml b/dcore/Cargo.toml index f5ff2418c..432c9cf3c 100644 --- a/dcore/Cargo.toml +++ b/dcore/Cargo.toml @@ -23,7 +23,6 @@ deno_core_testing.workspace = true clap = "4.5.1" deno_core.workspace = true fastwebsockets = { version = "0.6", features = ["upgrade", "unstable-split"] } -futures.workspace = true http = { version = "1.0" } http-body-util = { version = "0.1" } hyper = { version = "=1.1.0", features = ["full"] } diff --git a/ops/op2/config.rs b/ops/op2/config.rs index 8a52ecb1b..d3c4a01ba 100644 --- a/ops/op2/config.rs +++ b/ops/op2/config.rs @@ -24,6 +24,10 @@ pub(crate) struct MacroConfig { pub reentrant: bool, /// Marks an op as a method on a wrapped object. pub method: Option, + /// Marks an op as a constructor + pub constructor: bool, + /// Marks an op as a static member + pub static_member: bool, /// Marks an op with no side effects. pub no_side_effects: bool, } @@ -63,7 +67,15 @@ impl MacroConfig { } for flag in flags { - if flag == "fast" { + if flag == "method" { + // Doesn't need any special handling, its more of a marker. + continue; + } + if flag == "constructor" { + config.constructor = true; + } else if flag == "static_method" { + config.static_member = true; + } else if flag == "fast" { config.fast = true; } else if flag.starts_with("fast(") { let tokens = @@ -158,6 +170,9 @@ impl MacroConfig { ( $($flags:tt $( ( $( $args:ty ),* ) )? ),+ ) => { Self::from_token_trees(flags, args) } + ( # [ $($flags:tt),+ ] ) => { + Self::from_flags(flags.into_iter().map(|flag| flag.to_string())) + } }) }) .map_err(|_| Op2Error::PatternMatchFailed("attribute", attr_string))??; diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs index 26e8d575e..766aee9ec 100644 --- a/ops/op2/dispatch_fast.rs +++ b/ops/op2/dispatch_fast.rs @@ -433,6 +433,17 @@ pub(crate) fn generate_dispatch_fast( quote!() }; + let with_isolate = if generator_state.needs_fast_isolate + && !generator_state.needs_fast_scope + { + generator_state.needs_opctx = true; + gs_quote!(generator_state(opctx, scope) => + (let mut #scope = unsafe { &mut *#opctx.isolate };) + ) + } else { + quote!() + }; + let with_opctx = if generator_state.needs_opctx { generator_state.needs_fast_api_callback_options = true; gs_quote!(generator_state(opctx, fast_api_callback_options) => { @@ -543,6 +554,7 @@ pub(crate) fn generate_dispatch_fast( #with_scope #with_opctx #with_js_runtime_state + #with_isolate #with_self let #result = { #(#call_args)* diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs index 30893dbee..e0672e720 100644 --- a/ops/op2/dispatch_slow.rs +++ b/ops/op2/dispatch_slow.rs @@ -836,6 +836,12 @@ pub fn return_value_infallible( ArgMarker::Number => { gs_quote!(generator_state(result) => (deno_core::_ops::RustToV8Marker::::from(#result))) } + ArgMarker::Cppgc if generator_state.use_this_cppgc => { + generator_state.needs_isolate = true; + gs_quote!(generator_state(result, scope) => ( + Some(deno_core::cppgc::wrap_object(&mut #scope, args.this(), #result)) + )) + } ArgMarker::Cppgc => { let marker = quote!(deno_core::_ops::RustToV8Marker::::from); if ret_type.is_option() { @@ -910,7 +916,12 @@ pub fn return_value_v8_value( quote!(deno_core::_ops::RustToV8Marker::::from(#result)) } ArgMarker::Cppgc => { - let marker = quote!(deno_core::_ops::RustToV8Marker::::from); + let marker = if !generator_state.use_this_cppgc { + quote!(deno_core::_ops::RustToV8Marker::::from) + } else { + quote!((|x| { deno_core::cppgc::wrap_object(#scope, args.this(), x) })) + }; + if ret_type.is_option() { quote!(#result.map(#marker)) } else { diff --git a/ops/op2/generator_state.rs b/ops/op2/generator_state.rs index 4f8738c6d..0d122f18f 100644 --- a/ops/op2/generator_state.rs +++ b/ops/op2/generator_state.rs @@ -43,12 +43,16 @@ pub struct GeneratorState { pub needs_args: bool, pub needs_retval: bool, pub needs_scope: bool, + pub needs_fast_scope: bool, + pub needs_fast_isolate: bool, pub needs_isolate: bool, pub needs_opstate: bool, pub needs_opctx: bool, pub needs_js_runtime_state: bool, pub needs_fast_api_callback_options: bool, pub needs_self: bool, + /// Wrap the `this` with cppgc object + pub use_this_cppgc: bool, } /// Quotes a set of generator_state fields, along with variables captured from diff --git a/ops/op2/mod.rs b/ops/op2/mod.rs index 84d94cacc..ad1f6aff6 100644 --- a/ops/op2/mod.rs +++ b/ops/op2/mod.rs @@ -32,6 +32,7 @@ pub mod dispatch_fast; pub mod dispatch_shared; pub mod dispatch_slow; pub mod generator_state; +pub mod object_wrap; pub mod signature; pub mod signature_retval; @@ -61,6 +62,8 @@ pub enum Op2Error { TooManyFastAlternatives, #[error("The flags for this attribute were not sorted alphabetically. They should be listed as '({0})'.")] ImproperlySortedAttribute(String), + #[error("Only one constructor is allowed per object")] + MultipleConstructors, } #[derive(Debug, Error)] @@ -81,12 +84,16 @@ pub(crate) fn op2( attr: TokenStream, item: TokenStream, ) -> Result { - let func = parse2::(item)?; + let Ok(func) = parse2::(item.clone()) else { + let impl_block = parse2::(item)?; + return object_wrap::generate_impl_ops(impl_block); + }; + let config = MacroConfig::from_tokens(attr)?; generate_op2(config, func) } -fn generate_op2( +pub(crate) fn generate_op2( config: MacroConfig, func: ItemFn, ) -> Result { @@ -178,12 +185,15 @@ fn generate_op2( moves: vec![], needs_retval: false, needs_scope: false, + needs_fast_isolate: false, + needs_fast_scope: false, needs_isolate: false, needs_opctx: false, needs_opstate: false, needs_js_runtime_state: false, needs_fast_api_callback_options: false, needs_self: config.method.is_some(), + use_this_cppgc: config.constructor, }; let mut slow_generator_state = base_generator_state.clone(); diff --git a/ops/op2/object_wrap.rs b/ops/op2/object_wrap.rs new file mode 100644 index 000000000..77266d06e --- /dev/null +++ b/ops/op2/object_wrap.rs @@ -0,0 +1,135 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use proc_macro2::TokenStream; +use quote::quote; +use quote::ToTokens; +use syn::ImplItem; +use syn::ItemFn; +use syn::ItemImpl; + +use crate::op2::generate_op2; +use crate::op2::MacroConfig; +use crate::op2::Op2Error; + +// Object wrap for Cppgc-backed objects +// +// This module generates the glue code declarations +// for `impl` blocks to create JS objects in Rust +// using the op2 infra. +// +// ```rust +// #[op] +// impl MyObject { +// #[constructor] // <-- first attribute defines binding type +// #[cppgc] // <-- attributes for op2 +// fn new() -> MyObject { +// MyObject::new() +// } +// +// #[static_method] +// #[cppgc] +// fn static_method() -> MyObject { +// // ... +// } +// +// #[method] +// #[smi] +// fn x(&self) -> i32 { +// // ... +// } +// } +// +// The generated OpMethodDecl that can be passed to +// `deno_core::extension!` macro to register the object +// +// ```rust +// deno_core::extension!( +// ..., +// objects = [MyObject], +// ) +// ``` +// +// ```js +// import { MyObject } from "ext:core/ops"; +// ``` +// +// Currently supported bindings: +// - constructor +// - methods +// - static methods +// +// Planned support: +// - getters +// - setters +// +pub(crate) fn generate_impl_ops( + item: ItemImpl, +) -> Result { + let mut tokens = TokenStream::new(); + + let self_ty = &item.self_ty; + let self_ty_ident = self_ty.to_token_stream().to_string(); + + // State + let mut constructor = None; + let mut methods = Vec::new(); + let mut static_methods = Vec::new(); + + for item in item.items { + if let ImplItem::Fn(mut method) = item { + /* First attribute idents, for all functions in block */ + let attrs = method.attrs.swap_remove(0); + + let ident = method.sig.ident.clone(); + let func = ItemFn { + attrs: method.attrs, + vis: method.vis, + sig: method.sig, + block: Box::new(method.block), + }; + + let mut config = MacroConfig::from_tokens(quote! { + #attrs + })?; + if config.constructor { + if constructor.is_some() { + return Err(Op2Error::MultipleConstructors); + } + + constructor = Some(ident); + } else if config.static_member { + static_methods.push(ident); + } else { + methods.push(ident); + config.method = Some(self_ty_ident.clone()); + } + + let op = generate_op2(config, func); + tokens.extend(op); + } + } + + let res = quote! { + impl #self_ty { + pub const DECL: deno_core::_ops::OpMethodDecl = deno_core::_ops::OpMethodDecl { + methods: &[ + #( + #self_ty::#methods(), + )* + ], + static_methods: &[ + #( + #self_ty::#static_methods(), + )* + ], + constructor: #self_ty::#constructor(), + name: ::deno_core::__op_name_fast!(#self_ty), + id: || ::std::any::TypeId::of::<#self_ty>() + }; + + #tokens + } + }; + + Ok(res) +} diff --git a/testing/checkin/runner/extensions.rs b/testing/checkin/runner/extensions.rs index d9b97001e..129277a62 100644 --- a/testing/checkin/runner/extensions.rs +++ b/testing/checkin/runner/extensions.rs @@ -49,12 +49,16 @@ deno_core::extension!( ops_worker::op_worker_await_close, ops_worker::op_worker_terminate, ], + objects = [ + ops::DOMPoint + ], esm_entry_point = "ext:checkin_runtime/__init.js", esm = [ dir "checkin/runtime", "__init.js", "checkin:async" = "async.ts", "checkin:console" = "console.ts", + "checkin:object" = "object.ts", "checkin:error" = "error.ts", "checkin:timers" = "timers.ts", "checkin:worker" = "worker.ts", diff --git a/testing/checkin/runner/ops.rs b/testing/checkin/runner/ops.rs index 24eb3fe4c..03d0866f2 100644 --- a/testing/checkin/runner/ops.rs +++ b/testing/checkin/runner/ops.rs @@ -2,14 +2,15 @@ use std::cell::RefCell; use std::rc::Rc; +use deno_core::error::AnyError; use deno_core::op2; use deno_core::stats::RuntimeActivityDiff; use deno_core::stats::RuntimeActivitySnapshot; use deno_core::stats::RuntimeActivityStats; use deno_core::stats::RuntimeActivityStatsFactory; use deno_core::stats::RuntimeActivityStatsFilter; +use deno_core::v8; use deno_core::GarbageCollected; -use deno_core::OpDecl; use deno_core::OpState; use super::extensions::SomeType; @@ -69,37 +70,76 @@ pub fn op_stats_delete( test_data.take::(name); } -pub struct Stateful { - name: String, +pub struct DOMPoint { + pub x: f64, + pub y: f64, + pub z: f64, + pub w: f64, } -impl GarbageCollected for Stateful {} +impl GarbageCollected for DOMPoint {} -impl Stateful { - #[op2(method(Stateful))] - #[string] - fn get_name(&self) -> String { - self.name.clone() +#[op2] +impl DOMPoint { + #[constructor] + #[cppgc] + fn new( + x: Option, + y: Option, + z: Option, + w: Option, + ) -> DOMPoint { + DOMPoint { + x: x.unwrap_or(0.0), + y: y.unwrap_or(0.0), + z: z.unwrap_or(0.0), + w: w.unwrap_or(0.0), + } } - #[op2(fast, method(Stateful))] - #[smi] - fn len(&self) -> u32 { - self.name.len() as u32 + #[static_method] + #[cppgc] + fn from_point( + scope: &mut v8::HandleScope, + other: v8::Local, + ) -> Result { + fn get( + scope: &mut v8::HandleScope, + other: v8::Local, + key: &str, + ) -> Option { + let key = v8::String::new(scope, key).unwrap(); + other + .get(scope, key.into()) + .map(|x| x.to_number(scope).unwrap().value()) + } + + Ok(DOMPoint { + x: get(scope, other, "x").unwrap_or(0.0), + y: get(scope, other, "y").unwrap_or(0.0), + z: get(scope, other, "z").unwrap_or(0.0), + w: get(scope, other, "w").unwrap_or(0.0), + }) } - #[op2(async, method(Stateful))] - async fn delay(&self, #[smi] millis: u32) { - tokio::time::sleep(std::time::Duration::from_millis(millis as u64)).await; - println!("name: {}", self.name); + #[fast] + fn x(&self) -> f64 { + self.x + } + #[fast] + fn y(&self) -> f64 { + self.y + } + #[fast] + fn w(&self) -> f64 { + self.w + } + #[fast] + fn z(&self) -> f64 { + self.z } } -// Make sure this compiles, we'll use it when we add registration. -#[allow(dead_code)] -const STATEFUL_DECL: [OpDecl; 3] = - [Stateful::get_name(), Stateful::len(), Stateful::delay()]; - #[op2(fast)] pub fn op_nop_generic(state: &mut OpState) { state.take::(); diff --git a/testing/checkin/runtime/__init.js b/testing/checkin/runtime/__init.js index 9b4ac1e00..73ad97692 100644 --- a/testing/checkin/runtime/__init.js +++ b/testing/checkin/runtime/__init.js @@ -5,9 +5,11 @@ import * as error from "checkin:error"; import * as timers from "checkin:timers"; import * as worker from "checkin:worker"; import * as throw_ from "checkin:throw"; +import * as object from "checkin:object"; async; error; throw_; +object; globalThis.console = console.console; globalThis.setTimeout = timers.setTimeout; diff --git a/testing/checkin/runtime/object.ts b/testing/checkin/runtime/object.ts new file mode 100644 index 000000000..8ffc560c3 --- /dev/null +++ b/testing/checkin/runtime/object.ts @@ -0,0 +1,5 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { DOMPoint } from "ext:core/ops"; + +export { DOMPoint }; diff --git a/testing/ops.d.ts b/testing/ops.d.ts index c89a661ae..cc45158c8 100644 --- a/testing/ops.d.ts +++ b/testing/ops.d.ts @@ -19,3 +19,11 @@ export function op_worker_recv(...any: any[]): any; export function op_worker_send(...any: any[]): any; export function op_worker_spawn(...any: any[]): any; export function op_worker_terminate(...any: any[]): any; + +export class DOMPoint { + constructor(x?: number, y?: number, z?: number, w?: number); + static from_point( + other: { x?: number; y?: number; z?: number; w?: number }, + ): DOMPoint; + x(): number; +} diff --git a/testing/tsconfig.json b/testing/tsconfig.json index 292944138..526644477 100644 --- a/testing/tsconfig.json +++ b/testing/tsconfig.json @@ -11,7 +11,8 @@ "checkin:testing": ["checkin/runtime/testing.ts"], "checkin:throw": ["checkin/runtime/throw.ts"], "checkin:timers": ["checkin/runtime/timers.ts"], - "checkin:worker": ["checkin/runtime/worker.ts"] + "checkin:worker": ["checkin/runtime/worker.ts"], + "checkin:object": ["checkin/runtime/object.ts"] }, "lib": ["ESNext", "DOM", "ES2023.Array"], "module": "ESNext", diff --git a/testing/unit/resource_test.ts b/testing/unit/resource_test.ts index 5b52a7d0f..700a9275c 100644 --- a/testing/unit/resource_test.ts +++ b/testing/unit/resource_test.ts @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assert, assertArrayEquals, assertEquals, test } from "checkin:testing"; +import { DOMPoint } from "checkin:object"; const { op_pipe_create, @@ -62,3 +63,23 @@ test(async function testCppgcAsync() { const resource = await op_async_make_cppgc_resource(); assertEquals(await op_async_get_cppgc_resource(resource), 42); }); + +test(function testDomPoint() { + const p1 = new DOMPoint(100, 100); + const p2 = new DOMPoint(); + const p3 = DOMPoint.from_point({ x: 200 }); + const p4 = DOMPoint.from_point({ x: 0, y: 100, z: 99.9, w: 100 }); + assertEquals(p1.x(), 100); + assertEquals(p2.x(), 0); + assertEquals(p3.x(), 200); + assertEquals(p4.x(), 0); + + let caught; + try { + // @ts-expect-error bad arg test + new DOMPoint("bad"); + } catch (e) { + caught = e; + } + assert(caught); +});