diff --git a/src/rust/jsg-macros/lib.rs b/src/rust/jsg-macros/lib.rs index 4754c206560..86553c0ff72 100644 --- a/src/rust/jsg-macros/lib.rs +++ b/src/rust/jsg-macros/lib.rs @@ -42,6 +42,31 @@ pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream { }) }); + let field_extractions = fields.named.iter().filter_map(|field| { + if !matches!(field.vis, syn::Visibility::Public(_)) { + return None; + } + let field_name = field.ident.as_ref()?; + let field_name_str = field_name.to_string(); + let field_type = &field.ty; + Some(quote! { + let #field_name = { + let prop = obj.get(lock, #field_name_str) + .ok_or_else(|| jsg::Error::new_type_error( + format!("Missing property '{}'", #field_name_str) + ))?; + <#field_type as jsg::FromJS>::from_js(lock, prop)? + }; + }) + }); + + let field_names = fields.named.iter().filter_map(|field| { + if !matches!(field.vis, syn::Visibility::Public(_)) { + return None; + } + field.ident.as_ref() + }); + quote! { #input @@ -73,8 +98,15 @@ pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream { impl jsg::FromJS for #name { type ResultType = Self; - fn from_js(_lock: &mut jsg::Lock, _value: jsg::v8::Local) -> Result { - todo!("Struct from_js is not yet supported") + fn from_js(lock: &mut jsg::Lock, value: jsg::v8::Local) -> Result { + if !value.is_object() { + return Err(jsg::Error::new_type_error( + format!("Expected object but got {}", value.type_of()) + )); + } + let obj: jsg::v8::Local<'_, jsg::v8::Object> = value.into(); + #(#field_extractions)* + Ok(Self { #(#field_names),* }) } } diff --git a/src/rust/jsg-test/tests/arrays.rs b/src/rust/jsg-test/tests/arrays.rs new file mode 100644 index 00000000000..ebb48ccc6ee --- /dev/null +++ b/src/rust/jsg-test/tests/arrays.rs @@ -0,0 +1,531 @@ +use jsg::ResourceState; +use jsg::ResourceTemplate; +use jsg::ToJS; +use jsg_macros::jsg_method; +use jsg_macros::jsg_resource; +use jsg_macros::jsg_struct; + +#[jsg_struct] +struct Person { + pub name: String, + pub age: f64, +} + +#[jsg_resource] +struct ArrayResource { + _state: ResourceState, +} + +#[jsg_resource] +impl ArrayResource { + #[jsg_method] + pub fn sum(&self, numbers: Vec) -> f64 { + numbers.iter().sum() + } + + #[jsg_method] + pub fn sum_slice(&self, numbers: &[f64]) -> f64 { + numbers.iter().sum() + } + + #[jsg_method] + pub fn join_strings(&self, strings: &[String]) -> String { + strings.join("-") + } + + #[jsg_method] + pub fn double(&self, numbers: Vec) -> Vec { + numbers.into_iter().map(|n| n * 2.0).collect() + } + + #[jsg_method] + pub fn concat_strings(&self, strings: Vec) -> String { + strings.join(", ") + } + + #[jsg_method] + pub fn split_string(&self, s: &str) -> Vec { + s.split(',').map(|s| s.trim().to_owned()).collect() + } + + #[jsg_method] + pub fn filter_positive(&self, numbers: Vec) -> Vec { + numbers.into_iter().filter(|&n| n > 0.0).collect() + } + + #[jsg_method] + pub fn reverse_bytes(&self, bytes: Vec) -> Vec { + bytes.into_iter().rev().collect() + } + + #[jsg_method] + pub fn sum_i32(&self, numbers: Vec) -> f64 { + numbers.iter().map(|&n| f64::from(n)).sum() + } + + #[jsg_method] + pub fn filter_adults(&self, people: Vec) -> Vec { + people.into_iter().filter(|p| p.age >= 18.0).collect() + } +} + +#[test] +fn resource_accepts_array_parameter() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let resource = jsg::Ref::new(ArrayResource { + _state: ResourceState::default(), + }); + let mut template = ArrayResourceTemplate::new(lock); + let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + ctx.set_global("arr", wrapped); + + let result: f64 = ctx.eval(lock, "arr.sum([1, 2, 3, 4, 5])").unwrap(); + assert!((result - 15.0).abs() < f64::EPSILON); + + let result: String = ctx + .eval(lock, "arr.concatStrings(['hello', 'world'])") + .unwrap(); + assert_eq!(result, "hello, world"); + Ok(()) + }); +} + +#[test] +fn resource_accepts_slice_parameter() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let resource = jsg::Ref::new(ArrayResource { + _state: ResourceState::default(), + }); + let mut template = ArrayResourceTemplate::new(lock); + let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + ctx.set_global("arr", wrapped); + + let result: f64 = ctx.eval(lock, "arr.sumSlice([1, 2, 3, 4, 5])").unwrap(); + assert!((result - 15.0).abs() < f64::EPSILON); + + let result: String = ctx.eval(lock, "arr.joinStrings(['a', 'b', 'c'])").unwrap(); + assert_eq!(result, "a-b-c"); + + let result: f64 = ctx.eval(lock, "arr.sumSlice([])").unwrap(); + assert!((result - 0.0).abs() < f64::EPSILON); + Ok(()) + }); +} + +#[test] +fn resource_returns_array() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let resource = jsg::Ref::new(ArrayResource { + _state: ResourceState::default(), + }); + let mut template = ArrayResourceTemplate::new(lock); + let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + ctx.set_global("arr", wrapped); + + let result: Vec = ctx.eval(lock, "arr.double([1, 2, 3])").unwrap(); + assert_eq!(result, vec![2.0, 4.0, 6.0]); + + let result: Vec = ctx.eval(lock, "arr.splitString('a, b, c')").unwrap(); + assert_eq!(result, vec!["a", "b", "c"]); + + let result: Vec = ctx + .eval(lock, "arr.filterPositive([-1, 2, -3, 4, 0])") + .unwrap(); + assert_eq!(result, vec![2.0, 4.0]); + Ok(()) + }); +} + +#[test] +fn resource_accepts_typed_array_parameter() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let resource = jsg::Ref::new(ArrayResource { + _state: ResourceState::default(), + }); + let mut template = ArrayResourceTemplate::new(lock); + let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + ctx.set_global("arr", wrapped); + + let result: Vec = ctx + .eval(lock, "arr.reverseBytes(new Uint8Array([1, 2, 3]))") + .unwrap(); + assert_eq!(result, vec![3, 2, 1]); + + let result: f64 = ctx + .eval(lock, "arr.sumI32(new Int32Array([-10, 20, -5]))") + .unwrap(); + assert!((result - 5.0).abs() < f64::EPSILON); + Ok(()) + }); +} + +#[test] +fn resource_returns_typed_array() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let resource = jsg::Ref::new(ArrayResource { + _state: ResourceState::default(), + }); + let mut template = ArrayResourceTemplate::new(lock); + let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + ctx.set_global("arr", wrapped); + + let is_u8: bool = ctx + .eval( + lock, + "arr.reverseBytes(new Uint8Array([1, 2, 3])) instanceof Uint8Array", + ) + .unwrap(); + assert!(is_u8); + Ok(()) + }); +} + +#[test] +fn resource_accepts_and_returns_struct_array() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let resource = jsg::Ref::new(ArrayResource { + _state: ResourceState::default(), + }); + let mut template = ArrayResourceTemplate::new(lock); + let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + ctx.set_global("arr", wrapped); + + let result: Vec = ctx + .eval( + lock, + "arr.filterAdults([{name: 'Alice', age: 25}, {name: 'Bob', age: 15}, {name: 'Charlie', age: 30}])", + ) + .unwrap(); + assert_eq!(result.len(), 2); + assert_eq!(result[0].name, "Alice"); + assert_eq!(result[1].name, "Charlie"); + Ok(()) + }); +} + +#[test] +fn vec_to_js_creates_array() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let vec = vec!["hello".to_owned(), "world".to_owned()]; + let js_val = vec.to_js(lock); + ctx.set_global("arr", js_val); + + let is_array: bool = ctx.eval(lock, "Array.isArray(arr)").unwrap(); + assert!(is_array); + let length: f64 = ctx.eval(lock, "arr.length").unwrap(); + assert!((length - 2.0).abs() < f64::EPSILON); + let first: String = ctx.eval(lock, "arr[0]").unwrap(); + assert_eq!(first, "hello"); + + let vec = vec![1.5, 2.5, 3.5]; + let js_val = vec.to_js(lock); + ctx.set_global("nums", js_val); + + let sum: f64 = ctx.eval(lock, "nums[0] + nums[1] + nums[2]").unwrap(); + assert!((sum - 7.5).abs() < f64::EPSILON); + + let vec = vec![true, false, true]; + let js_val = vec.to_js(lock); + ctx.set_global("bools", js_val); + + let result: Vec = ctx.eval(lock, "bools").unwrap(); + assert_eq!(result, vec![true, false, true]); + Ok(()) + }); +} + +#[test] +fn vec_from_js_parses_array() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let strings: Vec = ctx.eval(lock, "['a', 'b', 'c']").unwrap(); + assert_eq!(strings, vec!["a", "b", "c"]); + + let numbers: Vec = ctx.eval(lock, "[1, 2, 3]").unwrap(); + assert_eq!(numbers, vec![1.0, 2.0, 3.0]); + Ok(()) + }); +} + +#[test] +fn vec_empty_roundtrip() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let vec: Vec = vec![]; + let js_val = vec.to_js(lock); + ctx.set_global("arr", js_val); + + let length: f64 = ctx.eval(lock, "arr.length").unwrap(); + assert!(length.abs() < f64::EPSILON); + + let result: Vec = ctx.eval(lock, "arr").unwrap(); + assert!(result.is_empty()); + Ok(()) + }); +} + +#[test] +fn vec_nested_arrays() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let nested = vec![vec![1.0, 2.0], vec![3.0, 4.0]]; + let js_val = nested.to_js(lock); + ctx.set_global("matrix", js_val); + + let first_row_sum: f64 = ctx.eval(lock, "matrix[0][0] + matrix[0][1]").unwrap(); + assert!((first_row_sum - 3.0).abs() < f64::EPSILON); + + let second_row_sum: f64 = ctx.eval(lock, "matrix[1][0] + matrix[1][1]").unwrap(); + assert!((second_row_sum - 7.0).abs() < f64::EPSILON); + Ok(()) + }); +} + +#[test] +fn vec_from_non_array_returns_error() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let result: Result, _> = ctx.eval(lock, "'not an array'"); + assert!(result.is_err()); + Ok(()) + }); +} + +#[test] +fn typed_array_to_js() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let vec: Vec = vec![1, 2, 255]; + ctx.set_global("u8_arr", vec.to_js(lock)); + let check: bool = ctx.eval(lock, "u8_arr instanceof Uint8Array").unwrap(); + assert!(check); + let val: f64 = ctx.eval(lock, "u8_arr[2]").unwrap(); + assert!((val - 255.0).abs() < f64::EPSILON); + + let vec: Vec = vec![1, 2, 65535]; + ctx.set_global("u16_arr", vec.to_js(lock)); + let check: bool = ctx.eval(lock, "u16_arr instanceof Uint16Array").unwrap(); + assert!(check); + let val: f64 = ctx.eval(lock, "u16_arr[2]").unwrap(); + assert!((val - 65535.0).abs() < f64::EPSILON); + + let vec: Vec = vec![1, 2, 4_294_967_295]; + ctx.set_global("u32_arr", vec.to_js(lock)); + let check: bool = ctx.eval(lock, "u32_arr instanceof Uint32Array").unwrap(); + assert!(check); + let val: f64 = ctx.eval(lock, "u32_arr[2]").unwrap(); + assert!((val - 4_294_967_295.0).abs() < f64::EPSILON); + + let vec: Vec = vec![-128, 0, 127]; + ctx.set_global("i8_arr", vec.to_js(lock)); + let check: bool = ctx.eval(lock, "i8_arr instanceof Int8Array").unwrap(); + assert!(check); + let val: f64 = ctx.eval(lock, "i8_arr[0]").unwrap(); + assert!((val - (-128.0)).abs() < f64::EPSILON); + + let vec: Vec = vec![-32768, 0, 32767]; + ctx.set_global("i16_arr", vec.to_js(lock)); + let check: bool = ctx.eval(lock, "i16_arr instanceof Int16Array").unwrap(); + assert!(check); + let val: f64 = ctx.eval(lock, "i16_arr[0]").unwrap(); + assert!((val - (-32768.0)).abs() < f64::EPSILON); + + let vec: Vec = vec![-2_147_483_648, 0, 2_147_483_647]; + ctx.set_global("i32_arr", vec.to_js(lock)); + let check: bool = ctx.eval(lock, "i32_arr instanceof Int32Array").unwrap(); + assert!(check); + let val: f64 = ctx.eval(lock, "i32_arr[0]").unwrap(); + assert!((val - (-2_147_483_648.0)).abs() < f64::EPSILON); + + let is_array: bool = ctx.eval(lock, "Array.isArray(u8_arr)").unwrap(); + assert!(!is_array); + Ok(()) + }); +} + +#[test] +fn typed_array_from_js() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let u8_arr: Vec = ctx.eval(lock, "new Uint8Array([10, 20, 255])").unwrap(); + assert_eq!(u8_arr, vec![10, 20, 255]); + + let u16_arr: Vec = ctx + .eval(lock, "new Uint16Array([100, 200, 65535])") + .unwrap(); + assert_eq!(u16_arr, vec![100, 200, 65535]); + + let u32_arr: Vec = ctx + .eval(lock, "new Uint32Array([1000, 2000, 4294967295])") + .unwrap(); + assert_eq!(u32_arr, vec![1000, 2000, 4_294_967_295]); + + let i8_arr: Vec = ctx.eval(lock, "new Int8Array([-128, 0, 127])").unwrap(); + assert_eq!(i8_arr, vec![-128, 0, 127]); + + let i16_arr: Vec = ctx + .eval(lock, "new Int16Array([-32768, 0, 32767])") + .unwrap(); + assert_eq!(i16_arr, vec![-32768, 0, 32767]); + + let i32_arr: Vec = ctx + .eval(lock, "new Int32Array([-2147483648, 0, 2147483647])") + .unwrap(); + assert_eq!(i32_arr, vec![-2_147_483_648, 0, 2_147_483_647]); + Ok(()) + }); +} + +#[test] +fn typed_array_empty_roundtrip() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let vec: Vec = vec![]; + ctx.set_global("arr", vec.to_js(lock)); + + let length: f64 = ctx.eval(lock, "arr.length").unwrap(); + assert!(length.abs() < f64::EPSILON); + + let result: Vec = ctx.eval(lock, "arr").unwrap(); + assert!(result.is_empty()); + Ok(()) + }); +} + +#[test] +fn typed_array_type_mismatch_returns_error() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let result: Result, _> = ctx.eval(lock, "new Int8Array([1, 2, 3])"); + assert!(result.is_err()); + + let result: Result, _> = ctx.eval(lock, "new Uint32Array([1, 2, 3])"); + assert!(result.is_err()); + + let result: Result, _> = ctx.eval(lock, "[1, 2, 3]"); + assert!(result.is_err()); + + let result: Result, _> = ctx.eval(lock, "'hello'"); + assert!(result.is_err()); + Ok(()) + }); +} + +#[test] +fn large_typed_array_roundtrip() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, ctx| { + let vec: Vec = (0_u32..65536).map(|i| (i % 256) as u8).collect(); + ctx.set_global("arr", vec.clone().to_js(lock)); + + let length: f64 = ctx.eval(lock, "arr.length").unwrap(); + assert!((length - 65536.0).abs() < f64::EPSILON); + + let result: Vec = ctx.eval(lock, "arr").unwrap(); + assert_eq!(result, vec); + Ok(()) + }); +} + +#[test] +fn typed_array_iter_uint8() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let data: Vec = vec![10, 20, 30]; + let js_val = data.to_js(lock); + let typed: jsg::v8::Local<'_, jsg::v8::Uint8Array> = + unsafe { jsg::v8::Local::from_ffi(lock.isolate(), js_val.into_ffi()) }; + + assert_eq!(typed.len(), 3); + assert!(!typed.is_empty()); + + assert_eq!(typed.get(0), 10); + assert_eq!(typed.get(1), 20); + assert_eq!(typed.get(2), 30); + + let sum: u8 = typed.iter().fold(0u8, u8::wrapping_add); + assert_eq!(sum, 60); + + let collected: Vec = typed.iter().collect(); + assert_eq!(collected, vec![10, 20, 30]); + + Ok(()) + }); +} + +#[test] +fn typed_array_into_iter_uint8() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let data: Vec = vec![1, 2, 3, 4, 5]; + let js_val = data.to_js(lock); + let typed: jsg::v8::Local<'_, jsg::v8::Uint8Array> = + unsafe { jsg::v8::Local::from_ffi(lock.isolate(), js_val.into_ffi()) }; + + let sum: u8 = typed.into_iter().sum(); + assert_eq!(sum, 15); + + Ok(()) + }); +} + +#[test] +fn typed_array_iter_int32() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let data: Vec = vec![-100, 0, 100]; + let js_val = data.to_js(lock); + let typed: jsg::v8::Local<'_, jsg::v8::Int32Array> = + unsafe { jsg::v8::Local::from_ffi(lock.isolate(), js_val.into_ffi()) }; + + assert_eq!(typed.len(), 3); + assert_eq!(typed.get(0), -100); + assert_eq!(typed.get(1), 0); + assert_eq!(typed.get(2), 100); + + let sum: i32 = typed.iter().sum(); + assert_eq!(sum, 0); + + Ok(()) + }); +} + +#[test] +fn typed_array_iter_reverse() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let data: Vec = vec![1, 2, 3, 4]; + let js_val = data.to_js(lock); + let typed: jsg::v8::Local<'_, jsg::v8::Uint8Array> = + unsafe { jsg::v8::Local::from_ffi(lock.isolate(), js_val.into_ffi()) }; + + let reversed: Vec = typed.iter().rev().collect(); + assert_eq!(reversed, vec![4, 3, 2, 1]); + + Ok(()) + }); +} + +#[test] +fn typed_array_empty() { + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let data: Vec = vec![]; + let js_val = data.to_js(lock); + let typed: jsg::v8::Local<'_, jsg::v8::Uint8Array> = + unsafe { jsg::v8::Local::from_ffi(lock.isolate(), js_val.into_ffi()) }; + + assert_eq!(typed.len(), 0); + assert!(typed.is_empty()); + assert_eq!(typed.iter().count(), 0); + + Ok(()) + }); +} diff --git a/src/rust/jsg-test/tests/mod.rs b/src/rust/jsg-test/tests/mod.rs index e4154031292..32f3a1ef01b 100644 --- a/src/rust/jsg-test/tests/mod.rs +++ b/src/rust/jsg-test/tests/mod.rs @@ -1,3 +1,4 @@ +mod arrays; mod eval; mod jsg_oneof; mod jsg_struct; diff --git a/src/rust/jsg/ffi.c++ b/src/rust/jsg/ffi.c++ index 817d9c21801..cf564fc4e2e 100644 --- a/src/rust/jsg/ffi.c++ +++ b/src/rust/jsg/ffi.c++ @@ -12,6 +12,40 @@ using namespace kj_rs; namespace workerd::rust::jsg { +#define DEFINE_TYPED_ARRAY_NEW(name, v8_type, elem_type) \ + Local local_new_##name(Isolate* isolate, const elem_type* data, size_t length) { \ + auto backingStore = v8::ArrayBuffer::NewBackingStore(isolate, length * sizeof(elem_type)); \ + memcpy(backingStore->Data(), data, length * sizeof(elem_type)); \ + auto arrayBuffer = v8::ArrayBuffer::New(isolate, std::move(backingStore)); \ + return to_ffi(v8::v8_type::New(arrayBuffer, 0, length)); \ + } + +#define DEFINE_TYPED_ARRAY_UNWRAP(name, v8_type, elem_type) \ + ::rust::Vec unwrap_##name(Isolate* isolate, Local value) { \ + auto v8Val = local_from_ffi(kj::mv(value)); \ + KJ_REQUIRE(v8Val->Is##v8_type()); \ + auto typed = v8Val.As(); \ + ::rust::Vec result; \ + result.reserve(typed->Length()); \ + auto data = reinterpret_cast( \ + static_cast(typed->Buffer()->Data()) + typed->ByteOffset()); \ + for (size_t i = 0; i < typed->Length(); i++) { \ + result.push_back(data[i]); \ + } \ + return result; \ + } + +#define DEFINE_TYPED_ARRAY_GET(name, v8_type, elem_type) \ + elem_type local_##name##_get(Isolate* isolate, const Local& array, size_t index) { \ + auto typed = local_as_ref_from_ffi(array); \ + KJ_REQUIRE(index < typed->Length(), "index out of bounds"); \ + auto data = reinterpret_cast( \ + static_cast(typed->Buffer()->Data()) + typed->ByteOffset()); \ + return data[index]; \ + } + +// ============================================================================= + // Local void local_drop(Local value) { // Convert from FFI representation and let v8::Local destructor handle cleanup @@ -98,6 +132,42 @@ bool local_is_native_error(const Local& val) { return local_as_ref_from_ffi(val)->IsNativeError(); } +bool local_is_array(const Local& val) { + return local_as_ref_from_ffi(val)->IsArray(); +} + +bool local_is_uint8_array(const Local& val) { + return local_as_ref_from_ffi(val)->IsUint8Array(); +} + +bool local_is_uint16_array(const Local& val) { + return local_as_ref_from_ffi(val)->IsUint16Array(); +} + +bool local_is_uint32_array(const Local& val) { + return local_as_ref_from_ffi(val)->IsUint32Array(); +} + +bool local_is_int8_array(const Local& val) { + return local_as_ref_from_ffi(val)->IsInt8Array(); +} + +bool local_is_int16_array(const Local& val) { + return local_as_ref_from_ffi(val)->IsInt16Array(); +} + +bool local_is_int32_array(const Local& val) { + return local_as_ref_from_ffi(val)->IsInt32Array(); +} + +bool local_is_array_buffer(const Local& val) { + return local_as_ref_from_ffi(val)->IsArrayBuffer(); +} + +bool local_is_array_buffer_view(const Local& val) { + return local_as_ref_from_ffi(val)->IsArrayBufferView(); +} + ::rust::String local_type_of(Isolate* isolate, const Local& val) { auto v8Val = local_as_ref_from_ffi(val); v8::Local typeStr = v8Val->TypeOf(isolate); @@ -134,6 +204,35 @@ kj::Maybe local_object_get_property(Isolate* isolate, const Local& object return to_ffi(kj::mv(result)); } +// Local +Local local_new_array(Isolate* isolate, size_t length) { + return to_ffi(v8::Array::New(isolate, length)); +} + +uint32_t local_array_length(Isolate* isolate, const Local& array) { + return local_as_ref_from_ffi(array)->Length(); +} + +Local local_array_get(Isolate* isolate, const Local& array, uint32_t index) { + auto context = isolate->GetCurrentContext(); + auto v8Array = local_as_ref_from_ffi(array); + return to_ffi(::workerd::jsg::check(v8Array->Get(context, index))); +} + +void local_array_set(Isolate* isolate, Local& array, uint32_t index, Local value) { + auto context = isolate->GetCurrentContext(); + auto v8Array = local_as_ref_from_ffi(array); + ::workerd::jsg::check(v8Array->Set(context, index, local_from_ffi(kj::mv(value)))); +} + +// TypedArray creation functions +DEFINE_TYPED_ARRAY_NEW(uint8_array, Uint8Array, uint8_t) +DEFINE_TYPED_ARRAY_NEW(uint16_array, Uint16Array, uint16_t) +DEFINE_TYPED_ARRAY_NEW(uint32_array, Uint32Array, uint32_t) +DEFINE_TYPED_ARRAY_NEW(int8_array, Int8Array, int8_t) +DEFINE_TYPED_ARRAY_NEW(int16_array, Int16Array, int16_t) +DEFINE_TYPED_ARRAY_NEW(int32_array, Int32Array, int32_t) + // Wrappers Local wrap_resource(Isolate* isolate, size_t resource, const Global& tmpl, size_t drop_callback) { auto self = reinterpret_cast(resource); @@ -184,8 +283,59 @@ size_t unwrap_resource(Isolate* isolate, Local value) { static_cast(::workerd::jsg::Wrappable::WRAPPED_OBJECT_FIELD_INDEX))); } -// Global +// TypedArray unwrap functions +DEFINE_TYPED_ARRAY_UNWRAP(uint8_array, Uint8Array, uint8_t) +DEFINE_TYPED_ARRAY_UNWRAP(uint16_array, Uint16Array, uint16_t) +DEFINE_TYPED_ARRAY_UNWRAP(uint32_array, Uint32Array, uint32_t) +DEFINE_TYPED_ARRAY_UNWRAP(int8_array, Int8Array, int8_t) +DEFINE_TYPED_ARRAY_UNWRAP(int16_array, Int16Array, int16_t) +DEFINE_TYPED_ARRAY_UNWRAP(int32_array, Int32Array, int32_t) +// Uses V8's Array::Iterate() which is faster than indexed access. +// Returns Global handles because Local handles get reused during iteration. +::rust::Vec local_array_iterate(Isolate* isolate, Local value) { + auto context = isolate->GetCurrentContext(); + auto v8Val = local_from_ffi(kj::mv(value)); + + KJ_REQUIRE(v8Val->IsArray(), "Value must be an array"); + auto arr = v8Val.As(); + + struct Data { + Isolate* isolate; + ::rust::Vec* result; + }; + + ::rust::Vec result; + result.reserve(arr->Length()); + Data data{isolate, &result}; + + auto iterateResult = arr->Iterate(context, + [](uint32_t index, v8::Local element, + void* userData) -> v8::Array::CallbackResult { + auto* d = static_cast(userData); + d->result->push_back(to_ffi(v8::Global(d->isolate, element))); + return v8::Array::CallbackResult::kContinue; + }, + &data); + + KJ_REQUIRE(iterateResult.IsJust(), "Iteration failed"); + return result; +} + +// Local +size_t local_typed_array_length(Isolate* isolate, const Local& array) { + return local_as_ref_from_ffi(array)->Length(); +} + +// TypedArray element getter functions +DEFINE_TYPED_ARRAY_GET(uint8_array, Uint8Array, uint8_t) +DEFINE_TYPED_ARRAY_GET(uint16_array, Uint16Array, uint16_t) +DEFINE_TYPED_ARRAY_GET(uint32_array, Uint32Array, uint32_t) +DEFINE_TYPED_ARRAY_GET(int8_array, Int8Array, int8_t) +DEFINE_TYPED_ARRAY_GET(int16_array, Int16Array, int16_t) +DEFINE_TYPED_ARRAY_GET(int32_array, Int32Array, int32_t) + +// Global void global_drop(Global value) { global_from_ffi(kj::mv(value)); } diff --git a/src/rust/jsg/ffi.h b/src/rust/jsg/ffi.h index aab36bfb710..cbcff717507 100644 --- a/src/rust/jsg/ffi.h +++ b/src/rust/jsg/ffi.h @@ -36,6 +36,13 @@ Local local_new_boolean(Isolate* isolate, bool value); Local local_new_object(Isolate* isolate); Local local_new_null(Isolate* isolate); Local local_new_undefined(Isolate* isolate); +Local local_new_array(Isolate* isolate, size_t length); +Local local_new_uint8_array(Isolate* isolate, const uint8_t* data, size_t length); +Local local_new_uint16_array(Isolate* isolate, const uint16_t* data, size_t length); +Local local_new_uint32_array(Isolate* isolate, const uint32_t* data, size_t length); +Local local_new_int8_array(Isolate* isolate, const int8_t* data, size_t length); +Local local_new_int16_array(Isolate* isolate, const int16_t* data, size_t length); +Local local_new_int32_array(Isolate* isolate, const int32_t* data, size_t length); bool local_eq(const Local& lhs, const Local& rhs); bool local_has_value(const Local& val); bool local_is_string(const Local& val); @@ -46,6 +53,15 @@ bool local_is_undefined(const Local& val); bool local_is_null_or_undefined(const Local& val); bool local_is_object(const Local& val); bool local_is_native_error(const Local& val); +bool local_is_array(const Local& val); +bool local_is_uint8_array(const Local& val); +bool local_is_uint16_array(const Local& val); +bool local_is_uint32_array(const Local& val); +bool local_is_int8_array(const Local& val); +bool local_is_int16_array(const Local& val); +bool local_is_int32_array(const Local& val); +bool local_is_array_buffer(const Local& val); +bool local_is_array_buffer_view(const Local& val); ::rust::String local_type_of(Isolate* isolate, const Local& val); // Local @@ -53,6 +69,21 @@ void local_object_set_property(Isolate* isolate, Local& object, ::rust::Str key, bool local_object_has_property(Isolate* isolate, const Local& object, ::rust::Str key); kj::Maybe local_object_get_property(Isolate* isolate, const Local& object, ::rust::Str key); +// Local +uint32_t local_array_length(Isolate* isolate, const Local& array); +Local local_array_get(Isolate* isolate, const Local& array, uint32_t index); +void local_array_set(Isolate* isolate, Local& array, uint32_t index, Local value); +::rust::Vec local_array_iterate(Isolate* isolate, Local value); + +// Local +size_t local_typed_array_length(Isolate* isolate, const Local& array); +uint8_t local_uint8_array_get(Isolate* isolate, const Local& array, size_t index); +uint16_t local_uint16_array_get(Isolate* isolate, const Local& array, size_t index); +uint32_t local_uint32_array_get(Isolate* isolate, const Local& array, size_t index); +int8_t local_int8_array_get(Isolate* isolate, const Local& array, size_t index); +int16_t local_int16_array_get(Isolate* isolate, const Local& array, size_t index); +int32_t local_int32_array_get(Isolate* isolate, const Local& array, size_t index); + // Global void global_drop(Global value); Global global_clone(const Global& value); @@ -68,6 +99,12 @@ ::rust::String unwrap_string(Isolate* isolate, Local value); bool unwrap_boolean(Isolate* isolate, Local value); double unwrap_number(Isolate* isolate, Local value); size_t unwrap_resource(Isolate* isolate, Local value); +::rust::Vec unwrap_uint8_array(Isolate* isolate, Local value); +::rust::Vec unwrap_uint16_array(Isolate* isolate, Local value); +::rust::Vec unwrap_uint32_array(Isolate* isolate, Local value); +::rust::Vec unwrap_int8_array(Isolate* isolate, Local value); +::rust::Vec unwrap_int16_array(Isolate* isolate, Local value); +::rust::Vec unwrap_int32_array(Isolate* isolate, Local value); // FunctionCallbackInfo Isolate* fci_get_isolate(FunctionCallbackInfo* args); diff --git a/src/rust/jsg/lib.rs b/src/rust/jsg/lib.rs index 0f8219ec2e3..8044ac7a66d 100644 --- a/src/rust/jsg/lib.rs +++ b/src/rust/jsg/lib.rs @@ -13,6 +13,12 @@ pub mod modules; pub mod v8; mod wrappable; +pub use v8::Int8Array; +pub use v8::Int16Array; +pub use v8::Int32Array; +pub use v8::Uint8Array; +pub use v8::Uint16Array; +pub use v8::Uint32Array; pub use v8::ffi::ExceptionType; pub use wrappable::FromJS; pub use wrappable::ToJS; diff --git a/src/rust/jsg/v8.rs b/src/rust/jsg/v8.rs index 94c4c40446d..fd72bd1e1ea 100644 --- a/src/rust/jsg/v8.rs +++ b/src/rust/jsg/v8.rs @@ -68,6 +68,37 @@ pub mod ffi { pub unsafe fn local_new_object(isolate: *mut Isolate) -> Local; pub unsafe fn local_new_null(isolate: *mut Isolate) -> Local; pub unsafe fn local_new_undefined(isolate: *mut Isolate) -> Local; + pub unsafe fn local_new_array(isolate: *mut Isolate, length: usize) -> Local; + pub unsafe fn local_new_uint8_array( + isolate: *mut Isolate, + data: *const u8, + length: usize, + ) -> Local; + pub unsafe fn local_new_uint16_array( + isolate: *mut Isolate, + data: *const u16, + length: usize, + ) -> Local; + pub unsafe fn local_new_uint32_array( + isolate: *mut Isolate, + data: *const u32, + length: usize, + ) -> Local; + pub unsafe fn local_new_int8_array( + isolate: *mut Isolate, + data: *const i8, + length: usize, + ) -> Local; + pub unsafe fn local_new_int16_array( + isolate: *mut Isolate, + data: *const i16, + length: usize, + ) -> Local; + pub unsafe fn local_new_int32_array( + isolate: *mut Isolate, + data: *const i32, + length: usize, + ) -> Local; pub unsafe fn local_eq(lhs: &Local, rhs: &Local) -> bool; pub unsafe fn local_has_value(value: &Local) -> bool; pub unsafe fn local_is_string(value: &Local) -> bool; @@ -78,6 +109,15 @@ pub mod ffi { pub unsafe fn local_is_null_or_undefined(value: &Local) -> bool; pub unsafe fn local_is_object(value: &Local) -> bool; pub unsafe fn local_is_native_error(value: &Local) -> bool; + pub unsafe fn local_is_array(value: &Local) -> bool; + pub unsafe fn local_is_uint8_array(value: &Local) -> bool; + pub unsafe fn local_is_uint16_array(value: &Local) -> bool; + pub unsafe fn local_is_uint32_array(value: &Local) -> bool; + pub unsafe fn local_is_int8_array(value: &Local) -> bool; + pub unsafe fn local_is_int16_array(value: &Local) -> bool; + pub unsafe fn local_is_int32_array(value: &Local) -> bool; + pub unsafe fn local_is_array_buffer(value: &Local) -> bool; + pub unsafe fn local_is_array_buffer_view(value: &Local) -> bool; pub unsafe fn local_type_of(isolate: *mut Isolate, value: &Local) -> String; // Local @@ -98,6 +138,50 @@ pub mod ffi { key: &str, ) -> KjMaybe; + // Local + pub unsafe fn local_array_length(isolate: *mut Isolate, array: &Local) -> u32; + pub unsafe fn local_array_get(isolate: *mut Isolate, array: &Local, index: u32) -> Local; + pub unsafe fn local_array_set( + isolate: *mut Isolate, + array: &mut Local, + index: u32, + value: Local, + ); + pub unsafe fn local_array_iterate(isolate: *mut Isolate, value: Local) -> Vec; + + // Local + pub unsafe fn local_typed_array_length(isolate: *mut Isolate, array: &Local) -> usize; + pub unsafe fn local_uint8_array_get( + isolate: *mut Isolate, + array: &Local, + index: usize, + ) -> u8; + pub unsafe fn local_uint16_array_get( + isolate: *mut Isolate, + array: &Local, + index: usize, + ) -> u16; + pub unsafe fn local_uint32_array_get( + isolate: *mut Isolate, + array: &Local, + index: usize, + ) -> u32; + pub unsafe fn local_int8_array_get( + isolate: *mut Isolate, + array: &Local, + index: usize, + ) -> i8; + pub unsafe fn local_int16_array_get( + isolate: *mut Isolate, + array: &Local, + index: usize, + ) -> i16; + pub unsafe fn local_int32_array_get( + isolate: *mut Isolate, + array: &Local, + index: usize, + ) -> i32; + // Global pub unsafe fn global_drop(value: Global); pub unsafe fn global_clone(value: &Global) -> Global; @@ -113,6 +197,12 @@ pub mod ffi { pub unsafe fn unwrap_string(isolate: *mut Isolate, value: Local) -> String; pub unsafe fn unwrap_boolean(isolate: *mut Isolate, value: Local) -> bool; pub unsafe fn unwrap_number(isolate: *mut Isolate, value: Local) -> f64; + pub unsafe fn unwrap_uint8_array(isolate: *mut Isolate, value: Local) -> Vec; + pub unsafe fn unwrap_uint16_array(isolate: *mut Isolate, value: Local) -> Vec; + pub unsafe fn unwrap_uint32_array(isolate: *mut Isolate, value: Local) -> Vec; + pub unsafe fn unwrap_int8_array(isolate: *mut Isolate, value: Local) -> Vec; + pub unsafe fn unwrap_int16_array(isolate: *mut Isolate, value: Local) -> Vec; + pub unsafe fn unwrap_int32_array(isolate: *mut Isolate, value: Local) -> Vec; // FunctionCallbackInfo pub unsafe fn fci_get_isolate(args: *mut FunctionCallbackInfo) -> *mut Isolate; @@ -227,6 +317,14 @@ impl Display for Local<'_, Value> { #[derive(Debug)] pub struct Object; pub struct FunctionTemplate; +pub struct Array; +pub struct TypedArray; +pub struct Uint8Array; +pub struct Uint16Array; +pub struct Uint32Array; +pub struct Int8Array; +pub struct Int16Array; +pub struct Int32Array; // Generic Local<'a, T> handle with lifetime #[derive(Debug)] @@ -281,6 +379,14 @@ impl<'a, T> Local<'a, T> { &self.handle } + /// Returns a mutable reference to the underlying FFI handle. + /// + /// # Safety + /// The caller must ensure the returned reference is not used after this `Local` is dropped. + pub unsafe fn as_ffi_mut(&mut self) -> &mut ffi::Local { + &mut self.handle + } + pub fn null(lock: &mut crate::Lock) -> Local<'a, Value> { unsafe { Local::from_ffi(lock.isolate(), ffi::local_new_null(lock.isolate().as_ffi())) } } @@ -336,6 +442,51 @@ impl<'a, T> Local<'a, T> { unsafe { ffi::local_is_native_error(&self.handle) } } + /// Returns true if the value is a JavaScript array. + pub fn is_array(&self) -> bool { + unsafe { ffi::local_is_array(&self.handle) } + } + + /// Returns true if the value is a `Uint8Array`. + pub fn is_uint8_array(&self) -> bool { + unsafe { ffi::local_is_uint8_array(&self.handle) } + } + + /// Returns true if the value is a `Uint16Array`. + pub fn is_uint16_array(&self) -> bool { + unsafe { ffi::local_is_uint16_array(&self.handle) } + } + + /// Returns true if the value is a `Uint32Array`. + pub fn is_uint32_array(&self) -> bool { + unsafe { ffi::local_is_uint32_array(&self.handle) } + } + + /// Returns true if the value is an `Int8Array`. + pub fn is_int8_array(&self) -> bool { + unsafe { ffi::local_is_int8_array(&self.handle) } + } + + /// Returns true if the value is an `Int16Array`. + pub fn is_int16_array(&self) -> bool { + unsafe { ffi::local_is_int16_array(&self.handle) } + } + + /// Returns true if the value is an `Int32Array`. + pub fn is_int32_array(&self) -> bool { + unsafe { ffi::local_is_int32_array(&self.handle) } + } + + /// Returns true if the value is an `ArrayBuffer`. + pub fn is_array_buffer(&self) -> bool { + unsafe { ffi::local_is_array_buffer(&self.handle) } + } + + /// Returns true if the value is an `ArrayBufferView`. + pub fn is_array_buffer_view(&self) -> bool { + unsafe { ffi::local_is_array_buffer_view(&self.handle) } + } + /// Returns the JavaScript type of the underlying value as a string. /// /// Uses V8's native `TypeOf` method which returns the same result as @@ -364,6 +515,15 @@ impl<'a> Local<'a, Value> { pub fn to_global(self, lock: &'a mut Lock) -> Global { unsafe { ffi::local_to_global(lock.isolate().as_ffi(), self.into_ffi()).into() } } + + /// Casts this value to an Array. + /// + /// # Safety + /// The caller must ensure this value is actually an array (check with `is_array()`). + pub unsafe fn as_array(self) -> Local<'a, Array> { + debug_assert!(self.is_array()); + unsafe { Local::from_ffi(self.isolate, self.into_ffi()) } + } } impl PartialEq for Local<'_, Value> { @@ -385,6 +545,356 @@ impl<'a> From> for Local<'a, Value> { } } +impl<'a> From> for Local<'a, Value> { + fn from(value: Local<'a, Array>) -> Self { + debug_assert!(value.is_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl Local<'_, Array> { + /// Creates a new JavaScript array with the given length. + pub fn new<'a>(lock: &mut crate::Lock, len: usize) -> Local<'a, Array> { + let isolate = lock.isolate(); + unsafe { Local::from_ffi(isolate, ffi::local_new_array(isolate.as_ffi(), len)) } + } + + /// Returns the length of the array. + #[inline] + pub fn len(&self) -> usize { + unsafe { ffi::local_array_length(self.isolate.as_ffi(), &self.handle) as usize } + } + + /// Returns true if the array is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Sets an element at the given index. + pub fn set(&mut self, index: usize, value: Local<'_, Value>) { + unsafe { + ffi::local_array_set( + self.isolate.as_ffi(), + &mut self.handle, + index as u32, + value.into_ffi(), + ); + } + } + + /// Iterates over array elements using V8's native `Array::Iterate()`. + /// Returns Global handles because Local handles get reused during iteration. + pub fn iterate(self) -> Vec> { + unsafe { ffi::local_array_iterate(self.isolate.as_ffi(), self.into_ffi()) } + .into_iter() + .map(|g| unsafe { Global::from_ffi(g) }) + .collect() + } +} + +impl<'a> From> for Local<'a, Value> { + fn from(value: Local<'a, Uint8Array>) -> Self { + debug_assert!(value.is_uint8_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, Value> { + fn from(value: Local<'a, Uint16Array>) -> Self { + debug_assert!(value.is_uint16_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, Value> { + fn from(value: Local<'a, Uint32Array>) -> Self { + debug_assert!(value.is_uint32_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, Value> { + fn from(value: Local<'a, Int8Array>) -> Self { + debug_assert!(value.is_int8_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, Value> { + fn from(value: Local<'a, Int16Array>) -> Self { + debug_assert!(value.is_int16_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, Value> { + fn from(value: Local<'a, Int32Array>) -> Self { + debug_assert!(value.is_int32_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +// TypedArray base type conversions +impl<'a> From> for Local<'a, Value> { + fn from(value: Local<'a, TypedArray>) -> Self { + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, TypedArray> { + fn from(value: Local<'a, Uint8Array>) -> Self { + debug_assert!(value.is_uint8_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, TypedArray> { + fn from(value: Local<'a, Uint16Array>) -> Self { + debug_assert!(value.is_uint16_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, TypedArray> { + fn from(value: Local<'a, Uint32Array>) -> Self { + debug_assert!(value.is_uint32_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, TypedArray> { + fn from(value: Local<'a, Int8Array>) -> Self { + debug_assert!(value.is_int8_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, TypedArray> { + fn from(value: Local<'a, Int16Array>) -> Self { + debug_assert!(value.is_int16_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +impl<'a> From> for Local<'a, TypedArray> { + fn from(value: Local<'a, Int32Array>) -> Self { + debug_assert!(value.is_int32_array()); + unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } + } +} + +// `TypedArray`-specific implementations +impl Local<'_, TypedArray> { + /// Returns the number of elements in this `TypedArray`. + pub fn len(&self) -> usize { + unsafe { ffi::local_typed_array_length(self.isolate.as_ffi(), &self.handle) } + } + + /// Returns true if the `TypedArray` is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +// ============================================================================= +// `TypedArray` Iterator Types +// ============================================================================= + +/// Iterator over `TypedArray` elements by reference. +/// +/// Created by calling `iter()` on a `Local<'a, TypedArray>`. +/// Does not consume the array, allowing multiple iterations. +pub struct TypedArrayIter<'a, 'b, T, E> { + array: &'b Local<'a, T>, + index: usize, + len: usize, + _marker: PhantomData, +} + +/// Owning iterator over `TypedArray` elements. +/// +/// Created by calling `into_iter()` on a `Local<'a, TypedArray>`. +/// Consumes the array handle. +pub struct TypedArrayIntoIter<'a, T, E> { + array: Local<'a, T>, + index: usize, + len: usize, + _marker: PhantomData, +} + +// ============================================================================= +// `TypedArray` Implementation Macro +// ============================================================================= + +/// Implements methods and traits for a specific `TypedArray` type. +/// +/// For each `TypedArray` marker type (e.g., `Uint8Array`), this macro generates: +/// - `len()`, `is_empty()`, `get(index)` methods +/// - `iter()` method for borrowing iteration +/// - `IntoIterator` for owned and borrowed iteration +/// - `Iterator` implementations for both iterator types +macro_rules! impl_typed_array { + ($marker:ident, $elem:ty, $get_fn:ident) => { + impl<'a> Local<'a, $marker> { + /// Returns the number of elements in this `TypedArray`. + #[inline] + pub fn len(&self) -> usize { + unsafe { ffi::local_typed_array_length(self.isolate.as_ffi(), &self.handle) } + } + + /// Returns `true` if the `TypedArray` contains no elements. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the element at `index`. + /// + /// # Panics + /// + /// Panics if `index >= self.len()`. + #[inline] + pub fn get(&self, index: usize) -> $elem { + debug_assert!(index < self.len(), "index out of bounds"); + unsafe { ffi::$get_fn(self.isolate.as_ffi(), &self.handle, index) } + } + + /// Returns an iterator over the elements. + /// + /// The iterator yields elements by value (copied from V8 memory). + #[inline] + pub fn iter(&self) -> TypedArrayIter<'a, '_, $marker, $elem> { + TypedArrayIter { + array: self, + index: 0, + len: self.len(), + _marker: PhantomData, + } + } + } + + // Owned iteration: `for x in array` + impl<'a> IntoIterator for Local<'a, $marker> { + type Item = $elem; + type IntoIter = TypedArrayIntoIter<'a, $marker, $elem>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + let len = self.len(); + TypedArrayIntoIter { + array: self, + index: 0, + len, + _marker: PhantomData, + } + } + } + + // Borrowed iteration: `for x in &array` + impl<'a, 'b> IntoIterator for &'b Local<'a, $marker> { + type Item = $elem; + type IntoIter = TypedArrayIter<'a, 'b, $marker, $elem>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter() + } + } + + impl<'a, 'b> Iterator for TypedArrayIter<'a, 'b, $marker, $elem> { + type Item = $elem; + + #[inline] + fn next(&mut self) -> Option { + if self.index < self.len { + let value = unsafe { + ffi::$get_fn(self.array.isolate.as_ffi(), &self.array.handle, self.index) + }; + self.index += 1; + Some(value) + } else { + None + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let remaining = self.len - self.index; + (remaining, Some(remaining)) + } + } + + impl<'a, 'b> ExactSizeIterator for TypedArrayIter<'a, 'b, $marker, $elem> {} + + impl<'a, 'b> DoubleEndedIterator for TypedArrayIter<'a, 'b, $marker, $elem> { + #[inline] + fn next_back(&mut self) -> Option { + if self.index < self.len { + self.len -= 1; + let value = unsafe { + ffi::$get_fn(self.array.isolate.as_ffi(), &self.array.handle, self.len) + }; + Some(value) + } else { + None + } + } + } + + impl<'a, 'b> std::iter::FusedIterator for TypedArrayIter<'a, 'b, $marker, $elem> {} + + impl<'a> Iterator for TypedArrayIntoIter<'a, $marker, $elem> { + type Item = $elem; + + #[inline] + fn next(&mut self) -> Option { + if self.index < self.len { + let value = unsafe { + ffi::$get_fn(self.array.isolate.as_ffi(), &self.array.handle, self.index) + }; + self.index += 1; + Some(value) + } else { + None + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let remaining = self.len - self.index; + (remaining, Some(remaining)) + } + } + + impl<'a> ExactSizeIterator for TypedArrayIntoIter<'a, $marker, $elem> {} + + impl<'a> DoubleEndedIterator for TypedArrayIntoIter<'a, $marker, $elem> { + #[inline] + fn next_back(&mut self) -> Option { + if self.index < self.len { + self.len -= 1; + let value = unsafe { + ffi::$get_fn(self.array.isolate.as_ffi(), &self.array.handle, self.len) + }; + Some(value) + } else { + None + } + } + } + + impl<'a> std::iter::FusedIterator for TypedArrayIntoIter<'a, $marker, $elem> {} + }; +} + +impl_typed_array!(Uint8Array, u8, local_uint8_array_get); +impl_typed_array!(Uint16Array, u16, local_uint16_array_get); +impl_typed_array!(Uint32Array, u32, local_uint32_array_get); +impl_typed_array!(Int8Array, i8, local_int8_array_get); +impl_typed_array!(Int16Array, i16, local_int16_array_get); +impl_typed_array!(Int32Array, i32, local_int32_array_get); + // Object-specific implementations impl<'a> Local<'a, Object> { pub fn set(&mut self, lock: &mut Lock, key: &str, value: Local<'a, Value>) { @@ -418,6 +928,7 @@ impl<'a> Local<'a, Object> { impl<'a> From> for Local<'a, Object> { fn from(value: Local<'a, Value>) -> Self { + debug_assert!(value.is_object()); unsafe { Self::from_ffi(value.isolate, value.into_ffi()) } } } @@ -449,7 +960,7 @@ impl Global { &self.handle } - pub fn as_local<'a>(&self, lock: &mut Lock) -> Local<'a, FunctionTemplate> { + pub fn as_local<'a>(&self, lock: &mut Lock) -> Local<'a, T> { unsafe { Local::from_ffi( lock.isolate(), @@ -611,7 +1122,7 @@ impl<'a> FunctionCallbackInfo<'a> { } pub fn get(&self, index: usize) -> Local<'a, Value> { - debug_assert!(index <= self.len()); + debug_assert!(index <= self.len(), "index out of bounds"); unsafe { Local::from_ffi(self.isolate(), ffi::fci_get_arg(self.0, index)) } } diff --git a/src/rust/jsg/wrappable.rs b/src/rust/jsg/wrappable.rs index de2c2217eea..01f65f221a3 100644 --- a/src/rust/jsg/wrappable.rs +++ b/src/rust/jsg/wrappable.rs @@ -2,10 +2,14 @@ // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 +#![allow(clippy::allow_attributes)] + //! Traits for converting between Rust and JavaScript values. //! //! # Supported Types //! +//! ## Primitive Types +//! //! | Rust Type | JavaScript Type | //! |-----------|-----------------| //! | `()` | `undefined` | @@ -18,6 +22,33 @@ //! | `Result` | `T` or throws | //! | `NonCoercible` | `T` (strict type checking) | //! | `T: Struct` | `object` | +//! | `Vec` | `Array` | +//! | `&[T]` | `Array` (parameter only) | +//! +//! ## `TypedArray` Types +//! +//! These specialized `Vec` types map directly to JavaScript `TypedArray`s for +//! efficient binary data transfer: +//! +//! | Rust Type | JavaScript Type | +//! |-----------|-----------------| +//! | `Vec` | `Uint8Array` | +//! | `Vec` | `Uint16Array` | +//! | `Vec` | `Uint32Array` | +//! | `Vec` | `Int8Array` | +//! | `Vec` | `Int16Array` | +//! | `Vec` | `Int32Array` | +//! +//! ## Integer Parameter Types +//! +//! Integer types (`u8`, `u16`, `u32`, `i8`, `i16`, `i32`) can be used as method +//! parameters. JavaScript numbers are converted via truncation: +//! +//! - Values are truncated toward zero (e.g., `3.7` → `3`, `-2.9` → `-2`) +//! - Out-of-range values wrap (e.g., `256.0` → `0` for `u8`) +//! - `NaN` becomes `0` +//! +//! For strict validation, wrap parameters in `NonCoercible` or validate manually. use crate::Error; use crate::Lock; @@ -142,6 +173,54 @@ impl> FromJS for &T { } } +// Slice type - allows functions to accept &[T] parameters. +// JavaScript arrays are converted to Vec, then borrowed as &[T] by the macro. +impl Type for &[T] { + fn class_name() -> &'static str { + "Array" + } + + fn is_exact(value: &v8::Local) -> bool { + value.is_array() + } +} + +impl> FromJS for &[T] { + type ResultType = Vec; + + fn from_js(lock: &mut Lock, value: v8::Local) -> Result { + Vec::::from_js(lock, value) + } +} + +// Integer types - JavaScript numbers are IEEE 754 doubles (f64) +// +// Conversion behavior: +// - Values are truncated toward zero (e.g., 3.7 → 3, -2.9 → -2) +// - Values outside the target type's range wrap around (e.g., 256.0 → 0 for u8) +// - NaN becomes 0 +// - Infinity wraps to 0 for unsigned types, or type MIN/MAX for signed types +// +// This matches JavaScript's behavior for TypedArray element assignment. +// For strict validation, use `NonCoercible` or validate in your method. +macro_rules! impl_integer_from_js { + ($($type:ty),*) => { + $( + impl FromJS for $type { + type ResultType = Self; + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + fn from_js(lock: &mut Lock, value: v8::Local) -> Result { + let num = unsafe { v8::ffi::unwrap_number(lock.isolate().as_ffi(), value.into_ffi()) }; + Ok(num as $type) + } + } + )* + }; +} + +impl_integer_from_js!(u8, u16, u32, i8, i16, i32); + // ============================================================================= // Wrapper type implementations // ============================================================================= @@ -233,3 +312,182 @@ impl FromJS for Nullable { } } } + +// ============================================================================= +// Array type implementations (Vec) +// ============================================================================= + +impl Type for Vec { + fn class_name() -> &'static str { + "Array" + } + + fn is_exact(value: &v8::Local) -> bool { + value.is_array() + } +} + +impl ToJS for Vec { + fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> + where + 'b: 'a, + { + let mut array = v8::Local::::new(lock, self.len()); + for (i, item) in self.into_iter().enumerate() { + array.set(i, item.to_js(lock)); + } + array.into() + } +} + +impl> FromJS for Vec { + type ResultType = Self; + + fn from_js(lock: &mut Lock, value: v8::Local) -> Result { + if !value.is_array() { + return Err(Error::new_type_error(format!( + "Expected Array but got {}", + value.type_of() + ))); + } + + let globals = unsafe { value.as_array() }.iterate(); + let mut result = Self::with_capacity(globals.len()); + for global in globals { + let local = global.as_local(lock); + result.push(T::from_js(lock, local)?); + } + Ok(result) + } +} + +// ============================================================================= +// TypedArray marker type implementations +// ============================================================================= +// +// These implement `Type` for TypedArray marker structs (e.g., `v8::Uint8Array`). +// Marker types are used with `Local<'a, T>` handles for type-safe references. + +macro_rules! impl_typed_array_type { + ($marker:ident, $js_name:literal, $is_check:ident) => { + impl Type for v8::$marker { + fn class_name() -> &'static str { + $js_name + } + + fn is_exact(value: &v8::Local) -> bool { + value.$is_check() + } + } + }; +} + +impl_typed_array_type!(Uint8Array, "Uint8Array", is_uint8_array); +impl_typed_array_type!(Uint16Array, "Uint16Array", is_uint16_array); +impl_typed_array_type!(Uint32Array, "Uint32Array", is_uint32_array); +impl_typed_array_type!(Int8Array, "Int8Array", is_int8_array); +impl_typed_array_type!(Int16Array, "Int16Array", is_int16_array); +impl_typed_array_type!(Int32Array, "Int32Array", is_int32_array); + +// ============================================================================= +// Vec specialized implementations for TypedArrays +// ============================================================================= +// +// These implementations map Rust `Vec` types directly to JavaScript TypedArrays: +// - `Vec` <-> `Uint8Array` +// - `Vec` <-> `Uint16Array` +// - `Vec` <-> `Uint32Array` +// - `Vec` <-> `Int8Array` +// - `Vec` <-> `Int16Array` +// - `Vec` <-> `Int32Array` + +macro_rules! impl_vec_typed_array { + ($elem:ty, $js_name:literal, $is_check:ident, $new_fn:ident, $unwrap_fn:ident) => { + impl Type for Vec<$elem> { + fn class_name() -> &'static str { + $js_name + } + + fn is_exact(value: &v8::Local) -> bool { + value.$is_check() + } + } + + impl ToJS for Vec<$elem> { + fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> + where + 'b: 'a, + { + let isolate = lock.isolate(); + unsafe { + v8::Local::from_ffi( + isolate, + v8::ffi::$new_fn(isolate.as_ffi(), self.as_ptr(), self.len()), + ) + } + } + } + + impl FromJS for Vec<$elem> { + type ResultType = Self; + + fn from_js( + lock: &mut Lock, + value: v8::Local, + ) -> Result { + if !value.$is_check() { + return Err(Error::new_type_error(format!( + "Expected {} but got {}", + $js_name, + value.type_of() + ))); + } + + Ok(unsafe { v8::ffi::$unwrap_fn(lock.isolate().as_ffi(), value.into_ffi()) }) + } + } + }; +} + +impl_vec_typed_array!( + u8, + "Uint8Array", + is_uint8_array, + local_new_uint8_array, + unwrap_uint8_array +); +impl_vec_typed_array!( + u16, + "Uint16Array", + is_uint16_array, + local_new_uint16_array, + unwrap_uint16_array +); +impl_vec_typed_array!( + u32, + "Uint32Array", + is_uint32_array, + local_new_uint32_array, + unwrap_uint32_array +); +impl_vec_typed_array!( + i8, + "Int8Array", + is_int8_array, + local_new_int8_array, + unwrap_int8_array +); +impl_vec_typed_array!( + i16, + "Int16Array", + is_int16_array, + local_new_int16_array, + unwrap_int16_array +); +impl_vec_typed_array!( + i32, + "Int32Array", + is_int32_array, + local_new_int32_array, + unwrap_int32_array +);