From 3bbd8c291a78804dee738e7e40d5acdda67b17bc Mon Sep 17 00:00:00 2001 From: Svyatoslav Kuzmich Date: Mon, 19 Dec 2022 14:45:56 +0100 Subject: [PATCH] [Wasm] Add @WasmImport annotation Imports top-level function from given module --- .../jetbrains/kotlin/backend/wasm/compiler.kt | 33 +++++++--- .../wasm/ir2wasm/DeclarationGenerator.kt | 27 ++++++-- .../kotlin/backend/wasm/ir2wasm/JsHelpers.kt | 16 +---- .../ir2wasm/WasmCompiledModuleFragment.kt | 1 + .../wasm/ir2wasm/WasmModuleCodegenContext.kt | 4 ++ ...DeclarationsToTopLevelFunctionsLowering.kt | 6 ++ .../kotlin/backend/wasm/utils/Annotations.kt | 12 ++++ .../codegen/boxWasmJsInterop/wasmImport.kt | 66 +++++++++++++++++++ .../kotlin/js/testOld/BasicWasmBoxTest.kt | 13 ++++ ...CodegenWasmJsInteropWasmTestGenerated.java | 5 ++ .../wasm/src/kotlin/wasm/Annotations.kt | 21 ++++++ 11 files changed, 178 insertions(+), 26 deletions(-) create mode 100644 compiler/testData/codegen/boxWasmJsInterop/wasmImport.kt create mode 100644 libraries/stdlib/wasm/src/kotlin/wasm/Annotations.kt diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt index 3ff307cb699..8a51fb54a80 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt @@ -10,6 +10,7 @@ import org.jetbrains.kotlin.backend.common.phaser.invokeToplevel import org.jetbrains.kotlin.backend.common.serialization.linkerissues.checkNoUnboundSymbols import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmCompiledModuleFragment import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmModuleFragmentGenerator +import org.jetbrains.kotlin.backend.wasm.ir2wasm.toJsStringLiteral import org.jetbrains.kotlin.backend.wasm.lower.markExportedDeclarations import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.ir.backend.js.MainModule @@ -179,12 +180,28 @@ fun WasmCompiledModuleFragment.generateJs(): String { return ifNotCached; } """.trimIndent() - val jsCodeBody = jsFuns.joinToString(",\n") { "\"" + it.importName + "\" : " + it.jsCode } - val jsCodeBodyIndented = jsCodeBody.prependIndent(" ") - val jsCode = - "\nconst js_code = {\n$jsCodeBodyIndented\n};\n" + val imports = jsModuleImports + .toList() + .sorted() + .joinToString("\n") { + val moduleSpecifier = it.toJsStringLiteral() + " $moduleSpecifier: await import($moduleSpecifier)," + } - return runtime + jsCode + val jsCodeBody = jsFuns.joinToString(",\n") { + "${it.importName.toJsStringLiteral()} : ${it.jsCode}" + } + val jsCodeBodyIndented = jsCodeBody.prependIndent(" ") + val importObject = """ +const _import_object = { +${imports} + js_code: { +${jsCodeBodyIndented} + } +}; +""" + + return runtime + importObject } fun generateJsWasmLoader(wasmFilePath: String, externalJs: String): String = @@ -210,17 +227,17 @@ fun generateJsWasmLoader(wasmFilePath: String, externalJs: String): String = const dirpath = path.dirname(filepath); const wasmBuffer = fs.readFileSync(path.resolve(dirpath, '$wasmFilePath')); const wasmModule = new WebAssembly.Module(wasmBuffer); - wasmInstance = new WebAssembly.Instance(wasmModule, { js_code }); + wasmInstance = new WebAssembly.Instance(wasmModule, _import_object); } if (isD8) { const wasmBuffer = read('$wasmFilePath', 'binary'); const wasmModule = new WebAssembly.Module(wasmBuffer); - wasmInstance = new WebAssembly.Instance(wasmModule, { js_code }); + wasmInstance = new WebAssembly.Instance(wasmModule, _import_object); } if (isBrowser) { - wasmInstance = (await WebAssembly.instantiateStreaming(fetch('$wasmFilePath'), { js_code })).instance; + wasmInstance = (await WebAssembly.instantiateStreaming(fetch('$wasmFilePath'), _import_object)).instance; } const wasmExports = wasmInstance.exports; diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt index dc00e28a80d..65bfdd26479 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt @@ -67,11 +67,28 @@ class DeclarationGenerator( return } - val jsCode = declaration.getJsFunAnnotation() ?: if (declaration.isExternal) declaration.name.asString() else null - val importedName = jsCode?.let { - val jsCodeName = jsCodeName(declaration) - context.addJsFun(jsCodeName, it) - WasmImportPair("js_code", jsCodeName(declaration)) + val wasmImportModule = declaration.getWasmImportDescriptor() + + val jsCode = declaration.getJsFunAnnotation() + // TODO: Why are we importing declarations by with raw declaration.name.asString() jsCode? + ?: if (declaration.isExternal && wasmImportModule == null) declaration.name.asString() else null + + val importedName = when { + wasmImportModule != null -> { + check(declaration.isExternal) { "Non-external fun with @WasmImport ${declaration.fqNameWhenAvailable}"} + context.addJsModuleImport(wasmImportModule.moduleName) + wasmImportModule + } + jsCode != null -> { + // TODO: check consistency (currently fails with generated __convertKotlinClosureToJsClosure) + // check(declaration.isExternal) { "Non-external fun with @JsFun ${declaration.fqNameWhenAvailable}"} + val jsCodeName = jsCodeName(declaration) + context.addJsFun(jsCodeName, jsCode) + WasmImportPair("js_code", jsCodeName(declaration)) + } + else -> { + null + } } if (declaration.isFakeOverride) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/JsHelpers.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/JsHelpers.kt index ce94c92dbf6..fbb064c1101 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/JsHelpers.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/JsHelpers.kt @@ -5,17 +5,7 @@ package org.jetbrains.kotlin.backend.wasm.ir2wasm -import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.jsAssignment -import org.jetbrains.kotlin.js.backend.ast.JsArrayLiteral -import org.jetbrains.kotlin.js.backend.ast.JsBlock -import org.jetbrains.kotlin.js.backend.ast.JsNameRef -import org.jetbrains.kotlin.js.backend.ast.JsStringLiteral +import org.jetbrains.kotlin.js.backend.JsToStringGenerationVisitor -fun generateStringLiteralsSupport(literals: List): String { - return JsBlock( - jsAssignment( - JsNameRef("stringLiterals", "runtime"), - JsArrayLiteral(literals.map { JsStringLiteral(it) }) - ).makeStmt() - ).toString() -} \ No newline at end of file +fun String.toJsStringLiteral(): CharSequence = + JsToStringGenerationVisitor.javaScriptString(this) \ No newline at end of file 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 3b12b3f7607..55621c9781a 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 @@ -58,6 +58,7 @@ class WasmCompiledModuleFragment(val irBuiltIns: IrBuiltIns) { class JsCodeSnippet(val importName: String, val jsCode: String) val jsFuns = mutableListOf() + val jsModuleImports = mutableSetOf() class FunWithPriority(val function: WasmFunction, val priority: String) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContext.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContext.kt index 44559deda3a..4553f97cb22 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContext.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContext.kt @@ -198,5 +198,9 @@ class WasmModuleCodegenContext( wasmFragment.jsFuns += WasmCompiledModuleFragment.JsCodeSnippet(importName = importName, jsCode = jsCode) } + + fun addJsModuleImport(module: String) { + wasmFragment.jsModuleImports += module + } } diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt index 9ed824cdc8c..1d2bc31494f 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt @@ -12,6 +12,7 @@ import org.jetbrains.kotlin.backend.wasm.WasmBackendContext import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.backend.wasm.utils.getJsFunAnnotation +import org.jetbrains.kotlin.backend.wasm.utils.getWasmImportDescriptor import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget @@ -175,6 +176,11 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa } fun processExternalSimpleFunction(function: IrSimpleFunction) { + // Skip JS interop adapters form WasmImport. + // It needs to keep original signature to interop with other Wasm modules. + if (function.getWasmImportDescriptor() != null) + return + val jsFun = function.getJsFunAnnotation() // Wrap external functions without @JsFun to lambdas `foo` -> `(a, b) => foo(a, b)`. // This way we wouldn't fail if we don't call them. diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/utils/Annotations.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/utils/Annotations.kt index 1d49e3a7e0d..e7e1ede3cdc 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/utils/Annotations.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/utils/Annotations.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.backend.wasm.utils import org.jetbrains.kotlin.ir.backend.js.utils.getSingleConstStringArgument import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.expressions.IrClassReference import org.jetbrains.kotlin.ir.expressions.IrConst import org.jetbrains.kotlin.ir.types.makeNullable @@ -20,6 +21,17 @@ import org.jetbrains.kotlin.wasm.ir.WasmImportPair fun IrAnnotationContainer.hasExcludedFromCodegenAnnotation(): Boolean = hasAnnotation(FqName("kotlin.wasm.internal.ExcludedFromCodegen")) +fun IrFunction.getWasmImportDescriptor(): WasmImportPair? { + val annotation = getAnnotation(FqName("kotlin.wasm.WasmImport")) + ?: return null + + @Suppress("UNCHECKED_CAST") + return WasmImportPair( + (annotation.getValueArgument(0) as IrConst).value, + (annotation.getValueArgument(1) as? IrConst)?.value ?: this.name.asString() + ) +} + fun IrAnnotationContainer.getWasmOpAnnotation(): String? = getAnnotation(FqName("kotlin.wasm.internal.WasmOp"))?.getSingleConstStringArgument() diff --git a/compiler/testData/codegen/boxWasmJsInterop/wasmImport.kt b/compiler/testData/codegen/boxWasmJsInterop/wasmImport.kt new file mode 100644 index 00000000000..9a802505109 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/wasmImport.kt @@ -0,0 +1,66 @@ +// TARGET_BACKEND: WASM + +// FILE: 1.mjs + +export function add(x, y) { return x + y; } + +export function giveMeFive(x) { + if (x !== 5) + throw "I expected 5"; +} + +// FILE: 2.mjs + +function sub(x, y) { return x - y; }; + +export { sub }; +export { sub as "(˹˻𔗎𝅳𝅵𝅷𝅹⁽₍❨❪⟮﴾︵﹙(⦅󠀨❲❴⟦⟨⟪⟬⦇⦉⦕⸢⸤︗︷︹︻︽︿﹁﹃﹇﹛﹝[{「󠁛󠁻«‘“‹❮" } +export { sub as "~!@#\$%^&*()_+\`-={}|[]\\\\:\\\";'<>?,./" } +export { sub as "" } +export { sub as "\n \r \t" } +export default sub; + +// FILE: wasmImport.kt +import kotlin.wasm.WasmImport + +@WasmImport("./1.mjs", "add") +external fun addImportRenamed(x: Int, y: Int): Int + +@WasmImport("./1.mjs") +external fun giveMeFive(x: Int): Unit // Test unit return type + +@WasmImport("./1.mjs") +external fun add(x: Int, y: Int): Int + +@WasmImport("./2.mjs") +external fun sub(x: Float, y: Float): Float + +@WasmImport("./2.mjs", "(˹˻𔗎𝅳𝅵𝅷𝅹⁽₍❨❪⟮﴾︵﹙(⦅󠀨❲❴⟦⟨⟪⟬⦇⦉⦕⸢⸤︗︷︹︻︽︿﹁﹃﹇﹛﹝[{「󠁛󠁻«‘“‹❮") +external fun sub2(x: Float, y: Float): Float + +@WasmImport("./2.mjs", "~!@#\$%^&*()_+`-={}|[]\\\\:\\\";'<>?,./") +external fun sub3(x: Float, y: Float): Float + +@WasmImport("./2.mjs", "") +external fun sub4(x: Float, y: Float): Float + +@WasmImport("./2.mjs", "\n \r \t") +external fun sub5(x: Float, y: Float): Float + +@WasmImport("./2.mjs", "default") +external fun sub6(x: Float, y: Float): Float + +fun box(): String { + if (addImportRenamed(5, 6) != 11) return "Fail1" + if (add(5, 6) != 11) return "Fail1" + giveMeFive(5) + + if (sub(5f, 6f) != -1f) return "Fail2" + if (sub2(5f, 6f) != -1f) return "Fail3" + if (sub3(5f, 6f) != -1f) return "Fail4" + if (sub4(5f, 6f) != -1f) return "Fail5" + if (sub5(5f, 6f) != -1f) return "Fail6" + if (sub6(5f, 6f) != -1f) return "Fail7" + + return "OK" +} \ No newline at end of file diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/testOld/BasicWasmBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/testOld/BasicWasmBoxTest.kt index af21320de7d..3e612733968 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/testOld/BasicWasmBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/testOld/BasicWasmBoxTest.kt @@ -67,6 +67,7 @@ abstract class BasicWasmBoxTest( val kotlinFiles = mutableListOf() val jsFilesBefore = mutableListOf() val jsFilesAfter = mutableListOf() + val mjsFiles = mutableListOf() inputFiles.forEach { val name = it.fileName @@ -79,6 +80,9 @@ abstract class BasicWasmBoxTest( name.endsWith(".js") -> jsFilesBefore += name + + name.endsWith(".mjs") -> + mjsFiles += name } } @@ -189,6 +193,9 @@ abstract class BasicWasmBoxTest( println(" ------ $name Test file://$path/test.mjs") val projectName = "kotlin" println(" ------ $name HTML http://0.0.0.0:63342/$projectName/${dir.path}/index.html") + for (mjsPath: String in mjsFiles) { + println(" ------ $name External ESM file://$path/${File(mjsPath).name}") + } File(dir, "index.html").writeText( """ @@ -216,6 +223,12 @@ abstract class BasicWasmBoxTest( writeCompilationResult(res, dir, "index", sourceMapFileName = null) File(dir, "test.mjs").writeText(testJs) + + for (mjsPath: String in mjsFiles) { + val mjsFile = File(mjsPath) + File(dir, mjsFile.name).writeText(mjsFile.readText()) + } + ExternalTool(System.getProperty("javascript.engine.path.V8")) .run( "--experimental-wasm-gc", 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 7bc38049002..afb9f051594 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 @@ -84,4 +84,9 @@ public class IrCodegenWasmJsInteropWasmTestGenerated extends AbstractIrCodegenWa public void testTypes() throws Exception { runTest("compiler/testData/codegen/boxWasmJsInterop/types.kt"); } + + @TestMetadata("wasmImport.kt") + public void testWasmImport() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/wasmImport.kt"); + } } diff --git a/libraries/stdlib/wasm/src/kotlin/wasm/Annotations.kt b/libraries/stdlib/wasm/src/kotlin/wasm/Annotations.kt new file mode 100644 index 00000000000..aa1ef991c40 --- /dev/null +++ b/libraries/stdlib/wasm/src/kotlin/wasm/Annotations.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2022 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 + +/** + * Imports a function from the given [module] with the given optional [name]. + * The declaration name will be used if the [name] argument is not provided. + * + * Can only be used on top-level external functions. + * + * In JavaScript environment, + * function will be imported from ES module without type adapters. + */ +@Target(AnnotationTarget.FUNCTION) +public annotation class WasmImport( + val module: String, + val name: String = "" +) \ No newline at end of file