[Wasm] Fix js interop closure signature

Before we used IrType.render() which is not unique.
This changes adds a proper serialization of adapter signature to string.
This commit is contained in:
Svyatoslav Kuzmich
2023-03-17 18:10:44 +01:00
committed by Space Team
parent e99b453816
commit 1b7c63bd49
4 changed files with 71 additions and 27 deletions
@@ -58,10 +58,10 @@ class WasmBackendContext(
override val mapping = JsMapping()
val closureCallExports = mutableMapOf<IrSimpleType, IrSimpleFunction>()
val kotlinClosureToJsConverters = mutableMapOf<IrSimpleType, IrSimpleFunction>()
val jsClosureCallers = mutableMapOf<IrSimpleType, IrSimpleFunction>()
val jsToKotlinClosures = mutableMapOf<IrSimpleType, IrSimpleFunction>()
val closureCallExports = mutableMapOf<String, IrSimpleFunction>()
val kotlinClosureToJsConverters = mutableMapOf<String, IrSimpleFunction>()
val jsClosureCallers = mutableMapOf<String, IrSimpleFunction>()
val jsToKotlinClosures = mutableMapOf<String, IrSimpleFunction>()
val jsModuleAndQualifierReferences =
mutableSetOf<JsModuleAndQualifierReference>()
@@ -10,11 +10,11 @@ import org.jetbrains.kotlin.backend.common.DeclarationTransformer
import org.jetbrains.kotlin.backend.common.ir.addDispatchReceiver
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
import org.jetbrains.kotlin.backend.common.serialization.cityHash64
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
import org.jetbrains.kotlin.backend.wasm.ir2wasm.isBuiltInWasmRefType
import org.jetbrains.kotlin.backend.wasm.ir2wasm.isExported
import org.jetbrains.kotlin.backend.wasm.ir2wasm.isExternalType
import org.jetbrains.kotlin.backend.wasm.ir2wasm.toJsStringLiteral
import org.jetbrains.kotlin.backend.wasm.utils.getJsFunAnnotation
import org.jetbrains.kotlin.backend.wasm.utils.getWasmImportDescriptor
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
@@ -31,6 +31,7 @@ import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.util.OperatorNameConventions
@@ -279,7 +280,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
// Thus, we export helper "caller" method that JavaScript will use to call kotlin closures:
//
// @JsExport
// fun __callFunction_<signatureHash>(f: dataref, p1: JsType1, p2: JsType2, ...): JsTypeRes {
// fun __callFunction_<signatureString>(f: dataref, p1: JsType1, p2: JsType2, ...): JsTypeRes {
// return adapt(
// cast<FunctionN>(f).invoke(
// adapt(p1),
@@ -289,7 +290,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
// )
// }
//
context.closureCallExports.getOrPut(this) {
context.closureCallExports.getOrPut(functionTypeInfo.signatureString) {
createKotlinClosureCaller(functionTypeInfo)
}
@@ -297,11 +298,11 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
// using above-mentioned "caller" export:
//
// @JsFun("""(f) => {
// (p1, p2, ...) => <wasm-exports>.__callFunction_<signatureHash>(f, p1, p2, ...)
// (p1, p2, ...) => <wasm-exports>.__callFunction_<signatureString>(f, p1, p2, ...)
// }""")
// external fun __convertKotlinClosureToJsClosure_<signatureHash>(f: dataref): ExternalRef
// external fun __convertKotlinClosureToJsClosure_<signatureString>(f: dataref): ExternalRef
//
val kotlinToJsClosureConvertor = context.kotlinClosureToJsConverters.getOrPut(this) {
val kotlinToJsClosureConvertor = context.kotlinClosureToJsConverters.getOrPut(functionTypeInfo.signatureString) {
createKotlinToJsClosureConvertor(functionTypeInfo)
}
return FunctionBasedAdapter(kotlinToJsClosureConvertor)
@@ -402,21 +403,21 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
// Thus, we import helper "caller" method that WebAssembly will use to call JS closures:
//
// @JsFun("(f, p0, p1, ...) => f(p0, p1, ...)")
// external fun __callJsClosure_<signatureHash>(f: ExternalRef, p0: JsType1, p1: JsType2, ...): JsResType
// external fun __callJsClosure_<signatureString>(f: ExternalRef, p0: JsType1, p1: JsType2, ...): JsResType
//
val jsClosureCaller = context.jsClosureCallers.getOrPut(this) {
val jsClosureCaller = context.jsClosureCallers.getOrPut(functionTypeInfo.signatureString) {
createJsClosureCaller(functionTypeInfo)
}
// Converter functions creates new Kotlin closure that delegate to JS closure
// using above-mentioned "caller" import:
//
// fun __convertJsClosureToKotlinClosure_<signatureHash>(f: ExternalRef) : FunctionN<KotlinType1, ..., KotlinResType> =
// fun __convertJsClosureToKotlinClosure_<signatureString>(f: ExternalRef) : FunctionN<KotlinType1, ..., KotlinResType> =
// { p0: KotlinType1, p1: KotlinType2, ... ->
// adapt(__callJsClosure_<signatureHash>(f, adapt(p0), adapt(p1), ..))
// adapt(__callJsClosure_<signatureString>(f, adapt(p0), adapt(p1), ..))
// }
//
val jsToKotlinClosure = context.jsToKotlinClosures.getOrPut(this) {
val jsToKotlinClosure = context.jsToKotlinClosures.getOrPut(functionTypeInfo.signatureString) {
createJsToKotlinClosureConverter(functionTypeInfo, jsClosureCaller)
}
return FunctionBasedAdapter(jsToKotlinClosure)
@@ -427,7 +428,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
private fun createKotlinClosureCaller(info: FunctionTypeInfo): IrSimpleFunction {
val result = context.irFactory.buildFun {
name = Name.identifier("__callFunction_${info.hashString}")
name = Name.identifier("__callFunction_${info.signatureString}")
returnType = info.adaptedResultType
}
result.parent = currentParent
@@ -464,7 +465,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
private fun createKotlinToJsClosureConvertor(info: FunctionTypeInfo): IrSimpleFunction {
val result = context.irFactory.buildFun {
name = Name.identifier("__convertKotlinClosureToJsClosure_${info.hashString}")
name = Name.identifier("__convertKotlinClosureToJsClosure_${info.signatureString}")
returnType = context.wasmSymbols.jsAnyType
isExternal = true
}
@@ -479,9 +480,9 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
val jsCode = buildString {
append("(f) => (")
appendParameterList(arity)
append(") => wasmExports.__callFunction_")
append(info.hashString)
append("(f, ")
append(") => wasmExports[")
append("__callFunction_${info.signatureString}".toJsStringLiteral())
append("](f, ")
appendParameterList(arity)
append(")")
}
@@ -500,7 +501,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
): IrSimpleFunction {
val functionType = info.functionType
val result = context.irFactory.buildFun {
name = Name.identifier("__convertJsClosureToKotlinClosure_${info.hashString}")
name = Name.identifier("__convertJsClosureToKotlinClosure_${info.signatureString}")
returnType = functionType
}
result.parent = currentParent
@@ -510,7 +511,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
}
val closureClass = context.irFactory.buildClass {
name = Name.identifier("__JsClosureToKotlinClosure_${info.hashString}")
name = Name.identifier("__JsClosureToKotlinClosure_${info.signatureString}")
}.apply {
createImplicitParameterDeclarationWithWrappedDescriptor()
superTypes = listOf(functionType)
@@ -581,7 +582,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
private fun createJsClosureCaller(info: FunctionTypeInfo): IrSimpleFunction {
val result = context.irFactory.buildFun {
name = Name.identifier("__callJsClosure_${info.hashString}")
name = Name.identifier("__callJsClosure_${info.signatureString}")
returnType = info.adaptedResultType
isExternal = true
}
@@ -621,10 +622,6 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
}
}
val hashString: String =
// Rendering type to improve hash stability across compiler runs
functionType.render().cityHash64().toULong().toString(Character.MAX_RADIX)
val originalParameterTypes: List<IrType> =
functionType.arguments.dropLast(1).map { (it as IrTypeProjection).type }
@@ -652,6 +649,37 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
val adaptedResultType: IrType =
(if (toJs) resultAdapter?.toType else resultAdapter?.fromType) ?: originalResultType
val signatureString: String = jsInteropNotNullTypeSignature(this)
}
private fun jsInteropNotNullTypeSignature(type: JsInteropFunctionsLowering.FunctionTypeInfo): String {
val parameterTypes = type.originalParameterTypes.joinToString(separator = ",") { jsInteropTypeSignature(it) }
val resultType = jsInteropTypeSignature(type.originalResultType)
return "(($parameterTypes)->$resultType)"
}
private fun jsInteropNotNullTypeSignature(type: IrType): String {
if (isExternalType(type)) {
return "Js"
}
require(type is IrSimpleType)
if (type.isFunction()) {
return jsInteropNotNullTypeSignature(FunctionTypeInfo(type, true))
}
val klass = type.classOrNull ?: error("Unsupported JS interop type: ${type.render()}")
if (klass.owner.packageFqName == FqName("kotlin")) {
return klass.owner.name.identifier
}
error("Unsupported JS interop type: ${type.render()}")
}
private fun jsInteropTypeSignature(type: IrType): String {
return if (type.isNullable()) {
jsInteropNotNullTypeSignature(type.makeNotNull()) + "?"
} else {
jsInteropNotNullTypeSignature(type)
}
}
interface InteropTypeAdapter {
@@ -0,0 +1,11 @@
// TARGET_BACKEND: WASM
// Type of functions `(T) -> T` are rendered the same way in IrType::render
fun <T : JsAny?> foo(f: (T) -> T, x: Int): Int = js("f(x)")
fun <T : JsAny?> foo(f: (T) -> T, x: String): String = js("f(x)")
fun box(): String {
if (foo<JsAny>({ it }, 10) != 10) return "Fail 1"
if (foo<JsAny>({ it }, "20") != "20") return "Fail 2"
return "OK"
}
@@ -105,6 +105,11 @@ public class IrCodegenWasmJsInteropWasmTestGenerated extends AbstractIrCodegenWa
runTest("compiler/testData/codegen/boxWasmJsInterop/kotlinToJsAdapters.kt");
}
@TestMetadata("lambdaAdapterNameClash.kt")
public void testLambdaAdapterNameClash() throws Exception {
runTest("compiler/testData/codegen/boxWasmJsInterop/lambdaAdapterNameClash.kt");
}
@TestMetadata("longStrings.kt")
public void testLongStrings() throws Exception {
runTest("compiler/testData/codegen/boxWasmJsInterop/longStrings.kt");