[Wasm] Support JsModule and JsQualifier

This commit is contained in:
Svyatoslav Kuzmich
2023-01-10 21:49:51 +01:00
committed by Space Team
parent 75d3ae4466
commit d14d4c8510
18 changed files with 669 additions and 31 deletions
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.backend.wasm
import org.jetbrains.kotlin.backend.common.ir.Ir
import org.jetbrains.kotlin.backend.common.ir.Symbols
import org.jetbrains.kotlin.backend.wasm.ir2wasm.JsModuleAndQualifierReference
import org.jetbrains.kotlin.backend.wasm.lower.WasmSharedVariablesManager
import org.jetbrains.kotlin.backend.wasm.utils.WasmInlineClassesUtils
import org.jetbrains.kotlin.config.CompilerConfiguration
@@ -23,7 +24,6 @@ import org.jetbrains.kotlin.ir.declarations.impl.IrExternalPackageFragmentImpl
import org.jetbrains.kotlin.ir.declarations.impl.IrFileImpl
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
import org.jetbrains.kotlin.ir.symbols.impl.DescriptorlessExternalPackageFragmentSymbol
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.IrSimpleType
import org.jetbrains.kotlin.ir.types.IrTypeSystemContext
import org.jetbrains.kotlin.ir.types.IrTypeSystemContextImpl
@@ -63,6 +63,9 @@ class WasmBackendContext(
val jsClosureCallers = mutableMapOf<IrSimpleType, IrSimpleFunction>()
val jsToKotlinClosures = mutableMapOf<IrSimpleType, IrSimpleFunction>()
val jsModuleAndQualifierReferences =
mutableSetOf<JsModuleAndQualifierReference>()
override val coroutineSymbols =
JsCommonCoroutineSymbols(symbolTable, module,this)
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.backend.wasm
import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
import org.jetbrains.kotlin.backend.common.phaser.invokeToplevel
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.checkNoUnboundSymbols
import org.jetbrains.kotlin.backend.wasm.ir2wasm.JsModuleAndQualifierReference
import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmCompiledModuleFragment
import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmModuleFragmentGenerator
import org.jetbrains.kotlin.backend.wasm.ir2wasm.toJsStringLiteral
@@ -107,7 +108,10 @@ fun compileWasm(
null
}
val jsUninstantiatedWrapper = compiledWasmModule.generateAsyncJsWrapper("./$baseFileName.wasm")
val jsUninstantiatedWrapper = compiledWasmModule.generateAsyncJsWrapper(
"./$baseFileName.wasm",
backendContext.jsModuleAndQualifierReferences
)
val jsWrapper = generateEsmExportsWrapper("./$baseFileName.uninstantiated.mjs")
val os = ByteArrayOutputStream()
@@ -172,43 +176,74 @@ private fun generateSourceMap(
return sourceMapBuilder.build()
}
fun WasmCompiledModuleFragment.generateAsyncJsWrapper(wasmFilePath: String): String {
fun WasmCompiledModuleFragment.generateAsyncJsWrapper(
wasmFilePath: String,
jsModuleAndQualifierReferences: Set<JsModuleAndQualifierReference>
): String {
val jsCodeBody = jsFuns.joinToString(",\n") {
"${it.importName.toJsStringLiteral()} : ${it.jsCode}"
}
val jsCodeBodyIndented = jsCodeBody.prependIndent(" ")
val jsCodeBodyIndented = jsCodeBody.prependIndent(" ")
val imports = jsModuleImports
.toList()
.sorted()
.joinToString("") {
val moduleSpecifier = it.toJsStringLiteral()
" $moduleSpecifier: imports[$moduleSpecifier] ?? await import($moduleSpecifier),\n"
" $moduleSpecifier: await _importModule($moduleSpecifier),\n"
}
val referencesToQualifiedAndImportedDeclarations = jsModuleAndQualifierReferences
.map {
val module = it.module
val qualifier = it.qualifier
buildString {
append(" const ")
append(it.jsVariableName)
append(" = ")
if (module != null) {
append("(await _importModule(${module.toJsStringLiteral()}))")
if (qualifier != null)
append(".")
}
if (qualifier != null) {
append(qualifier)
}
append(";")
}
}.sorted()
.joinToString("\n")
//language=js
return """
const externrefBoxes = new WeakMap();
// ref must be non-null
function tryGetOrSetExternrefBox(ref, ifNotCached) {
if (typeof ref !== 'object') return ifNotCached;
const cachedBox = externrefBoxes.get(ref);
if (cachedBox !== void 0) return cachedBox;
externrefBoxes.set(ref, ifNotCached);
return ifNotCached;
}
const js_code = {
$jsCodeBodyIndented
}
// Placed here to give access to it from externals (js_code)
let wasmInstance;
let require;
let wasmExports;
export async function instantiate(imports={}, runInitializer=true) {
const externrefBoxes = new WeakMap();
// ref must be non-null
function tryGetOrSetExternrefBox(ref, ifNotCached) {
if (typeof ref !== 'object') return ifNotCached;
const cachedBox = externrefBoxes.get(ref);
if (cachedBox !== void 0) return cachedBox;
externrefBoxes.set(ref, ifNotCached);
return ifNotCached;
}
async function _importModule(x) {
return imports[x] ?? await import(x);
}
$referencesToQualifiedAndImportedDeclarations
const js_code = {
$jsCodeBodyIndented
}
// Placed here to give access to it from externals (js_code)
let wasmInstance;
let require;
let wasmExports;
const isNodeJs = (typeof process !== 'undefined') && (process.release.name === 'node');
const isD8 = !isNodeJs && (typeof d8 !== 'undefined');
const isBrowser = !isNodeJs && !isD8 && (typeof window !== 'undefined');
@@ -6,6 +6,20 @@
package org.jetbrains.kotlin.backend.wasm.ir2wasm
import org.jetbrains.kotlin.js.backend.JsToStringGenerationVisitor
import java.util.Base64
fun String.toJsStringLiteral(): CharSequence =
JsToStringGenerationVisitor.javaScriptString(this)
JsToStringGenerationVisitor.javaScriptString(this)
data class JsModuleAndQualifierReference(
val module: String?,
val qualifier: String?,
) {
val jsVariableName = run {
// Encode variable name as base64 to have a valid unique JS identifier
val encoder = Base64.getEncoder().withoutPadding()
val moduleBase64 = module?.let { encoder.encodeToString(module.encodeToByteArray()) }.orEmpty()
val qualifierBase64 = qualifier?.let { encoder.encodeToString(qualifier.encodeToByteArray()) }.orEmpty()
"_ref_${moduleBase64}_$qualifierBase64"
}
}
@@ -8,12 +8,15 @@ package org.jetbrains.kotlin.backend.wasm.lower
import org.jetbrains.kotlin.backend.common.FileLoweringPass
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
import org.jetbrains.kotlin.backend.wasm.ir2wasm.JsModuleAndQualifierReference
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.getJsModule
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
import org.jetbrains.kotlin.ir.backend.js.utils.getJsQualifier
import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget
import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter
import org.jetbrains.kotlin.ir.builders.declarations.buildFun
@@ -108,7 +111,7 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa
val dispatchReceiver = getter.dispatchReceiverParameter
val jsCode =
if (dispatchReceiver == null)
"() => $propName"
"() => ${referenceTopLevelExternalDeclaration(property)}"
else
"(_this) => _this.$propName"
@@ -130,7 +133,7 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa
val dispatchReceiver = setter.dispatchReceiverParameter
val jsCode =
if (dispatchReceiver == null)
"(v) => $propName = v"
"(v) => ${referenceTopLevelExternalDeclaration(property)} = v"
else
"(_this, v) => _this.$propName = v"
@@ -159,12 +162,19 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa
return
}
append('.')
append(klass.getJsNameOrKotlinName())
} else {
append(referenceTopLevelExternalDeclaration(klass))
}
append(klass.getJsNameOrKotlinName())
}
fun processExternalConstructor(constructor: IrConstructor) {
val klass = constructor.constructedClass
// External interfaces can have synthetic primary constructors in K/JS
if (klass.isInterface)
return
processFunctionOrConstructor(
function = constructor,
name = klass.name,
@@ -183,14 +193,26 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa
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.
if (jsFun != null && function.valueParameters.all { it.defaultValue == null && it.varargElementType == null })
if (jsFun != null &&
function.valueParameters.all { it.defaultValue == null && it.varargElementType == null } &&
currentFile.getJsQualifier() == null &&
currentFile.getJsModule() == null
) {
return
}
val jsFunctionReference = when {
jsFun != null -> "($jsFun)"
function.isTopLevelDeclaration -> referenceTopLevelExternalDeclaration(function)
else -> function.getJsNameOrKotlinName().identifier
}
processFunctionOrConstructor(
function = function,
name = function.name,
returnType = function.returnType,
isConstructor = false,
jsFunctionReference = jsFun?.let { "($it)" } ?: function.getJsNameOrKotlinName().identifier
jsFunctionReference = jsFunctionReference
)
}
@@ -341,6 +363,25 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa
addedDeclarations += res
return res
}
private fun referenceTopLevelExternalDeclaration(declaration: IrDeclarationWithName): String {
var name = declaration.getJsNameOrKotlinName().identifier
val qualifier = currentFile.getJsQualifier()
val module = currentFile.getJsModule()
?: declaration.getJsModule()?.also {
// JsModule on top level declarations imports "default"
name = "default"
}
if (qualifier == null && module == null)
return name
val qualifieReference = JsModuleAndQualifierReference(module, qualifier)
context.jsModuleAndQualifierReferences += qualifieReference
return qualifieReference.jsVariableName + "." + name
}
}
/**