diff --git a/pkg/dart2wasm/lib/js/inline_expander.dart b/pkg/dart2wasm/lib/js/inline_expander.dart index 1e79c095aeb..089d8d7805b 100644 --- a/pkg/dart2wasm/lib/js/inline_expander.dart +++ b/pkg/dart2wasm/lib/js/inline_expander.dart @@ -88,7 +88,7 @@ class InlineExpander { String parameterString = 'x$j'; DartType type = originalArgument.getStaticType(_staticTypeContext); dartPositionalParameters.add(VariableDeclaration(parameterString, - type: type, isSynthesized: true)); + type: _toExternalType(type), isSynthesized: true)); if (originalArgument is! VariableGet) { allArgumentsAreGet = false; } @@ -121,4 +121,18 @@ class InlineExpander { _methodCollector.addMethod(dartProcedure, jsMethodName, codeTemplate); return result; } + + // The `JS("...some js code ...", arg0, arg1)` expressions will produce + // wasm imports. We want to only use types in those wasm imports that binaryen + // allows under closed world assumptions (and not blindly use `arg`s static + // type). + // + // For now we special case `WasmArray<>` which we turn into a generic `array` + // type (super type of all wasm arrays). + DartType _toExternalType(DartType type) { + if (type is InterfaceType && type.classNode == _util.wasmArrayClass) { + return InterfaceType(_util.wasmArrayRefClass, type.declaredNullability); + } + return type; + } } diff --git a/pkg/dart2wasm/lib/js/runtime_blob.dart b/pkg/dart2wasm/lib/js/runtime_blob.dart index a6ab0e99e10..76697fbb615 100644 --- a/pkg/dart2wasm/lib/js/runtime_blob.dart +++ b/pkg/dart2wasm/lib/js/runtime_blob.dart @@ -129,6 +129,26 @@ const jsRuntimeBlobPart3 = r''' "fromCharCode": (i) => String.fromCharCode(i), "length": (s) => s.length, "substring": (s, a, b) => s.substring(a, b), + "fromCharCodeArray": (a, start, end) => { + if (end <= start) return ''; + + const read = dartInstance.exports.$wasmI16ArrayGet; + let result = ''; + let index = start; + const chunkLength = Math.min(end - index, 500); + let array = new Array(chunkLength); + while (index < end) { + const newChunkLength = Math.min(end - index, 500); + for (let i = 0; i < newChunkLength; i++) { + array[i] = read(a, index++); + } + if (newChunkLength < chunkLength) { + array = array.slice(0, newChunkLength); + } + result += String.fromCharCode(...array); + } + return result; + }, }; const deferredLibraryHelper = { diff --git a/pkg/dart2wasm/lib/js/util.dart b/pkg/dart2wasm/lib/js/util.dart index 0f9f6ae33c6..d2008c11c29 100644 --- a/pkg/dart2wasm/lib/js/util.dart +++ b/pkg/dart2wasm/lib/js/util.dart @@ -27,6 +27,8 @@ class CoreTypesUtil { final Procedure jsValueUnboxTarget; final Procedure numToIntTarget; final Class wasmExternRefClass; + final Class wasmArrayClass; + final Class wasmArrayRefClass; final Procedure wrapDartFunctionTarget; CoreTypesUtil(this.coreTypes, this._extensionIndex) @@ -64,6 +66,9 @@ class CoreTypesUtil { .firstWhere((p) => p.name.text == 'unbox'), wasmExternRefClass = coreTypes.index.getClass('dart:_wasm', 'WasmExternRef'), + wasmArrayClass = coreTypes.index.getClass('dart:_wasm', 'WasmArray'), + wasmArrayRefClass = + coreTypes.index.getClass('dart:_wasm', 'WasmArrayRef'), wrapDartFunctionTarget = coreTypes.index .getTopLevelProcedure('dart:_js_helper', '_wrapDartFunction'); diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart index 8e38d8c909f..37cca501833 100644 --- a/pkg/dart2wasm/lib/translator.dart +++ b/pkg/dart2wasm/lib/translator.dart @@ -690,7 +690,7 @@ class Translator with KernelNodes { /// (`anyref`, `funcref` or `externref`). /// This function can be called before the class info is built. w.ValueType translateExternalType(DartType type) { - bool isPotentiallyNullable = type.isPotentiallyNullable; + final bool isPotentiallyNullable = type.isPotentiallyNullable; if (type is InterfaceType) { Class cls = type.classNode; if (cls == wasmFuncRefClass || cls == wasmFunctionClass) { @@ -699,6 +699,16 @@ class Translator with KernelNodes { if (cls == wasmExternRefClass) { return w.RefType.extern(nullable: isPotentiallyNullable); } + if (cls == wasmArrayRefClass) { + return w.RefType.array(nullable: isPotentiallyNullable); + } + if (cls == wasmArrayClass) { + final elementType = + translateExternalStorageType(type.typeArguments.single); + return w.RefType.def( + wasmArrayType(elementType, '$elementType', mutable: true), + nullable: isPotentiallyNullable); + } if (!isPotentiallyNullable) { w.StorageType? builtin = builtinTypes[cls]; if (builtin != null && builtin.isPrimitive) { @@ -711,6 +721,22 @@ class Translator with KernelNodes { return w.RefType.any(nullable: true); } + w.StorageType translateExternalStorageType(DartType type) { + if (type is InterfaceType) { + final cls = type.classNode; + if (isWasmType(cls)) { + final isNullable = type.isPotentiallyNullable; + final w.StorageType? builtin = builtinTypes[cls]; + if (builtin != null) { + if (!isNullable) return builtin; + if (builtin.isPrimitive) throw "Wasm numeric types can't be nullable"; + return (builtin as w.RefType).withNullability(isNullable); + } + } + } + return translateExternalType(type) as w.RefType; + } + /// Creates a global reference to [f] in [module]. [f] must also be located /// in [module]. w.Global makeFunctionRef(w.ModuleBuilder module, w.BaseFunction f) { diff --git a/sdk/lib/_internal/wasm/lib/js_helper_patch.dart b/sdk/lib/_internal/wasm/lib/js_helper_patch.dart index 7bdd6ba3fc0..1ddd5e8e4e0 100644 --- a/sdk/lib/_internal/wasm/lib/js_helper_patch.dart +++ b/sdk/lib/_internal/wasm/lib/js_helper_patch.dart @@ -15,63 +15,22 @@ import 'dart:typed_data'; @pragma('wasm:prefer-inline') JSStringImpl jsStringFromDartString(String s) { if (s is OneByteString) { + final fromArray = s.array; + final toArray = WasmArray(fromArray.length); + for (int i = 0; i < fromArray.length; ++i) { + toArray.write(i, fromArray.readUnsigned(i)); + } return JSStringImpl( - JS( - r''' - (s, length) => { - if (length == 0) return ''; - - const read = dartInstance.exports.$stringRead1; - let result = ''; - let index = 0; - const chunkLength = Math.min(length - index, 500); - let array = new Array(chunkLength); - while (index < length) { - const newChunkLength = Math.min(length - index, 500); - for (let i = 0; i < newChunkLength; i++) { - array[i] = read(s, index++); - } - if (newChunkLength < chunkLength) { - array = array.slice(0, newChunkLength); - } - result += String.fromCharCode(...array); - } - return result; - } - ''', - jsObjectFromDartObject(s), - s.length.toWasmI32(), + _jsStringFromCharCodeArray( + toArray, + 0.toWasmI32(), + toArray.length.toWasmI32(), ), ); } if (s is TwoByteString) { return JSStringImpl( - JS( - r''' - (s, length) => { - if (length == 0) return ''; - - const read = dartInstance.exports.$stringRead2; - let result = ''; - let index = 0; - const chunkLength = Math.min(length - index, 500); - let array = new Array(chunkLength); - while (index < length) { - const newChunkLength = Math.min(length - index, 500); - for (let i = 0; i < newChunkLength; i++) { - array[i] = read(s, index++); - } - if (newChunkLength < chunkLength) { - array = array.slice(0, newChunkLength); - } - result += String.fromCharCode(...array); - } - return result; - } - ''', - jsObjectFromDartObject(s), - s.length.toWasmI32(), - ), + _jsStringFromCharCodeArray(s.array, 0.toWasmI32(), s.length.toWasmI32()), ); } @@ -79,70 +38,34 @@ JSStringImpl jsStringFromDartString(String s) { } @patch -@pragma('wasm:prefer-inline') -String jsStringToDartString(JSStringImpl s) { - final length = s.length; - if (length == 0) return ''; - - return JS(r''' - (s) => { - let length = s.length; - let range = 0; - for (let i = 0; i < length; i++) { - range |= s.codePointAt(i); - } - const exports = dartInstance.exports; - if (range < 256) { - if (length <= 10) { - if (length == 1) { - return exports.$stringAllocate1_1(s.codePointAt(0)); - } - if (length == 2) { - return exports.$stringAllocate1_2(s.codePointAt(0), s.codePointAt(1)); - } - if (length == 3) { - return exports.$stringAllocate1_3(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2)); - } - if (length == 4) { - return exports.$stringAllocate1_4(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3)); - } - if (length == 5) { - return exports.$stringAllocate1_5(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4)); - } - if (length == 6) { - return exports.$stringAllocate1_6(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5)); - } - if (length == 7) { - return exports.$stringAllocate1_7(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6)); - } - if (length == 8) { - return exports.$stringAllocate1_8(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6), s.codePointAt(7)); - } - if (length == 9) { - return exports.$stringAllocate1_9(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6), s.codePointAt(7), s.codePointAt(8)); - } - if (length == 10) { - return exports.$stringAllocate1_10(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6), s.codePointAt(7), s.codePointAt(8), s.codePointAt(9)); - } - } - const dartString = exports.$stringAllocate1(length); - const write = exports.$stringWrite1; - for (let i = 0; i < length; i++) { - write(dartString, i, s.codePointAt(i)); - } - return dartString; - } else { - const dartString = exports.$stringAllocate2(length); - const write = exports.$stringWrite2; - for (let i = 0; i < length; i++) { - write(dartString, i, s.charCodeAt(i)); - } - return dartString; - } +String jsStringToDartString(JSStringImpl jsString) { + final length = jsString.length; + two_byte: + { + final oneByteString = OneByteString.withLength(length); + final array = oneByteString.array; + for (int i = 0; i < length; ++i) { + final int codeUnit = jsString.codeUnitAtUnchecked(i); + if (codeUnit > 255) break two_byte; + array.write(i, codeUnit); } - ''', s.toExternRef); + return oneByteString; + } + final twoByteString = TwoByteString.withLength(length); + final array = twoByteString.array; + for (int i = 0; i < length; ++i) { + array.write(i, jsString.codeUnitAtUnchecked(i)); + } + return twoByteString; } +@pragma("wasm:import", "wasm:js-string.fromCharCodeArray") +external WasmExternRef _jsStringFromCharCodeArray( + WasmArray array, + WasmI32 start, + WasmI32 end, +); + @pragma('wasm:prefer-inline') void _copyFromWasmI8Array( WasmExternRef jsArray, @@ -541,219 +464,6 @@ WasmExternRef jsDataViewFromDartByteData(ByteData l, int length) { ); } -@pragma("wasm:export", "\$stringAllocate1") -OneByteString _stringAllocate1(WasmI32 length) { - return OneByteString.withLength(length.toIntSigned()); -} - -@pragma("wasm:export", "\$stringAllocate1_1") -OneByteString _stringAllocate1_1(WasmI32 a0) { - final result = OneByteString.withLength(1); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringAllocate1_2") -OneByteString _stringAllocate1_2(WasmI32 a0, WasmI32 a1) { - final result = OneByteString.withLength(2); - result.setUnchecked(1, a1.toIntSigned()); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringAllocate1_3") -OneByteString _stringAllocate1_3(WasmI32 a0, WasmI32 a1, WasmI32 a2) { - final result = OneByteString.withLength(3); - result.setUnchecked(2, a2.toIntSigned()); - result.setUnchecked(1, a1.toIntSigned()); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringAllocate1_4") -OneByteString _stringAllocate1_4( - WasmI32 a0, - WasmI32 a1, - WasmI32 a2, - WasmI32 a3, -) { - final result = OneByteString.withLength(4); - result.setUnchecked(3, a3.toIntSigned()); - result.setUnchecked(2, a2.toIntSigned()); - result.setUnchecked(1, a1.toIntSigned()); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringAllocate1_5") -OneByteString _stringAllocate1_5( - WasmI32 a0, - WasmI32 a1, - WasmI32 a2, - WasmI32 a3, - WasmI32 a4, -) { - final result = OneByteString.withLength(5); - result.setUnchecked(4, a4.toIntSigned()); - result.setUnchecked(3, a3.toIntSigned()); - result.setUnchecked(2, a2.toIntSigned()); - result.setUnchecked(1, a1.toIntSigned()); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringAllocate1_6") -OneByteString _stringAllocate1_6( - WasmI32 a0, - WasmI32 a1, - WasmI32 a2, - WasmI32 a3, - WasmI32 a4, - WasmI32 a5, -) { - final result = OneByteString.withLength(6); - result.setUnchecked(5, a5.toIntSigned()); - result.setUnchecked(4, a4.toIntSigned()); - result.setUnchecked(3, a3.toIntSigned()); - result.setUnchecked(2, a2.toIntSigned()); - result.setUnchecked(1, a1.toIntSigned()); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringAllocate1_7") -OneByteString _stringAllocate1_7( - WasmI32 a0, - WasmI32 a1, - WasmI32 a2, - WasmI32 a3, - WasmI32 a4, - WasmI32 a5, - WasmI32 a6, -) { - final result = OneByteString.withLength(7); - result.setUnchecked(6, a6.toIntSigned()); - result.setUnchecked(5, a5.toIntSigned()); - result.setUnchecked(4, a4.toIntSigned()); - result.setUnchecked(3, a3.toIntSigned()); - result.setUnchecked(2, a2.toIntSigned()); - result.setUnchecked(1, a1.toIntSigned()); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringAllocate1_8") -OneByteString _stringAllocate1_8( - WasmI32 a0, - WasmI32 a1, - WasmI32 a2, - WasmI32 a3, - WasmI32 a4, - WasmI32 a5, - WasmI32 a6, - WasmI32 a7, -) { - final result = OneByteString.withLength(8); - result.setUnchecked(7, a7.toIntSigned()); - result.setUnchecked(6, a6.toIntSigned()); - result.setUnchecked(5, a5.toIntSigned()); - result.setUnchecked(4, a4.toIntSigned()); - result.setUnchecked(3, a3.toIntSigned()); - result.setUnchecked(2, a2.toIntSigned()); - result.setUnchecked(1, a1.toIntSigned()); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringAllocate1_9") -OneByteString _stringAllocate1_9( - WasmI32 a0, - WasmI32 a1, - WasmI32 a2, - WasmI32 a3, - WasmI32 a4, - WasmI32 a5, - WasmI32 a6, - WasmI32 a7, - WasmI32 a8, -) { - final result = OneByteString.withLength(9); - result.setUnchecked(8, a8.toIntSigned()); - result.setUnchecked(7, a7.toIntSigned()); - result.setUnchecked(6, a6.toIntSigned()); - result.setUnchecked(5, a5.toIntSigned()); - result.setUnchecked(4, a4.toIntSigned()); - result.setUnchecked(3, a3.toIntSigned()); - result.setUnchecked(2, a2.toIntSigned()); - result.setUnchecked(1, a1.toIntSigned()); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringAllocate1_10") -OneByteString _stringAllocate1_10( - WasmI32 a0, - WasmI32 a1, - WasmI32 a2, - WasmI32 a3, - WasmI32 a4, - WasmI32 a5, - WasmI32 a6, - WasmI32 a7, - WasmI32 a8, - WasmI32 a9, -) { - final result = OneByteString.withLength(10); - result.setUnchecked(9, a9.toIntSigned()); - result.setUnchecked(8, a8.toIntSigned()); - result.setUnchecked(7, a7.toIntSigned()); - result.setUnchecked(6, a6.toIntSigned()); - result.setUnchecked(5, a5.toIntSigned()); - result.setUnchecked(4, a4.toIntSigned()); - result.setUnchecked(3, a3.toIntSigned()); - result.setUnchecked(2, a2.toIntSigned()); - result.setUnchecked(1, a1.toIntSigned()); - result.setUnchecked(0, a0.toIntSigned()); - return result; -} - -@pragma("wasm:export", "\$stringRead1") -WasmI32 _stringRead1(WasmExternRef? ref, WasmI32 index) { - final string = unsafeCastOpaque( - unsafeCast(ref).internalize(), - ); - return string.codeUnitAtUnchecked(index.toIntSigned()).toWasmI32(); -} - -@pragma("wasm:export", "\$stringWrite1") -void _stringWrite1(WasmExternRef? ref, WasmI32 index, WasmI32 codePoint) { - final string = unsafeCastOpaque( - unsafeCast(ref).internalize(), - ); - string.setUnchecked(index.toIntSigned(), codePoint.toIntSigned()); -} - -@pragma("wasm:export", "\$stringAllocate2") -TwoByteString _stringAllocate2(WasmI32 length) { - return TwoByteString.withLength(length.toIntSigned()); -} - -@pragma("wasm:export", "\$stringRead2") -WasmI32 _stringRead2(WasmExternRef? ref, WasmI32 index) { - final string = unsafeCastOpaque( - unsafeCast(ref).internalize(), - ); - return string.codeUnitAtUnchecked(index.toIntSigned()).toWasmI32(); -} - -@pragma("wasm:export", "\$stringWrite2") -void _stringWrite2(WasmExternRef? ref, WasmI32 index, WasmI32 codePoint) { - final string = unsafeCastOpaque( - unsafeCast(ref).internalize(), - ); - string.setUnchecked(index.toIntSigned(), codePoint.toIntSigned()); -} - @pragma("wasm:export", "\$wasmI8ArrayGet") WasmI32 _wasmI8ArrayGet(WasmExternRef? ref, WasmI32 index) { final array = unsafeCastOpaque>( diff --git a/sdk/lib/_internal/wasm/lib/js_string.dart b/sdk/lib/_internal/wasm/lib/js_string.dart index ea32379b358..cab02849ad5 100644 --- a/sdk/lib/_internal/wasm/lib/js_string.dart +++ b/sdk/lib/_internal/wasm/lib/js_string.dart @@ -60,7 +60,6 @@ final class JSStringImpl implements String, StringUncheckedOperationsBase { final s = o.toString(); final jsString = s is JSStringImpl ? js.JSValue.boxT(s.toExternRef) : s.toJS; - // array._setUnchecked(i, jsString); array[i] = jsString; } return JSStringImpl( diff --git a/sdk/lib/_internal/wasm/lib/string.dart b/sdk/lib/_internal/wasm/lib/string.dart index 14f40bd417b..4219e9f0701 100644 --- a/sdk/lib/_internal/wasm/lib/string.dart +++ b/sdk/lib/_internal/wasm/lib/string.dart @@ -31,6 +31,9 @@ extension OneByteStringUncheckedOperations on OneByteString { @pragma('wasm:prefer-inline') void setUnchecked(int index, int codePoint) => _setAt(index, codePoint); + + @pragma('wasm:prefer-inline') + WasmArray get array => _array; } extension TwoByteStringUncheckedOperations on TwoByteString { @@ -43,6 +46,9 @@ extension TwoByteStringUncheckedOperations on TwoByteString { @pragma('wasm:prefer-inline') void setUnchecked(int index, int codePoint) => _setAt(index, codePoint); + + @pragma('wasm:prefer-inline') + WasmArray get array => _array; } /// Static function for `OneByteString._array` to avoid making `_array` public. diff --git a/tests/web/wasm/string_copy_test.dart b/tests/web/wasm/string_copy_test.dart new file mode 100644 index 00000000000..09f0402583a --- /dev/null +++ b/tests/web/wasm/string_copy_test.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:js_interop'; + +import 'package:expect/expect.dart'; + +main() async { + final String oneByteString = makeLongString('012345789'); + final String twoByteString = makeLongString('01234δΈ­6789'); + + Expect.equals(oneByteString, roundTrip(oneByteString)); + Expect.equals(twoByteString, roundTrip(twoByteString)); +} + +/// Ensure we make a very long string, ensuring that we'll also hit slow paths +/// in the string copy implementation. +String makeLongString(String string) { + while (string.length < 1024 * 1024) { + string = string + string; + } + return string; +} + +/// Copies the string to JS and back again to a dart internal String. +String roundTrip(String dartString) { + final JSString jsString = dartString.toJS; + + // Using string interpolation will force conversion to internal string (vs + // `JSStringImpl`) + final string = 'A${jsString.toDart}Z'; + return string.substring(1, string.length - 1); +}