2
0
Эх сурвалжийг харах

Expose construction capabilities with code for JsArrayBuffer (#1893)

Marko Lahma 1 жил өмнө
parent
commit
62b0243fca

+ 73 - 0
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]"));
+    }
+
+    /// <summary>
+    /// Converts a byte array to an ArrayBuffer.
+    /// </summary>
+    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<byte> that will affect how ArrayBufferInstance works (offset)
+
+            result = JsValue.Null;
+            return false;
+        }
+    }
+}

+ 40 - 12
Jint/Native/ArrayBuffer/ArrayBufferConstructor.cs

@@ -11,7 +11,7 @@ namespace Jint.Native.ArrayBuffer;
 /// <summary>
 /// https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-constructor
 /// </summary>
-internal sealed class ArrayBufferConstructor : Constructor
+public sealed class ArrayBufferConstructor : Constructor
 {
     private static readonly JsString _functionName = new("ArrayBuffer");
 
@@ -47,20 +47,19 @@ internal sealed class ArrayBufferConstructor : Constructor
     }
 
     /// <summary>
-    /// 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.
     /// </summary>
-    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);
     }
 
     /// <summary>
-    /// https://tc39.es/ecma262/#sec-get-arraybuffer-@@species
+    /// Constructs a new JsArrayBuffer with given byte length and optional max byte length.
     /// </summary>
-    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 @@ internal sealed class ArrayBufferConstructor : Constructor
         return AllocateArrayBuffer(newTarget, byteLength, requestedMaxByteLength);
     }
 
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-get-arraybuffer-@@species
+    /// </summary>
+    private static JsValue Species(JsValue thisObject, JsValue[] arguments)
+    {
+        return thisObject;
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-arraybuffer.isview
+    /// </summary>
+    private static JsValue IsView(JsValue thisObject, JsValue[] arguments)
+    {
+        var arg = arguments.At(0);
+        return arg is JsDataView or JsTypedArray;
+    }
+
     /// <summary>
     /// https://tc39.es/ecma262/#sec-allocatearraybuffer
     /// </summary>
@@ -90,15 +106,27 @@ internal sealed class ArrayBufferConstructor : Constructor
             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<uint?>(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<byte>();
-        obj._arrayBufferData = block;
+                return buffer;
+            },
+            new ConstructState(block, byteLength, maxByteLength));
 
         return obj;
     }
+
+    private readonly record struct ConstructState(byte[]? Block, ulong ByteLength, uint? MaxByteLength);
 }

+ 33 - 21
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 @@ public class JsArrayBuffer : ObjectInstance
             ExceptionHelper.ThrowRangeError(engine.Realm, "arrayBufferMaxByteLength cannot be larger than int32.MaxValue");
         }
 
+        _arrayBufferData = data;
         _arrayBufferMaxByteLength = (int?) arrayBufferMaxByteLength;
     }
 
@@ -104,6 +106,11 @@ public class JsArrayBuffer : ObjectInstance
     /// </summary>
     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 @@ public class JsArrayBuffer : ObjectInstance
 
         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 @@ public class JsArrayBuffer : ObjectInstance
         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 @@ public class JsArrayBuffer : ObjectInstance
                     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

+ 2 - 1
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)
         {

+ 1 - 4
Jint/Native/JsTypedArray.cs

@@ -28,10 +28,7 @@ namespace Jint.Native
             uint length) : base(engine)
         {
             _intrinsics = intrinsics;
-            _viewedArrayBuffer = new JsArrayBuffer(engine)
-            {
-                _arrayBufferData = System.Array.Empty<byte>()
-            };
+            _viewedArrayBuffer = new JsArrayBuffer(engine, []);
 
             _arrayElementType = type;
             _contentType = type != TypedArrayElementType.BigInt64 && type != TypedArrayElementType.BigUint64

+ 13 - 6
Jint/Native/SharedArrayBuffer/SharedArrayBufferConstructor.cs

@@ -94,16 +94,23 @@ internal sealed class SharedArrayBufferConstructor : Constructor
             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<uint?, uint>(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);
 }

+ 35 - 31
Jint/Native/TypedArray/TypedArrayConstructor.cs

@@ -42,6 +42,13 @@ namespace Jint.Native.TypedArray
             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 @@ namespace Jint.Native.TypedArray
                 ExceptionHelper.ThrowTypeError(_realm);
             }
 
-            Func<Intrinsics, ObjectInstance> 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 @@ namespace Jint.Native.TypedArray
             }
 
             var elementLength = TypeConverter.ToIndex(_realm, firstArgument);
-            return AllocateTypedArray(newTarget, proto, elementLength);
+            return AllocateTypedArray(newTarget, elementLength);
         }
 
         /// <summary>
@@ -184,29 +176,25 @@ namespace Jint.Native.TypedArray
         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 @@ namespace Jint.Native.TypedArray
             }
             else
             {
-                if (length.IsUndefined())
+                if (length == null)
                 {
                     if (bufferByteLength % elementSize != 0)
                     {
@@ -275,8 +263,24 @@ namespace Jint.Native.TypedArray
         /// <summary>
         /// https://tc39.es/ecma262/#sec-allocatetypedarray
         /// </summary>
-        private JsTypedArray AllocateTypedArray(JsValue newTarget, Func<Intrinsics, ObjectInstance> defaultProto, uint length = 0)
+        private JsTypedArray AllocateTypedArray(JsValue newTarget, uint length = 0)
         {
+            Func<Intrinsics, ObjectInstance> 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)

+ 1 - 1
Jint/Runtime/Intrinsics.cs

@@ -140,7 +140,7 @@ namespace Jint.Runtime
         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 =>