Repository: denoland/deno_core
Pull request #33 (2023-07-11 16:46:03)

feat(ops): Basic buffer support for op2 
Adds:

 - `#[buffer] &[u8]`
 - `#[buffer] &mut [u8]`

TODO:

 - `#[buffer] *const u8` (all primitive types) -- not recommended but used in FFI
 - `#[buffer] *mut u8` (all primitive types)
 - `#[buffer] bytes::Bytes`
 - `#[buffer] JSBuffer`
 - `#[buffer] V8Slice`
 - `#[buffer] Vec<..>`
 - `#[buffer] Box<[..]>`
 
TODO: Alternative buffer modes:

 - `#[buffer(shared)]` (the default)
 - `#[buffer(copy)]` 
 - `#[buffer(detach)]`


aapoalas: **thought:** It's possible for V8 ArrayBuffers to be created from external pointers that do not follow the normal alignment rule of V8 ABs (code says always aligned to 4 bytes, experience seems to suggest this is 8 bytes for 64 bit environments). In these cases the TypedArrays will then happily allow for themselves to be created at odd offsets. This needs to be remembered when eg. a `Float64Array` is converted into a Rust slice.

Patch:
diff --git a/core/runtime/ops.rs b/core/runtime/ops.rs
index e316804..14741b5 100644
--- a/core/runtime/ops.rs
+++ b/core/runtime/ops.rs
@@ -343,6 +343,33 @@ 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(
+  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");
+    };
+
+  let store = buf.get_backing_store();
+  if store.is_resizable_by_user_javascript() {
+    return Err("expected non-resizable buffer");
+  }
+  let slice =
+    unsafe { serde_v8::V8Slice::from_parts(store, offset..(offset + length)) };
+  Ok(slice)
+}
+
 #[cfg(test)]
 mod tests {
   use crate::error::generic_error;
@@ -394,6 +421,9 @@ mod tests {
       op_state_mut,
       op_state_mut_attr,
       op_state_multi_attr,
+      op_buffer_slice,
+      op_buffer_slice_callback,
+      op_buffer_slice_unsafe_callback,
     ],
     state = |state| {
       state.put(1234u32);
@@ -941,4 +971,124 @@ mod tests {
     )?;
     Ok(())
   }
+
+  #[op2(core, fast)]
+  pub fn op_buffer_slice(
+    #[buffer] input: &[u8],
+    inlen: usize,
+    #[buffer] output: &mut [u8],
+    outlen: usize,
+  ) {
+    assert_eq!(inlen, input.len());
+    assert_eq!(outlen, output.len());
+    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>>
+  {
+    // Uint8Array -> Uint8Array
+    run_test2(
+      10000,
+      "op_buffer_slice",
+      r"
+      let out = new Uint8Array(10);
+      op_buffer_slice(new Uint8Array([1,2,3]), 3, out, 10);
+      assert(out[0] == 1);",
+    )?;
+    // Uint8Array -> raw ArrayBuffer
+    run_test2(
+      10000,
+      "op_buffer_slice",
+      r"
+      let out = new ArrayBuffer(10);
+      op_buffer_slice(new Uint8Array([1,2,3]), 3, out, 10);
+      assert(new Uint8Array(out)[0] == 1);",
+    )?;
+    // ArrayBuffer -> ArrayBuffer
+    run_test2(
+      10000,
+      "op_buffer_slice",
+      r"
+      let inbuf = new ArrayBuffer(10);
+      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);
+      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(())
+  }
+
+  // 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_unsafe_callback(
+    scope: &mut v8::HandleScope,
+    buffer: v8::Local<v8::ArrayBuffer>,
+    callback: v8::Local<v8::Function>,
+  ) {
+    println!("{:?}", buffer.data());
+    let recv = callback.into();
+    callback.call(scope, recv, &[]);
+    println!("{:?}", buffer.data());
+  }
+
+  #[ignore]
+  #[tokio::test]
+  async fn test_op_unsafe() -> Result<(), Box<dyn std::error::Error>> {
+    run_test2(
+      1,
+      "op_buffer_slice_unsafe_callback",
+      r"
+      let inbuf = new ArrayBuffer(1024 * 1024, { maxByteLength: 10 * 1024 * 1024 });
+      op_buffer_slice_unsafe_callback(inbuf, () => {
+        inbuf.resize(0);
+      });
+      ",
+    )?;
+    Ok(())
+  }
 }
diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs
index c0b82ef..1767455 100644
--- a/ops/op2/dispatch_fast.rs
+++ b/ops/op2/dispatch_fast.rs
@@ -3,6 +3,7 @@ use super::dispatch_shared::v8_intermediate_to_arg;
 use super::dispatch_shared::v8_to_arg;
 use super::generator_state::GeneratorState;
 use super::signature::Arg;
+use super::signature::Buffer;
 use super::signature::NumericArg;
 use super::signature::ParsedSignature;
 use super::signature::RefType;
