Skip to content

Commit

Permalink
Expose construction capabilities with code for JsArrayBuffer (#1893)
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma committed Jun 15, 2024
1 parent 2de58cd commit 62b0243
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 76 deletions.
73 changes: 73 additions & 0 deletions Jint.Tests.PublicInterface/ArrayBufferTests.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
52 changes: 40 additions & 12 deletions Jint/Native/ArrayBuffer/ArrayBufferConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -47,20 +47,19 @@ protected override void Initialize()
}

/// <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)
Expand All @@ -78,6 +77,23 @@ public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget)
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>
Expand All @@ -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<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);
}
54 changes: 33 additions & 21 deletions Jint/Native/JsArrayBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ public class JsArrayBuffer : ObjectInstance

internal JsArrayBuffer(
Engine engine,
byte[] data,
uint? arrayBufferMaxByteLength = null) : base(engine)
{
if (arrayBufferMaxByteLength is > int.MaxValue)
{
ExceptionHelper.ThrowRangeError(engine.Realm, "arrayBufferMaxByteLength cannot be larger than int32.MaxValue");
}

_arrayBufferData = data;
_arrayBufferMaxByteLength = (int?) arrayBufferMaxByteLength;
}

Expand Down Expand Up @@ -104,6 +106,11 @@ internal void DetachArrayBuffer(JsValue? key = null)
/// </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!;

Expand Down Expand Up @@ -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
};

Expand All @@ -196,10 +197,21 @@ internal TypedArrayValue RawBytesToNumeric(TypedArrayElementType type, int byteI
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)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Jint/Native/JsSharedArrayBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
5 changes: 1 addition & 4 deletions Jint/Native/JsTypedArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ public sealed class JsTypedArray : ObjectInstance
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
Expand Down
19 changes: 13 additions & 6 deletions Jint/Native/SharedArrayBuffer/SharedArrayBufferConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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);
}
Loading

0 comments on commit 62b0243

Please sign in to comment.