[dart2wasm] Take advantage of fast `js-string` builtins
Some wasm engines have started to optimize the `js-string` builtin
proposal (e.g. V8) and those that haven't yet are probaly going to
do soon.
So we can start taking advantage of it in dart2wasm.
=> We make use of them in the JS<->Dart string copy code.
=> This will also provide a better baseline when evaluating whether
switching to JS stringes entirely makes sense.
A somewhat unrelated (but necessary for this CL) change is to tighten
the types we use in `@pragma('wasm:import')` and
`@pragma('wasm:export')` in some cases:
We should only use 'pure' wasm types (i.e. not wasm struct / function
types we define for dart classes & functions) and mostly non-composed
types in import/exports as the `--closed-world` optimizations from
binaryen rely on that (and error otherwise).
Overall this leads to significant improvements in Dart<->JS
string copies.
The `WasmDataTransfer.*{From,To}BrowserString` benchmarks
improve something between 50-100%.
Change-Id: I2048113c462ecb2047402c0616d2b3b1f45773f5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/400641
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
diff --git a/pkg/dart2wasm/lib/js/inline_expander.dart b/pkg/dart2wasm/lib/js/inline_expander.dart
index 1e79c09..089d8d7 100644
--- a/pkg/dart2wasm/lib/js/inline_expander.dart
+++ b/pkg/dart2wasm/lib/js/inline_expander.dart
@@ -88,7 +88,7 @@
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 @@
_methodCollector.addMethod(dartProcedure, jsMethodName, codeTemplate);
return result;
}
+
+ // The `JS<foo>("...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<N>`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 a6ab0e9..76697fb 100644
--- a/pkg/dart2wasm/lib/js/runtime_blob.dart
+++ b/pkg/dart2wasm/lib/js/runtime_blob.dart
@@ -129,6 +129,26 @@
"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 0f9f6ae..d2008c1 100644
--- a/pkg/dart2wasm/lib/js/util.dart
+++ b/pkg/dart2wasm/lib/js/util.dart
@@ -27,6 +27,8 @@
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 @@
.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 8e38d8c..37cca50 100644
--- a/pkg/dart2wasm/lib/translator.dart
+++ b/pkg/dart2wasm/lib/translator.dart
@@ -690,7 +690,7 @@
/// (`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 @@
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 @@
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 7bdd6ba..1ddd5e8 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 @@
@pragma('wasm:prefer-inline')
JSStringImpl jsStringFromDartString(String s) {
if (s is OneByteString) {
+ final fromArray = s.array;
+ final toArray = WasmArray<WasmI16>(fromArray.length);
+ for (int i = 0; i < fromArray.length; ++i) {
+ toArray.write(i, fromArray.readUnsigned(i));
+ }
return JSStringImpl(
- JS<WasmExternRef>(
- 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<WasmExternRef>(
- 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 @@
}
@patch
-@pragma('wasm:prefer-inline')
-String jsStringToDartString(JSStringImpl s) {
- final length = s.length;
- if (length == 0) return '';
-
- return JS<String>(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<WasmI16> array,
+ WasmI32 start,
+ WasmI32 end,
+);
+
@pragma('wasm:prefer-inline')
void _copyFromWasmI8Array(
WasmExternRef jsArray,
@@ -541,219 +464,6 @@
);
}
-@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<OneByteString>(
- unsafeCast<WasmExternRef>(ref).internalize(),
- );
- return string.codeUnitAtUnchecked(index.toIntSigned()).toWasmI32();
-}
-
-@pragma("wasm:export", "\$stringWrite1")
-void _stringWrite1(WasmExternRef? ref, WasmI32 index, WasmI32 codePoint) {
- final string = unsafeCastOpaque<OneByteString>(
- unsafeCast<WasmExternRef>(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<TwoByteString>(
- unsafeCast<WasmExternRef>(ref).internalize(),
- );
- return string.codeUnitAtUnchecked(index.toIntSigned()).toWasmI32();
-}
-
-@pragma("wasm:export", "\$stringWrite2")
-void _stringWrite2(WasmExternRef? ref, WasmI32 index, WasmI32 codePoint) {
- final string = unsafeCastOpaque<TwoByteString>(
- unsafeCast<WasmExternRef>(ref).internalize(),
- );
- string.setUnchecked(index.toIntSigned(), codePoint.toIntSigned());
-}
-
@pragma("wasm:export", "\$wasmI8ArrayGet")
WasmI32 _wasmI8ArrayGet(WasmExternRef? ref, WasmI32 index) {
final array = unsafeCastOpaque<WasmArray<WasmI8>>(
diff --git a/sdk/lib/_internal/wasm/lib/js_string.dart b/sdk/lib/_internal/wasm/lib/js_string.dart
index ea32379..cab0284 100644
--- a/sdk/lib/_internal/wasm/lib/js_string.dart
+++ b/sdk/lib/_internal/wasm/lib/js_string.dart
@@ -60,7 +60,6 @@
final s = o.toString();
final jsString =
s is JSStringImpl ? js.JSValue.boxT<JSAny?>(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 14f40bd..4219e9f 100644
--- a/sdk/lib/_internal/wasm/lib/string.dart
+++ b/sdk/lib/_internal/wasm/lib/string.dart
@@ -31,6 +31,9 @@
@pragma('wasm:prefer-inline')
void setUnchecked(int index, int codePoint) => _setAt(index, codePoint);
+
+ @pragma('wasm:prefer-inline')
+ WasmArray<WasmI8> get array => _array;
}
extension TwoByteStringUncheckedOperations on TwoByteString {
@@ -43,6 +46,9 @@
@pragma('wasm:prefer-inline')
void setUnchecked(int index, int codePoint) => _setAt(index, codePoint);
+
+ @pragma('wasm:prefer-inline')
+ WasmArray<WasmI16> 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 0000000..09f0402
--- /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);
+}