Repository: denoland/deno_core
Pull request #34 (2023-07-12 01:29:28)

feat(ops): Support #[buffer(copy)], add self-documenting test
Adds slow and fast call support for:

 - `#[buffer(copy)] Vec<u8>`
 - `#[buffer(copy)] Box<[u8]>`
 - `#[buffer(copy)] bytes::Bytes`

We also start generating and testing a README with various parameter types. 



Patch:
diff --git a/core/runtime/ops.rs b/core/runtime/ops.rs
index 14741b5..0c339ba 100644
--- a/core/runtime/ops.rs
+++ b/core/runtime/ops.rs
@@ -343,28 +343,29 @@ pub fn serde_v8_to_rust<'a, T: Deserialize<'a>>(
   from_v8(scope, input)
 }
 
-/// Retrieve a [`serde_v8::V8Slice`] from a value.
-#[allow(clippy::result_unit_err)]
-pub fn to_nonresizable_v8_slice(
+/// Retrieve a [`serde_v8::V8Slice`] from a typed array in an [`v8::ArrayBufferView`].
+pub fn to_v8_slice<'a, T>(
   scope: &mut v8::HandleScope,
-  input: v8::Local<v8::Value>,
-) -> Result<serde_v8::V8Slice, &'static str> {
-  let (buf, offset, length) =
-    if let Ok(buf) = v8::Local::<v8::ArrayBufferView>::try_from(input) {
-      let Some(buffer) = buf.buffer(scope) else {
-        return Err("buffer missing");
-      };
-      (buffer, buf.byte_offset(), buf.byte_length())
-    } else if let Ok(buf) = v8::Local::<v8::ArrayBuffer>::try_from(input) {
-      (buf, 0, buf.byte_length())
-    } else {
-      return Err("expected ArrayBuffer or ArrayBufferView");
+  input: v8::Local<'a, v8::Value>,
+) -> Result<serde_v8::V8Slice, &'static str>
+where
+  v8::Local<'a, T>: TryFrom<v8::Local<'a, v8::Value>>,
+  v8::Local<'a, v8::ArrayBufferView>: From<v8::Local<'a, T>>,
+{
+  let (store, offset, length) = if let Ok(buf) = v8::Local::<T>::try_from(input)
+  {
+    let buf: v8::Local<v8::ArrayBufferView> = buf.into();
+    let Some(buffer) = buf.buffer(scope) else {
+      return Err("buffer missing");
     };
-
-  let store = buf.get_backing_store();
-  if store.is_resizable_by_user_javascript() {
-    return Err("expected non-resizable buffer");
-  }
+    (
+      buffer.get_backing_store(),
+      buf.byte_offset(),
+      buf.byte_length(),
+    )
+  } else {
+    return Err("expected typed ArrayBufferView");
+  };
   let slice =
     unsafe { serde_v8::V8Slice::from_parts(store, offset..(offset + length)) };
   Ok(slice)
@@ -422,8 +423,8 @@ mod tests {
       op_state_mut_attr,
       op_state_multi_attr,
       op_buffer_slice,
-      op_buffer_slice_callback,
       op_buffer_slice_unsafe_callback,
+      op_buffer_copy,
     ],
     state = |state| {
       state.put(1234u32);
@@ -446,6 +447,8 @@ mod tests {
       extensions: vec![testing::init_ops_and_esm()],
       ..Default::default()
     });
+    let err_mapper =
+      |err| generic_error(format!("{op} test failed ({test}): {err:?}"));
     runtime
       .execute_script(
         "",
@@ -463,7 +466,7 @@ mod tests {
           .into(),
         ),
       )
-      .unwrap();
+      .map_err(err_mapper)?;
     FAIL.with(|b| b.set(false));
     runtime.execute_script(
       "",
@@ -984,21 +987,6 @@ mod tests {
     output[0] = input[0];
   }
 
-  // TODO(mmastrac): This is a dangerous op that we'll use to test resizable buffers in a later pass.
-  #[op2(core)]
-  pub fn op_buffer_slice_callback(
-    scope: &mut v8::HandleScope,
-    index: usize,
-    #[buffer] input: &[u8],
-    #[buffer] output: &mut [u8],
-    callback: v8::Local<v8::Function>,
-  ) {
-    output[index] = input[index];
-    let recv = callback.into();
-    callback.call(scope, recv, &[]);
-    output[index] = input[index];
-  }
-
   #[tokio::test]
   pub async fn test_op_buffer_slice() -> Result<(), Box<dyn std::error::Error>>
   {
@@ -1011,16 +999,19 @@ mod tests {
       op_buffer_slice(new Uint8Array([1,2,3]), 3, out, 10);
       assert(out[0] == 1);",
     )?;
-    // Uint8Array -> raw ArrayBuffer
+    // Uint8Array(ArrayBuffer) -> Uint8Array(ArrayBuffer)
     run_test2(
       10000,
       "op_buffer_slice",
       r"
+      let inbuf = new ArrayBuffer(10);
+      let in_u8 = new Uint8Array(inbuf);
+      in_u8[0] = 1;
       let out = new ArrayBuffer(10);
-      op_buffer_slice(new Uint8Array([1,2,3]), 3, out, 10);
+      op_buffer_slice(in_u8, 10, new Uint8Array(out), 10);
       assert(new Uint8Array(out)[0] == 1);",
     )?;
-    // ArrayBuffer -> ArrayBuffer
+    // Uint8Array(ArrayBuffer, 5, 5) -> Uint8Array(ArrayBuffer)
     run_test2(
       10000,
       "op_buffer_slice",
@@ -1029,37 +1020,21 @@ mod tests {
       let in_u8 = new Uint8Array(inbuf);
       in_u8[5] = 1;
       let out = new ArrayBuffer(10);
-      op_buffer_slice(new Uint8Array(inbuf, 5, 5), 5, out, 10);
+      op_buffer_slice(new Uint8Array(inbuf, 5, 5), 5, new Uint8Array(out), 10);
+      assert(new Uint8Array(out)[0] == 1);",
+    )?;
+    // Resizable
+    run_test2(
+      10000,
+      "op_buffer_slice",
+      r"
+      let inbuf = new ArrayBuffer(10, { maxByteLength: 100 });
+      let in_u8 = new Uint8Array(inbuf);
+      in_u8[5] = 1;
+      let out = new ArrayBuffer(10, { maxByteLength: 100 });
+      op_buffer_slice(new Uint8Array(inbuf, 5, 5), 5, new Uint8Array(out), 10);
       assert(new Uint8Array(out)[0] == 1);",
     )?;
-    // TODO(mmastrac): We aren't going to solve resizable buffers right now
-    // run_test2(
-    //   10000,
-    //   "op_buffer_slice",
-    //   r"
-    //   let inbuf = new ArrayBuffer(10, { maxByteLength: 100 });
-    //   let in_u8 = new Uint8Array(inbuf);
-    //   in_u8[5] = 1;
-    //   let out = new ArrayBuffer(10, { maxByteLength: 100 });
-    //   op_buffer_slice(new Uint8Array(inbuf, 5, 5), 5, out, 10);
-    //   assert(new Uint8Array(out)[0] == 1);",
-    // )?;
-    // run_test2(
-    //   1,
-    //   "op_buffer_slice_callback",
-    //   r"
-    //   let inbuf = new ArrayBuffer(10, { maxByteLength: 100 });
-    //   let outbuf = new ArrayBuffer(10, { maxByteLength: 100 });
-    //   op_buffer_slice_callback(5, inbuf, outbuf, () => {
-    //     try {
-    //       inbuf.resize(1);
-    //       outbuf.resize(1);
-    //     } catch (e) {
-    //       assert(false);
-    //     }
-    //   });
-    //   "
-    // )?;
     Ok(())
   }
 
@@ -1091,4 +1066,37 @@ mod tests {
     )?;
     Ok(())
   }
+
+  /// Ensures that three copies are independent. Note that we cannot mutate the
+  /// `bytes::Bytes`.
+  #[op2(core, fast)]
+  #[allow(clippy::boxed_local)] // Clippy bug? It warns about input2
+  pub fn op_buffer_copy(
+    #[buffer(copy)] mut input1: Vec<u8>,
+    #[buffer(copy)] mut input2: Box<[u8]>,
+    #[buffer(copy)] input3: bytes::Bytes,
+  ) {
+    assert_eq!(input1[0], input2[0]);
+    assert_eq!(input2[0], input3[0]);
+    input1[0] = 0xff;
+    assert_ne!(input1[0], input2[0]);
+    assert_eq!(input2[0], input3[0]);
+    input2[0] = 0xff;
+    assert_eq!(input1[0], input2[0]);
+    assert_ne!(input2[0], input3[0]);
+  }
+
+  #[tokio::test]
+  pub async fn test_op_buffer_copy() -> Result<(), Box<dyn std::error::Error>> {
+    run_test2(
+      10000,
+      "op_buffer_copy",
+      r"
+      let input = new Uint8Array(10);
+      input[0] = 1;
+      op_buffer_copy(input, input, input);
+      assert(input[0] == 1);",
+    )?;
+    Ok(())
+  }
 }
diff --git a/ops/op2/README.md b/ops/op2/README.md
new file mode 100644
index 0000000..9490447
--- /dev/null
+++ b/ops/op2/README.md
@@ -0,0 +1,568 @@
+# op2
+
+`#[op2]` is the in-progress replacement for `#[op]`.
+
+# Strings
+
+`String`s in Rust are always UTF-8. `String`s in v8, however, are either
+two-byte UTF-16 or one-byte Latin-1. One-byte Latin-1 strings are not
+byte-compatible with UTF-8, as characters with the index 128-255 require two
+bytes to encode in UTF-8.
+
+Because of this, `String`s in `op`s always require a copy (at least) to ensure
+that we are not incorrectly passing Latin-1 data to methods that expect a UTF-8
+string. At this time there is no way to avoid this copy, though the `op` code
+does attempt to avoid any allocations where possible by making use of a stack
+buffer.
+
+# Parameters
+
+<!-- START -->
+<table><tr><th>Rust</th><th>Fastcall</th><th>v8</th></tr>
+<tr>
+<td>
+
+```rust
+bool
+```
+
+</td><td>
+✅
+</td><td>
+Bool
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+i8
+```
+
+</td><td>
+✅
+</td><td>
+Uint32, Int32, Number, BigInt
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+u8
+```
+
+</td><td>
+✅
+</td><td>
+Uint32, Int32, Number, BigInt
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+i16
+```
+
+</td><td>
+✅
+</td><td>
+Uint32, Int32, Number, BigInt
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+u16
+```
+
+</td><td>
+✅
+</td><td>
+Uint32, Int32, Number, BigInt
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+i32
+```
+
+</td><td>
+✅
+</td><td>
+Uint32, Int32, Number, BigInt
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+u32
+```
+
+</td><td>
+✅
+</td><td>
+Uint32, Int32, Number, BigInt
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+isize
+```
+
+</td><td>
+✅
+</td><td>
+Uint32, Int32, Number, BigInt
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+usize
+```
+
+</td><td>
+✅
+</td><td>
+Uint32, Int32, Number, BigInt
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[string] String
+```
+
+</td><td>
+✅
+</td><td>
+String
+</td><td>
+Fastcall available only if string is Latin-1. Will always create an allocated, UTF-8 copy of the String data.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[string] &str
+```
+
+</td><td>
+✅
+</td><td>
+String
+</td><td>
+Fastcall available only if string is Latin-1. Will create an owned `String` copy of the String data if it doesn't fit on the stack. Will never allocate in a fastcall, but will copy Latin-1 -> UTF-8.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[string] Cow<str>
+```
+
+</td><td>
+✅
+</td><td>
+String
+</td><td>
+Fastcall available only if string is Latin-1. Will create a `Cow::Owned` copy of the String data if it doesn't fit on the stack. Will always be `Cow::Borrowed` in a fastcall, but will copy Latin-1 -> UTF-8.
+</td></tr>
+<tr>
+<td>
+
+```rust
+&v8::Value
+```
+
+</td><td>
+✅
+</td><td>
+any
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&v8::String
+```
+
+</td><td>
+✅
+</td><td>
+String
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&v8::Object
+```
+
+</td><td>
+✅
+</td><td>
+Object
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&v8::Function
+```
+
+</td><td>
+✅
+</td><td>
+Function
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&v8::...
+```
+
+</td><td>
+✅
+</td><td>
+...
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&mut v8::Value
+```
+
+</td><td>
+✅
+</td><td>
+any
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&mut v8::String
+```
+
+</td><td>
+✅
+</td><td>
+String
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&mut v8::Object
+```
+
+</td><td>
+✅
+</td><td>
+Object
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&mut v8::Function
+```
+
+</td><td>
+✅
+</td><td>
+Function
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&mut v8::...
+```
+
+</td><td>
+✅
+</td><td>
+...
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+v8::Local<v8::Value>
+```
+
+</td><td>
+✅
+</td><td>
+any
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+v8::Local<v8::String>
+```
+
+</td><td>
+✅
+</td><td>
+String
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+v8::Local<v8::Object>
+```
+
+</td><td>
+✅
+</td><td>
+Object
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+v8::Local<v8::Function>
+```
+
+</td><td>
+✅
+</td><td>
+Function
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+v8::Local<v8::...>
+```
+
+</td><td>
+✅
+</td><td>
+...
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[serde] SerdeType
+```
+
+</td><td>
+
+</td><td>
+any
+</td><td>
+⚠️ May be slow.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[serde] (Tuple, Tuple)
+```
+
+</td><td>
+
+</td><td>
+any
+</td><td>
+⚠️ May be slow.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[buffer] &mut [u8]
+```
+
+</td><td>
+✅
+</td><td>
+UInt8Array (resizable=true,false)
+</td><td>
+⚠️ JS may modify the contents of the slice if V8 is called re-entrantly.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[buffer] &[u8]
+```
+
+</td><td>
+✅
+</td><td>
+UInt8Array (resizable=true,false)
+</td><td>
+⚠️ JS may modify the contents of the slice if V8 is called re-entrantly.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[buffer(copy)] Vec<u8>
+```
+
+</td><td>
+✅
+</td><td>
+ArrayBuffer, ArrayBufferView, SharedArrayBuffer
+</td><td>
+Safe, but forces a copy.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[buffer(copy)] Box<[u8]>
+```
+
+</td><td>
+✅
+</td><td>
+ArrayBuffer, ArrayBufferView, SharedArrayBuffer
+</td><td>
+Safe, but forces a copy.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[buffer(copy)] bytes::Bytes
+```
+
+</td><td>
+✅
+</td><td>
+ArrayBuffer, ArrayBufferView, SharedArrayBuffer
+</td><td>
+Safe, but forces a copy.
+</td></tr>
+<tr>
+<td>
+
+```rust
+&OpState
+```
+
+</td><td>
+✅
+</td><td>
+
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+&mut OpState
+```
+
+</td><td>
+✅
+</td><td>
+
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+Rc<RefCell<OpState>>
+```
+
+</td><td>
+✅
+</td><td>
+
+</td><td>
+
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[state] &StateObject
+```
+
+</td><td>
+✅
+</td><td>
+
+</td><td>
+Extracts an object from `OpState`.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[state] &mut StateObject
+```
+
+</td><td>
+✅
+</td><td>
+
+</td><td>
+Extracts an object from `OpState`.
+</td></tr>
+</table>
diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs
index 1767455..a23c246 100644
--- a/ops/op2/dispatch_fast.rs
+++ b/ops/op2/dispatch_fast.rs
@@ -292,6 +292,15 @@ fn map_v8_fastcall_arg_to_arg(
     Arg::Buffer(Buffer::Slice(_, NumericArg::u8)) => {
       quote!(let #arg_ident = unsafe { #arg_ident.as_mut().unwrap() }.get_storage_if_aligned().unwrap();)
     }
+    Arg::Buffer(Buffer::Vec(NumericArg::u8)) => {
+      quote!(let #arg_ident = unsafe { #arg_ident.as_mut().unwrap() }.get_storage_if_aligned().unwrap().to_vec();)
+    }
+    Arg::Buffer(Buffer::BoxSlice(NumericArg::u8)) => {
+      quote!(let #arg_ident = unsafe { #arg_ident.as_mut().unwrap() }.get_storage_if_aligned().unwrap().to_vec().into_boxed_slice();)
+    }
+    Arg::Buffer(Buffer::Bytes) => {
+      quote!(let #arg_ident = unsafe { #arg_ident.as_mut().unwrap() }.get_storage_if_aligned().unwrap().to_vec().into();)
+    }
     Arg::Ref(RefType::Ref, Special::OpState) => {
       *needs_opctx = true;
       quote!(let #arg_ident = &#opctx.state.borrow();)
@@ -388,7 +397,12 @@ fn map_arg_to_v8_fastcall_type(
   arg: &Arg,
 ) -> Result<Option<V8FastCallType>, V8MappingError> {
   let rv = match arg {
-    Arg::Buffer(Buffer::Slice(_, NumericArg::u8)) => V8FastCallType::Uint8Array,
+    Arg::Buffer(
+      Buffer::Slice(_, NumericArg::u8)
+      | Buffer::Vec(NumericArg::u8)
+      | Buffer::BoxSlice(NumericArg::u8)
+      | Buffer::Bytes,
+    ) => V8FastCallType::Uint8Array,
     Arg::Buffer(_) => return Ok(None),
     // Virtual OpState arguments
     Arg::RcRefCell(Special::OpState)
diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs
index c296474..e63f5ea 100644
--- a/ops/op2/dispatch_slow.rs
+++ b/ops/op2/dispatch_slow.rs
@@ -381,8 +381,14 @@ pub fn from_arg_buffer(
   } = generator_state;
 
   *needs_scope = true;
+
+  // TODO(mmastrac): Other buffer types
+  let array = NumericArg::u8
+    .v8_array_type()
+    .expect("Could not retrieve the v8 type");
+
   let make_v8slice = quote! {
-    let mut #arg_ident = match unsafe { #deno_core::_ops::to_nonresizable_v8_slice(&mut #scope, #arg_ident) } {
+    let mut #arg_ident = match unsafe { #deno_core::_ops::to_v8_slice::<#deno_core::v8::#array>(&mut #scope, #arg_ident) } {
       Ok(#arg_ident) => #arg_ident,
       Err(#err) => {
         #throw_exception
@@ -394,6 +400,16 @@ pub fn from_arg_buffer(
     Buffer::Slice(_, NumericArg::u8) => {
       quote!(let #arg_ident = &mut #arg_ident;)
     }
+    Buffer::Vec(NumericArg::u8) => {
+      quote!(let #arg_ident = #arg_ident.to_vec();)
+    }
+    Buffer::BoxSlice(NumericArg::u8) => {
+      quote!(let #arg_ident = #arg_ident.to_boxed_slice();)
+    }
+    Buffer::Bytes => {
+      // TODO(mmastrac): This assumes #[buffer(copy)]
+      quote!(let #arg_ident = #arg_ident.to_vec().into();)
+    }
     _ => {
       return Err(V8MappingError::NoMapping(
         "a buffer argument",
diff --git a/ops/op2/mod.rs b/ops/op2/mod.rs
index 9f53489..b9ba9e4 100644
--- a/ops/op2/mod.rs
+++ b/ops/op2/mod.rs
@@ -337,4 +337,72 @@ mod tests {
       );
     }
   }
+
+  #[test]
+  fn test_valid_args_md() {
+    let update_expected = std::env::var("UPDATE_EXPECTED").is_ok();
+    let md = include_str!("valid_args.md");
+    let separator = "\n<!-- START -->\n";
+    let header = include_str!("README.md").split(separator).next().unwrap();
+    let mut actual = format!("{header}{separator}<table><tr><th>Rust</th><th>Fastcall</th><th>v8</th></tr>\n");
+
+    // Skip the header and table line
+    for line in md.split('\n').skip(2).filter(|s| {
+      !s.trim().is_empty() && !s.split('|').nth(1).unwrap().trim().is_empty()
+    }) {
+      let expansion = if line.contains("**V8**") {
+        let mut expansion = vec![];
+        for key in ["String", "Object", "Function", "..."] {
+          expansion.push(line.replace("**V8**", key))
+        }
+        expansion
+      } else {
+        vec![line.to_owned()]
+      };
+      for line in expansion {
+        let components = line
+          .split('|')
+          .skip(2)
+          .map(|s| s.trim())
+          .collect::<Vec<_>>();
+        let type_param = components.first().unwrap().to_owned();
+        let fastcall = components.get(1).unwrap().to_owned();
+        let fast = fastcall == "X";
+        let v8 = components.get(2).unwrap().to_owned();
+        let notes = components.get(3).unwrap().to_owned();
+        let (attr, ty) = if type_param.starts_with('#') {
+          type_param.split_once(' ').expect(
+            "Expected an attribute separated by a space (ie: #[attr] type)",
+          )
+        } else {
+          ("", type_param)
+        };
+
+        if !line.contains("...") {
+          let function = format!("fn op_test({} x: {}) {{}}", attr, ty);
+          let function =
+            syn2::parse_str::<ItemFn>(&function).expect("Failed to parse type");
+          let sig = parse_signature(vec![], function.sig.clone())
+            .expect("Failed to parse signature");
+          println!("Parsed signature: {sig:?}");
+          generate_op2(MacroConfig { core: false, fast }, function)
+            .expect("Failed to generate op");
+        }
+        actual += &format!("<tr>\n<td>\n\n```rust\n{}\n```\n\n</td><td>\n{}\n</td><td>\n{}\n</td><td>\n{}\n</td></tr>\n", type_param, if fast { "✅" } else { "" }, v8, notes);
+      }
+    }
+    actual += "</table>\n";
+
+    if update_expected {
+      std::fs::write("op2/README.md", actual)
+        .expect("Failed to write expectation file");
+    } else {
+      let expected = std::fs::read_to_string("op2/README.md")
+        .expect("Failed to read expectation file");
+      assert_eq!(
+        expected, actual,
+        "Failed to match expectation. Use UPDATE_EXPECTED=1."
+      );
+    }
+  }
 }
diff --git a/ops/op2/signature.rs b/ops/op2/signature.rs
index 7fee0c6..b766763 100644
--- a/ops/op2/signature.rs
+++ b/ops/op2/signature.rs
@@ -144,31 +144,29 @@ pub enum Special {
   FastApiCallbackOptions,
 }
 
+/// Buffers are complicated and may be shared/owned, shared/unowned, a copy, or detached.
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
 pub enum Buffer {
-  /// [`&[u8]`], [`&mut [u8]`], [`&[u32]`], etc...
+  /// Shared/unowned, may be resizable. [`&[u8]`], [`&mut [u8]`], [`&[u32]`], etc...
   Slice(RefType, NumericArg),
-  /// [`*const u8`], [`*mut u8`], [`*const u32`], etc...
+  /// Shared/unowned, may be resizable. [`*const u8`], [`*mut u8`], [`*const u32`], etc...
   Ptr(RefType, NumericArg),
-  /// [`Box<[u8]>`], [`Box<[u32]>`], etc...
+  /// Owned, copy. [`Box<[u8]>`], [`Box<[u32]>`], etc...
   BoxSlice(NumericArg),
-  /// [`Vec<u8>`], [`Vec<u32>`], etc...
+  /// Owned, copy. [`Vec<u8>`], [`Vec<u32>`], etc...
   Vec(NumericArg),
-  /// Owned in `bytes::Bytes`
+  /// Maybe shared or a copy. Stored in `bytes::Bytes`
   Bytes,
-  /// Owned in `serde_v8::V8Slice`
+  /// Shared, not resizable (or resizable and detatched), stored in `serde_v8::V8Slice`
   V8Slice,
-  /// Owned in `serde_v8::JSBuffer`
+  /// Shared, not resizable (or resizable and detatched), stored in `serde_v8::JSBuffer`
   JSBuffer,
 }
 
 impl Buffer {
   fn is_valid_mode(&self, mode: BufferMode) -> bool {
     match self {
-      Buffer::Bytes => matches!(
-        mode,
-        BufferMode::Copy | BufferMode::Detach | BufferMode::Unsafe
-      ),
+      Buffer::Bytes => matches!(mode, BufferMode::Copy),
       Buffer::JSBuffer => matches!(
         mode,
         BufferMode::Copy | BufferMode::Detach | BufferMode::Unsafe
@@ -177,14 +175,8 @@ impl Buffer {
         mode,
         BufferMode::Copy | BufferMode::Detach | BufferMode::Unsafe
       ),
-      Buffer::Vec(..) => matches!(
-        mode,
-        BufferMode::Copy | BufferMode::Detach | BufferMode::Unsafe
-      ),
-      Buffer::BoxSlice(..) => matches!(
-        mode,
-        BufferMode::Copy | BufferMode::Detach | BufferMode::Unsafe
-      ),
+      Buffer::Vec(..) => matches!(mode, BufferMode::Copy),
+      Buffer::BoxSlice(..) => matches!(mode, BufferMode::Copy),
       Buffer::Slice(..) => {
         matches!(mode, BufferMode::Detach | BufferMode::Unsafe)
       }
@@ -882,10 +874,11 @@ fn parse_type(attrs: Attributes, ty: &Type) -> Result<Arg, ArgError> {
           // Denylist of serde_v8 types with better alternatives
           let ty = of.into_token_stream();
           let token = stringify_token(of.path.clone());
-          if let Ok(err) = std::panic::catch_unwind(|| {
+          if let Ok(Some(err)) = std::panic::catch_unwind(|| {
             use syn2 as syn;
             rules!(ty => {
-              ( $( serde_v8:: )? Value $( < $_lifetime:lifetime >)? ) => "use v8::Value",
+              ( $( serde_v8:: )? Value $( < $_lifetime:lifetime >)? ) => Some("use v8::Value"),
+              ( $_ty:ty ) => None,
             })
           }) {
             return Err(ArgError::InvalidSerdeType(stringify_token(ty), err));
@@ -1023,7 +1016,6 @@ fn parse_arg(arg: FnArg) -> Result<Arg, ArgError> {
 #[cfg(test)]
 mod tests {
   use super::*;
-  use crate::op2::signature::parse_signature;
   use syn2::parse_str;
   use syn2::ItemFn;
 
@@ -1032,15 +1024,15 @@ mod tests {
   macro_rules! test {
     (
       // Function attributes
-      $(# [ $fn_attr:ident ])?
+      $(# [ $fn_attr:meta ])?
       // fn name < 'scope, GENERIC1, GENERIC2, ... >
       fn $name:ident $( < $scope:lifetime $( , $generic:ident)* >)?
       (
         // Argument attribute, argument
-        $( $(# [ $attr:ident ])? $ident:ident : $ty:ty ),*
+        $( $(# [ $attr:meta ])? $ident:ident : $ty:ty ),*
       )
       // Return value
-      $(-> $(# [ $ret_attr:ident ])? $ret:ty)?
+      $(-> $(# [ $ret_attr:meta ])? $ret:ty)?
       // Where clause
       $( where $($trait:ident : $bounds:path),* )?
       ;
@@ -1192,7 +1184,7 @@ mod tests {
     (State(Ref, Something), OptionState(Ref, Something)) -> Infallible(Void)
   );
   test!(
-    #[buffer] fn op_buffers(#[buffer] a: Vec<u8>, #[buffer] b: Box<[u8]>, #[buffer] c: bytes::Bytes, #[buffer] d: V8Slice, #[buffer] e: JSBuffer) -> Vec<u8>;
+    #[buffer(copy)] fn op_buffers(#[buffer(copy)] a: Vec<u8>, #[buffer(copy)] b: Box<[u8]>, #[buffer(copy)] c: bytes::Bytes, #[buffer] d: V8Slice, #[buffer] e: JSBuffer) -> Vec<u8>;
     (Buffer(Vec(u8)), Buffer(BoxSlice(u8)), Buffer(Bytes), Buffer(V8Slice), Buffer(JSBuffer)) -> Infallible(Buffer(Vec(u8)))
   );
 
diff --git a/ops/op2/valid_args.md b/ops/op2/valid_args.md
new file mode 100644
index 0000000..5d8436c
--- /dev/null
+++ b/ops/op2/valid_args.md
@@ -0,0 +1,41 @@
+| Supported | Rust                           | Fastcall | V8                                                  | Notes                                                                                                                                                                                                          |
+| --------- | ------------------------------ | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| X         | bool                           | X        | Bool                                                |                                                                                                                                                                                                                |
+| X         | i8                             | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | u8                             | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | i16                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | u16                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | i32                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | u32                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | isize                          | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | usize                          | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+|           | f32                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+|           | f64                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | #[string] String               | X        | String                                              | Fastcall available only if string is Latin-1. Will always create an allocated, UTF-8 copy of the String data.                                                                                                  |
+| X         | #[string] &str                 | X        | String                                              | Fastcall available only if string is Latin-1. Will create an owned `String` copy of the String data if it doesn't fit on the stack. Will never allocate in a fastcall, but will copy Latin-1 -> UTF-8.         |
+| X         | #[string] Cow<str>             | X        | String                                              | Fastcall available only if string is Latin-1. Will create a `Cow::Owned` copy of the String data if it doesn't fit on the stack. Will always be `Cow::Borrowed` in a fastcall, but will copy Latin-1 -> UTF-8. |
+| X         | &v8::Value                     | X        | any                                                 |                                                                                                                                                                                                                |
+| X         | &v8::**V8**                    | X        | **V8**                                              |                                                                                                                                                                                                                |
+| X         | &mut v8::Value                 | X        | any                                                 |                                                                                                                                                                                                                |
+| X         | &mut v8::**V8**                | X        | **V8**                                              |                                                                                                                                                                                                                |
+| X         | v8::Local<v8::Value>           | X        | any                                                 |                                                                                                                                                                                                                |
+| X         | v8::Local<v8::**V8**>          | X        | **V8**                                              |                                                                                                                                                                                                                |
+| X         | #[serde] SerdeType             |          | any                                                 | ⚠️ May be slow.                                                                                                                                                                                                 |
+| X         | #[serde] (Tuple, Tuple)        |          | any                                                 | ⚠️ May be slow.                                                                                                                                                                                                 |
+| X         | #[buffer] &mut [u8]            | X        | UInt8Array (resizable=true,false)                   | ⚠️ JS may modify the contents of the slice if V8 is called re-entrantly.                                                                                                                                        |
+| X         | #[buffer] &[u8]                | X        | UInt8Array (resizable=true,false)                   | ⚠️ JS may modify the contents of the slice if V8 is called re-entrantly.                                                                                                                                        |
+| X         | #[buffer(copy)] Vec<u8>        | X        | ArrayBuffer, ArrayBufferView, SharedArrayBuffer     | Safe, but forces a copy.                                                                                                                                                                                       |
+| X         | #[buffer(copy)] Box<[u8]>      | X        | ArrayBuffer, ArrayBufferView, SharedArrayBuffer     | Safe, but forces a copy.                                                                                                                                                                                       |
+| X         | #[buffer(copy)] bytes::Bytes   | X        | ArrayBuffer, ArrayBufferView, SharedArrayBuffer     | Safe, but forces a copy.                                                                                                                                                                                       |
+|           | #[buffer] V8Slice              | X        | ArrayBuffer, ArrayBufferView (resizable=false)      | ⚠️ JS may modify the contents of slices obtained from buffer.                                                                                                                                                   |
+|           | #[buffer(detach)] V8Slice      | X        | ArrayBuffer, ArrayBufferView (resizable=true,false) | Safe.                                                                                                                                                                                                          |
+|           | #[buffer] V8ResizableSlice     | X        | ArrayBuffer, ArrayBufferView (resizable=true)       | ⚠️ JS may modify the contents of slices obtained from buffer.                                                                                                                                                   |
+|           | #[buffer] JSBuffer             | X        | ArrayBuffer, ArrayBufferView (resizable=false)      | ⚠️ JS may modify the contents of slices obtained from buffer.                                                                                                                                                   |
+|           | #[buffer(detach)] JSBuffer     | X        | ArrayBuffer, ArrayBufferView (resizable=true,false) | Safe.                                                                                                                                                                                                          |
+|           | #[buffer(unsafe)] bytes::Bytes | X        | ArrayBuffer, ArrayBufferView (resizable=false)      | ⚠️ JS may modify the contents of the buffer.                                                                                                                                                                    |
+|           | #[buffer(detach)] bytes::Bytes | X        | ArrayBuffer, ArrayBufferView (resizable=true,false) | Safe.                                                                                                                                                                                                          |
+| X         | &OpState                       | X        |                                                     |                                                                                                                                                                                                                |
+| X         | &mut OpState                   | X        |                                                     |                                                                                                                                                                                                                |
+| X         | Rc<RefCell<OpState>>           | X        |                                                     |                                                                                                                                                                                                                |
+| X         | #[state] &StateObject          | X        |                                                     | Extracts an object from `OpState`.                                                                                                                                                                             |
+| X         | #[state] &mut StateObject      | X        |                                                     | Extracts an object from `OpState`.                                                                                                                                                                             |
diff --git a/serde_v8/magic/v8slice.rs b/serde_v8/magic/v8slice.rs
index ea00f2a..dc6e4b8 100644
--- a/serde_v8/magic/v8slice.rs
+++ b/serde_v8/magic/v8slice.rs
@@ -83,6 +83,16 @@ impl V8Slice {
       std::slice::from_raw_parts_mut(ptr, self.range.len())
     }
   }
+
+  /// Create a [`Vec<u8>`] copy of this slice data.
+  pub fn to_vec(&self) -> Vec<u8> {
+    self.as_slice().to_vec()
+  }
+
+  /// Create a [`Box<[u8]>`] copy of this slice data.
+  pub fn to_boxed_slice(&self) -> Box<[u8]> {
+    self.to_vec().into_boxed_slice()
+  }
 }
 
 pub(crate) fn to_ranged_buffer<'s>(


Test patch:
diff --git a/ops/op2/test_cases/sync/buffers.out b/ops/op2/test_cases/sync/buffers.out
index 4036b9d..26e7029 100644
--- a/ops/op2/test_cases/sync/buffers.out
+++ b/ops/op2/test_cases/sync/buffers.out
@@ -51,7 +51,7 @@ impl op_buffers {
         });
         let arg0 = args.get(0usize as i32);
         let mut arg0 = match unsafe {
-            deno_core::_ops::to_nonresizable_v8_slice(&mut scope, arg0)
+            deno_core::_ops::to_v8_slice::<deno_core::v8::Uint8Array>(&mut scope, arg0)
         } {
             Ok(arg0) => arg0,
             Err(arg0_err) => {
@@ -70,7 +70,7 @@ impl op_buffers {
         let arg0 = &mut arg0;
         let arg1 = args.get(1usize as i32);
         let mut arg1 = match unsafe {
-            deno_core::_ops::to_nonresizable_v8_slice(&mut scope, arg1)
+            deno_core::_ops::to_v8_slice::<deno_core::v8::Uint8Array>(&mut scope, arg1)
         } {
             Ok(arg1) => arg1,
             Err(arg1_err) => {
diff --git a/ops/op2/test_cases/sync/buffers_copy.out b/ops/op2/test_cases/sync/buffers_copy.out
new file mode 100644
index 0000000..50b2b7f
--- /dev/null
+++ b/ops/op2/test_cases/sync/buffers_copy.out
@@ -0,0 +1,125 @@
+#[allow(non_camel_case_types)]
+struct op_buffers {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_buffers {
+    const NAME: &'static str = stringify!(op_buffers);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal(
+        stringify!(op_buffers),
+        false,
+        false,
+        false,
+        3usize as u8,
+        Self::v8_fn_ptr as _,
+        Some({
+            use deno_core::v8::fast_api::Type;
+            use deno_core::v8::fast_api::CType;
+            deno_core::v8::fast_api::FastFunction::new(
+                &[
+                    Type::V8Value,
+                    Type::TypedArray(CType::Uint8),
+                    Type::TypedArray(CType::Uint8),
+                    Type::TypedArray(CType::Uint8),
+                ],
+                CType::Void,
+                Self::v8_fn_ptr_fast as *const ::std::ffi::c_void,
+            )
+        }),
+    );
+}
+impl op_buffers {
+    pub const fn name() -> &'static str {
+        stringify!(op_buffers)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    fn v8_fn_ptr_fast(
+        _: deno_core::v8::Local<deno_core::v8::Object>,
+        arg0: *mut deno_core::v8::fast_api::FastApiTypedArray<u8>,
+        arg1: *mut deno_core::v8::fast_api::FastApiTypedArray<u8>,
+        arg2: *mut deno_core::v8::fast_api::FastApiTypedArray<u8>,
+    ) -> () {
+        let arg0 = unsafe { arg0.as_mut().unwrap() }
+            .get_storage_if_aligned()
+            .unwrap()
+            .to_vec();
+        let arg1 = unsafe { arg1.as_mut().unwrap() }
+            .get_storage_if_aligned()
+            .unwrap()
+            .to_vec()
+            .into_boxed_slice();
+        let arg2 = unsafe { arg2.as_mut().unwrap() }
+            .get_storage_if_aligned()
+            .unwrap()
+            .to_vec()
+            .into();
+        let result = Self::call(arg0, arg1, arg2);
+        result
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let arg0 = args.get(0usize as i32);
+        let mut arg0 = match unsafe {
+            deno_core::_ops::to_v8_slice::<deno_core::v8::Uint8Array>(&mut scope, arg0)
+        } {
+            Ok(arg0) => arg0,
+            Err(arg0_err) => {
+                let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+                let msg = deno_core::v8::String::new_from_one_byte(
+                        &mut scope,
+                        arg0_err.as_bytes(),
+                        deno_core::v8::NewStringType::Normal,
+                    )
+                    .unwrap();
+                let exc = deno_core::v8::Exception::error(&mut scope, msg);
+                scope.throw_exception(exc);
+                return;
+            }
+        };
+        let arg0 = arg0.to_vec();
+        let arg1 = args.get(1usize as i32);
+        let mut arg1 = match unsafe {
+            deno_core::_ops::to_v8_slice::<deno_core::v8::Uint8Array>(&mut scope, arg1)
+        } {
+            Ok(arg1) => arg1,
+            Err(arg1_err) => {
+                let msg = deno_core::v8::String::new_from_one_byte(
+                        &mut scope,
+                        arg1_err.as_bytes(),
+                        deno_core::v8::NewStringType::Normal,
+                    )
+                    .unwrap();
+                let exc = deno_core::v8::Exception::error(&mut scope, msg);
+                scope.throw_exception(exc);
+                return;
+            }
+        };
+        let arg1 = arg1.to_boxed_slice();
+        let arg2 = args.get(2usize as i32);
+        let mut arg2 = match unsafe {
+            deno_core::_ops::to_v8_slice::<deno_core::v8::Uint8Array>(&mut scope, arg2)
+        } {
+            Ok(arg2) => arg2,
+            Err(arg2_err) => {
+                let msg = deno_core::v8::String::new_from_one_byte(
+                        &mut scope,
+                        arg2_err.as_bytes(),
+                        deno_core::v8::NewStringType::Normal,
+                    )
+                    .unwrap();
+                let exc = deno_core::v8::Exception::error(&mut scope, msg);
+                scope.throw_exception(exc);
+                return;
+            }
+        };
+        let arg2 = arg2.to_vec().into();
+        let result = Self::call(arg0, arg1, arg2);
+    }
+    #[inline(always)]
+    fn call(a: Vec<u8>, b: Box<[u8]>, c: bytes::Bytes) {}
+}
diff --git a/ops/op2/test_cases/sync/buffers_copy.rs b/ops/op2/test_cases/sync/buffers_copy.rs
new file mode 100644
index 0000000..c349194
--- /dev/null
+++ b/ops/op2/test_cases/sync/buffers_copy.rs
@@ -0,0 +1,5 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+#[op2(fast)]
+fn op_buffers(#[buffer(copy)] a: Vec<u8>, #[buffer(copy)] b: Box<[u8]>, #[buffer(copy)] c: bytes::Bytes) {
+}