mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-21 12:05:15 +00:00
LibJS: Use ArrayBuffer for typed array data
This is how the spec describes it, and it allows sharing data between multiple typed arrays. Typed arrays now support constructing from an existing ArrayBuffer, and has been prepared for constructing from another typed array or iterator as well.
This commit is contained in:
parent
32571dfa53
commit
cc5be96724
Notes:
sideshowbarker
2024-07-19 01:05:01 +09:00
Author: https://github.com/linusg Commit: https://github.com/SerenityOS/serenity/commit/cc5be967242 Pull-request: https://github.com/SerenityOS/serenity/pull/4307
5 changed files with 235 additions and 81 deletions
|
@ -156,6 +156,10 @@
|
|||
M(ThisHasNotBeenInitialized, "|this| has not been initialized") \
|
||||
M(ThisIsAlreadyInitialized, "|this| is already initialized") \
|
||||
M(ToObjectNullOrUndef, "ToObject on null or undefined") \
|
||||
M(TypedArrayInvalidBufferLength, "Invalid buffer length for {}: must be a multiple of {}, got {}") \
|
||||
M(TypedArrayInvalidByteOffset, "Invalid byte offset for {}: must be a multiple of {}, got {}") \
|
||||
M(TypedArrayOutOfRangeByteOffset, "Typed array byte offset {} is out of range for buffer with length {}") \
|
||||
M(TypedArrayOutOfRangeByteOffsetOrLength, "Typed array range {}:{} is out of range for buffer with length {}") \
|
||||
M(UnknownIdentifier, "'{}' is not defined") \
|
||||
/* LibWeb bindings */ \
|
||||
M(NotAByteString, "Argument to {}() must be a byte string") \
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2020, Linus Groh <mail@linusgroh.de>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
|
@ -24,68 +25,133 @@
|
|||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <LibJS/Runtime/ArrayBuffer.h>
|
||||
#include <LibJS/Runtime/GlobalObject.h>
|
||||
#include <LibJS/Runtime/TypedArray.h>
|
||||
#include <LibJS/Runtime/TypedArrayConstructor.h>
|
||||
|
||||
namespace JS {
|
||||
|
||||
#define JS_DEFINE_TYPED_ARRAY(ClassName, snake_name, PrototypeName, ConstructorName, Type) \
|
||||
ClassName* ClassName::create(GlobalObject& global_object, u32 length) \
|
||||
{ \
|
||||
return global_object.heap().allocate<ClassName>(global_object, length, *global_object.snake_name##_prototype()); \
|
||||
} \
|
||||
\
|
||||
ClassName::ClassName(u32 length, Object& prototype) \
|
||||
: TypedArray(length, prototype) \
|
||||
{ \
|
||||
} \
|
||||
ClassName::~ClassName() { } \
|
||||
\
|
||||
PrototypeName::PrototypeName(GlobalObject& global_object) \
|
||||
: Object(*global_object.typed_array_prototype()) \
|
||||
{ \
|
||||
} \
|
||||
PrototypeName::~PrototypeName() { } \
|
||||
\
|
||||
ConstructorName::ConstructorName(GlobalObject& global_object) \
|
||||
: TypedArrayConstructor(vm().names.ClassName, *global_object.typed_array_constructor()) \
|
||||
{ \
|
||||
} \
|
||||
ConstructorName::~ConstructorName() { } \
|
||||
void ConstructorName::initialize(GlobalObject& global_object) \
|
||||
{ \
|
||||
auto& vm = this->vm(); \
|
||||
NativeFunction::initialize(global_object); \
|
||||
define_property(vm.names.prototype, global_object.snake_name##_prototype(), 0); \
|
||||
define_property(vm.names.length, Value(1), Attribute::Configurable); \
|
||||
define_property(vm.names.BYTES_PER_ELEMENT, Value((i32)sizeof(Type)), 0); \
|
||||
} \
|
||||
Value ConstructorName::call() \
|
||||
{ \
|
||||
auto& vm = this->vm(); \
|
||||
vm.throw_exception<TypeError>(global_object(), ErrorType::ConstructorWithoutNew, vm.names.ClassName); \
|
||||
return {}; \
|
||||
} \
|
||||
Value ConstructorName::construct(Function&) \
|
||||
{ \
|
||||
auto& vm = this->vm(); \
|
||||
if (vm.argument_count() == 0) \
|
||||
return ClassName::create(global_object(), 0); \
|
||||
\
|
||||
if (vm.argument(0).is_object()) { \
|
||||
/* FIXME: Initialize from TypedArray, ArrayBuffer, Iterable or Array-like object */ \
|
||||
TODO(); \
|
||||
} \
|
||||
auto array_length = vm.argument(0).to_index(global_object()); \
|
||||
if (vm.exception()) { \
|
||||
/* Re-throw more specific RangeError */ \
|
||||
vm.clear_exception(); \
|
||||
vm.throw_exception<RangeError>(global_object(), ErrorType::InvalidLength, "typed array"); \
|
||||
return {}; \
|
||||
} \
|
||||
auto* array = ClassName::create(global_object(), array_length); \
|
||||
return array; \
|
||||
static void initialize_typed_array_from_array_buffer(GlobalObject& global_object, TypedArrayBase& typed_array, ArrayBuffer& array_buffer, Value byte_offset, Value length)
|
||||
{
|
||||
// 22.2.5.1.3 InitializeTypedArrayFromArrayBuffer, https://tc39.es/ecma262/#sec-initializetypedarrayfromarraybuffer
|
||||
|
||||
auto& vm = global_object.vm();
|
||||
auto element_size = typed_array.element_size();
|
||||
auto offset = byte_offset.to_index(global_object);
|
||||
if (vm.exception())
|
||||
return;
|
||||
if (offset % element_size != 0) {
|
||||
vm.throw_exception<RangeError>(global_object, ErrorType::TypedArrayInvalidByteOffset, typed_array.class_name(), element_size, offset);
|
||||
return;
|
||||
}
|
||||
size_t new_length { 0 };
|
||||
if (!length.is_undefined()) {
|
||||
new_length = length.to_index(global_object);
|
||||
if (vm.exception())
|
||||
return;
|
||||
}
|
||||
// FIXME: 8. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
|
||||
auto buffer_byte_length = array_buffer.byte_length();
|
||||
size_t new_byte_length;
|
||||
if (length.is_undefined()) {
|
||||
if (buffer_byte_length % element_size != 0) {
|
||||
vm.throw_exception<RangeError>(global_object, ErrorType::TypedArrayInvalidBufferLength, typed_array.class_name(), element_size, buffer_byte_length);
|
||||
return;
|
||||
}
|
||||
if (offset > buffer_byte_length) {
|
||||
vm.throw_exception<RangeError>(global_object, ErrorType::TypedArrayOutOfRangeByteOffset, offset, buffer_byte_length);
|
||||
return;
|
||||
}
|
||||
new_byte_length = buffer_byte_length - offset;
|
||||
} else {
|
||||
new_byte_length = new_length * element_size;
|
||||
if (offset + new_byte_length > buffer_byte_length) {
|
||||
vm.throw_exception<RangeError>(global_object, ErrorType::TypedArrayOutOfRangeByteOffsetOrLength, offset, offset + new_byte_length, buffer_byte_length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
typed_array.set_viewed_array_buffer(&array_buffer);
|
||||
typed_array.set_byte_length(new_byte_length);
|
||||
typed_array.set_byte_offset(offset);
|
||||
typed_array.set_array_length(new_byte_length / element_size);
|
||||
}
|
||||
|
||||
void TypedArrayBase::visit_edges(Visitor& visitor)
|
||||
{
|
||||
Object::visit_edges(visitor);
|
||||
visitor.visit(m_viewed_array_buffer);
|
||||
}
|
||||
|
||||
#define JS_DEFINE_TYPED_ARRAY(ClassName, snake_name, PrototypeName, ConstructorName, Type) \
|
||||
ClassName* ClassName::create(GlobalObject& global_object, u32 length) \
|
||||
{ \
|
||||
return global_object.heap().allocate<ClassName>(global_object, length, *global_object.snake_name##_prototype()); \
|
||||
} \
|
||||
\
|
||||
ClassName::ClassName(u32 length, Object& prototype) \
|
||||
: TypedArray(length, prototype) \
|
||||
{ \
|
||||
} \
|
||||
ClassName::~ClassName() { } \
|
||||
\
|
||||
PrototypeName::PrototypeName(GlobalObject& global_object) \
|
||||
: Object(*global_object.typed_array_prototype()) \
|
||||
{ \
|
||||
} \
|
||||
PrototypeName::~PrototypeName() { } \
|
||||
\
|
||||
ConstructorName::ConstructorName(GlobalObject& global_object) \
|
||||
: TypedArrayConstructor(vm().names.ClassName, *global_object.typed_array_constructor()) \
|
||||
{ \
|
||||
} \
|
||||
ConstructorName::~ConstructorName() { } \
|
||||
void ConstructorName::initialize(GlobalObject& global_object) \
|
||||
{ \
|
||||
auto& vm = this->vm(); \
|
||||
NativeFunction::initialize(global_object); \
|
||||
define_property(vm.names.prototype, global_object.snake_name##_prototype(), 0); \
|
||||
define_property(vm.names.length, Value(1), Attribute::Configurable); \
|
||||
define_property(vm.names.BYTES_PER_ELEMENT, Value((i32)sizeof(Type)), 0); \
|
||||
} \
|
||||
Value ConstructorName::call() \
|
||||
{ \
|
||||
auto& vm = this->vm(); \
|
||||
vm.throw_exception<TypeError>(global_object(), ErrorType::ConstructorWithoutNew, vm.names.ClassName); \
|
||||
return {}; \
|
||||
} \
|
||||
Value ConstructorName::construct(Function&) \
|
||||
{ \
|
||||
auto& vm = this->vm(); \
|
||||
if (vm.argument_count() == 0) \
|
||||
return ClassName::create(global_object(), 0); \
|
||||
\
|
||||
auto first_argument = vm.argument(0); \
|
||||
if (first_argument.is_object()) { \
|
||||
auto* typed_array = ClassName::create(global_object(), 0); \
|
||||
if (first_argument.as_object().is_typed_array()) { \
|
||||
/* FIXME: Initialize from TypedArray */ \
|
||||
TODO(); \
|
||||
} else if (first_argument.as_object().is_array_buffer()) { \
|
||||
auto& array_buffer = static_cast<ArrayBuffer&>(first_argument.as_object()); \
|
||||
initialize_typed_array_from_array_buffer(global_object(), *typed_array, array_buffer, vm.argument(1), vm.argument(2)); \
|
||||
if (vm.exception()) \
|
||||
return {}; \
|
||||
} else { \
|
||||
/* FIXME: Initialize from Iterator or Array-like object */ \
|
||||
TODO(); \
|
||||
} \
|
||||
return typed_array; \
|
||||
} \
|
||||
\
|
||||
auto array_length = first_argument.to_index(global_object()); \
|
||||
if (vm.exception()) { \
|
||||
/* Re-throw more specific RangeError */ \
|
||||
vm.clear_exception(); \
|
||||
vm.throw_exception<RangeError>(global_object(), ErrorType::InvalidLength, "typed array"); \
|
||||
return {}; \
|
||||
} \
|
||||
return ClassName::create(global_object(), array_length); \
|
||||
}
|
||||
|
||||
#undef __JS_ENUMERATE
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <LibJS/Runtime/ArrayBuffer.h>
|
||||
#include <LibJS/Runtime/GlobalObject.h>
|
||||
#include <LibJS/Runtime/TypedArrayConstructor.h>
|
||||
#include <LibJS/Runtime/VM.h>
|
||||
|
@ -36,16 +37,31 @@ class TypedArrayBase : public Object {
|
|||
JS_OBJECT(TypedArrayBase, Object);
|
||||
|
||||
public:
|
||||
u32 length() const { return m_length; }
|
||||
u32 array_length() const { return m_array_length; }
|
||||
u32 byte_length() const { return m_byte_length; }
|
||||
u32 byte_offset() const { return m_byte_offset; }
|
||||
ArrayBuffer* viewed_array_buffer() const { return m_viewed_array_buffer; }
|
||||
|
||||
void set_array_length(u32 length) { m_array_length = length; }
|
||||
void set_byte_length(u32 length) { m_byte_length = length; }
|
||||
void set_byte_offset(u32 offset) { m_byte_offset = offset; }
|
||||
void set_viewed_array_buffer(ArrayBuffer* array_buffer) { m_viewed_array_buffer = array_buffer; }
|
||||
|
||||
virtual size_t element_size() const = 0;
|
||||
|
||||
protected:
|
||||
TypedArrayBase(u32 length, Object& prototype)
|
||||
TypedArrayBase(Object& prototype)
|
||||
: Object(prototype)
|
||||
, m_length(length)
|
||||
{
|
||||
}
|
||||
|
||||
u32 m_length { 0 };
|
||||
u32 m_array_length { 0 };
|
||||
u32 m_byte_length { 0 };
|
||||
u32 m_byte_offset { 0 };
|
||||
ArrayBuffer* m_viewed_array_buffer { nullptr };
|
||||
|
||||
private:
|
||||
virtual void visit_edges(Visitor&) override;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
|
@ -53,28 +69,22 @@ class TypedArray : public TypedArrayBase {
|
|||
JS_OBJECT(TypedArray, TypedArrayBase);
|
||||
|
||||
public:
|
||||
virtual ~TypedArray() override
|
||||
{
|
||||
ASSERT(m_data);
|
||||
free(m_data);
|
||||
m_data = nullptr;
|
||||
}
|
||||
|
||||
virtual bool put_by_index(u32 property_index, Value value) override
|
||||
{
|
||||
if (property_index >= m_length)
|
||||
property_index += m_byte_offset / sizeof(T);
|
||||
if (property_index >= m_array_length)
|
||||
return Base::put_by_index(property_index, value);
|
||||
|
||||
if constexpr (sizeof(T) < 4) {
|
||||
auto number = value.to_i32(global_object());
|
||||
if (vm().exception())
|
||||
return {};
|
||||
m_data[property_index] = number;
|
||||
data()[property_index] = number;
|
||||
} else if constexpr (sizeof(T) == 4) {
|
||||
auto number = value.to_double(global_object());
|
||||
if (vm().exception())
|
||||
return {};
|
||||
m_data[property_index] = number;
|
||||
data()[property_index] = number;
|
||||
} else {
|
||||
static_assert(DependentFalse<T>, "TypedArray::put_by_index with unhandled type size");
|
||||
}
|
||||
|
@ -83,13 +93,14 @@ public:
|
|||
|
||||
virtual Value get_by_index(u32 property_index) const override
|
||||
{
|
||||
if (property_index >= m_length)
|
||||
property_index += m_byte_offset / sizeof(T);
|
||||
if (property_index >= m_array_length)
|
||||
return Base::get_by_index(property_index);
|
||||
|
||||
if constexpr (sizeof(T) < 4) {
|
||||
return Value((i32)m_data[property_index]);
|
||||
return Value((i32)data()[property_index]);
|
||||
} else if constexpr (sizeof(T) == 4) {
|
||||
auto value = m_data[property_index];
|
||||
auto value = data()[property_index];
|
||||
if constexpr (NumericLimits<T>::is_signed()) {
|
||||
if (value > NumericLimits<i32>::max() || value < NumericLimits<i32>::min())
|
||||
return Value((double)value);
|
||||
|
@ -103,20 +114,29 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
T* data() { return m_data; }
|
||||
const T* data() const { return m_data; }
|
||||
T* data() const { return reinterpret_cast<T*>(m_viewed_array_buffer->buffer().data()); }
|
||||
|
||||
virtual size_t element_size() const override { return sizeof(T); };
|
||||
|
||||
protected:
|
||||
TypedArray(u32 length, Object& prototype)
|
||||
: TypedArrayBase(length, prototype)
|
||||
TypedArray(ArrayBuffer& array_buffer, u32 array_length, Object& prototype)
|
||||
: TypedArrayBase(prototype)
|
||||
{
|
||||
m_data = (T*)calloc(m_length, sizeof(T));
|
||||
m_viewed_array_buffer = &array_buffer;
|
||||
m_array_length = array_length;
|
||||
m_byte_length = m_viewed_array_buffer->byte_length();
|
||||
}
|
||||
|
||||
TypedArray(u32 array_length, Object& prototype)
|
||||
: TypedArrayBase(prototype)
|
||||
{
|
||||
m_viewed_array_buffer = ArrayBuffer::create(global_object(), array_length * sizeof(T));
|
||||
m_array_length = array_length;
|
||||
m_byte_length = m_viewed_array_buffer->byte_length();
|
||||
}
|
||||
|
||||
private:
|
||||
virtual bool is_typed_array() const final { return true; }
|
||||
|
||||
T* m_data { nullptr };
|
||||
};
|
||||
|
||||
#define JS_DECLARE_TYPED_ARRAY(ClassName, snake_name, PrototypeName, ConstructorName, Type) \
|
||||
|
|
|
@ -64,7 +64,7 @@ JS_DEFINE_NATIVE_GETTER(TypedArrayPrototype::length_getter)
|
|||
auto typed_array = typed_array_from(vm, global_object);
|
||||
if (!typed_array)
|
||||
return {};
|
||||
return Value(typed_array->length());
|
||||
return Value(typed_array->array_length());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -45,6 +45,70 @@ test("typed arrays inherit from TypedArray", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("typed array can share the same ArrayBuffer", () => {
|
||||
const arrayBuffer = new ArrayBuffer(2);
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const uint16Array = new Uint16Array(arrayBuffer);
|
||||
expect(uint8Array[0]).toBe(0);
|
||||
expect(uint8Array[1]).toBe(0);
|
||||
expect(uint16Array[0]).toBe(0);
|
||||
expect(uint16Array[1]).toBeUndefined();
|
||||
uint16Array[0] = 54321;
|
||||
expect(uint8Array[0]).toBe(0x31);
|
||||
expect(uint8Array[1]).toBe(0xd4);
|
||||
expect(uint16Array[0]).toBe(54321);
|
||||
expect(uint16Array[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("typed array from ArrayBuffer with custom length and offset", () => {
|
||||
const arrayBuffer = new ArrayBuffer(10);
|
||||
const uint8ArrayAll = new Uint8Array(arrayBuffer);
|
||||
const uint16ArrayPartial = new Uint16Array(arrayBuffer, 2, 4);
|
||||
// Affects two bytes of the buffer, beginning at offset
|
||||
uint16ArrayPartial[0] = 52651
|
||||
// Out of relative bounds, doesn't affect buffer
|
||||
uint16ArrayPartial[4] = 123
|
||||
expect(uint8ArrayAll[0]).toBe(0);
|
||||
expect(uint8ArrayAll[1]).toBe(0);
|
||||
expect(uint8ArrayAll[2]).toBe(0xab);
|
||||
expect(uint8ArrayAll[3]).toBe(0xcd);
|
||||
expect(uint8ArrayAll[5]).toBe(0);
|
||||
expect(uint8ArrayAll[6]).toBe(0);
|
||||
expect(uint8ArrayAll[7]).toBe(0);
|
||||
expect(uint8ArrayAll[8]).toBe(0);
|
||||
expect(uint8ArrayAll[9]).toBe(0);
|
||||
});
|
||||
|
||||
test("typed array from ArrayBuffer errors", () => {
|
||||
expect(() => {
|
||||
new Uint16Array(new ArrayBuffer(1));
|
||||
}).toThrowWithMessage(
|
||||
RangeError,
|
||||
"Invalid buffer length for Uint16Array: must be a multiple of 2, got 1"
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
new Uint16Array(new ArrayBuffer(), 1);
|
||||
}).toThrowWithMessage(
|
||||
RangeError,
|
||||
"Invalid byte offset for Uint16Array: must be a multiple of 2, got 1"
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
new Uint16Array(new ArrayBuffer(), 2);
|
||||
}).toThrowWithMessage(
|
||||
RangeError,
|
||||
"Typed array byte offset 2 is out of range for buffer with length 0"
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
new Uint16Array(new ArrayBuffer(7), 2, 3);
|
||||
}).toThrowWithMessage(
|
||||
RangeError,
|
||||
"Typed array range 2:8 is out of range for buffer with length 7"
|
||||
);
|
||||
});
|
||||
|
||||
test("TypedArray is not exposed on the global object", () => {
|
||||
expect(globalThis.TypedArray).toBeUndefined();
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue