Repository: denoland/deno_core
Pull request #962 (2024-11-13 02:34:28)

feat: expose a version of prepare stack trace callback that doesn't patch callsites
this PR has no effect on the CLI, but exposing this allows deploy to avoid patching callsites on old deployments.

Basically in deploy, when creating the isolate I would expect something like

```rust
if !is_new_deployment {
  isolate.set_prepare_stack_trace_callback(
    deno_core::error::prepare_stack_trace_callback_with_original_callsites
  );
}
```

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/denoland/deno_core/pull/962?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) Report
Attention: Patch coverage is `75.75758%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 81.32%. Comparing base [(`0c7f83e`)](https://app.codecov.io/gh/denoland/deno_core/commit/0c7f83ea8daabe8f541fc93ae97529dc446f5f24?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) to head [(`5ecfe5d`)](https://app.codecov.io/gh/denoland/deno_core/commit/5ecfe5d15c92a58c0a21a665e13f6815df9b84a5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).
> Report is 163 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/denoland/deno_core/pull/962?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) | Patch % | Lines |
|---|---|---|
| [core/error.rs](https://app.codecov.io/gh/denoland/deno_core/pull/962?src=pr&el=tree&filepath=core%2Ferror.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland#diff-Y29yZS9lcnJvci5ycw==) | 75.75% | [8 Missing :warning: ](https://app.codecov.io/gh/denoland/deno_core/pull/962?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #962      +/-   ##
==========================================
- Coverage   81.43%   81.32%   -0.12%     
==========================================
  Files          97       97              
  Lines       23877    25002    +1125     
==========================================
+ Hits        19445    20333     +888     
- Misses       4432     4669     +237     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/denoland/deno_core/pull/962?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index c55c9b2..314d8dc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -636,6 +636,7 @@ dependencies = [
  "proc-macro-rules",
  "proc-macro2",
  "quote",
+ "stringcase",
  "strum",
  "strum_macros",
  "syn",
@@ -1999,6 +2000,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "stringcase"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04028eeb851ed08af6aba5caa29f2d59a13ed168cee4d6bd753aeefcf1d636b0"
+
 [[package]]
 name = "strsim"
 version = "0.11.1"
diff --git a/core/cppgc.rs b/core/cppgc.rs
index 4327e3f..af7a05d 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::<T>();
+  let obj = if let Some(templ) =
+    opstate.try_borrow_untyped::<v8::Global<v8::FunctionTemplate>>(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::<CPPGC_TAG, CppGcObject<T>>(scope, obj, &member);
+    v8::Object::wrap::<CPPGC_TAG, CppGcObject<T>>(isolate, obj, &member);
   }
-
   obj
 }
 
@@ -94,7 +119,6 @@ pub fn try_unwrap_cppgc_object<'sc, T: GarbageCollected + 'static>(
   let Ok(obj): Result<v8::Local<v8::Object>, _> = val.try_into() else {
     return None;
   };
-
   if !obj.is_api_wrapper() {
     return None;
   }
diff --git a/core/error.rs b/core/error.rs
index 05d5ff2..b994c2b 100644
--- a/core/error.rs
+++ b/core/error.rs
@@ -1233,10 +1233,8 @@ pub(crate) fn make_callsite_prototype<'s>(
   template.new_instance(scope).unwrap()
 }
 
-// called by V8 whenever a stack trace is created.
-// note that this is called _instead_ of `Error.prepareStackTrace`
-// so we have to explicitly call it to keep user code wrking
-pub fn prepare_stack_trace_callback<'s>(
+#[inline(always)]
+fn prepare_stack_trace_inner<'s, const PATCH_CALLSITES: bool>(
   scope: &mut v8::HandleScope<'s>,
   error: v8::Local<'s, v8::Value>,
   callsites: v8::Local<'s, v8::Array>,
@@ -1257,28 +1255,34 @@ pub fn prepare_stack_trace_callback<'s>(
       .and_then(|f| f.try_cast::<v8::Function>().ok())
   });
 
+  // Note that the callback is called _instead_ of `Error.prepareStackTrace`
+  // so we have to explicitly call the global function
   if let Some(prepare_fn) = prepare_fn {
-    // User defined `Error.prepareStackTrace`.
-    // Patch the callsites to have file paths, then call
-    // the user's function
-    let len = callsites.length();
-    let mut patched = Vec::with_capacity(len as usize);
-    let template = JsRuntime::state_from(scope)
-      .callsite_prototype
-      .borrow()
-      .clone()
-      .unwrap();
-    let prototype = v8::Local::new(scope, template);
-    for i in 0..len {
-      let callsite =
-        callsites.get_index(scope, i).unwrap().cast::<v8::Object>();
-      patched.push(make_patched_callsite(scope, callsite, prototype).into());
-    }
-    let patched_callsites = v8::Array::new_with_elements(scope, &patched);
+    let callsites = if PATCH_CALLSITES {
+      // User defined `Error.prepareStackTrace`.
+      // Patch the callsites to have file paths, then call
+      // the user's function
+      let len = callsites.length();
+      let mut patched = Vec::with_capacity(len as usize);
+      let template = JsRuntime::state_from(scope)
+        .callsite_prototype
+        .borrow()
+        .clone()
+        .unwrap();
+      let prototype = v8::Local::new(scope, template);
+      for i in 0..len {
+        let callsite =
+          callsites.get_index(scope, i).unwrap().cast::<v8::Object>();
+        patched.push(make_patched_callsite(scope, callsite, prototype).into());
+      }
+      v8::Array::new_with_elements(scope, &patched)
+    } else {
+      callsites
+    };
 
-    // call the user's `prepareStackTrace` with our patched "callsite" objects
+    // call the user's `prepareStackTrace` with our "callsite" objects
     let this = global_error.unwrap().into();
-    let args = &[error, patched_callsites.into()];
+    let args = &[error, callsites.into()];
     return prepare_fn
       .call(scope, this, args)
       .unwrap_or_else(|| v8::undefined(scope).into());
@@ -1288,7 +1292,35 @@ pub fn prepare_stack_trace_callback<'s>(
   format_stack_trace(scope, error, callsites)
 }
 
-fn format_stack_trace<'s>(
+/// Callback to prepare an error stack trace. This callback is invoked by V8 whenever a stack trace is created.
+/// This will patch callsite objects to translate file URLs to file paths before they're
+/// exposed to user-defined `prepareStackTrace` implementations.
+///
+/// This function is not used by default, but it can
+/// be set directly on the [v8 isolate][v8::Isolate::set_prepare_stack_trace_callback]
+pub fn prepare_stack_trace_callback_with_original_callsites<'s>(
+  scope: &mut v8::HandleScope<'s>,
+  error: v8::Local<'s, v8::Value>,
+  callsites: v8::Local<'s, v8::Array>,
+) -> v8::Local<'s, v8::Value> {
+  prepare_stack_trace_inner::<false>(scope, error, callsites)
+}
+
+/// Callback to prepare an error stack trace. This callback is invoked by V8 whenever a stack trace is created.
+/// This will patch callsite objects to translate file URLs to file paths before they're
+/// exposed to user-defined `prepareStackTrace` implementations.
+///
+/// This function is the default callback, set on creating a `JsRuntime`, but the callback can also
+/// be set directly on the [v8 isolate][v8::Isolate::set_prepare_stack_trace_callback]
+pub fn prepare_stack_trace_callback<'s>(
+  scope: &mut v8::HandleScope<'s>,
+  error: v8::Local<'s, v8::Value>,
+  callsites: v8::Local<'s, v8::Array>,
+) -> v8::Local<'s, v8::Value> {
+  prepare_stack_trace_inner::<true>(scope, error, callsites)
+}
+
+pub fn format_stack_trace<'s>(
   scope: &mut v8::HandleScope<'s>,
   error: v8::Local<'s, v8::Value>,
   callsites: v8::Local<'s, v8::Array>,
diff --git a/core/extension_set.rs b/core/extension_set.rs
index 5bbef18..1eabccf 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<OpDecl> {
+) -> (Vec<OpDecl>, Vec<OpMethodDecl>) {
   // 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<Box<OpMiddlewareFn>> = 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.
@@ -119,25 +132,28 @@ fn check_no_duplicate_op_names(ops: &[OpDecl]) {
   }
 }
 
+#[allow(clippy::too_many_arguments)]
 pub fn create_op_ctxs(
   op_decls: Vec<OpDecl>,
+  op_method_decls: Vec<OpMethodDecl>,
   op_metrics_factory_fn: Option<OpMetricsFactoryFn>,
   op_driver: Rc<OpDriverImpl>,
   op_state: Rc<RefCell<OpState>>,
   runtime_state: Rc<JsRuntimeState>,
   get_error_class_fn: GetErrorClassFn,
   enable_stack_trace_arg_in_ops: bool,
-) -> 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(),
@@ -147,12 +163,37 @@ pub fn create_op_ctxs(
       get_error_class_fn,
       metrics_fn,
       enable_stack_trace_arg_in_ops,
-    );
+    )
+  };
 
-    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 2454185..11bc573 100644
--- a/core/extensions.rs
+++ b/core/extensions.rs
@@ -176,12 +176,32 @@ 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::<T>() 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, PartialEq)]
+pub enum AccessorType {
+  Getter,
+  Setter,
+  None,
+}
+
 #[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 accessor_type: AccessorType,
   pub arg_count: u8,
   pub no_side_effects: bool,
   /// The slow dispatch call. If metrics are disabled, the `v8::Function` is created with this callback.
@@ -208,6 +228,7 @@ impl OpDecl {
     no_side_effects: bool,
     slow_fn: OpFnRef,
     slow_fn_with_metrics: OpFnRef,
+    accessor_type: AccessorType,
     fast_fn: Option<CFunction>,
     fast_fn_with_metrics: Option<CFunction>,
     metadata: OpMetadata,
@@ -222,12 +243,17 @@ impl OpDecl {
       no_side_effects,
       slow_fn,
       slow_fn_with_metrics,
+      accessor_type,
       fast_fn,
       fast_fn_with_metrics,
       metadata,
     }
   }
 
+  pub fn is_accessor(&self) -> bool {
+    self.accessor_type != AccessorType::None
+  }
+
   /// Returns a copy of this `OpDecl` that replaces underlying functions
   /// with noops.
   pub fn disable(self) -> Self {
@@ -384,6 +410,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 +474,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 +610,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<GlobalTemplateMiddlewareFn>,
   pub global_object_middleware: Option<GlobalObjectMiddlewareFn>,
@@ -603,6 +634,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 +653,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 +708,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 +722,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 fad5c3f..b7fb834 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<T: 'static>(&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<T: 'static>(&self) -> bool {
     let type_id = TypeId::of::<T>();
@@ -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<T: 'static>(&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<T: 'static>(&self) -> &T {
     self.try_borrow().unwrap_or_else(|| missing::<T>())
diff --git a/core/lib.rs b/core/lib.rs
index 92d959c..4e01455 100644
--- a/core/lib.rs
+++ b/core/lib.rs
@@ -71,6 +71,7 @@ pub use crate::convert::ToV8;
 pub use crate::cppgc::GarbageCollected;
 pub use crate::error::GetErrorClassFn;
 pub use crate::error::JsErrorCreateFn;
+pub use crate::extensions::AccessorType;
 pub use crate::extensions::Extension;
 pub use crate::extensions::ExtensionFileSource;
 pub use crate::extensions::ExtensionFileSourceCode;
@@ -177,6 +178,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 dccd14d..8aaf528 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<OpCtx>,
+  /// Per-op context for the static methods
+  pub static_methods: Vec<OpCtx>,
+}
+
 /// 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 2b16156..877f74a 100644
--- a/core/runtime/bindings.rs
+++ b/core/runtime/bindings.rs
@@ -1,8 +1,11 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
 use anyhow::Context;
+use std::cell::RefCell;
+use std::collections::HashMap;
 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 +26,19 @@ 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::AccessorType;
 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 +51,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 +91,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 +345,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<RefCell<OpState>>,
   context: v8::Local<'s, v8::Context>,
   op_ctxs: &[OpCtx],
+  op_method_ctxs: &[OpMethodCtx],
 ) {
   let global = context.global(scope);
 
@@ -363,15 +383,98 @@ 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);
+
+    let accessor_store = create_accessor_store(op_method_ctx);
+
+    for method in op_method_ctx.methods.iter() {
+      op_ctx_template_or_accessor(&accessor_store, scope, prototype, method);
+    }
+
+    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>(
+fn op_ctx_template_or_accessor<'s>(
+  accessor_store: &AccessorStore,
   scope: &mut v8::HandleScope<'s>,
+  tmpl: v8::Local<'s, v8::ObjectTemplate>,
   op_ctx: &OpCtx,
-) -> v8::Local<'s, v8::Function> {
+) {
+  if !op_ctx.decl.is_accessor() {
+    let op_fn = op_ctx_template(scope, op_ctx);
+    let method_key = op_ctx.decl.name_fast.v8_string(scope);
+
+    tmpl.set(method_key.into(), op_fn.into());
+
+    return;
+  }
+
+  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);
+
+  if let Some((named_getter, named_setter)) =
+    accessor_store.get(op_ctx.decl.name)
+  {
+    let getter_raw = if named_getter.metrics_enabled() {
+      named_getter.decl.slow_fn_with_metrics
+    } else {
+      named_getter.decl.slow_fn
+    };
+
+    let getter_fn = v8::FunctionTemplate::builder_raw(getter_raw)
+      .data(external.into())
+      .build(scope);
+
+    let setter_fn = if let Some(setter) = named_setter {
+      let setter_raw = if setter.metrics_enabled() {
+        setter.decl.slow_fn_with_metrics
+      } else {
+        setter.decl.slow_fn
+      };
+
+      Some(
+        v8::FunctionTemplate::builder_raw(setter_raw)
+          .data(external.into())
+          .build(scope),
+      )
+    } else {
+      None
+    };
+
+    let key = op_ctx.decl.name_fast.v8_string(scope).into();
+    tmpl.set_accessor_property(
+      key,
+      Some(getter_fn),
+      setter_fn,
+      v8::PropertyAttribute::default(),
+    );
+  }
+}
+
+pub(crate) fn op_ctx_template<'s>(
+  scope: &mut v8::HandleScope<'s>,
+  op_ctx: &OpCtx,
+) -> 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,11 +501,58 @@ 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
 }
 
+type AccessorStore<'a> = HashMap<String, (&'a OpCtx, Option<&'a OpCtx>)>;
+
+fn create_accessor_store(ctx: &OpMethodCtx) -> AccessorStore {
+  let mut store = AccessorStore::new();
+
+  for method in ctx.methods.iter() {
+    // Populate all setters first.
+    if method.decl.accessor_type == AccessorType::Setter {
+      let key = method.decl.name_fast.to_string();
+
+      // All setters must start with "__set_".
+      let key = key.strip_prefix("__set_").expect("Invalid setter name");
+
+      // There must be a getter for each setter.
+      let getter = ctx
+        .methods
+        .iter()
+        .find(|m| {
+          m.decl.name == key && m.decl.accessor_type == AccessorType::Getter
+        })
+        .expect("Getter not found for setter");
+
+      store.insert(key.to_string(), (getter, Some(method)));
+    }
+  }
+
+  // Populate getters without setters.
+  for method in ctx.methods.iter() {
+    if method.decl.accessor_type == AccessorType::Getter {
+      let key = method.decl.name_fast.to_string();
+
+      store.entry(key).or_insert((method, None));
+    }
+  }
+
+  store
+}
+
 pub extern "C" fn wasm_async_resolve_promise_callback(
   _isolate: *mut v8::Isolate,
   context: v8::Local<v8::Context>,
@@ -820,10 +970,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<v8::Object>,
 ) -> 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 +1003,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);
+
+    let accessor_store = create_accessor_store(ctx);
+
+    for method in ctx.methods.iter() {
+      op_ctx_template_or_accessor(&accessor_store, scope, prototype, method);
+    }
+
+    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 998c8f6..b4d3aa6 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<ExceptionState>,
   pub(crate) has_next_tick_scheduled: Cell<bool>,
@@ -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 aeccadb..3d3f918 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -863,15 +863,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(),
@@ -920,6 +921,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,
@@ -954,6 +956,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`...
@@ -962,6 +971,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(),
     ));
 
