[Wasm] Add @WasmImport annotation
Imports top-level function from given module
This commit is contained in:
@@ -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.common.serialization.linkerissues.checkNoUnboundSymbols
|
||||||
import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmCompiledModuleFragment
|
import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmCompiledModuleFragment
|
||||||
import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmModuleFragmentGenerator
|
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.backend.wasm.lower.markExportedDeclarations
|
||||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||||
import org.jetbrains.kotlin.ir.backend.js.MainModule
|
import org.jetbrains.kotlin.ir.backend.js.MainModule
|
||||||
@@ -179,12 +180,28 @@ fun WasmCompiledModuleFragment.generateJs(): String {
|
|||||||
return ifNotCached;
|
return ifNotCached;
|
||||||
} """.trimIndent()
|
} """.trimIndent()
|
||||||
|
|
||||||
val jsCodeBody = jsFuns.joinToString(",\n") { "\"" + it.importName + "\" : " + it.jsCode }
|
val imports = jsModuleImports
|
||||||
val jsCodeBodyIndented = jsCodeBody.prependIndent(" ")
|
.toList()
|
||||||
val jsCode =
|
.sorted()
|
||||||
"\nconst js_code = {\n$jsCodeBodyIndented\n};\n"
|
.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 =
|
fun generateJsWasmLoader(wasmFilePath: String, externalJs: String): String =
|
||||||
@@ -210,17 +227,17 @@ fun generateJsWasmLoader(wasmFilePath: String, externalJs: String): String =
|
|||||||
const dirpath = path.dirname(filepath);
|
const dirpath = path.dirname(filepath);
|
||||||
const wasmBuffer = fs.readFileSync(path.resolve(dirpath, '$wasmFilePath'));
|
const wasmBuffer = fs.readFileSync(path.resolve(dirpath, '$wasmFilePath'));
|
||||||
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
||||||
wasmInstance = new WebAssembly.Instance(wasmModule, { js_code });
|
wasmInstance = new WebAssembly.Instance(wasmModule, _import_object);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isD8) {
|
if (isD8) {
|
||||||
const wasmBuffer = read('$wasmFilePath', 'binary');
|
const wasmBuffer = read('$wasmFilePath', 'binary');
|
||||||
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
||||||
wasmInstance = new WebAssembly.Instance(wasmModule, { js_code });
|
wasmInstance = new WebAssembly.Instance(wasmModule, _import_object);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
wasmInstance = (await WebAssembly.instantiateStreaming(fetch('$wasmFilePath'), { js_code })).instance;
|
wasmInstance = (await WebAssembly.instantiateStreaming(fetch('$wasmFilePath'), _import_object)).instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasmExports = wasmInstance.exports;
|
const wasmExports = wasmInstance.exports;
|
||||||
|
|||||||
+22
-5
@@ -67,11 +67,28 @@ class DeclarationGenerator(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val jsCode = declaration.getJsFunAnnotation() ?: if (declaration.isExternal) declaration.name.asString() else null
|
val wasmImportModule = declaration.getWasmImportDescriptor()
|
||||||
val importedName = jsCode?.let {
|
|
||||||
val jsCodeName = jsCodeName(declaration)
|
val jsCode = declaration.getJsFunAnnotation()
|
||||||
context.addJsFun(jsCodeName, it)
|
// TODO: Why are we importing declarations by with raw declaration.name.asString() jsCode?
|
||||||
WasmImportPair("js_code", jsCodeName(declaration))
|
?: 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)
|
if (declaration.isFakeOverride)
|
||||||
|
|||||||
+3
-13
@@ -5,17 +5,7 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.backend.wasm.ir2wasm
|
package org.jetbrains.kotlin.backend.wasm.ir2wasm
|
||||||
|
|
||||||
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.jsAssignment
|
import org.jetbrains.kotlin.js.backend.JsToStringGenerationVisitor
|
||||||
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
|
|
||||||
|
|
||||||
fun generateStringLiteralsSupport(literals: List<String>): String {
|
fun String.toJsStringLiteral(): CharSequence =
|
||||||
return JsBlock(
|
JsToStringGenerationVisitor.javaScriptString(this)
|
||||||
jsAssignment(
|
|
||||||
JsNameRef("stringLiterals", "runtime"),
|
|
||||||
JsArrayLiteral(literals.map { JsStringLiteral(it) })
|
|
||||||
).makeStmt()
|
|
||||||
).toString()
|
|
||||||
}
|
|
||||||
+1
@@ -58,6 +58,7 @@ class WasmCompiledModuleFragment(val irBuiltIns: IrBuiltIns) {
|
|||||||
class JsCodeSnippet(val importName: String, val jsCode: String)
|
class JsCodeSnippet(val importName: String, val jsCode: String)
|
||||||
|
|
||||||
val jsFuns = mutableListOf<JsCodeSnippet>()
|
val jsFuns = mutableListOf<JsCodeSnippet>()
|
||||||
|
val jsModuleImports = mutableSetOf<String>()
|
||||||
|
|
||||||
class FunWithPriority(val function: WasmFunction, val priority: String)
|
class FunWithPriority(val function: WasmFunction, val priority: String)
|
||||||
|
|
||||||
|
|||||||
+4
@@ -198,5 +198,9 @@ class WasmModuleCodegenContext(
|
|||||||
wasmFragment.jsFuns +=
|
wasmFragment.jsFuns +=
|
||||||
WasmCompiledModuleFragment.JsCodeSnippet(importName = importName, jsCode = jsCode)
|
WasmCompiledModuleFragment.JsCodeSnippet(importName = importName, jsCode = jsCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addJsModuleImport(module: String) {
|
||||||
|
wasmFragment.jsModuleImports += module
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
|
|||||||
import org.jetbrains.kotlin.descriptors.ClassKind
|
import org.jetbrains.kotlin.descriptors.ClassKind
|
||||||
import org.jetbrains.kotlin.ir.IrElement
|
import org.jetbrains.kotlin.ir.IrElement
|
||||||
import org.jetbrains.kotlin.backend.wasm.utils.getJsFunAnnotation
|
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.UNDEFINED_OFFSET
|
||||||
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
|
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
|
||||||
import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget
|
import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget
|
||||||
@@ -175,6 +176,11 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun processExternalSimpleFunction(function: IrSimpleFunction) {
|
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()
|
val jsFun = function.getJsFunAnnotation()
|
||||||
// Wrap external functions without @JsFun to lambdas `foo` -> `(a, b) => foo(a, b)`.
|
// 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.
|
// This way we wouldn't fail if we don't call them.
|
||||||
|
|||||||
@@ -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.backend.js.utils.getSingleConstStringArgument
|
||||||
import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer
|
import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer
|
||||||
import org.jetbrains.kotlin.ir.declarations.IrClass
|
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.IrClassReference
|
||||||
import org.jetbrains.kotlin.ir.expressions.IrConst
|
import org.jetbrains.kotlin.ir.expressions.IrConst
|
||||||
import org.jetbrains.kotlin.ir.types.makeNullable
|
import org.jetbrains.kotlin.ir.types.makeNullable
|
||||||
@@ -20,6 +21,17 @@ import org.jetbrains.kotlin.wasm.ir.WasmImportPair
|
|||||||
fun IrAnnotationContainer.hasExcludedFromCodegenAnnotation(): Boolean =
|
fun IrAnnotationContainer.hasExcludedFromCodegenAnnotation(): Boolean =
|
||||||
hasAnnotation(FqName("kotlin.wasm.internal.ExcludedFromCodegen"))
|
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<String>).value,
|
||||||
|
(annotation.getValueArgument(1) as? IrConst<String>)?.value ?: this.name.asString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun IrAnnotationContainer.getWasmOpAnnotation(): String? =
|
fun IrAnnotationContainer.getWasmOpAnnotation(): String? =
|
||||||
getAnnotation(FqName("kotlin.wasm.internal.WasmOp"))?.getSingleConstStringArgument()
|
getAnnotation(FqName("kotlin.wasm.internal.WasmOp"))?.getSingleConstStringArgument()
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -67,6 +67,7 @@ abstract class BasicWasmBoxTest(
|
|||||||
val kotlinFiles = mutableListOf<String>()
|
val kotlinFiles = mutableListOf<String>()
|
||||||
val jsFilesBefore = mutableListOf<String>()
|
val jsFilesBefore = mutableListOf<String>()
|
||||||
val jsFilesAfter = mutableListOf<String>()
|
val jsFilesAfter = mutableListOf<String>()
|
||||||
|
val mjsFiles = mutableListOf<String>()
|
||||||
|
|
||||||
inputFiles.forEach {
|
inputFiles.forEach {
|
||||||
val name = it.fileName
|
val name = it.fileName
|
||||||
@@ -79,6 +80,9 @@ abstract class BasicWasmBoxTest(
|
|||||||
|
|
||||||
name.endsWith(".js") ->
|
name.endsWith(".js") ->
|
||||||
jsFilesBefore += name
|
jsFilesBefore += name
|
||||||
|
|
||||||
|
name.endsWith(".mjs") ->
|
||||||
|
mjsFiles += name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +193,9 @@ abstract class BasicWasmBoxTest(
|
|||||||
println(" ------ $name Test file://$path/test.mjs")
|
println(" ------ $name Test file://$path/test.mjs")
|
||||||
val projectName = "kotlin"
|
val projectName = "kotlin"
|
||||||
println(" ------ $name HTML http://0.0.0.0:63342/$projectName/${dir.path}/index.html")
|
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(
|
File(dir, "index.html").writeText(
|
||||||
"""
|
"""
|
||||||
@@ -216,6 +223,12 @@ abstract class BasicWasmBoxTest(
|
|||||||
|
|
||||||
writeCompilationResult(res, dir, "index", sourceMapFileName = null)
|
writeCompilationResult(res, dir, "index", sourceMapFileName = null)
|
||||||
File(dir, "test.mjs").writeText(testJs)
|
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"))
|
ExternalTool(System.getProperty("javascript.engine.path.V8"))
|
||||||
.run(
|
.run(
|
||||||
"--experimental-wasm-gc",
|
"--experimental-wasm-gc",
|
||||||
|
|||||||
+5
@@ -84,4 +84,9 @@ public class IrCodegenWasmJsInteropWasmTestGenerated extends AbstractIrCodegenWa
|
|||||||
public void testTypes() throws Exception {
|
public void testTypes() throws Exception {
|
||||||
runTest("compiler/testData/codegen/boxWasmJsInterop/types.kt");
|
runTest("compiler/testData/codegen/boxWasmJsInterop/types.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("wasmImport.kt")
|
||||||
|
public void testWasmImport() throws Exception {
|
||||||
|
runTest("compiler/testData/codegen/boxWasmJsInterop/wasmImport.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 = ""
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user