diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt index 2797cea1504..3591f8787dc 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt @@ -185,6 +185,7 @@ class WasmSymbols( val nullableDoubleIeee754Equals = getInternalFunction("nullableDoubleIeee754Equals") val unsafeGetScratchRawMemory = getInternalFunction("unsafeGetScratchRawMemory") + val unsafeGetScratchRawMemorySize = getInternalFunction("unsafeGetScratchRawMemorySize") val startCoroutineUninterceptedOrReturnIntrinsics = (0..2).map { getInternalFunction("startCoroutineUninterceptedOrReturnIntrinsic$it") } diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt index 7af7a2494a6..012cabce4d4 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt @@ -465,6 +465,10 @@ class BodyGenerator( body.buildConstI32Symbol(context.scratchMemAddr) } + wasmSymbols.unsafeGetScratchRawMemorySize -> { + body.buildConstI32Symbol(WasmSymbol(context.scratchMemSizeInBytes)) + } + else -> { return false } diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmBaseCodegenContext.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmBaseCodegenContext.kt index 1c772c862cd..a2dd9197a08 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmBaseCodegenContext.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmBaseCodegenContext.kt @@ -16,6 +16,7 @@ interface WasmBaseCodegenContext { val backendContext: WasmBackendContext val scratchMemAddr: WasmSymbol + val scratchMemSizeInBytes: Int fun referenceFunction(irFunction: IrFunctionSymbol): WasmSymbol fun referenceGlobalField(irField: IrFieldSymbol): WasmSymbol diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmCompiledModuleFragment.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmCompiledModuleFragment.kt index 4928bea6eba..da1b237105c 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmCompiledModuleFragment.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmCompiledModuleFragment.kt @@ -60,7 +60,7 @@ class WasmCompiledModuleFragment(val irBuiltIns: IrBuiltIns) { val initFunctions = mutableListOf() val scratchMemAddr = WasmSymbol() - private val scratchMemSizeInBytes = 65_536 + val scratchMemSizeInBytes = 65_536 open class ReferencableElements { val unbound = mutableMapOf>() diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContextImpl.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContextImpl.kt index 0fc696c0980..c805b91b505 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContextImpl.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContextImpl.kt @@ -25,6 +25,9 @@ class WasmModuleCodegenContextImpl( override val scratchMemAddr: WasmSymbol get() = wasmFragment.scratchMemAddr + override val scratchMemSizeInBytes: Int + get() = wasmFragment.scratchMemSizeInBytes + override fun transformType(irType: IrType): WasmType { return with(typeTransformer) { irType.toWasmValueType() } } diff --git a/compiler/testData/codegen/boxWasmJsInterop/longStrings.kt b/compiler/testData/codegen/boxWasmJsInterop/longStrings.kt new file mode 100644 index 00000000000..2265c5ff7f1 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/longStrings.kt @@ -0,0 +1,19 @@ +// FILE: externals.js + +function id(str) { + return str +} + +// FILE: externals.kt + +external fun id(str: String): String + +fun box(): String { + var x = "1234567890" + for (i in 1 until 20) { + x += x + val stringFromJs = id(x) + if (!stringFromJs.equals(x)) return "FAIL" + } + return "OK" +} \ No newline at end of file diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java index 4365ac664aa..b30c2e02bab 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java @@ -55,6 +55,12 @@ public class IrCodegenWasmJsInteropJsTestGenerated extends AbstractIrCodegenWasm runTest("compiler/testData/codegen/boxWasmJsInterop/jsExport.kt"); } + @Test + @TestMetadata("longStrings.kt") + public void testLongStrings() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/longStrings.kt"); + } + @Test @TestMetadata("types.kt") public void testTypes() throws Exception { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java index 1551e8b9504..5b98157d273 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java @@ -55,6 +55,11 @@ public class IrCodegenWasmJsInteropWasmTestGenerated extends AbstractIrCodegenWa runTest("compiler/testData/codegen/boxWasmJsInterop/jsExport.kt"); } + @TestMetadata("longStrings.kt") + public void testLongStrings() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/longStrings.kt"); + } + @TestMetadata("nullableExternRefs.kt") public void testNullableExternRefs() throws Exception { runTest("compiler/testData/codegen/boxWasmJsInterop/nullableExternRefs.kt"); diff --git a/libraries/stdlib/wasm/builtins/kotlin/String.kt b/libraries/stdlib/wasm/builtins/kotlin/String.kt index 83cd884b8fe..2b45996ce9b 100644 --- a/libraries/stdlib/wasm/builtins/kotlin/String.kt +++ b/libraries/stdlib/wasm/builtins/kotlin/String.kt @@ -91,4 +91,8 @@ public class String private constructor(internal val chars: WasmCharArray) : Com } } -internal fun stringLiteral(startAddr: Int, length: Int) = String.unsafeFromCharArray(unsafeRawMemoryToWasmCharArray(startAddr, length)) +internal fun stringLiteral(startAddr: Int, length: Int): String { + val array = WasmCharArray(length) + unsafeRawMemoryToWasmCharArray(startAddr, 0, length, array) + return String.unsafeFromCharArray(array) +} diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt index ff5a5558396..7efda871db4 100644 --- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt +++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt @@ -138,49 +138,87 @@ internal fun anyToExternRef(x: Any): ExternalInterfaceType { x.asWasmExternRef() } -@JsFun("""(addr) => { - const mem16 = new Uint16Array(wasmExports.memory.buffer); - const mem32 = new Int32Array(wasmExports.memory.buffer); - const len = mem32[addr / 4]; - const str_start_addr = (addr + 4) / 2; - const slice = mem16.slice(str_start_addr, str_start_addr + len); - return String.fromCharCode.apply(null, slice); -} -""") -internal external fun importStringFromWasm(addr: Int): ExternalInterfaceType - @JsFun("x => x.length") internal external fun stringLength(x: ExternalInterfaceType): Int -internal fun convertJsStringToKotlinString(x: ExternalInterfaceType): String { - val length = stringLength(x) - val addr = unsafeGetScratchRawMemory(INT_SIZE_BYTES + length * CHAR_SIZE_BYTES) - jsWriteStringIntoMemory(x, addr) - return String.unsafeFromCharArray(unsafeRawMemoryToWasmCharArray(addr, length)) +// kotlin string to js string export +// TODO Uint16Array may work with byte endian different with Wasm (i.e. little endian) +@JsFun("""(address, length, prefix) => { + const mem16 = new Uint16Array(wasmExports.memory.buffer, address, length); + const str = String.fromCharCode.apply(null, mem16); + return (prefix == null) ? str : prefix + str; } - - -//language=js -@JsFun( -""" (str, addr) => { - const memory = new DataView(wasmExports.memory.buffer); - for (var i = 0; i < str.length; i++) { - memory.setInt16(addr + i * 2, str.charCodeAt(i), true); - } -} -""" -) -internal external fun jsWriteStringIntoMemory(str: ExternalInterfaceType, addr: Int) - +""") +internal external fun importStringFromWasm(address: Int, length: Int, prefix: ExternalInterfaceType?): ExternalInterfaceType internal fun kotlinToJsStringAdapter(x: String?): ExternalInterfaceType? { // Using nullable String to represent default value // for parameters with default values - if (x == null) - return null - return importStringFromWasm(exportString(x)) + if (x == null) return null + if (x.isEmpty()) return jsEmptyString() + + val srcArray = x.chars + val stringLength = srcArray.len() + val maxStringLength = unsafeGetScratchRawMemorySize() / CHAR_SIZE_BYTES + + val memBuffer = unsafeGetScratchRawMemory(stringLength.coerceAtMost(maxStringLength) * CHAR_SIZE_BYTES) + + var result: ExternalInterfaceType? = null + var srcStartIndex = 0 + while (srcStartIndex < stringLength - maxStringLength) { + unsafeWasmCharArrayToRawMemory(srcArray, srcStartIndex, maxStringLength, memBuffer) + result = importStringFromWasm(memBuffer, maxStringLength, result) + srcStartIndex += maxStringLength + } + + unsafeWasmCharArrayToRawMemory(srcArray, srcStartIndex, stringLength - srcStartIndex, memBuffer) + return importStringFromWasm(memBuffer, stringLength - srcStartIndex, result) } +// js string to kotlin string import +// TODO Uint16Array may work with byte endian different with Wasm (i.e. little endian) +//language=js +@JsFun( + """ (src, srcOffset, srcLength, dstAddr) => { + const mem16 = new Uint16Array(wasmExports.memory.buffer, dstAddr, srcLength); + let arrayIndex = 0; + let srcIndex = srcOffset; + while (arrayIndex < srcLength) { + mem16.set([src.charCodeAt(srcIndex)], arrayIndex); + srcIndex++; + arrayIndex++; + } + } +""" +) +internal external fun jsExportStringToWasm(src: ExternalInterfaceType, srcOffset: Int, srcLength: Int, dstAddr: Int) + +internal fun jsToKotlinStringAdapter(x: ExternalInterfaceType): String { + val stringLength = stringLength(x) + val dstArray = WasmCharArray(stringLength) + if (stringLength == 0) { + return String.unsafeFromCharArray(dstArray) + } + val maxStringLength = unsafeGetScratchRawMemorySize() / CHAR_SIZE_BYTES + + val memBuffer = unsafeGetScratchRawMemory(stringLength.coerceAtMost(maxStringLength) * CHAR_SIZE_BYTES) + + var srcStartIndex = 0 + while (srcStartIndex < stringLength - maxStringLength) { + jsExportStringToWasm(x, srcStartIndex, maxStringLength, memBuffer) + unsafeRawMemoryToWasmCharArray(memBuffer, srcStartIndex, maxStringLength, dstArray) + srcStartIndex += maxStringLength + } + + jsExportStringToWasm(x, srcStartIndex, stringLength - srcStartIndex, memBuffer) + unsafeRawMemoryToWasmCharArray(memBuffer, srcStartIndex, stringLength - srcStartIndex, dstArray) + return String.unsafeFromCharArray(dstArray) +} + + +@JsFun("() => ''") +internal external fun jsEmptyString(): ExternalInterfaceType + @JsFun("() => true") internal external fun jsTrue(): ExternalInterfaceType @@ -196,9 +234,6 @@ internal fun kotlinToJsAnyAdapter(x: Any): ExternalInterfaceType = internal fun jsToKotlinAnyAdapter(x: ExternalInterfaceType): Any? = externRefToAny(x) -internal fun jsToKotlinStringAdapter(x: ExternalInterfaceType): String = - convertJsStringToKotlinString(x) - internal fun jsToKotlinByteAdapter(x: Int): Byte = x.toByte() internal fun jsToKotlinShortAdapter(x: Int): Short = x.toShort() internal fun jsToKotlinCharAdapter(x: Int): Char = x.toChar() diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ImportExport.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ImportExport.kt deleted file mode 100644 index c43082c8fcc..00000000000 --- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ImportExport.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package kotlin.wasm.internal - -// This is called when exported function returns a string. It writes [i32 length, [i16 chars ...]] into a temporary raw memory area and -// returns pointer to the start of it. -// Note: currently there is a single temporary raw memory area so it's not possible to export more than one string at a time. -internal fun exportString(src: String?): Int { - if (src == null) - throw IllegalArgumentException("Exporting null string") - - val retAddr = unsafeGetScratchRawMemory(INT_SIZE_BYTES + src.length * CHAR_SIZE_BYTES) - wasm_i32_store(retAddr, src.length) - unsafeWasmCharArrayToRawMemory(src.chars, retAddr + INT_SIZE_BYTES) - return retAddr -} - -// See importStringToJs for the JS-side import for strings \ No newline at end of file diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt index cd71e09f55d..aec46c3cf3d 100644 --- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt +++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt @@ -6,14 +6,17 @@ package kotlin.wasm.internal internal const val CHAR_SIZE_BYTES = 2 -internal const val INT_SIZE_BYTES = 4 -internal fun unsafeRawMemoryToChar(addr: Int) = wasm_i32_load16_u(addr).toChar() - -internal fun unsafeRawMemoryToWasmCharArray(startAddr: Int, length: Int): WasmCharArray { - val result = WasmCharArray(length) - result.fill(length) { unsafeRawMemoryToChar(startAddr + it * CHAR_SIZE_BYTES) } - return result +internal fun unsafeRawMemoryToWasmCharArray(srcAddr: Int, dstOffset: Int, dstLength: Int, dst: WasmCharArray) { + var curAddr = srcAddr + val srcAddrEndOffset = srcAddr + dstLength * CHAR_SIZE_BYTES + var dstIndex = dstOffset + while (curAddr < srcAddrEndOffset) { + val char = wasm_i32_load16_u(curAddr).toChar() + dst.set(dstIndex, char) + curAddr += CHAR_SIZE_BYTES + dstIndex++ + } } // Returns a pointer into a temporary scratch segment in the raw wasm memory. Aligned by 4. @@ -22,14 +25,19 @@ internal fun unsafeRawMemoryToWasmCharArray(startAddr: Int, length: Int): WasmCh internal fun unsafeGetScratchRawMemory(sizeBytes: Int): Int = implementedAsIntrinsic +@ExcludedFromCodegen +internal fun unsafeGetScratchRawMemorySize(): Int = + implementedAsIntrinsic + // Assumes there is enough space at the destination, fails with wasm trap otherwise. -internal fun unsafeWasmCharArrayToRawMemory(src: WasmCharArray, dstAddr: Int) { +internal fun unsafeWasmCharArrayToRawMemory(src: WasmCharArray, srcOffset: Int, srcLength: Int, dstAddr: Int) { var curAddr = dstAddr - var i = 0 - while (i < src.len()) { - wasm_i32_store16(curAddr, src.get(i)) + val srcEndOffset = srcOffset + srcLength + var srcIndex = srcOffset + while (srcIndex < srcEndOffset) { + wasm_i32_store16(curAddr, src.get(srcIndex)) curAddr += CHAR_SIZE_BYTES - i++ + srcIndex++ } }