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 #1890

Merged
merged 2 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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);
}
2 changes: 2 additions & 0 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
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);
}
68 changes: 36 additions & 32 deletions Jint/Native/TypedArray/TypedArrayConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,48 +42,39 @@ 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())
{
ExceptionHelper.ThrowTypeError(_realm);
}

Func<Intrinsics, ObjectInstance> proto = _arrayElementType switch
{
TypedArrayElementType.Float16 => static intrinsics => intrinsics.Float16Array.PrototypeObject,
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 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
Expand All @@ -104,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);
}

/// <summary>
Expand Down Expand Up @@ -185,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)
{
Expand All @@ -219,7 +206,7 @@ private void InitializeTypedArrayFromArrayBuffer(
}
else
{
if (length.IsUndefined())
if (length == null)
{
if (bufferByteLength % elementSize != 0)
{
Expand Down Expand Up @@ -276,8 +263,25 @@ private static void InitializeTypedArrayFromArrayLike(JsTypedArray o, ObjectInst
/// <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.Float16 => static intrinsics => intrinsics.Float16Array.PrototypeObject,
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)
Expand Down
2 changes: 1 addition & 1 deletion Jint/Runtime/Intrinsics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,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 =>
Expand Down
Loading