[WASM] Implemented Wasm unhandled exceptions on JS site
This commit is contained in:
@@ -530,6 +530,12 @@ private val removeInitializersForLazyProperties = makeWasmModulePhase(
|
|||||||
description = "Remove property initializers if they was initialized lazily"
|
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(
|
private val propertyAccessorInlinerLoweringPhase = makeWasmModulePhase(
|
||||||
::PropertyAccessorInlineLowering,
|
::PropertyAccessorInlineLowering,
|
||||||
name = "PropertyAccessorInlineLowering",
|
name = "PropertyAccessorInlineLowering",
|
||||||
@@ -608,6 +614,8 @@ val wasmPhases = NamedCompilerPhase(
|
|||||||
addContinuationToFunctionCallsLoweringPhase then
|
addContinuationToFunctionCallsLoweringPhase then
|
||||||
addMainFunctionCallsLowering then
|
addMainFunctionCallsLowering then
|
||||||
|
|
||||||
|
unhandledExceptionLowering then
|
||||||
|
|
||||||
tryCatchCanonicalization then
|
tryCatchCanonicalization then
|
||||||
returnableBlockLoweringPhase then
|
returnableBlockLoweringPhase then
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ class WasmSymbols(
|
|||||||
|
|
||||||
internal val eagerInitialization: IrClassSymbol = getIrClass(FqName("kotlin.EagerInitialization"))
|
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 throwNullPointerException = getInternalFunction("THROW_NPE")
|
||||||
override val throwISE = getInternalFunction("THROW_ISE")
|
override val throwISE = getInternalFunction("THROW_ISE")
|
||||||
override val throwTypeCastException = getInternalFunction("THROW_CCE")
|
override val throwTypeCastException = getInternalFunction("THROW_CCE")
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
|
|||||||
* WebAssembly allows only constant expressions to be used directly in
|
* WebAssembly allows only constant expressions to be used directly in
|
||||||
* field initializers.
|
* 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.
|
* TODO: Make field initialization lazy. Needs design.
|
||||||
*/
|
*/
|
||||||
class FieldInitializersLowering(val context: WasmBackendContext) : FileLoweringPass {
|
class FieldInitializersLowering(val context: WasmBackendContext) : FileLoweringPass {
|
||||||
|
|||||||
+140
@@ -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 outputWasmFile = File("$outputFileBase.wasm")
|
||||||
val outputJsFile = File("$outputFileBase.js")
|
val outputJsFile = File("$outputFileBase.js")
|
||||||
val outputBrowserDir = File("$outputFileBase.browser")
|
val outputBrowserDir = File("$outputFileBase.browser")
|
||||||
val outputFileNoDceBase = outputDir.absolutePath + "/" + getTestName(true) + "/noDce"
|
val outputFileNoDceBase = "${outputFileBase}NoDce"
|
||||||
val outputWatNoDceFile = File("$outputFileNoDceBase.wat")
|
val outputWatNoDceFile = File("$outputFileNoDceBase.wat")
|
||||||
val outputWasmNoDceFile = File("$outputFileNoDceBase.wasm")
|
val outputWasmNoDceFile = File("$outputFileNoDceBase.wasm")
|
||||||
val outputJsNoDceFile = File("$outputFileNoDceBase.js")
|
val outputJsNoDceFile = File("$outputFileNoDceBase.js")
|
||||||
@@ -118,7 +118,6 @@ abstract class BasicWasmBoxTest(
|
|||||||
println(" ------ WAT file://$outputWatFile")
|
println(" ------ WAT file://$outputWatFile")
|
||||||
println(" ------ WASM file://$outputWasmFile")
|
println(" ------ WASM file://$outputWasmFile")
|
||||||
println(" ------ JS file://$outputJsFile")
|
println(" ------ JS file://$outputJsFile")
|
||||||
println(" ------ (No-DCE files in noDce sub-directory)")
|
|
||||||
PhaseConfig(
|
PhaseConfig(
|
||||||
wasmPhases,
|
wasmPhases,
|
||||||
dumpToDirectory = dumpOutputDir.path,
|
dumpToDirectory = dumpOutputDir.path,
|
||||||
@@ -140,7 +139,6 @@ abstract class BasicWasmBoxTest(
|
|||||||
AnalyzerWithCompilerReport(config.configuration)
|
AnalyzerWithCompilerReport(config.configuration)
|
||||||
)
|
)
|
||||||
|
|
||||||
File(outputFileNoDceBase).mkdirs()
|
|
||||||
compileAndRun(
|
compileAndRun(
|
||||||
phaseConfig = phaseConfig,
|
phaseConfig = phaseConfig,
|
||||||
sourceModule = sourceModule,
|
sourceModule = sourceModule,
|
||||||
@@ -157,7 +155,6 @@ abstract class BasicWasmBoxTest(
|
|||||||
jsFilesAfter = jsFilesAfter
|
jsFilesAfter = jsFilesAfter
|
||||||
)
|
)
|
||||||
|
|
||||||
File(outputFileBase).mkdirs()
|
|
||||||
compileAndRun(
|
compileAndRun(
|
||||||
phaseConfig = phaseConfig,
|
phaseConfig = phaseConfig,
|
||||||
sourceModule = sourceModule,
|
sourceModule = sourceModule,
|
||||||
@@ -222,15 +219,8 @@ abstract class BasicWasmBoxTest(
|
|||||||
const wasmBinary = read(String.raw`${outputWasmFile.absoluteFile}`, 'binary');
|
const wasmBinary = read(String.raw`${outputWasmFile.absoluteFile}`, 'binary');
|
||||||
const wasmModule = new WebAssembly.Module(wasmBinary);
|
const wasmModule = new WebAssembly.Module(wasmBinary);
|
||||||
wasmInstance = new WebAssembly.Instance(wasmModule, { runtime, js_code });
|
wasmInstance = new WebAssembly.Instance(wasmModule, { runtime, js_code });
|
||||||
wasmInstance.exports.__init();
|
|
||||||
|
|
||||||
const ${sanitizeName(config.moduleId)} = wasmInstance.exports;
|
const ${sanitizeName(config.moduleId)} = wasmInstance.exports;
|
||||||
|
${createJsRun(wasmInstance = "wasmInstance", testFunction = testFunction, dceEnabled = dceEnabled)}
|
||||||
wasmInstance.exports.startUnitTests?.();
|
|
||||||
|
|
||||||
const actualResult = wasmInstance.exports.$testFunction();
|
|
||||||
if (actualResult !== "OK")
|
|
||||||
throw `Wrong box result '${'$'}{actualResult}' (with DCE=${dceEnabled}); Expected "OK"`;
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
outputJsFile.write(compilerResult.js + "\n" + testRunner)
|
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) {
|
private fun createDirectoryToRunInBrowser(directory: File, compilerResult: WasmCompilerResult, dceEnabled: Boolean) {
|
||||||
val browserRunner =
|
val browserRunner =
|
||||||
"""
|
"""
|
||||||
const response = await fetch("index.wasm");
|
const response = await fetch("index.wasm");
|
||||||
const wasmBinary = await response.arrayBuffer();
|
const wasmBinary = await response.arrayBuffer();
|
||||||
wasmInstance = (await WebAssembly.instantiate(wasmBinary, { runtime, js_code })).instance;
|
wasmInstance = (await WebAssembly.instantiate(wasmBinary, { runtime, js_code })).instance;
|
||||||
wasmInstance.exports.__init();
|
${createJsRun(wasmInstance = "wasmInstance", testFunction = "box", dceEnabled = dceEnabled)}
|
||||||
wasmInstance.exports.startUnitTests?.();
|
|
||||||
|
|
||||||
const actualResult = wasmInstance.exports.box();
|
|
||||||
if (actualResult !== "OK")
|
|
||||||
throw `Wrong box result '${'$'}{actualResult}' (with DCE=${dceEnabled}); Expected "OK"`;
|
|
||||||
console.log("Test passed!");
|
console.log("Test passed!");
|
||||||
""".trimIndent()
|
""".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
|
||||||
Reference in New Issue
Block a user