@@ -61,9 +62,15 @@ impl V8FastCallType {
       V8FastCallType::SeqOneByteString => {
         quote!(*mut #deno_core::v8::fast_api::FastApiOneByteString)
       }
-      V8FastCallType::Uint8Array
-      | V8FastCallType::Uint32Array
-      | V8FastCallType::Float64Array => unreachable!(),
+      V8FastCallType::Uint8Array => {
+        quote!(*mut #deno_core::v8::fast_api::FastApiTypedArray<u8>)
+      }
+      V8FastCallType::Uint32Array => {
+        quote!(*mut #deno_core::v8::fast_api::FastApiTypedArray<u32>)
+      }
+      V8FastCallType::Float64Array => {
+        quote!(*mut #deno_core::v8::fast_api::FastApiTypedArray<f64>)
+      }
       V8FastCallType::Virtual => unreachable!("invalid virtual argument"),
     }
   }
@@ -282,6 +289,9 @@ fn map_v8_fastcall_arg_to_arg(
 ) -> Result<TokenStream, V8MappingError> {
   let arg_temp = format_ident!("{}_temp", arg_ident);
   let res = match arg {
+    Arg::Buffer(Buffer::Slice(_, NumericArg::u8)) => {
+      quote!(let #arg_ident = unsafe { #arg_ident.as_mut().unwrap() }.get_storage_if_aligned().unwrap();)
+    }
     Arg::Ref(RefType::Ref, Special::OpState) => {
       *needs_opctx = true;
       quote!(let #arg_ident = &#opctx.state.borrow();)
@@ -378,6 +388,8 @@ 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(_) => return Ok(None),
     // Virtual OpState arguments
     Arg::RcRefCell(Special::OpState)
     | Arg::Ref(_, Special::OpState)
diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs
index e7bf0c4..c296474 100644
--- a/ops/op2/dispatch_slow.rs
+++ b/ops/op2/dispatch_slow.rs
@@ -3,6 +3,7 @@ use super::dispatch_shared::v8_intermediate_to_arg;
 use super::dispatch_shared::v8_to_arg;
 use super::generator_state::GeneratorState;
 use super::signature::Arg;
+use super::signature::Buffer;
 use super::signature::NumericArg;
 use super::signature::ParsedSignature;
 use super::signature::RefType;
@@ -41,8 +42,8 @@ pub(crate) fn generate_dispatch_slow(
     });
   }
 
-  // Collect virtual arguments in a deferred list that we compute at the very end. This allows us to copy
-  // the scope borrow.
+  // Collect virtual arguments in a deferred list that we compute at the very end. This allows us to borrow
+  // the scope/opstate in the intermediate stages.
   let mut deferred = TokenStream::new();
   let mut input_index = 0;
 
@@ -269,6 +270,10 @@ pub fn from_arg(
         let #arg_ident = #deno_core::_ops::to_str(&mut #scope, &#arg_ident, &mut #arg_temp);
       }
     }
+    Arg::Buffer(buffer) => {
+      let arg_ident = arg_ident.clone();
+      from_arg_buffer(generator_state, &arg_ident, buffer)?
+    }
     Arg::Ref(_, Special::HandleScope) => {
       *needs_scope = true;
       quote!(let #arg_ident = &mut #scope;)
@@ -360,6 +365,49 @@ pub fn from_arg(
   Ok(res)
 }
 
+pub fn from_arg_buffer(
+  generator_state: &mut GeneratorState,
+  arg_ident: &Ident,
+  buffer: &Buffer,
+) -> Result<TokenStream, V8MappingError> {
+  let err = format_ident!("{}_err", arg_ident);
+  let throw_exception = throw_type_error_static_string(generator_state, &err)?;
+
+  let GeneratorState {
+    deno_core,
+    scope,
+    needs_scope,
+    ..
+  } = generator_state;
+
+  *needs_scope = true;
+  let make_v8slice = quote! {
+    let mut #arg_ident = match unsafe { #deno_core::_ops::to_nonresizable_v8_slice(&mut #scope, #arg_ident) } {
+      Ok(#arg_ident) => #arg_ident,
+      Err(#err) => {
+        #throw_exception
+      }
+    };
+  };
+
+  let make_arg = match buffer {
+    Buffer::Slice(_, NumericArg::u8) => {
+      quote!(let #arg_ident = &mut #arg_ident;)
+    }
+    _ => {
+      return Err(V8MappingError::NoMapping(
+        "a buffer argument",
+        Arg::Buffer(*buffer),
+      ))
+    }
+  };
+
+  Ok(quote! {
+    #make_v8slice
+    #make_arg
+  })
+}
+
 pub fn call(
   generator_state: &mut GeneratorState,
 ) -> Result<TokenStream, V8MappingError> {
@@ -623,3 +671,27 @@ fn throw_type_error_string(
     return;
   })
 }
+
+/// Generates code to throw an exception from a string variable, adding required additional dependencies as needed.
+fn throw_type_error_static_string(
+  generator_state: &mut GeneratorState,
+  message: &Ident,
+) -> Result<TokenStream, V8MappingError> {
+  let maybe_scope = if generator_state.needs_scope {
+    quote!()
+  } else {
+    with_scope(generator_state)
+  };
+
+  let GeneratorState {
+    deno_core, scope, ..
+  } = &generator_state;
+
+  Ok(quote! {
+    #maybe_scope
+    let msg = #deno_core::v8::String::new_from_one_byte(&mut #scope, #message.as_bytes(), #deno_core::v8::NewStringType::Normal).unwrap();
+    let exc = #deno_core::v8::Exception::error(&mut #scope, msg);
+    #scope.throw_exception(exc);
+    return;
+  })
+}
diff --git a/ops/op2/signature.rs b/ops/op2/signature.rs
index a302129..7fee0c6 100644
--- a/ops/op2/signature.rs
+++ b/ops/op2/signature.rs
@@ -17,6 +17,7 @@ use syn2::FnArg;
 use syn2::GenericParam;
 use syn2::Generics;
 use syn2::Pat;
+use syn2::Path;
 use syn2::ReturnType;
 use syn2::Signature;
 use syn2::Type;
@@ -47,6 +48,27 @@ pub enum NumericArg {
   usize,
 }
 
+impl NumericArg {
+  /// Returns the primary mapping from this primitive to an associated V8 typed array.
+  pub fn v8_array_type(self) -> Option<V8Arg> {
+    use NumericArg::*;
+    use V8Arg::*;
+    Some(match self {
+      i8 => Int8Array,
+      u8 => Uint8Array,
+      i16 => Int16Array,
+      u16 => Uint16Array,
+      i32 => Int32Array,
+      u32 => Uint32Array,
+      i64 => BigInt64Array,
+      u64 => BigUint64Array,
+      f32 => Float32Array,
+      f64 => Float64Array,
+      _ => return None,
+    })
+  }
+}
+
 impl ToTokens for NumericArg {
   fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
     let ident = Ident::new(self.into(), Span::call_site());
@@ -122,6 +144,63 @@ pub enum Special {
   FastApiCallbackOptions,
 }
 
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum Buffer {
+  /// [`&[u8]`], [`&mut [u8]`], [`&[u32]`], etc...
+  Slice(RefType, NumericArg),
+  /// [`*const u8`], [`*mut u8`], [`*const u32`], etc...
+  Ptr(RefType, NumericArg),
+  /// [`Box<[u8]>`], [`Box<[u32]>`], etc...
+  BoxSlice(NumericArg),
+  /// [`Vec<u8>`], [`Vec<u32>`], etc...
+  Vec(NumericArg),
+  /// Owned in `bytes::Bytes`
+  Bytes,
+  /// Owned in `serde_v8::V8Slice`
+  V8Slice,
+  /// Owned 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::JSBuffer => matches!(
+        mode,
+        BufferMode::Copy | BufferMode::Detach | BufferMode::Unsafe
+      ),
+      Buffer::V8Slice => matches!(
+        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::Slice(..) => {
+        matches!(mode, BufferMode::Detach | BufferMode::Unsafe)
+      }
+      Buffer::Ptr(..) => {
+        matches!(mode, BufferMode::Detach | BufferMode::Unsafe)
+      }
+    }
+  }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum External {
+  /// c_void
+  Ptr(RefType),
+}
+
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
 pub enum RefType {
   Ref,
@@ -135,12 +214,12 @@ pub enum RefType {
 pub enum Arg {
   Void,
   Special(Special),
+  Buffer(Buffer),
+  External(External),
   Ref(RefType, Special),
   RcRefCell(Special),
   Option(Special),
   OptionNumeric(NumericArg),
-  Slice(RefType, NumericArg),
-  Ptr(RefType, NumericArg),
   OptionV8Local(V8Arg),
   V8Local(V8Arg),
   V8Global(V8Arg),
@@ -214,6 +293,7 @@ impl Arg {
 
 enum ParsedType {
   TSpecial(Special),
+  TBuffer(Buffer),
   TV8(V8Arg),
   // TODO(mmastrac): We need to carry the mut status through somehow
   TV8Mut(V8Arg),
@@ -249,16 +329,33 @@ pub struct ParsedSignature {
   pub generic_bounds: BTreeMap<String, String>,
 }
 
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+enum BufferMode {
+  /// Unsafely shared buffers that may possibly change on the JavaScript side upon re-entry into
+  /// V8. Rust code should not treat these as traditional buffers.
+  Unsafe,
+  /// Shared buffers that are copied from V8 unconditionally. May be expensive, but these
+  /// buffers are guaranteed to be owned by Rust.
+  Copy,
+  /// Buffers that are detached and owned purely by Rust. JavaScript will no longer have
+  /// access to these buffers and will see zero-sized buffers rather than the contents
+  /// that were passed in here.
+  Detach,
+}
+
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
 enum AttributeModifier {
   /// #[serde], for serde_v8 types.
   Serde,
-  /// #[smi], for small integers
+  /// #[smi], for non-integral ID types representing small integers (-2³¹ and 2³¹-1 on 64-bit platforms,
+  /// see https://medium.com/fhinkel/v8-internals-how-small-is-a-small-integer-e0badc18b6da).
   Smi,
   /// #[string], for strings.
   String,
-  /// #[state], for automatic OpState extraction
+  /// #[state], for automatic OpState extraction.
   State,
+  /// #[buffer], for buffers.
+  Buffer(BufferMode),
 }
 
 #[derive(Error, Debug)]
@@ -291,6 +388,8 @@ pub enum ArgError {
   InvalidSelf,
   #[error("Invalid argument type: {0}")]
   InvalidType(String),
+  #[error("Invalid numeric argument type: {0}")]
+  InvalidNumericType(String),
   #[error(
     "Invalid argument type path (should this be #[smi] or #[serde]?): {0}"
   )]
@@ -305,6 +404,10 @@ pub enum ArgError {
   InvalidSerdeType(String, &'static str),
   #[error("Invalid #[string] type: {0}")]
   InvalidStringType(String),
+  #[error("Invalid #[buffer] type: {0}")]
+  InvalidBufferType(String),
+  #[error("Invalid #[buffer] mode {0} for {1}")]
+  InvalidBufferMode(String, String),
   #[error("Cannot use #[serde] for type: {0}")]
   InvalidSerdeAttributeType(String),
   #[error("Invalid v8 type: {0}")]
@@ -313,8 +416,12 @@ pub enum ArgError {
   InternalError(String),
   #[error("Missing a #[string] attribute")]
   MissingStringAttribute,
+  #[error("Missing a #[buffer] attribute")]
+  MissingBufferAttribute,
   #[error("Invalid #[state] type '{0}'")]
   InvalidStateType(String),
+  #[error("Unknown or invalid attribute '{0}'")]
+  InvalidAttribute(String),
 }
 
 #[derive(Copy, Clone, Default)]
@@ -337,6 +444,9 @@ fn stringify_token(tokens: impl ToTokens) -> String {
     .map(|s| s.to_string())
     .collect::<Vec<_>>()
     .join("")
+    // Ick.
+    // TODO(mmastrac): Should we pretty-format this instead?
+    .replace(" , ", ", ")
 }
 
 pub fn parse_signature(
@@ -488,10 +598,12 @@ fn parse_generics(
 }
 
 fn parse_attributes(attributes: &[Attribute]) -> Result<Attributes, ArgError> {
-  let attrs = attributes
-    .iter()
-    .filter_map(parse_attribute)
-    .collect::<Vec<_>>();
+  let mut attrs = vec![];
+  for attr in attributes {
+    if let Some(attr) = parse_attribute(attr)? {
+      attrs.push(attr)
+    }
+  }
 
   if attrs.is_empty() {
     return Ok(Attributes::default());
@@ -504,23 +616,33 @@ fn parse_attributes(attributes: &[Attribute]) -> Result<Attributes, ArgError> {
   })
 }
 
+/// Is this a special attribute that we understand?
 pub fn is_attribute_special(attr: &Attribute) -> bool {
-  parse_attribute(attr).is_some()
+  parse_attribute(attr).unwrap_or_default().is_some()
 }
 
-fn parse_attribute(attr: &Attribute) -> Option<AttributeModifier> {
+/// Parses an attribute, returning None if this is an attribute we support but is
+/// otherwise unknown (ie: doc comments).
+fn parse_attribute(
+  attr: &Attribute,
+) -> Result<Option<AttributeModifier>, ArgError> {
   let tokens = attr.into_token_stream();
-  use syn2 as syn;
-  std::panic::catch_unwind(|| {
+  let res = std::panic::catch_unwind(|| {
+    use syn2 as syn;
     rules!(tokens => {
       (#[serde]) => Some(AttributeModifier::Serde),
       (#[smi]) => Some(AttributeModifier::Smi),
       (#[string]) => Some(AttributeModifier::String),
       (#[state]) => Some(AttributeModifier::State),
-      (#[$_attr:meta]) => None,
+      (#[buffer]) => Some(AttributeModifier::Buffer(BufferMode::Unsafe)),
+      (#[buffer(unsafe)]) => Some(AttributeModifier::Buffer(BufferMode::Unsafe)),
+      (#[buffer(copy)]) => Some(AttributeModifier::Buffer(BufferMode::Copy)),
+      (#[buffer(detach)]) => Some(AttributeModifier::Buffer(BufferMode::Detach)),
+      (#[allow ($_rule:path)]) => None,
+      (#[doc = $_attr:literal]) => None,
     })
-  })
-  .expect("Failed to parse an attribute")
+  }).map_err(|_| ArgError::InvalidAttribute(stringify_token(attr)))?;
+  Ok(res)
 }
 
 fn parse_return(
@@ -560,6 +682,19 @@ fn parse_return(
   }
 }
 
+fn parse_numeric_type(tp: &Path) -> Result<NumericArg, ArgError> {
+  if tp.segments.len() == 1 {
+    let segment = tp.segments.first().unwrap().ident.to_string();
+    for numeric in NumericArg::iter() {
+      if Into::<&'static str>::into(numeric) == segment.as_str() {
+        return Ok(numeric);
+      }
+    }
+  }
+
+  Err(ArgError::InvalidNumericType(stringify_token(tp)))
+}
+
 /// Parse a raw type into a container + type, allowing us to simplify the typechecks elsewhere in
 /// this code.
 fn parse_type_path(
@@ -570,30 +705,39 @@ fn parse_type_path(
   use ParsedType::*;
   use ParsedTypeContainer::*;
 
-  if tp.path.segments.len() == 1 {
-    let segment = tp.path.segments.first().unwrap().ident.to_string();
-    for numeric in NumericArg::iter() {
-      if Into::<&'static str>::into(numeric) == segment.as_str() {
-        return Ok(CBare(TNumeric(numeric)));
-      }
-    }
-  }
-
   use syn2 as syn;
 
   let tokens = tp.clone().into_token_stream();
-  let res = std::panic::catch_unwind(|| {
+  let res = if let Ok(numeric) = parse_numeric_type(&tp.path) {
+    CBare(TNumeric(numeric))
+  } else {
+    std::panic::catch_unwind(|| {
     rules!(tokens => {
       ( $( std :: str  :: )? String ) => {
         Ok(CBare(TSpecial(Special::String)))
       }
       // Note that the reference is checked below
-      ( $( std :: str  :: )? str ) => {
+      ( $( std :: str :: )? str ) => {
         Ok(CBare(TSpecial(Special::RefStr)))
       }
       ( $( std :: borrow :: )? Cow < str > ) => {
         Ok(CBare(TSpecial(Special::CowStr)))
       }
+      ( $( std :: vec ::)? Vec < $ty:path > ) => {
+        Ok(CBare(TBuffer(Buffer::Vec(parse_numeric_type(&ty)?))))
+      }
+      ( $( std :: boxed ::)? Box < [ $ty:path ] > ) => {
+        Ok(CBare(TBuffer(Buffer::BoxSlice(parse_numeric_type(&ty)?))))
+      }
+      ( $( serde_v8 :: )? V8Slice ) => {
+        Ok(CBare(TBuffer(Buffer::V8Slice)))
+      }
+      ( $( serde_v8 :: )? JSBuffer ) => {
+        Ok(CBare(TBuffer(Buffer::JSBuffer)))
+      }
+      ( $( bytes :: )? Bytes ) => {
+        Ok(CBare(TBuffer(Buffer::Bytes)))
+      }
       ( $( std :: ffi :: )? c_void ) => Ok(CBare(TNumeric(NumericArg::__VOID__))),
       ( OpState ) => Ok(CBare(TSpecial(Special::OpState))),
       ( v8 :: HandleScope $( < $_scope:lifetime >)? ) => Ok(CBare(TSpecial(Special::HandleScope))),
@@ -606,6 +750,7 @@ fn parse_type_path(
         match parse_type(attrs, &ty)? {
           Arg::Special(special) => Ok(COption(TSpecial(special))),
           Arg::Numeric(numeric) => Ok(COption(TNumeric(numeric))),
+          Arg::Buffer(buffer) => Ok(COption(TBuffer(buffer))),
           Arg::V8Ref(RefType::Ref, v8) => Ok(COption(TV8(v8))),
           Arg::V8Ref(RefType::Mut, v8) => Ok(COption(TV8Mut(v8))),
           Arg::V8Local(v8) => Ok(COptionV8Local(TV8(v8))),
@@ -614,7 +759,8 @@ fn parse_type_path(
       }
       ( $any:ty ) => Err(ArgError::InvalidTypePath(stringify_token(any))),
     })
-  }).map_err(|e| ArgError::InternalError(format!("parse_type_path {e:?}")))??;
+  }).map_err(|e| ArgError::InternalError(format!("parse_type_path {e:?}")))??
+  };
 
   // Ensure that we have the correct reference state. This is a bit awkward but it's
   // the easiest way to work with the 'rules!' macro above.
@@ -639,10 +785,25 @@ fn parse_type_path(
         return Err(ArgError::MissingStringAttribute);
       }
     }
+    CBare(TBuffer(buffer)) => {
+      if let Some(AttributeModifier::Buffer(mode)) = attrs.primary {
+        if !buffer.is_valid_mode(mode) {
+          return Err(ArgError::InvalidBufferMode(
+            format!("{mode:?}"),
+            stringify_token(tp),
+          ));
+        }
+      } else {
+        return Err(ArgError::MissingBufferAttribute);
+      }
+    }
     CBare(_) => {
       if attrs.primary == Some(AttributeModifier::String) {
         return Err(ArgError::InvalidStringType(stringify_token(tp)));
       }
+      if let Some(AttributeModifier::Buffer(_)) = attrs.primary {
+        return Err(ArgError::InvalidBufferType(stringify_token(tp)));
+      }
     }
     _ => {
       // Ignore for other containers
@@ -742,6 +903,9 @@ fn parse_type(attrs: Attributes, ty: &Type) -> Result<Arg, ArgError> {
       AttributeModifier::String => {
         // We handle this as part of the normal parsing process
       }
+      AttributeModifier::Buffer(_) => {
+        // We handle this as part of the normal parsing process
+      }
       AttributeModifier::Smi => {
         return Ok(Arg::Numeric(NumericArg::__SMI__));
       }
@@ -762,10 +926,33 @@ fn parse_type(attrs: Attributes, ty: &Type) -> Result<Arg, ArgError> {
         RefType::Ref
       };
       match &*of.elem {
-        Type::Slice(of) => match parse_type(attrs, &of.elem)? {
-          Arg::Numeric(numeric) => Ok(Arg::Slice(mut_type, numeric)),
-          _ => Err(ArgError::InvalidType(stringify_token(ty))),
-        },
+        // Note that we only allow numeric slices here -- if we decide to allow slices of things like v8 values,
+        // this branch will need to be re-written.
+        Type::Slice(of) => {
+          if let Type::Path(path) = &*of.elem {
+            match parse_numeric_type(&path.path)? {
+              NumericArg::__VOID__ => {
+                Ok(Arg::External(External::Ptr(mut_type)))
+              }
+              numeric => {
+                if let Some(AttributeModifier::Buffer(mode)) = attrs.primary {
+                  let buffer = Buffer::Slice(mut_type, numeric);
+                  if !buffer.is_valid_mode(mode) {
+                    return Err(ArgError::InvalidBufferMode(
+                      format!("{mode:?}"),
+                      stringify_token(ty),
+                    ));
+                  }
+                  Ok(Arg::Buffer(buffer))
+                } else {
+                  Err(ArgError::InvalidBufferType(stringify_token(ty)))
+                }
+              }
+            }
+          } else {
+            Err(ArgError::InvalidType(stringify_token(ty)))
+          }
+        }
         Type::Path(of) => match parse_type_path(attrs, true, of)? {
           CBare(TSpecial(Special::RefStr)) => Ok(Arg::Special(Special::RefStr)),
           COption(TSpecial(Special::RefStr)) => {
@@ -786,7 +973,23 @@ fn parse_type(attrs: Attributes, ty: &Type) -> Result<Arg, ArgError> {
       };
       match &*of.elem {
         Type::Path(of) => match parse_type_path(attrs, false, of)? {
-          CBare(TNumeric(numeric)) => Ok(Arg::Ptr(mut_type, numeric)),
+          CBare(TNumeric(numeric)) if numeric == NumericArg::__VOID__ => {
+            Ok(Arg::External(External::Ptr(mut_type)))
+          }
+          CBare(TNumeric(numeric)) => {
+            if let Some(AttributeModifier::Buffer(mode)) = attrs.primary {
+              let buffer = Buffer::Ptr(mut_type, numeric);
+              if !buffer.is_valid_mode(mode) {
+                return Err(ArgError::InvalidBufferMode(
+                  format!("{mode:?}"),
+                  stringify_token(ty),
+                ));
+              }
+              Ok(Arg::Buffer(buffer))
+            } else {
+              Err(ArgError::InvalidBufferType(stringify_token(ty)))
+            }
+          }
           _ => Err(ArgError::InvalidType(stringify_token(ty))),
         },
         _ => Err(ArgError::InvalidType(stringify_token(ty))),
@@ -795,6 +998,7 @@ fn parse_type(attrs: Attributes, ty: &Type) -> Result<Arg, ArgError> {
     Type::Path(of) => match parse_type_path(attrs, false, of)? {
       CBare(TNumeric(numeric)) => Ok(Arg::Numeric(numeric)),
       CBare(TSpecial(special)) => Ok(Arg::Special(special)),
+      CBare(TBuffer(buffer)) => Ok(Arg::Buffer(buffer)),
       COption(TNumeric(special)) => Ok(Arg::OptionNumeric(special)),
       COption(TSpecial(special)) => Ok(Arg::Option(special)),
       CRcRefCell(TSpecial(special)) => Ok(Arg::RcRefCell(special)),
@@ -888,8 +1092,12 @@ mod tests {
       format!("{:?}", sig.args)
         .trim_matches(|c| c == '[' || c == ']')
         .replace('\n', " ")
+        .replace('"', "")
+    );
+    assert_eq!(
+      return_expected,
+      format!("{:?}", sig.ret_val).replace('"', "")
     );
-    assert_eq!(return_expected, format!("{:?}", sig.ret_val));
   }
 
   macro_rules! expect_fail {
@@ -916,24 +1124,24 @@ mod tests {
     (Ref(Mut, OpState), Numeric(u32)) -> Infallible(Void)
   );
   test!(
-    fn op_slices(r#in: &[u8], out: &mut [u8]);
-    (Slice(Ref, u8), Slice(Mut, u8)) -> Infallible(Void)
+    fn op_slices(#[buffer] r#in: &[u8], #[buffer] out: &mut [u8]);
+    (Buffer(Slice(Ref, u8)), Buffer(Slice(Mut, u8))) -> Infallible(Void)
   );
   test!(
     #[serde] fn op_serde(#[serde] input: package::SerdeInputType) -> Result<package::SerdeReturnType, Error>;
-    (SerdeV8("package::SerdeInputType")) -> Result(SerdeV8("package::SerdeReturnType"))
+    (SerdeV8(package::SerdeInputType)) -> Result(SerdeV8(package::SerdeReturnType))
   );
   test!(
     #[serde] fn op_serde_tuple(#[serde] input: (A, B)) -> (A, B);
-    (SerdeV8("(A , B)")) -> Infallible(SerdeV8("(A , B)"))
+    (SerdeV8((A, B))) -> Infallible(SerdeV8((A, B)))
   );
   test!(
     fn op_local(input: v8::Local<v8::String>) -> Result<v8::Local<v8::String>, Error>;
     (V8Local(String)) -> Result(V8Local(String))
   );
   test!(
-    fn op_resource(#[smi] rid: ResourceId, buffer: &[u8]);
-    (Numeric(__SMI__), Slice(Ref, u8)) ->  Infallible(Void)
+    fn op_resource(#[smi] rid: ResourceId, #[buffer] buffer: &[u8]);
+    (Numeric(__SMI__), Buffer(Slice(Ref, u8))) ->  Infallible(Void)
   );
   test!(
     fn op_option_numeric_result(state: &mut OpState) -> Result<Option<u32>, AnyError>;
@@ -941,7 +1149,7 @@ mod tests {
   );
   test!(
     fn op_ffi_read_f64(state: &mut OpState, ptr: * mut c_void, offset: isize) -> Result <f64, AnyError>;
-    (Ref(Mut, OpState), Ptr(Mut, __VOID__), Numeric(isize)) -> Result(Numeric(f64))
+    (Ref(Mut, OpState), External(Ptr(Mut)), Numeric(isize)) -> Result(Numeric(f64))
   );
   test!(
     fn op_print(#[string] msg: &str, is_err: bool) -> Result<(), Error>;
@@ -981,7 +1189,11 @@ mod tests {
   );
   test!(
     fn op_state_attr(#[state] something: &Something, #[state] another: Option<&Something>);
-    (State(Ref, "Something"), OptionState(Ref, "Something")) -> Infallible(Void)
+    (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(Vec(u8)), Buffer(BoxSlice(u8)), Buffer(Bytes), Buffer(V8Slice), Buffer(JSBuffer)) -> Infallible(Buffer(Vec(u8)))
   );
 
   // Args
@@ -1001,6 +1213,27 @@ mod tests {
     ArgError("s", MissingStringAttribute),
     fn f(s: Cow<str>) {}
   );
+  expect_fail!(
+    op_with_invalid_string,
+    ArgError("x", InvalidStringType("u32")),
+    fn f(#[string] x: u32) {}
+  );
+  expect_fail!(
+    op_with_invalid_buffer,
+    ArgError("x", InvalidBufferType("u32")),
+    fn f(#[buffer] x: u32) {}
+  );
+  expect_fail!(
+    op_with_bad_attr,
+    RetError(InvalidAttribute("#[badattr]")),
+    #[badattr]
+    fn f() {}
+  );
+  expect_fail!(
+    op_with_bad_attr2,
+    ArgError("a", InvalidAttribute("#[badattr]")),
+    fn f(#[badattr] a: u32) {}
+  );
 
   // Generics
 
diff --git a/serde_v8/lib.rs b/serde_v8/lib.rs
index 59c8fd4..2778268 100644
--- a/serde_v8/lib.rs
+++ b/serde_v8/lib.rs
@@ -23,6 +23,7 @@ pub use magic::bytestring::ByteString;
 pub use magic::detached_buffer::DetachedBuffer;
 pub use magic::string_or_buffer::StringOrBuffer;
 pub use magic::u16string::U16String;
+pub use magic::v8slice::V8Slice;
 pub use magic::ExternalPointer;
 pub use magic::Global;
 pub use magic::Value;
diff --git a/serde_v8/magic/v8slice.rs b/serde_v8/magic/v8slice.rs
index 2b103f1..ea00f2a 100644
--- a/serde_v8/magic/v8slice.rs
+++ b/serde_v8/magic/v8slice.rs
@@ -31,26 +31,56 @@ pub struct V8Slice {
 unsafe impl Send for V8Slice {}
 
 impl V8Slice {
+  /// Create a V8Slice from raw parts.
+  ///
+  /// # Safety
+  ///
+  /// The `range` passed to this function *must* be within the bounds of the backing store, as we may
+  /// create a slice from this. The [`v8::BackingStore`] must be valid, and valid for use for the purposes
+  /// of this `V8Slice` (ie: the caller must understand the repercussions of using shared/resizable
+  /// buffers).
+  pub unsafe fn from_parts(
+    store: v8::SharedRef<v8::BackingStore>,
+    range: Range<usize>,
+  ) -> Self {
+    Self { store, range }
+  }
+
   fn as_slice(&self) -> &[u8] {
+    let store = &self.store;
+    let Some(ptr) = store.data() else {
+      return &[];
+    };
+    let ptr = ptr.cast::<u8>().as_ptr();
     // SAFETY: v8::SharedRef<v8::BackingStore> is similar to Arc<[u8]>,
     // it points to a fixed continuous slice of bytes on the heap.
-    // We assume it's initialized and thus safe to read (though may not contain meaningful data)
-    unsafe { &*(&self.store[self.range.clone()] as *const _ as *const [u8]) }
+    // We assume it's initialized and thus safe to read (though may not contain
+    // meaningful data).
+    // Note that we are likely violating Rust's safety rules here by assuming
+    // nobody is mutating this buffer elsewhere, however in practice V8Slices
+    // do not have overlapping read/write phases.
+    unsafe {
+      let ptr = ptr.add(self.range.start);
+      std::slice::from_raw_parts(ptr, self.range.len())
+    }
   }
 
   fn as_slice_mut(&mut self) -> &mut [u8] {
-    #[allow(clippy::cast_ref_to_mut)]
+    let store = &self.store;
+    let Some(ptr) = store.data() else {
+      return &mut [];
+    };
+    let ptr = ptr.cast::<u8>().as_ptr();
     // SAFETY: v8::SharedRef<v8::BackingStore> is similar to Arc<[u8]>,
     // it points to a fixed continuous slice of bytes on the heap.
-    // It's safe-ish to mutate concurrently because it can not be
-    // shrunk/grown/moved/reallocated, thus avoiding dangling refs (unlike a Vec).
-    // Concurrent writes can't lead to meaningful structural invalidation
-    // since we treat them as opaque buffers / "bags of bytes",
-    // concurrent mutation is simply an accepted fact of life.
-    // And in practice V8Slices also do not have overallping read/write phases.
-    // TLDR: permissive interior mutability on slices of bytes is "fine"
+    // We assume it's initialized and thus safe to read (though may not contain
+    // meaningful data).
+    // Note that we are likely violating Rust's safety rules here by assuming
+    // nobody is mutating this buffer elsewhere, however in practice V8Slices
+    // do not have overlapping read/write phases.
     unsafe {
-      &mut *(&self.store[self.range.clone()] as *const _ as *mut [u8])
+      let ptr = ptr.add(self.range.start);
+      std::slice::from_raw_parts_mut(ptr, self.range.len())
     }
   }
 }
@@ -161,15 +191,15 @@ unsafe fn v8slice_clone(
 }
 
 unsafe fn v8slice_to_vec(
-  data: &rawbytes::AtomicPtr<()>,
+  _data: &rawbytes::AtomicPtr<()>,
   ptr: *const u8,
   len: usize,
 ) -> Vec<u8> {
-  let rc = Rc::from_raw(*data as *const V8Slice);
-  std::mem::forget(rc);
-  // NOTE: `bytes::Bytes` does bounds checking so we trust its ptr, len inputs
-  // and must use them to allow cloning Bytes it has sliced
-  Vec::from_raw_parts(ptr as _, len, len)
+  // SAFETY: It's extremely unlikely we can convert this backing store to a vector directly, as this would require
+  // the store to have been created with the appropriate allocator, and have no other references (either
+  // in JS or in Rust). We instead just create a copy here, using the raw buffer/length.
+  let vec = unsafe { std::slice::from_raw_parts(ptr, len).to_vec() };
+  vec
 }
 
 unsafe fn v8slice_drop(


Test patch:
diff --git a/ops/op2/test_cases/sync/buffers.out b/ops/op2/test_cases/sync/buffers.out
new file mode 100644
index 0000000..4036b9d
--- /dev/null
+++ b/ops/op2/test_cases/sync/buffers.out
@@ -0,0 +1,93 @@
+#[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,
+        2usize 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),
+                ],
+                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>,
+    ) -> () {
+        let arg0 = unsafe { arg0.as_mut().unwrap() }.get_storage_if_aligned().unwrap();
+        let arg1 = unsafe { arg1.as_mut().unwrap() }.get_storage_if_aligned().unwrap();
+        let result = Self::call(arg0, arg1);
+        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_nonresizable_v8_slice(&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 = &mut arg0;
+        let arg1 = args.get(1usize as i32);
+        let mut arg1 = match unsafe {
+            deno_core::_ops::to_nonresizable_v8_slice(&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 = &mut arg1;
+        let result = Self::call(arg0, arg1);
+    }
+    #[inline(always)]
+    fn call(a: &[u8], b: &mut [u8]) {}
+}
diff --git a/ops/op2/test_cases/sync/buffers.rs b/ops/op2/test_cases/sync/buffers.rs
new file mode 100644
index 0000000..2082edb
--- /dev/null
+++ b/ops/op2/test_cases/sync/buffers.rs
@@ -0,0 +1,5 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+#[op2(fast)]
+fn op_buffers(#[buffer] a: &[u8], #[buffer] b: &mut [u8]) {
+}