From 62b0243fca756f2da7dcc42ec7519278b338d094 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sat, 15 Jun 2024 21:08:26 +0300 Subject: [PATCH] Expose construction capabilities with code for JsArrayBuffer (#1893) --- .../ArrayBufferTests.cs | 73 +++++++++++++++++++ .../ArrayBuffer/ArrayBufferConstructor.cs | 52 ++++++++++--- Jint/Native/JsArrayBuffer.cs | 54 ++++++++------ Jint/Native/JsSharedArrayBuffer.cs | 3 +- Jint/Native/JsTypedArray.cs | 5 +- .../SharedArrayBufferConstructor.cs | 19 +++-- .../TypedArray/TypedArrayConstructor.cs | 66 +++++++++-------- Jint/Runtime/Intrinsics.cs | 2 +- 8 files changed, 198 insertions(+), 76 deletions(-) create mode 100644 Jint.Tests.PublicInterface/ArrayBufferTests.cs diff --git a/Jint.Tests.PublicInterface/ArrayBufferTests.cs b/Jint.Tests.PublicInterface/ArrayBufferTests.cs new file mode 100644 index 0000000000..d3da8010e3 --- /dev/null +++ b/Jint.Tests.PublicInterface/ArrayBufferTests.cs @@ -0,0 +1,73 @@ +using Jint.Native; +using Jint.Runtime.Interop; + +namespace Jint.Tests.Runtime; + +public class ArrayBufferTests +{ + [Fact] + public void CanConvertByteArrayToArrayBuffer() + { + var engine = new Engine(o => o.AddObjectConverter(new BytesToArrayBufferConverter())); + + var bytes = new byte[] { 17 }; + engine.SetValue("buffer", bytes); + + engine.Evaluate("var a = new Uint8Array(buffer)"); + + var typedArray = (JsTypedArray) engine.GetValue("a"); + Assert.Equal((uint) 1, typedArray.Length); + Assert.Equal(17, typedArray[0]); + Assert.Equal(JsValue.Undefined, typedArray[1]); + + Assert.Equal(1, engine.Evaluate("a.length")); + Assert.Equal(17, engine.Evaluate("a[0]")); + Assert.Equal(JsValue.Undefined, engine.Evaluate("a[1]")); + + bytes[0] = 42; + Assert.Equal(42, engine.Evaluate("a[0]")); + } + + [Fact] + public void CanCreateArrayBufferAndTypedArrayUsingCode() + { + var engine = new Engine(); + + var jsArrayBuffer = engine.Intrinsics.ArrayBuffer.Construct(1); + var jsTypedArray = engine.Intrinsics.Uint8Array.Construct(jsArrayBuffer); + jsTypedArray[0] = 17; + + engine.SetValue("buffer", jsArrayBuffer); + engine.SetValue("a", jsTypedArray); + + var typedArray = (JsTypedArray) engine.GetValue("a"); + Assert.Equal((uint) 1, typedArray.Length); + Assert.Equal(17, typedArray[0]); + Assert.Equal(JsValue.Undefined, typedArray[1]); + + Assert.Equal(1, engine.Evaluate("a.length")); + Assert.Equal(17, engine.Evaluate("a[0]")); + Assert.Equal(JsValue.Undefined, engine.Evaluate("a[1]")); + } + + /// + /// Converts a byte array to an ArrayBuffer. + /// + private sealed class BytesToArrayBufferConverter : IObjectConverter + { + public bool TryConvert(Engine engine, object value, out JsValue result) + { + if (value is byte[] bytes) + { + var buffer = engine.Intrinsics.ArrayBuffer.Construct(bytes); + result = buffer; + return true; + } + + // TODO: provide similar implementation for Memory that will affect how ArrayBufferInstance works (offset) + + result = JsValue.Null; + return false; + } + } +} diff --git a/Jint/Native/ArrayBuffer/ArrayBufferConstructor.cs b/Jint/Native/ArrayBuffer/ArrayBufferConstructor.cs index b978800ff6..439cb9af9c 100644 --- a/Jint/Native/ArrayBuffer/ArrayBufferConstructor.cs +++ b/Jint/Native/ArrayBuffer/ArrayBufferConstructor.cs @@ -11,7 +11,7 @@ namespace Jint.Native.ArrayBuffer; /// /// https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-constructor /// -internal sealed class ArrayBufferConstructor : Constructor +public sealed class ArrayBufferConstructor : Constructor { private static readonly JsString _functionName = new("ArrayBuffer"); @@ -47,20 +47,19 @@ protected override void Initialize() } /// - /// https://tc39.es/ecma262/#sec-arraybuffer.isview + /// Constructs a new JsArrayBuffer instance and takes ownership of the given byte array and uses it as backing store. /// - private static JsValue IsView(JsValue thisObject, JsValue[] arguments) + public JsArrayBuffer Construct(byte[] data) { - var arg = arguments.At(0); - return arg is JsDataView or JsTypedArray; + return CreateJsArrayBuffer(this, data, byteLength: (ulong) data.Length, maxByteLength: null); } /// - /// https://tc39.es/ecma262/#sec-get-arraybuffer-@@species + /// Constructs a new JsArrayBuffer with given byte length and optional max byte length. /// - private static JsValue Species(JsValue thisObject, JsValue[] arguments) + public JsArrayBuffer Construct(ulong byteLength, uint? maxByteLength = null) { - return thisObject; + return AllocateArrayBuffer(this, byteLength, maxByteLength); } public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) @@ -78,6 +77,23 @@ public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) return AllocateArrayBuffer(newTarget, byteLength, requestedMaxByteLength); } + /// + /// https://tc39.es/ecma262/#sec-get-arraybuffer-@@species + /// + private static JsValue Species(JsValue thisObject, JsValue[] arguments) + { + return thisObject; + } + + /// + /// https://tc39.es/ecma262/#sec-arraybuffer.isview + /// + private static JsValue IsView(JsValue thisObject, JsValue[] arguments) + { + var arg = arguments.At(0); + return arg is JsDataView or JsTypedArray; + } + /// /// https://tc39.es/ecma262/#sec-allocatearraybuffer /// @@ -90,15 +106,27 @@ internal JsArrayBuffer AllocateArrayBuffer(JsValue constructor, ulong byteLength ExceptionHelper.ThrowRangeError(_realm); } + return CreateJsArrayBuffer(constructor, block: null, byteLength, maxByteLength); + } + + private JsArrayBuffer CreateJsArrayBuffer(JsValue constructor, byte[]? block, ulong byteLength, uint? maxByteLength) + { var obj = OrdinaryCreateFromConstructor( constructor, static intrinsics => intrinsics.ArrayBuffer.PrototypeObject, - static (engine, _, state) => new JsArrayBuffer(engine, state!.Item1), - new Tuple(maxByteLength)); + static (engine, _, state) => + { + var buffer = new JsArrayBuffer(engine, [], state.MaxByteLength) + { + _arrayBufferData = state.Block ?? (state.ByteLength > 0 ? JsArrayBuffer.CreateByteDataBlock(engine.Realm, state.ByteLength) : []), + }; - var block = byteLength > 0 ? JsArrayBuffer.CreateByteDataBlock(_realm, byteLength) : System.Array.Empty(); - obj._arrayBufferData = block; + return buffer; + }, + new ConstructState(block, byteLength, maxByteLength)); return obj; } + + private readonly record struct ConstructState(byte[]? Block, ulong ByteLength, uint? MaxByteLength); } diff --git a/Jint/Native/JsArrayBuffer.cs b/Jint/Native/JsArrayBuffer.cs index 5d3a6aa050..68ecf75fd0 100644 --- a/Jint/Native/JsArrayBuffer.cs +++ b/Jint/Native/JsArrayBuffer.cs @@ -20,6 +20,7 @@ public class JsArrayBuffer : ObjectInstance internal JsArrayBuffer( Engine engine, + byte[] data, uint? arrayBufferMaxByteLength = null) : base(engine) { if (arrayBufferMaxByteLength is > int.MaxValue) @@ -27,6 +28,7 @@ internal JsArrayBuffer( ExceptionHelper.ThrowRangeError(engine.Realm, "arrayBufferMaxByteLength cannot be larger than int32.MaxValue"); } + _arrayBufferData = data; _arrayBufferMaxByteLength = (int?) arrayBufferMaxByteLength; } @@ -104,6 +106,11 @@ internal TypedArrayValue GetValueFromBuffer( /// internal TypedArrayValue RawBytesToNumeric(TypedArrayElementType type, int byteIndex, bool isLittleEndian) { + if (type is TypedArrayElementType.Uint8 or TypedArrayElementType.Uint8C) + { + return new TypedArrayValue(Types.Number, _arrayBufferData![byteIndex], default); + } + var elementSize = type.GetElementSize(); var rawBytes = _arrayBufferData!; @@ -155,25 +162,19 @@ internal TypedArrayValue RawBytesToNumeric(TypedArrayElementType type, int byteI TypedArrayValue? arrayValue = type switch { - TypedArrayElementType.Int8 => ((sbyte) rawBytes[byteIndex]), - TypedArrayElementType.Uint8 => (rawBytes[byteIndex]), - TypedArrayElementType.Uint8C =>(rawBytes[byteIndex]), - TypedArrayElementType.Int16 => (isLittleEndian - ? (short) (rawBytes[byteIndex] | (rawBytes[byteIndex + 1] << 8)) - : (short) (rawBytes[byteIndex + 1] | (rawBytes[byteIndex] << 8)) - ), - TypedArrayElementType.Uint16 => (isLittleEndian - ? (ushort) (rawBytes[byteIndex] | (rawBytes[byteIndex + 1] << 8)) - : (ushort) (rawBytes[byteIndex + 1] | (rawBytes[byteIndex] << 8)) - ), - TypedArrayElementType.Int32 => (isLittleEndian - ? rawBytes[byteIndex] | (rawBytes[byteIndex + 1] << 8) | (rawBytes[byteIndex + 2] << 16) | (rawBytes[byteIndex + 3] << 24) - : rawBytes[byteIndex + 3] | (rawBytes[byteIndex + 2] << 8) | (rawBytes[byteIndex + 1] << 16) | (rawBytes[byteIndex + 0] << 24) - ), - TypedArrayElementType.Uint32 => (isLittleEndian - ? (uint) (rawBytes[byteIndex] | (rawBytes[byteIndex + 1] << 8) | (rawBytes[byteIndex + 2] << 16) | (rawBytes[byteIndex + 3] << 24)) - : (uint) (rawBytes[byteIndex + 3] | (rawBytes[byteIndex + 2] << 8) | (rawBytes[byteIndex + 1] << 16) | (rawBytes[byteIndex] << 24)) - ), + TypedArrayElementType.Int8 => (sbyte) rawBytes[byteIndex], + TypedArrayElementType.Int16 => isLittleEndian + ? (short) (rawBytes[byteIndex] | (rawBytes[byteIndex + 1] << 8)) + : (short) (rawBytes[byteIndex + 1] | (rawBytes[byteIndex] << 8)), + TypedArrayElementType.Uint16 => isLittleEndian + ? (ushort) (rawBytes[byteIndex] | (rawBytes[byteIndex + 1] << 8)) + : (ushort) (rawBytes[byteIndex + 1] | (rawBytes[byteIndex] << 8)), + TypedArrayElementType.Int32 => isLittleEndian + ? rawBytes[byteIndex] | (rawBytes[byteIndex + 1] << 8) | (rawBytes[byteIndex + 2] << 16) | (rawBytes[byteIndex + 3] << 24) + : rawBytes[byteIndex + 3] | (rawBytes[byteIndex + 2] << 8) | (rawBytes[byteIndex + 1] << 16) | (rawBytes[byteIndex + 0] << 24), + TypedArrayElementType.Uint32 => isLittleEndian + ? (uint) (rawBytes[byteIndex] | (rawBytes[byteIndex + 1] << 8) | (rawBytes[byteIndex + 2] << 16) | (rawBytes[byteIndex + 3] << 24)) + : (uint) (rawBytes[byteIndex + 3] | (rawBytes[byteIndex + 2] << 8) | (rawBytes[byteIndex + 1] << 16) | (rawBytes[byteIndex] << 24)), _ => null }; @@ -196,10 +197,21 @@ internal void SetValueInBuffer( ArrayBufferOrder order, bool? isLittleEndian = null) { + if (type is TypedArrayElementType.Uint8) + { + var doubleValue = value.DoubleValue; + var intValue = double.IsNaN(doubleValue) || doubleValue == 0 || double.IsInfinity(doubleValue) + ? 0 + : (long) doubleValue; + + _arrayBufferData![byteIndex] = (byte) intValue; + return; + } + var block = _arrayBufferData!; // If isLittleEndian is not present, set isLittleEndian to the value of the [[LittleEndian]] field of the surrounding agent's Agent Record. var rawBytes = NumericToRawBytes(type, value, isLittleEndian ?? BitConverter.IsLittleEndian); - System.Array.Copy(rawBytes, 0, block, byteIndex, type.GetElementSize()); + System.Array.Copy(rawBytes, 0, block, byteIndex, type.GetElementSize()); } private byte[] NumericToRawBytes(TypedArrayElementType type, TypedArrayValue value, bool isLittleEndian) @@ -241,7 +253,7 @@ private byte[] NumericToRawBytes(TypedArrayElementType type, TypedArrayValue val rawBytes[0] = (byte) intValue; break; case TypedArrayElementType.Uint8C: - rawBytes[0] = (byte) TypeConverter.ToUint8Clamp(value.DoubleValue); + rawBytes[0] = TypeConverter.ToUint8Clamp(value.DoubleValue); break; case TypedArrayElementType.Int16: #if !NETSTANDARD2_1 diff --git a/Jint/Native/JsSharedArrayBuffer.cs b/Jint/Native/JsSharedArrayBuffer.cs index 418aa26512..787557a578 100644 --- a/Jint/Native/JsSharedArrayBuffer.cs +++ b/Jint/Native/JsSharedArrayBuffer.cs @@ -11,8 +11,9 @@ internal sealed class JsSharedArrayBuffer : JsArrayBuffer internal JsSharedArrayBuffer( Engine engine, + byte[] data, uint? arrayBufferMaxByteLength, - uint arrayBufferByteLengthData) : base(engine, arrayBufferMaxByteLength) + uint arrayBufferByteLengthData) : base(engine, data, arrayBufferMaxByteLength) { if (arrayBufferByteLengthData > int.MaxValue) { diff --git a/Jint/Native/JsTypedArray.cs b/Jint/Native/JsTypedArray.cs index 4dabe73d1d..ceb46ff93b 100644 --- a/Jint/Native/JsTypedArray.cs +++ b/Jint/Native/JsTypedArray.cs @@ -28,10 +28,7 @@ internal JsTypedArray( uint length) : base(engine) { _intrinsics = intrinsics; - _viewedArrayBuffer = new JsArrayBuffer(engine) - { - _arrayBufferData = System.Array.Empty() - }; + _viewedArrayBuffer = new JsArrayBuffer(engine, []); _arrayElementType = type; _contentType = type != TypedArrayElementType.BigInt64 && type != TypedArrayElementType.BigUint64 diff --git a/Jint/Native/SharedArrayBuffer/SharedArrayBufferConstructor.cs b/Jint/Native/SharedArrayBuffer/SharedArrayBufferConstructor.cs index b075809481..9885ce4182 100644 --- a/Jint/Native/SharedArrayBuffer/SharedArrayBufferConstructor.cs +++ b/Jint/Native/SharedArrayBuffer/SharedArrayBufferConstructor.cs @@ -94,16 +94,23 @@ private JsSharedArrayBuffer AllocateSharedArrayBuffer(JsValue constructor, uint ExceptionHelper.ThrowRangeError(_realm); } + var allocLength = maxByteLength.GetValueOrDefault(byteLength); + var obj = OrdinaryCreateFromConstructor( constructor, static intrinsics => intrinsics.SharedArrayBuffer.PrototypeObject, - static (engine, _, state) => new JsSharedArrayBuffer(engine, state!.Item1, state.Item2), - new Tuple(maxByteLength, byteLength)); - - var allocLength = maxByteLength.GetValueOrDefault(byteLength); - var block = JsSharedArrayBuffer.CreateSharedByteDataBlock(_realm, allocLength); - obj._arrayBufferData = block; + static (engine, _, state) => + { + var buffer = new JsSharedArrayBuffer(engine, [], state.MaxByteLength, state.ArrayBufferByteLengthData) + { + _arrayBufferData = state.Block ?? (state.ByteLength > 0 ? JsSharedArrayBuffer.CreateSharedByteDataBlock(engine.Realm, state.ByteLength) : []), + }; + return buffer; + }, + new ConstructState(Block: null, allocLength, maxByteLength, byteLength)); return obj; } + + private readonly record struct ConstructState(byte[]? Block, uint ByteLength, uint? MaxByteLength, uint ArrayBufferByteLengthData); } diff --git a/Jint/Native/TypedArray/TypedArrayConstructor.cs b/Jint/Native/TypedArray/TypedArrayConstructor.cs index 0a0eafbd5b..0ebe545c74 100644 --- a/Jint/Native/TypedArray/TypedArrayConstructor.cs +++ b/Jint/Native/TypedArray/TypedArrayConstructor.cs @@ -42,6 +42,13 @@ protected override void Initialize() SetProperties(properties); } + public JsTypedArray Construct(JsArrayBuffer buffer, int? byteOffset = null, int? length = null) + { + var o = AllocateTypedArray(this); + InitializeTypedArrayFromArrayBuffer(o, buffer, byteOffset, length); + return o; + } + public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) { if (newTarget.IsUndefined()) @@ -49,40 +56,25 @@ public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) ExceptionHelper.ThrowTypeError(_realm); } - Func proto = _arrayElementType switch - { - TypedArrayElementType.Float32 => static intrinsics => intrinsics.Float32Array.PrototypeObject, - TypedArrayElementType.Int8 => static intrinsics => intrinsics.Int8Array.PrototypeObject, - TypedArrayElementType.Int16 => static intrinsics => intrinsics.Int16Array.PrototypeObject, - TypedArrayElementType.Int32 => static intrinsics => intrinsics.Int32Array.PrototypeObject, - TypedArrayElementType.BigInt64 => static intrinsics => intrinsics.BigInt64Array.PrototypeObject, - TypedArrayElementType.Float64 => static intrinsics => intrinsics.Float64Array.PrototypeObject, - TypedArrayElementType.Uint8 => static intrinsics => intrinsics.Uint8Array.PrototypeObject, - TypedArrayElementType.Uint8C => static intrinsics => intrinsics.Uint8ClampedArray.PrototypeObject, - TypedArrayElementType.Uint16 => static intrinsics => intrinsics.Uint16Array.PrototypeObject, - TypedArrayElementType.Uint32 => static intrinsics => intrinsics.Uint32Array.PrototypeObject, - TypedArrayElementType.BigUint64 => static intrinsics => intrinsics.BigUint64Array.PrototypeObject, - _ => null! - }; var numberOfArgs = arguments.Length; if (numberOfArgs == 0) { - return AllocateTypedArray(newTarget, proto, 0); + return AllocateTypedArray(newTarget, 0); } var firstArgument = arguments[0]; if (firstArgument.IsObject()) { - var o = AllocateTypedArray(newTarget, proto); + var o = AllocateTypedArray(newTarget); if (firstArgument is JsTypedArray typedArrayInstance) { InitializeTypedArrayFromTypedArray(o, typedArrayInstance); } else if (firstArgument is JsArrayBuffer arrayBuffer) { - var byteOffset = numberOfArgs > 1 ? arguments[1] : Undefined; - var length = numberOfArgs > 2 ? arguments[2] : Undefined; + int? byteOffset = !arguments.At(1).IsUndefined() ? (int) TypeConverter.ToIndex(_realm, arguments[1]) : null; + int? length = !arguments.At(2).IsUndefined() ? (int) TypeConverter.ToIndex(_realm, arguments[2]) : null; InitializeTypedArrayFromArrayBuffer(o, arrayBuffer, byteOffset, length); } else @@ -103,7 +95,7 @@ public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) } var elementLength = TypeConverter.ToIndex(_realm, firstArgument); - return AllocateTypedArray(newTarget, proto, elementLength); + return AllocateTypedArray(newTarget, elementLength); } /// @@ -184,29 +176,25 @@ private void InitializeTypedArrayFromTypedArray(JsTypedArray o, JsTypedArray src private void InitializeTypedArrayFromArrayBuffer( JsTypedArray o, JsArrayBuffer buffer, - JsValue byteOffset, - JsValue length) + int? byteOffset, + int? length) { var elementSize = o._arrayElementType.GetElementSize(); - var offset = (int) TypeConverter.ToIndex(_realm, byteOffset); + var offset = byteOffset ?? 0; if (offset % elementSize != 0) { ExceptionHelper.ThrowRangeError(_realm, "Invalid offset"); } int newByteLength; - var newLength = 0; - if (!length.IsUndefined()) - { - newLength = (int) TypeConverter.ToIndex(_realm, length); - } + var newLength = length ?? 0; var bufferIsFixedLength = buffer.IsFixedLengthArrayBuffer; buffer.AssertNotDetached(); var bufferByteLength = IntrinsicTypedArrayPrototype.ArrayBufferByteLength(buffer, ArrayBufferOrder.SeqCst); - if (length.IsUndefined() && !bufferIsFixedLength) + if (length == null && !bufferIsFixedLength) { if (offset > bufferByteLength) { @@ -218,7 +206,7 @@ private void InitializeTypedArrayFromArrayBuffer( } else { - if (length.IsUndefined()) + if (length == null) { if (bufferByteLength % elementSize != 0) { @@ -275,8 +263,24 @@ private static void InitializeTypedArrayFromArrayLike(JsTypedArray o, ObjectInst /// /// https://tc39.es/ecma262/#sec-allocatetypedarray /// - private JsTypedArray AllocateTypedArray(JsValue newTarget, Func defaultProto, uint length = 0) + private JsTypedArray AllocateTypedArray(JsValue newTarget, uint length = 0) { + Func defaultProto = _arrayElementType switch + { + TypedArrayElementType.Float32 => static intrinsics => intrinsics.Float32Array.PrototypeObject, + TypedArrayElementType.Float64 => static intrinsics => intrinsics.Float64Array.PrototypeObject, + TypedArrayElementType.Int8 => static intrinsics => intrinsics.Int8Array.PrototypeObject, + TypedArrayElementType.Int16 => static intrinsics => intrinsics.Int16Array.PrototypeObject, + TypedArrayElementType.Int32 => static intrinsics => intrinsics.Int32Array.PrototypeObject, + TypedArrayElementType.BigInt64 => static intrinsics => intrinsics.BigInt64Array.PrototypeObject, + TypedArrayElementType.Uint8 => static intrinsics => intrinsics.Uint8Array.PrototypeObject, + TypedArrayElementType.Uint8C => static intrinsics => intrinsics.Uint8ClampedArray.PrototypeObject, + TypedArrayElementType.Uint16 => static intrinsics => intrinsics.Uint16Array.PrototypeObject, + TypedArrayElementType.Uint32 => static intrinsics => intrinsics.Uint32Array.PrototypeObject, + TypedArrayElementType.BigUint64 => static intrinsics => intrinsics.BigUint64Array.PrototypeObject, + _ => null! + }; + var proto = GetPrototypeFromConstructor(newTarget, defaultProto); var realm = GetFunctionRealm(newTarget); var obj = new JsTypedArray(_engine, realm.Intrinsics, _arrayElementType, length) diff --git a/Jint/Runtime/Intrinsics.cs b/Jint/Runtime/Intrinsics.cs index 6dbb9f67e9..5c0a991579 100644 --- a/Jint/Runtime/Intrinsics.cs +++ b/Jint/Runtime/Intrinsics.cs @@ -140,7 +140,7 @@ internal Intrinsics(Engine engine, Realm realm) internal DataViewConstructor DataView => _dataView ??= new DataViewConstructor(_engine, _realm, Function.PrototypeObject, Object.PrototypeObject); - internal ArrayBufferConstructor ArrayBuffer => + public ArrayBufferConstructor ArrayBuffer => _arrayBufferConstructor ??= new ArrayBufferConstructor(_engine, _realm, Function.PrototypeObject, Object.PrototypeObject); internal SharedArrayBufferConstructor SharedArrayBuffer =>