@@ -1028,8 +1038,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,
       );
     }
 
@@ -1243,6 +1255,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/ops/Cargo.toml b/ops/Cargo.toml
index 6de0ec3..319f968 100644
--- a/ops/Cargo.toml
+++ b/ops/Cargo.toml
@@ -22,6 +22,7 @@ strum.workspace = true
 strum_macros.workspace = true
 syn.workspace = true
 thiserror.workspace = true
+stringcase = "0.3.0"
 
 [dev-dependencies]
 pretty_assertions.workspace = true
diff --git a/ops/op2/config.rs b/ops/op2/config.rs
index 8a52ecb..956b638 100644
--- a/ops/op2/config.rs
+++ b/ops/op2/config.rs
@@ -24,8 +24,16 @@ pub(crate) struct MacroConfig {
   pub reentrant: bool,
   /// Marks an op as a method on a wrapped object.
   pub method: Option<String>,
+  /// 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,
+  /// Marks an op as a getter.
+  pub getter: bool,
+  /// Marks an op as a setter.
+  pub setter: bool,
 }
 
 impl MacroConfig {
@@ -63,7 +71,19 @@ 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 == "getter" {
+        config.getter = true;
+      } else if flag == "setter" {
+        config.setter = true;
+      } else if flag == "fast" {
         config.fast = true;
       } else if flag.starts_with("fast(") {
         let tokens =
@@ -158,6 +178,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 0eea673..ccbe5fd 100644
--- a/ops/op2/dispatch_fast.rs
+++ b/ops/op2/dispatch_fast.rs
@@ -458,6 +458,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) => {
@@ -569,6 +580,7 @@ pub(crate) fn generate_dispatch_fast(
       #with_opctx
       #with_stack_trace
       #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 9ffff1c..2582435 100644
--- a/ops/op2/dispatch_slow.rs
+++ b/ops/op2/dispatch_slow.rs
@@ -57,7 +57,7 @@ pub(crate) fn generate_dispatch_slow_call(
 }
 
 pub(crate) fn generate_dispatch_slow(
-  _config: &MacroConfig,
+  config: &MacroConfig,
   generator_state: &mut GeneratorState,
   signature: &ParsedSignature,
 ) -> Result<TokenStream, V8SignatureMappingError> {
@@ -68,9 +68,13 @@ pub(crate) fn generate_dispatch_slow(
   output.extend(gs_quote!(generator_state(result) => (let #result = {
     #args
   };)));
-  output.extend(return_value(generator_state, &signature.ret_val).map_err(
-    |s| V8SignatureMappingError::NoRetValMapping(s, signature.ret_val.clone()),
-  )?);
+  if !config.setter {
+    output.extend(return_value(generator_state, &signature.ret_val).map_err(
+      |s| {
+        V8SignatureMappingError::NoRetValMapping(s, signature.ret_val.clone())
+      },
+    )?);
+  }
 
   let with_stack_trace = if generator_state.needs_stack_trace {
     with_stack_trace(generator_state)
@@ -158,6 +162,7 @@ pub(crate) fn generate_dispatch_slow(
       extern "C" fn #slow_function_metrics<'s>(#info: *const deno_core::v8::FunctionCallbackInfo) {
         let info: &'s _ = unsafe { &*#info };
         let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(info);
+
         let #opctx: &'s _ = unsafe {
           &*(deno_core::v8::Local::<deno_core::v8::External>::cast_unchecked(args.data()).value()
               as *const deno_core::_ops::OpCtx)
@@ -864,6 +869,12 @@ pub fn return_value_infallible(
     ArgMarker::Number => {
       gs_quote!(generator_state(result) => (deno_core::_ops::RustToV8Marker::<deno_core::_ops::NumberMarker, _>::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::<deno_core::_ops::CppGcMarker, _>::from);
       if ret_type.is_option() {
@@ -938,7 +949,12 @@ pub fn return_value_v8_value(
       quote!(deno_core::_ops::RustToV8Marker::<deno_core::_ops::NumberMarker, _>::from(#result))
     }
     ArgMarker::Cppgc => {
-      let marker = quote!(deno_core::_ops::RustToV8Marker::<deno_core::_ops::CppGcMarker, _>::from);
+      let marker = if !generator_state.use_this_cppgc {
+        quote!(deno_core::_ops::RustToV8Marker::<deno_core::_ops::CppGcMarker, _>::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 58b599f..37a6d37 100644
--- a/ops/op2/generator_state.rs
+++ b/ops/op2/generator_state.rs
@@ -46,6 +46,8 @@ 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,
@@ -53,6 +55,8 @@ pub struct GeneratorState {
   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 6671014..8b39e3e 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,14 +84,18 @@ pub(crate) fn op2(
   attr: TokenStream,
   item: TokenStream,
 ) -> Result<TokenStream, Op2Error> {
-  let func = parse2::<ItemFn>(item)?;
+  let Ok(func) = parse2::<ItemFn>(item.clone()) else {
+    let impl_block = parse2::<syn::ItemImpl>(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,
+  mut func: ItemFn,
 ) -> Result<TokenStream, Op2Error> {
   // Create a copy of the original function, named "call"
   let call = Ident::new("call", Span::call_site());
@@ -110,7 +117,10 @@ fn generate_op2(
       FnArg::Typed(ty) => ty.attrs.clear(),
     }
   }
-
+  if config.setter {
+    // Prepend "__set_" to the setter function name.
+    func.sig.ident = format_ident!("__set_{}", func.sig.ident);
+  }
   let signature = parse_signature(func.attrs, func.sig.clone())?;
   if let Some(ident) = signature.lifetime.as_ref().map(|s| format_ident!("{s}"))
   {
@@ -180,6 +190,8 @@ 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,
@@ -187,6 +199,7 @@ fn generate_op2(
     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();
@@ -217,12 +230,16 @@ fn generate_op2(
       &signature,
     )? {
       Some((fast_definition, fast_metrics_definition, fast_fn)) => {
-        if !config.fast && !config.nofast && config.fast_alternatives.is_empty()
+        if !config.fast
+          && !config.nofast
+          && config.fast_alternatives.is_empty()
+          && !config.getter
+          && !config.setter
         {
           return Err(Op2Error::ShouldBeFast);
         }
         // nofast requires the function to be valid for fast
-        if config.nofast {
+        if config.nofast || config.getter || config.setter {
           (quote!(None), quote!(None), quote!())
         } else {
           (
@@ -233,7 +250,7 @@ fn generate_op2(
         }
       }
       None => {
-        if config.fast {
+        if config.fast || config.getter || config.setter {
           return Err(Op2Error::ShouldNotBeFast("fast"));
         }
         if config.nofast {
@@ -281,6 +298,14 @@ fn generate_op2(
     }
   };
 
+  let accessor_type = if config.getter {
+    quote!(::deno_core::AccessorType::Getter)
+  } else if config.setter {
+    quote!(::deno_core::AccessorType::Setter)
+  } else {
+    quote!(::deno_core::AccessorType::None)
+  };
+
   Ok(quote! {
     #[allow(non_camel_case_types)]
     #vis const fn #name <#(#generic : #bound),*> () -> ::deno_core::_ops::OpDecl {
@@ -301,6 +326,7 @@ fn generate_op2(
           /*no_side_effect*/ #no_side_effect,
           /*slow_fn*/ Self::#slow_function as _,
           /*slow_fn_metrics*/ Self::#slow_function_metrics as _,
+          /*accessor_type*/ #accessor_type,
           /*fast_fn*/ #fast_definition,
           /*fast_fn_metrics*/ #fast_definition_metrics,
           /*metadata*/ ::deno_core::OpMetadata {
diff --git a/ops/op2/object_wrap.rs b/ops/op2/object_wrap.rs
new file mode 100644
index 0000000..fc16f49
--- /dev/null
+++ b/ops/op2/object_wrap.rs
@@ -0,0 +1,151 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use proc_macro2::TokenStream;
+use quote::format_ident;
+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 method(&self) -> i32 {
+//      // ...
+//    }
+//
+//    #[getter]
+//    fn x(&self) -> i32 {}
+//
+//    #[setter]
+//    fn x(&self, x: 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";
+// ```
+//
+// Supported bindings:
+// - constructor
+// - methods
+// - static methods
+// - getters
+// - setters
+//
+pub(crate) fn generate_impl_ops(
+  item: ItemImpl,
+) -> Result<TokenStream, Op2Error> {
+  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);
+
+      /* Convert snake_case to camelCase */
+      method.sig.ident = format_ident!(
+        "{}",
+        stringcase::camel_case(&method.sig.ident.to_string())
+      );
+
+      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 {
+        if config.setter {
+          methods.push(format_ident!("__set_{}", 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)
+}


Test patch:
diff --git a/ops/op2/test_cases/async/async_arg_return.out b/ops/op2/test_cases/async/async_arg_return.out
index 911f4a6..5790f7d 100644
--- a/ops/op2/test_cases/async/async_arg_return.out
+++ b/ops/op2/test_cases/async/async_arg_return.out
@@ -14,6 +14,7 @@ pub const fn op_async() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_arg_return_result.out b/ops/op2/test_cases/async/async_arg_return_result.out
index 14a5cee..c26b338 100644
--- a/ops/op2/test_cases/async/async_arg_return_result.out
+++ b/ops/op2/test_cases/async/async_arg_return_result.out
@@ -14,6 +14,7 @@ pub const fn op_async() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_cppgc.out b/ops/op2/test_cases/async/async_cppgc.out
index a75f823..c9ccea8 100644
--- a/ops/op2/test_cases/async/async_cppgc.out
+++ b/ops/op2/test_cases/async/async_cppgc.out
@@ -14,6 +14,7 @@ const fn op_make_cppgc_object() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -138,6 +139,7 @@ const fn op_use_cppgc_object() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -262,6 +264,7 @@ const fn op_use_optional_cppgc_object() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_deferred.out b/ops/op2/test_cases/async/async_deferred.out
index a8316e2..7d39aff 100644
--- a/ops/op2/test_cases/async/async_deferred.out
+++ b/ops/op2/test_cases/async/async_deferred.out
@@ -14,6 +14,7 @@ pub const fn op_async_deferred() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/async/async_jsbuffer.out b/ops/op2/test_cases/async/async_jsbuffer.out
index 786bbdc..52b36a0 100644
--- a/ops/op2/test_cases/async/async_jsbuffer.out
+++ b/ops/op2/test_cases/async/async_jsbuffer.out
@@ -14,6 +14,7 @@ pub const fn op_async_v8_buffer() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_lazy.out b/ops/op2/test_cases/async/async_lazy.out
index 6121fb5..d6b1f53 100644
--- a/ops/op2/test_cases/async/async_lazy.out
+++ b/ops/op2/test_cases/async/async_lazy.out
@@ -14,6 +14,7 @@ pub const fn op_async_lazy() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/async/async_op_metadata.out b/ops/op2/test_cases/async/async_op_metadata.out
index 2321e32..bbbb515 100644
--- a/ops/op2/test_cases/async/async_op_metadata.out
+++ b/ops/op2/test_cases/async/async_op_metadata.out
@@ -14,6 +14,7 @@ const fn op_blob_read_part() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -121,6 +122,7 @@ const fn op_broadcast_recv() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_opstate.out b/ops/op2/test_cases/async/async_opstate.out
index e662b7c..fe4af77 100644
--- a/ops/op2/test_cases/async/async_opstate.out
+++ b/ops/op2/test_cases/async/async_opstate.out
@@ -14,6 +14,7 @@ pub const fn op_async_opstate() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_result.out b/ops/op2/test_cases/async/async_result.out
index 6be9fc1..9c26401 100644
--- a/ops/op2/test_cases/async/async_result.out
+++ b/ops/op2/test_cases/async/async_result.out
@@ -14,6 +14,7 @@ pub const fn op_async() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_result_impl.out b/ops/op2/test_cases/async/async_result_impl.out
index fa7c79d..3b8405d 100644
--- a/ops/op2/test_cases/async/async_result_impl.out
+++ b/ops/op2/test_cases/async/async_result_impl.out
@@ -14,6 +14,7 @@ pub const fn op_async_result_impl() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_result_smi.out b/ops/op2/test_cases/async/async_result_smi.out
index 6200ff4..046a02f 100644
--- a/ops/op2/test_cases/async/async_result_smi.out
+++ b/ops/op2/test_cases/async/async_result_smi.out
@@ -14,6 +14,7 @@ pub const fn op_async() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_stack_trace.out b/ops/op2/test_cases/async/async_stack_trace.out
index 776aa4c..84b8f56 100644
--- a/ops/op2/test_cases/async/async_stack_trace.out
+++ b/ops/op2/test_cases/async/async_stack_trace.out
@@ -14,6 +14,7 @@ pub const fn op_async_stack_trace() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_v8_global.out b/ops/op2/test_cases/async/async_v8_global.out
index 7f92913..9ba3b09 100644
--- a/ops/op2/test_cases/async/async_v8_global.out
+++ b/ops/op2/test_cases/async/async_v8_global.out
@@ -14,6 +14,7 @@ pub const fn op_async_v8_global() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/async/async_void.out b/ops/op2/test_cases/async/async_void.out
index 4e102c0..79082d4 100644
--- a/ops/op2/test_cases/async/async_void.out
+++ b/ops/op2/test_cases/async/async_void.out
@@ -14,6 +14,7 @@ pub const fn op_async() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/add.out b/ops/op2/test_cases/sync/add.out
index 9312d51..62459d7 100644
--- a/ops/op2/test_cases/sync/add.out
+++ b/ops/op2/test_cases/sync/add.out
@@ -14,6 +14,7 @@ const fn op_add() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/add_options.out b/ops/op2/test_cases/sync/add_options.out
index cae543d..53f11d3 100644
--- a/ops/op2/test_cases/sync/add_options.out
+++ b/ops/op2/test_cases/sync/add_options.out
@@ -14,6 +14,7 @@ pub const fn op_test_add_option() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/bigint.out b/ops/op2/test_cases/sync/bigint.out
index 1177b61..9e6040e 100644
--- a/ops/op2/test_cases/sync/bigint.out
+++ b/ops/op2/test_cases/sync/bigint.out
@@ -14,6 +14,7 @@ pub const fn op_bigint() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/bool.out b/ops/op2/test_cases/sync/bool.out
index 4994105..7085734 100644
--- a/ops/op2/test_cases/sync/bool.out
+++ b/ops/op2/test_cases/sync/bool.out
@@ -14,6 +14,7 @@ pub const fn op_bool() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/bool_result.out b/ops/op2/test_cases/sync/bool_result.out
index 71e138a..23d369c 100644
--- a/ops/op2/test_cases/sync/bool_result.out
+++ b/ops/op2/test_cases/sync/bool_result.out
@@ -14,6 +14,7 @@ pub const fn op_bool() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/buffers.out b/ops/op2/test_cases/sync/buffers.out
index 9d77492..e09241e 100644
--- a/ops/op2/test_cases/sync/buffers.out
+++ b/ops/op2/test_cases/sync/buffers.out
@@ -14,6 +14,7 @@ const fn op_buffers() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -296,6 +297,7 @@ const fn op_buffers_32() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -578,6 +580,7 @@ const fn op_buffers_option() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/buffers_copy.out b/ops/op2/test_cases/sync/buffers_copy.out
index 925f357..d61d82b 100644
--- a/ops/op2/test_cases/sync/buffers_copy.out
+++ b/ops/op2/test_cases/sync/buffers_copy.out
@@ -14,6 +14,7 @@ const fn op_buffers() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -252,6 +253,7 @@ const fn op_buffers_32() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/buffers_out.out b/ops/op2/test_cases/sync/buffers_out.out
index 2fb9d40..8040575 100644
--- a/ops/op2/test_cases/sync/buffers_out.out
+++ b/ops/op2/test_cases/sync/buffers_out.out
@@ -14,6 +14,7 @@ const fn op_buffers() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -136,6 +137,7 @@ const fn op_buffers_option() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -268,6 +270,7 @@ const fn op_arraybuffers() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/cfg.out b/ops/op2/test_cases/sync/cfg.out
index af8a895..016ccda 100644
--- a/ops/op2/test_cases/sync/cfg.out
+++ b/ops/op2/test_cases/sync/cfg.out
@@ -16,6 +16,7 @@ pub const fn op_maybe_windows() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -162,6 +163,7 @@ pub const fn op_maybe_windows() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/clippy_allow.out b/ops/op2/test_cases/sync/clippy_allow.out
index 1739ccf..ea8fea9 100644
--- a/ops/op2/test_cases/sync/clippy_allow.out
+++ b/ops/op2/test_cases/sync/clippy_allow.out
@@ -16,6 +16,7 @@ pub const fn op_extra_annotation() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -160,6 +161,7 @@ pub const fn op_clippy_internal() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/cppgc_resource.out b/ops/op2/test_cases/sync/cppgc_resource.out
index ff383ab..393d4b7 100644
--- a/ops/op2/test_cases/sync/cppgc_resource.out
+++ b/ops/op2/test_cases/sync/cppgc_resource.out
@@ -14,6 +14,7 @@ const fn op_cppgc_object() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -218,6 +219,7 @@ const fn op_option_cppgc_object() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -430,6 +432,7 @@ const fn op_make_cppgc_object() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -523,6 +526,7 @@ const fn op_use_cppgc_object() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -727,6 +731,7 @@ const fn op_make_cppgc_object_option() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -823,6 +828,7 @@ const fn op_use_cppgc_object_option() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/doc_comment.out b/ops/op2/test_cases/sync/doc_comment.out
index 1d12735..0a1452f 100644
--- a/ops/op2/test_cases/sync/doc_comment.out
+++ b/ops/op2/test_cases/sync/doc_comment.out
@@ -15,6 +15,7 @@ pub const fn op_has_doc_comment() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/fast_alternative.out b/ops/op2/test_cases/sync/fast_alternative.out
index 113fb39..770c06b 100644
--- a/ops/op2/test_cases/sync/fast_alternative.out
+++ b/ops/op2/test_cases/sync/fast_alternative.out
@@ -14,6 +14,7 @@ const fn op_slow() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({ op_fast().fast_fn() }),
             Some({ op_fast().fast_fn_with_metrics() }),
             ::deno_core::OpMetadata {
@@ -131,6 +132,7 @@ const fn op_fast() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -349,6 +351,7 @@ const fn op_slow_generic<T: Trait>() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({ op_fast_generic::<T>().fast_fn() }),
             Some({ op_fast_generic::<T>().fast_fn_with_metrics() }),
             ::deno_core::OpMetadata {
@@ -466,6 +469,7 @@ const fn op_fast_generic<T: Trait>() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/from_v8.out b/ops/op2/test_cases/sync/from_v8.out
index 33fe31a..80ba5b9 100644
--- a/ops/op2/test_cases/sync/from_v8.out
+++ b/ops/op2/test_cases/sync/from_v8.out
@@ -14,6 +14,7 @@ pub const fn op_from_v8_arg() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/generics.out b/ops/op2/test_cases/sync/generics.out
index 9bc1ae7..1be6185 100644
--- a/ops/op2/test_cases/sync/generics.out
+++ b/ops/op2/test_cases/sync/generics.out
@@ -14,6 +14,7 @@ pub const fn op_generics<T: Trait>() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -156,6 +157,7 @@ pub const fn op_generics_static<T: Trait + 'static>() -> ::deno_core::_ops::OpDe
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -298,6 +300,7 @@ pub const fn op_generics_static_where<T: Trait + 'static>() -> ::deno_core::_ops
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/nofast.out b/ops/op2/test_cases/sync/nofast.out
index 2761785..820bfc1 100644
--- a/ops/op2/test_cases/sync/nofast.out
+++ b/ops/op2/test_cases/sync/nofast.out
@@ -14,6 +14,7 @@ const fn op_nofast() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/op_state_attr.out b/ops/op2/test_cases/sync/op_state_attr.out
index f833850..cfae9d1 100644
--- a/ops/op2/test_cases/sync/op_state_attr.out
+++ b/ops/op2/test_cases/sync/op_state_attr.out
@@ -14,6 +14,7 @@ const fn op_state_rc() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/op_state_rc.out b/ops/op2/test_cases/sync/op_state_rc.out
index cc5288e..917a3b1 100644
--- a/ops/op2/test_cases/sync/op_state_rc.out
+++ b/ops/op2/test_cases/sync/op_state_rc.out
@@ -14,6 +14,7 @@ const fn op_state_rc() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/op_state_ref.out b/ops/op2/test_cases/sync/op_state_ref.out
index eb5b5ea..589e4ac 100644
--- a/ops/op2/test_cases/sync/op_state_ref.out
+++ b/ops/op2/test_cases/sync/op_state_ref.out
@@ -14,6 +14,7 @@ const fn op_state_ref() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -184,6 +185,7 @@ const fn op_state_mut() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -354,6 +356,7 @@ const fn op_state_and_v8() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -466,6 +469,7 @@ const fn op_state_and_v8_local() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/result_external.out b/ops/op2/test_cases/sync/result_external.out
index a58279a..3555150 100644
--- a/ops/op2/test_cases/sync/result_external.out
+++ b/ops/op2/test_cases/sync/result_external.out
@@ -14,6 +14,7 @@ pub const fn op_external_with_result() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/result_primitive.out b/ops/op2/test_cases/sync/result_primitive.out
index 7610731..6a79984 100644
--- a/ops/op2/test_cases/sync/result_primitive.out
+++ b/ops/op2/test_cases/sync/result_primitive.out
@@ -14,6 +14,7 @@ pub const fn op_u32_with_result() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/result_scope.out b/ops/op2/test_cases/sync/result_scope.out
index 7542b53..4af4bd4 100644
--- a/ops/op2/test_cases/sync/result_scope.out
+++ b/ops/op2/test_cases/sync/result_scope.out
@@ -14,6 +14,7 @@ pub const fn op_void_with_result() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/result_void.out b/ops/op2/test_cases/sync/result_void.out
index 5c03cf1..23f9850 100644
--- a/ops/op2/test_cases/sync/result_void.out
+++ b/ops/op2/test_cases/sync/result_void.out
@@ -14,6 +14,7 @@ pub const fn op_void_with_result() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/serde_v8.out b/ops/op2/test_cases/sync/serde_v8.out
index 1e2890b..ca0094d 100644
--- a/ops/op2/test_cases/sync/serde_v8.out
+++ b/ops/op2/test_cases/sync/serde_v8.out
@@ -14,6 +14,7 @@ pub const fn op_serde_v8() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/smi.out b/ops/op2/test_cases/sync/smi.out
index 1f4aca0..dc08d78 100644
--- a/ops/op2/test_cases/sync/smi.out
+++ b/ops/op2/test_cases/sync/smi.out
@@ -14,6 +14,7 @@ const fn op_smi_unsigned_return() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -292,6 +293,7 @@ const fn op_smi_signed_return() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
@@ -570,6 +572,7 @@ const fn op_smi_option() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/stack_trace.out b/ops/op2/test_cases/sync/stack_trace.out
index 0d41d1b..eb39a47 100644
--- a/ops/op2/test_cases/sync/stack_trace.out
+++ b/ops/op2/test_cases/sync/stack_trace.out
@@ -14,6 +14,7 @@ const fn op_stack_trace() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/stack_trace_scope.out b/ops/op2/test_cases/sync/stack_trace_scope.out
index 07d3bc2..25ee51f 100644
--- a/ops/op2/test_cases/sync/stack_trace_scope.out
+++ b/ops/op2/test_cases/sync/stack_trace_scope.out
@@ -14,6 +14,7 @@ const fn op_stack_trace() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/string_cow.out b/ops/op2/test_cases/sync/string_cow.out
index 9136093..4bb0fd9 100644
--- a/ops/op2/test_cases/sync/string_cow.out
+++ b/ops/op2/test_cases/sync/string_cow.out
@@ -14,6 +14,7 @@ const fn op_string_cow() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/string_onebyte.out b/ops/op2/test_cases/sync/string_onebyte.out
index 1d13440..5349dfc 100644
--- a/ops/op2/test_cases/sync/string_onebyte.out
+++ b/ops/op2/test_cases/sync/string_onebyte.out
@@ -14,6 +14,7 @@ const fn op_string_onebyte() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/string_option_return.out b/ops/op2/test_cases/sync/string_option_return.out
index 02a5c6f..9af4668 100644
--- a/ops/op2/test_cases/sync/string_option_return.out
+++ b/ops/op2/test_cases/sync/string_option_return.out
@@ -14,6 +14,7 @@ pub const fn op_string_return() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/string_owned.out b/ops/op2/test_cases/sync/string_owned.out
index 37ceec7..b932c75 100644
--- a/ops/op2/test_cases/sync/string_owned.out
+++ b/ops/op2/test_cases/sync/string_owned.out
@@ -14,6 +14,7 @@ const fn op_string_owned() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/string_ref.out b/ops/op2/test_cases/sync/string_ref.out
index e185c94..73e4eb0 100644
--- a/ops/op2/test_cases/sync/string_ref.out
+++ b/ops/op2/test_cases/sync/string_ref.out
@@ -14,6 +14,7 @@ const fn op_string_owned() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/string_return.out b/ops/op2/test_cases/sync/string_return.out
index 4366262..fd394f4 100644
--- a/ops/op2/test_cases/sync/string_return.out
+++ b/ops/op2/test_cases/sync/string_return.out
@@ -14,6 +14,7 @@ pub const fn op_string_return() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -111,6 +112,7 @@ pub const fn op_string_return_ref() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
@@ -208,6 +210,7 @@ pub const fn op_string_return_cow() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/to_v8.out b/ops/op2/test_cases/sync/to_v8.out
index 757d995..8185830 100644
--- a/ops/op2/test_cases/sync/to_v8.out
+++ b/ops/op2/test_cases/sync/to_v8.out
@@ -14,6 +14,7 @@ pub const fn op_to_v8_return() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/v8_global.out b/ops/op2/test_cases/sync/v8_global.out
index 878c5e7..467ba14 100644
--- a/ops/op2/test_cases/sync/v8_global.out
+++ b/ops/op2/test_cases/sync/v8_global.out
@@ -14,6 +14,7 @@ pub const fn op_global() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/v8_handlescope.out b/ops/op2/test_cases/sync/v8_handlescope.out
index 82f98d2..43a57a3 100644
--- a/ops/op2/test_cases/sync/v8_handlescope.out
+++ b/ops/op2/test_cases/sync/v8_handlescope.out
@@ -14,6 +14,7 @@ const fn op_handlescope() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/v8_lifetime.out b/ops/op2/test_cases/sync/v8_lifetime.out
index ba3b8c5..2980096 100644
--- a/ops/op2/test_cases/sync/v8_lifetime.out
+++ b/ops/op2/test_cases/sync/v8_lifetime.out
@@ -14,6 +14,7 @@ pub const fn op_v8_lifetime() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/ops/op2/test_cases/sync/v8_ref_option.out b/ops/op2/test_cases/sync/v8_ref_option.out
index c4dbe9c..2cdcabd 100644
--- a/ops/op2/test_cases/sync/v8_ref_option.out
+++ b/ops/op2/test_cases/sync/v8_ref_option.out
@@ -14,6 +14,7 @@ pub const fn op_v8_lifetime() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             Some({
                 use deno_core::v8::fast_api::Type as CType;
                 use deno_core::v8;
diff --git a/ops/op2/test_cases/sync/v8_string.out b/ops/op2/test_cases/sync/v8_string.out
index a997db4..e0b4b3e 100644
--- a/ops/op2/test_cases/sync/v8_string.out
+++ b/ops/op2/test_cases/sync/v8_string.out
@@ -14,6 +14,7 @@ const fn op_v8_string() -> ::deno_core::_ops::OpDecl {
             false,
             Self::v8_fn_ptr as _,
             Self::v8_fn_ptr_metrics as _,
+            ::deno_core::AccessorType::None,
             None,
             None,
             ::deno_core::OpMetadata {
diff --git a/testing/checkin/runner/extensions.rs b/testing/checkin/runner/extensions.rs
index d9b9700..129277a 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 24eb3fe..8ac41b1 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,36 +70,79 @@ pub fn op_stats_delete(
   test_data.take::<RuntimeActivityStats>(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<f64>,
+    y: Option<f64>,
+    z: Option<f64>,
+    w: Option<f64>,
+  ) -> 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<v8::Object>,
+  ) -> Result<DOMPoint, AnyError> {
+    fn get(
+      scope: &mut v8::HandleScope,
+      other: v8::Local<v8::Object>,
+      key: &str,
+    ) -> Option<f64> {
+      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);
+  #[getter]
+  fn x(&self) -> f64 {
+    self.x
   }
-}
 
-// 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()];
+  #[setter]
+  fn x(&self, _: f64) {}
+
+  #[getter]
+  fn y(&self) -> f64 {
+    self.y
+  }
+  #[getter]
+  fn w(&self) -> f64 {
+    self.w
+  }
+  #[getter]
+  fn z(&self) -> f64 {
+    self.z
+  }
+}
 
 #[op2(fast)]
 pub fn op_nop_generic<T: SomeType + 'static>(state: &mut OpState) {
diff --git a/testing/checkin/runtime/__init.js b/testing/checkin/runtime/__init.js
index 9b4ac1e..73ad976 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 0000000..8ffc560
--- /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 c89a661..63e238b 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 fromPoint(
+    other: { x?: number; y?: number; z?: number; w?: number },
+  ): DOMPoint;
+  x(): number;
+}
diff --git a/testing/tsconfig.json b/testing/tsconfig.json
index 2929441..5266444 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 5b52a7d..e16550a 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.fromPoint({ x: 200 });
+  const p4 = DOMPoint.fromPoint({ 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);
+});