[WASM] Implemented Wasm unhandled exceptions on JS site

This commit is contained in:
Igor Yakovlev
2022-01-21 17:00:30 +01:00
parent e32f9eb2ce
commit 0acb1c0c05
6 changed files with 198 additions and 19 deletions
@@ -530,6 +530,12 @@ private val removeInitializersForLazyProperties = makeWasmModulePhase(
description = "Remove property initializers if they was initialized lazily"
)
private val unhandledExceptionLowering = makeWasmModulePhase(
::UnhandledExceptionLowering,
name = "UnhandledExceptionLowering",
description = "Wrap JsExport functions with try-catch to convert unhandled Wasm exception into Js exception",
)
private val propertyAccessorInlinerLoweringPhase = makeWasmModulePhase(
::PropertyAccessorInlineLowering,
name = "PropertyAccessorInlineLowering",
@@ -608,6 +614,8 @@ val wasmPhases = NamedCompilerPhase(
addContinuationToFunctionCallsLoweringPhase then
addMainFunctionCallsLowering then
unhandledExceptionLowering then
tryCatchCanonicalization then
returnableBlockLoweringPhase then
@@ -65,6 +65,11 @@ class WasmSymbols(
internal val eagerInitialization: IrClassSymbol = getIrClass(FqName("kotlin.EagerInitialization"))
internal val isNotFirstWasmExportCall: IrPropertySymbol = symbolTable.referenceProperty(
getProperty(FqName.fromSegments(listOf("kotlin", "wasm", "internal", "isNotFirstWasmExportCall")))
)
internal val throwAsJsException: IrSimpleFunctionSymbol = getInternalFunction("throwAsJsException")
override val throwNullPointerException = getInternalFunction("THROW_NPE")
override val throwISE = getInternalFunction("THROW_ISE")
override val throwTypeCastException = getInternalFunction("THROW_CCE")
@@ -27,7 +27,7 @@ import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
* WebAssembly allows only constant expressions to be used directly in
* field initializers.
*
* TODO: Don't move constant expression initializers
* TODO: Don't move null and nullable constant initializers
* TODO: Make field initialization lazy. Needs design.
*/
class FieldInitializersLowering(val context: WasmBackendContext) : FileLoweringPass {
@@ -0,0 +1,140 @@
/*
* 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 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.common.lower.irCatch
import org.jetbrains.kotlin.backend.common.lower.irThrow
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
import org.jetbrains.kotlin.backend.wasm.ir2wasm.isExported
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.builders.declarations.buildVariable
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.expressions.IrBlockBody
import org.jetbrains.kotlin.ir.expressions.IrExpressionBody
import org.jetbrains.kotlin.ir.interpreter.toIrConst
import org.jetbrains.kotlin.name.Name
// This pass needed to wrap around unhandled exceptions from JsExport functions and throw JS exception for call from JS site
// @JsExport
// fun someExportedMethod() {
// error("some error message")
// }
//
// converts into
//
// @JsExport
// fun someExportedMethod() {
// val currentIsNotFirstWasmExportCall = isNotFirstWasmExportCall
// try {
// isNotFirstWasmExportCall = true
// error("some error message")
// } catch (e: Throwable) {
// if (currentIsNotFirstWasmExportCall) throw e else throwAsJsException(e)
// } finally {
// isNotFirstWasmExportCall = currentIsNotFirstWasmExportCall
// }
// }
// TODO Wrap fieldInitializer function (now it building by later FieldInitializersLowering)
internal class UnhandledExceptionLowering(val context: WasmBackendContext) : FileLoweringPass {
private val throwableType = context.irBuiltIns.throwableType
private val irBooleanType = context.wasmSymbols.irBuiltIns.booleanType
private val throwAsJsException = context.wasmSymbols.throwAsJsException
private val isNotFirstWasmExportCallGetter = context.wasmSymbols.isNotFirstWasmExportCall.owner.getter!!.symbol
private val isNotFirstWasmExportCallSetter = context.wasmSymbols.isNotFirstWasmExportCall.owner.setter!!.symbol
private fun processExportFunction(irFunction: IrFunction) {
val body = irFunction.body ?: return
if (body is IrBlockBody && body.statements.isEmpty()) return
val bodyType = when (body) {
is IrExpressionBody -> body.expression.type
is IrBlockBody -> context.irBuiltIns.unitType
else -> TODO(this::class.qualifiedName!!)
}
with(context.createIrBuilder(irFunction.symbol)) {
val currentIsNotFirstWasmExportCall = buildVariable(
parent = irFunction,
startOffset = UNDEFINED_OFFSET, endOffset = UNDEFINED_OFFSET,
origin = IrDeclarationOrigin.IR_TEMPORARY_VARIABLE,
name = Name.identifier("currentIsNotFirstWasmExportCall"),
type = irBooleanType
)
val e = buildVariable(
parent = irFunction,
startOffset = UNDEFINED_OFFSET, endOffset = UNDEFINED_OFFSET,
origin = IrDeclarationOrigin.CATCH_PARAMETER,
name = Name.identifier("e"),
type = throwableType
)
currentIsNotFirstWasmExportCall.initializer =
irGet(irBooleanType, null, isNotFirstWasmExportCallGetter)
val tryBody = irComposite {
+irSet(irBooleanType, null, isNotFirstWasmExportCallSetter, true.toIrConst(irBooleanType))
when (body) {
is IrBlockBody -> body.statements.forEach { +it }
is IrExpressionBody -> +body.expression
else -> TODO(this::class.qualifiedName!!)
}
}
val catch = irCatch(
catchParameter = e,
result = irIfThenElse(
type = bodyType,
condition = irGet(currentIsNotFirstWasmExportCall, irBooleanType),
thenPart = irThrow(irGet(e, throwableType)),
elsePart = irCall(throwAsJsException).apply { putValueArgument(0, irGet(e, throwableType)) }
)
)
val finally = irSet(
type = irBooleanType,
receiver = null,
setterSymbol = isNotFirstWasmExportCallSetter,
value = irGet(currentIsNotFirstWasmExportCall, irBooleanType)
)
val tryWrap = irTry(
type = bodyType,
tryResult = tryBody,
catches = listOf(catch),
finallyExpression = finally
)
when (body) {
is IrExpressionBody -> body.expression = irComposite {
+currentIsNotFirstWasmExportCall
+tryWrap
}
is IrBlockBody -> with(body.statements) {
clear()
add(currentIsNotFirstWasmExportCall)
add(tryWrap)
}
else -> TODO(this::class.qualifiedName!!)
}
}
}
override fun lower(irFile: IrFile) {
for (declaration in irFile.declarations) {
if (declaration is IrFunction && declaration.isExported()) {
processExportFunction(declaration)
}
}
}
}
@@ -70,7 +70,7 @@ abstract class BasicWasmBoxTest(
val outputWasmFile = File("$outputFileBase.wasm")
val outputJsFile = File("$outputFileBase.js")
val outputBrowserDir = File("$outputFileBase.browser")
val outputFileNoDceBase = outputDir.absolutePath + "/" + getTestName(true) + "/noDce"
val outputFileNoDceBase = "${outputFileBase}NoDce"
val outputWatNoDceFile = File("$outputFileNoDceBase.wat")
val outputWasmNoDceFile = File("$outputFileNoDceBase.wasm")
val outputJsNoDceFile = File("$outputFileNoDceBase.js")
@@ -118,7 +118,6 @@ abstract class BasicWasmBoxTest(
println(" ------ WAT file://$outputWatFile")
println(" ------ WASM file://$outputWasmFile")
println(" ------ JS file://$outputJsFile")
println(" ------ (No-DCE files in noDce sub-directory)")
PhaseConfig(
wasmPhases,
dumpToDirectory = dumpOutputDir.path,
@@ -140,7 +139,6 @@ abstract class BasicWasmBoxTest(
AnalyzerWithCompilerReport(config.configuration)
)
File(outputFileNoDceBase).mkdirs()
compileAndRun(
phaseConfig = phaseConfig,
sourceModule = sourceModule,
@@ -157,7 +155,6 @@ abstract class BasicWasmBoxTest(
jsFilesAfter = jsFilesAfter
)
File(outputFileBase).mkdirs()
compileAndRun(
phaseConfig = phaseConfig,
sourceModule = sourceModule,
@@ -222,15 +219,8 @@ abstract class BasicWasmBoxTest(
const wasmBinary = read(String.raw`${outputWasmFile.absoluteFile}`, 'binary');
const wasmModule = new WebAssembly.Module(wasmBinary);
wasmInstance = new WebAssembly.Instance(wasmModule, { runtime, js_code });
wasmInstance.exports.__init();
const ${sanitizeName(config.moduleId)} = wasmInstance.exports;
wasmInstance.exports.startUnitTests?.();
const actualResult = wasmInstance.exports.$testFunction();
if (actualResult !== "OK")
throw `Wrong box result '${'$'}{actualResult}' (with DCE=${dceEnabled}); Expected "OK"`;
${createJsRun(wasmInstance = "wasmInstance", testFunction = testFunction, dceEnabled = dceEnabled)}
""".trimIndent()
outputJsFile.write(compilerResult.js + "\n" + testRunner)
@@ -249,18 +239,30 @@ abstract class BasicWasmBoxTest(
)
}
private fun createJsRun(wasmInstance: String, testFunction: String, dceEnabled: Boolean) = """
let actualResult
try {
$wasmInstance.exports.__init();
$wasmInstance.exports.startUnitTests?.();
actualResult = $wasmInstance.exports.$testFunction();
} catch(e) {
console.log('Failed with exception!')
console.log('Message: ' + e.message)
console.log('Name: ' + e.name)
console.log('Stack:')
console.log(e.stack)
}
if (actualResult !== "OK")
throw `Wrong box result '${'$'}{actualResult}' (with DCE=${dceEnabled}); Expected "OK"`;
""".trimIndent()
private fun createDirectoryToRunInBrowser(directory: File, compilerResult: WasmCompilerResult, dceEnabled: Boolean) {
val browserRunner =
"""
const response = await fetch("index.wasm");
const wasmBinary = await response.arrayBuffer();
wasmInstance = (await WebAssembly.instantiate(wasmBinary, { runtime, js_code })).instance;
wasmInstance.exports.__init();
wasmInstance.exports.startUnitTests?.();
const actualResult = wasmInstance.exports.box();
if (actualResult !== "OK")
throw `Wrong box result '${'$'}{actualResult}' (with DCE=${dceEnabled}); Expected "OK"`;
${createJsRun(wasmInstance = "wasmInstance", testFunction = "box", dceEnabled = dceEnabled)}
console.log("Test passed!");
""".trimIndent()
@@ -0,0 +1,24 @@
/*
* 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.internal
@JsFun(
"""(message, wasmTypeName, stack) => {
const error = new Error();
error.message = message;
error.name = wasmTypeName;
error.stack = stack;
throw error;
}"""
)
private external fun throwJsError(message: String?, wasmTypeName: String?, stack: String)
internal fun throwAsJsException(t: Throwable): Nothing {
throwJsError(t.message, t::class.simpleName, t.stackTraceToString())
return error("Unreachable")
}
internal var isNotFirstWasmExportCall: Boolean = false