[wasm] add experimental string/Wasm GC array conversion fast-path

This CL adds two experimental JS builtins to convert between
i16 Wasm GC and JS strings. This is a non-standard experimental
feature only available with the flag --wasm-gc-js-interop.

WebAssembly.experimentalConvertArrayToString(array, start, count)
Convert the `count`-many WTF-16 code units starting at index `start`
into a JS string. Throws a TypeError if `array` is not an i16 array,
or if `start` and `count` are not numbers or not in range.

WebAssembly.experimentalConvertStringToArray(string, sampleArray)
Convert `string` to an i16 array. The `sampleArray` parameter needs
to be an arbitrary i16 array, which is only used to extract the rtt.
Throws a TypeError if `string` is not a string or `sampleArray` is not
an i16 array.

Change-Id: I7ac2f6bd89b8f638427f61da1bb01ccba90d735b
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3642301
Commit-Queue: Tobias Tebbi <tebbi@chromium.org>
Reviewed-by: Jakob Kummerow <jkummerow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#80505}
This commit is contained in:
Tobias Tebbi 2022-05-12 19:30:38 +00:00 committed by V8 LUCI CQ
parent 334cc5f8d6
commit b16d2a4e2f
8 changed files with 200 additions and 15 deletions

View File

@ -200,6 +200,10 @@ struct SliceIterator<T: type, Reference: type> {
return *this.NextReference() otherwise NoMore;
}
macro NextNotEmpty(): T {
return *this.NextReferenceNotEmpty();
}
macro NextReference(): Reference labels NoMore {
if (this.Empty()) {
goto NoMore;
@ -210,6 +214,13 @@ struct SliceIterator<T: type, Reference: type> {
}
}
macro NextReferenceNotEmpty(): Reference {
dcheck(!this.Empty());
const result = unsafe::NewReference<T>(this.object, this.start);
this.start += %SizeOf<T>();
return result;
}
object: HeapObject|TaggedZeroPattern;
start: intptr;
end: intptr;

View File

@ -315,7 +315,7 @@ builtin WasmAllocateStructWithRtt(rtt: Map): HeapObject {
macro WasmAllocateArray(
rtt: Map, length: uint32, elementSize: uint32,
initializationMode: constexpr InitializationMode): HeapObject {
initializationMode: constexpr InitializationMode): WasmArray {
// instanceSize = RoundUp(elementSize * length, kObjectAlignment)
// + WasmArray::kHeaderSize
const instanceSize: intptr =
@ -328,7 +328,7 @@ macro WasmAllocateArray(
// TODO(ishell): consider removing properties_or_hash field from WasmObjects.
%RawDownCast<WasmArray>(result).properties_or_hash = kEmptyFixedArray;
%RawDownCast<WasmArray>(result).length = length;
return result;
return %RawDownCast<WasmArray>(result);
}
builtin WasmAllocateArray_Uninitialized(
@ -663,4 +663,105 @@ builtin ThrowWasmTrapArrayOutOfBounds(): JSAny {
builtin ThrowWasmTrapArrayTooLarge(): JSAny {
tail WasmTrap(SmiConstant(MessageTemplate::kWasmTrapArrayTooLarge));
}
macro TryNumberToIntptr(value: JSAny): intptr labels Failure {
typeswitch (value) {
case (s: Smi): {
return Convert<intptr>(s);
}
case (hn: HeapNumber): deferred {
let value = Convert<float64>(hn);
if (Float64IsNaN(value)) goto Failure;
value = math::Float64Trunc(value);
return ChangeFloat64ToIntPtr(value);
}
case (JSAny): deferred {
goto Failure;
}
}
}
macro WasmTypeInfo(map: Map): WasmTypeInfo {
return UnsafeCast<WasmTypeInfo>(
map.constructor_or_back_pointer_or_native_context);
}
const kWasmI16ValueType: constexpr int32
generates 'wasm::ValueType::Primitive(wasm::kI16).raw_bit_field()';
const kWasmArrayTypeRepOffset:
constexpr intptr generates 'wasm::ArrayType::kRepOffset';
const kWasmValueTypeBitFieldOffset:
constexpr intptr generates 'wasm::ValueType::kBitFieldOffset';
macro IsWord16WasmArrayMap(map: Map): bool {
const arrayTypePtr: RawPtr<int32> = %RawDownCast<RawPtr<int32>>(
WasmTypeInfo(map).foreign_address_ptr + kWasmArrayTypeRepOffset +
kWasmValueTypeBitFieldOffset);
const arrayTypeRef: &int32 =
torque_internal::unsafe::NewOffHeapReference(arrayTypePtr);
return *arrayTypeRef == kWasmI16ValueType;
}
// Non-standard experimental feature.
transitioning javascript builtin ExperimentalWasmConvertArrayToString(
js-implicit context: NativeContext)(
array: JSAny, start: JSAny, count: JSAny): String {
try {
const start = TryNumberToIntptr(start) otherwise goto InvalidArgument;
const count = TryNumberToIntptr(count) otherwise goto InvalidArgument;
const array = Cast<WasmArray>(array) otherwise goto InvalidArgument;
if (!IsWord16WasmArrayMap(array.map)) goto InvalidArgument;
const arrayContent = torque_internal::unsafe::NewConstSlice<char16>(
array, kWasmArrayHeaderSize, Convert<intptr>(array.length));
return AllocateSeqTwoByteString(
Convert<uint32>(Unsigned(count)),
(Subslice(arrayContent, start, count) otherwise goto InvalidArgument)
.Iterator());
} label InvalidArgument deferred {
ThrowTypeError(MessageTemplate::kInvalidArgument);
}
}
// Non-standard experimental feature.
transitioning javascript builtin ExperimentalWasmConvertStringToArray(
js-implicit context: NativeContext)(
string: JSAny, sampleArray: JSAny): WasmArray {
try {
const sampleArray =
Cast<WasmArray>(sampleArray) otherwise goto InvalidArgument;
const arrayMap = sampleArray.map;
if (!IsWord16WasmArrayMap(arrayMap)) goto InvalidArgument;
const string = Cast<String>(string) otherwise goto InvalidArgument;
const length = string.length;
const result = WasmAllocateArray(
arrayMap, Unsigned(length), 2, InitializationMode::kUninitialized);
const arrayContent = torque_internal::unsafe::NewMutableSlice<char16>(
result, kWasmArrayHeaderSize, Convert<intptr>(length));
try {
StringToSlice(string) otherwise OneByte, TwoByte;
} label OneByte(slice: ConstSlice<char8>) {
let fromIt = slice.Iterator();
let toIt = arrayContent.Iterator();
while (true) {
let toRef = toIt.NextReference() otherwise break;
*toRef = %RawDownCast<char16>(Convert<uint16>(fromIt.NextNotEmpty()));
}
} label TwoByte(slice: ConstSlice<char16>) {
let fromIt = slice.Iterator();
let toIt = arrayContent.Iterator();
while (true) {
let toRef = toIt.NextReference() otherwise break;
*toRef = fromIt.NextNotEmpty();
}
}
return result;
} label InvalidArgument deferred {
ThrowTypeError(MessageTemplate::kInvalidArgument);
}
}
}

View File

@ -572,19 +572,6 @@ V8_NOINLINE Handle<JSFunction> InstallFunctionWithBuiltinId(
return fun;
}
V8_NOINLINE Handle<JSFunction> SimpleInstallFunction(
Isolate* isolate, Handle<JSObject> base, const char* name, Builtin call,
int len, bool adapt, PropertyAttributes attrs = DONT_ENUM) {
// Although function name does not have to be internalized the property name
// will be internalized during property addition anyway, so do it here now.
Handle<String> internalized_name =
isolate->factory()->InternalizeUtf8String(name);
Handle<JSFunction> fun =
SimpleCreateFunction(isolate, internalized_name, call, len, adapt);
JSObject::AddProperty(isolate, base, internalized_name, fun, attrs);
return fun;
}
V8_NOINLINE Handle<JSFunction> InstallFunctionAtSymbol(
Isolate* isolate, Handle<JSObject> base, Handle<Symbol> symbol,
const char* symbol_string, Builtin call, int len, bool adapt,
@ -908,6 +895,21 @@ Handle<Map> CreateNonConstructorMap(Isolate* isolate, Handle<Map> source_map,
} // namespace
Handle<JSFunction> SimpleInstallFunction(Isolate* isolate,
Handle<JSObject> base,
const char* name, Builtin call,
int len, bool adapt,
PropertyAttributes attrs) {
// Although function name does not have to be internalized the property name
// will be internalized during property addition anyway, so do it here now.
Handle<String> internalized_name =
isolate->factory()->InternalizeUtf8String(name);
Handle<JSFunction> fun =
SimpleCreateFunction(isolate, internalized_name, call, len, adapt);
JSObject::AddProperty(isolate, base, internalized_name, fun, attrs);
return fun;
}
void Genesis::CreateIteratorMaps(Handle<JSFunction> empty) {
// Create iterator-related meta-objects.
Handle<JSObject> iterator_prototype = factory()->NewJSObject(

View File

@ -129,6 +129,10 @@ class BootstrapperActive final {
Bootstrapper* bootstrapper_;
};
V8_NOINLINE Handle<JSFunction> SimpleInstallFunction(
Isolate* isolate, Handle<JSObject> base, const char* name, Builtin call,
int len, bool adapt, PropertyAttributes attrs = DONT_ENUM);
} // namespace internal
} // namespace v8

View File

@ -139,11 +139,15 @@ class ArrayType : public ZoneObject {
return rep_ != other.rep_ || mutability_ != other.mutability_;
}
static const intptr_t kRepOffset;
private:
const ValueType rep_;
const bool mutability_;
};
inline constexpr intptr_t ArrayType::kRepOffset = offsetof(ArrayType, rep_);
} // namespace wasm
} // namespace internal
} // namespace v8

View File

@ -550,6 +550,8 @@ class ValueType {
static constexpr int kKindBits = 5;
static constexpr int kHeapTypeBits = 20;
static const intptr_t kBitFieldOffset;
private:
// {hash_value} directly reads {bit_field_}.
friend size_t hash_value(ValueType type);
@ -576,6 +578,9 @@ class ValueType {
uint32_t bit_field_;
};
inline constexpr intptr_t ValueType::kBitFieldOffset =
offsetof(ValueType, bit_field_);
static_assert(sizeof(ValueType) <= kUInt32Size,
"ValueType is small and can be passed by value");
static_assert(ValueType::kLastUsedBit < 8 * sizeof(ValueType) - kSmiTagSize,

View File

@ -2797,6 +2797,17 @@ void WasmJs::Install(Isolate* isolate, bool exposed_on_global_object) {
InstallFunc(isolate, webassembly, "validate", WebAssemblyValidate, 1);
InstallFunc(isolate, webassembly, "instantiate", WebAssemblyInstantiate, 1);
// TODO(tebbi): Put this behind its own flag once --wasm-gc-js-interop gets
// closer to shipping.
if (FLAG_wasm_gc_js_interop) {
SimpleInstallFunction(
isolate, webassembly, "experimentalConvertArrayToString",
Builtin::kExperimentalWasmConvertArrayToString, 0, true);
SimpleInstallFunction(
isolate, webassembly, "experimentalConvertStringToArray",
Builtin::kExperimentalWasmConvertStringToArray, 0, true);
}
if (FLAG_wasm_test_streaming) {
isolate->set_wasm_streaming_callback(WasmStreamingCallbackForTesting);
}

View File

@ -0,0 +1,47 @@
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --experimental-wasm-gc --wasm-gc-js-interop
d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
var builder = new WasmModuleBuilder();
let i16Array = builder.addArray(kWasmI16, true);
builder.addFunction('getHelloArray', makeSig([], [kWasmAnyRef]))
.addBody([
...wasmI32Const(72), ...wasmI32Const(69), ...wasmI32Const(76),
...wasmI32Const(76), ...wasmI32Const(79), kGCPrefix, kExprArrayInitStatic,
i16Array, 5
])
.exportFunc();
builder.addFunction('getChar', makeSig([kWasmAnyRef, kWasmI32], [kWasmI32]))
.addBody([
kExprLocalGet, 0, kGCPrefix, kExprRefAsData, kGCPrefix,
kExprRefCastStatic, i16Array, kExprLocalGet, 1, kGCPrefix, kExprArrayGetS,
i16Array
])
.exportFunc();
const instance = builder.instantiate();
const getHelloArray = instance.exports.getHelloArray;
const getChar = instance.exports.getChar;
assertEquals(
WebAssembly.experimentalConvertArrayToString(getHelloArray(), 0, 5),
'HELLO');
assertEquals(
WebAssembly.experimentalConvertArrayToString(getHelloArray(), 1, 4),
'ELLO');
assertEquals(
WebAssembly.experimentalConvertArrayToString(getHelloArray(), 0, 3), 'HEL');
const string = 'foobar'
const array =
WebAssembly.experimentalConvertStringToArray('foobar', getHelloArray());
for (let i = 0; i < string.length; ++i) {
assertEquals(getChar(array, i), string.charCodeAt(i));
}