Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose construction capabilities with code for JsArrayBuffer #1893

Merged
merged 1 commit into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 TypedArrayValue GetValueFromBuffer(
/// </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 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)
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 @@ internal JsTypedArray(
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