[Wasm] Imporove external interface support

* Support boxing/unboxing when casting to Any
* Support ===, equals, hashCode, toString

* Support adapting String in interop boundary
This commit is contained in:
Svyatoslav Kuzmich
2021-10-15 23:04:22 +03:00
parent baa53b5cf3
commit 4fc461a2ff
30 changed files with 1240 additions and 81 deletions
@@ -0,0 +1,191 @@
/*
* Copyright 2010-2021 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
import kotlin.wasm.internal.reftypes.anyref
internal external interface ExternalInterfaceType
internal class JsExternalBox(val ref: ExternalInterfaceType) {
override fun toString(): String =
externrefToString(ref)
override fun equals(other: Any?): Boolean =
if (other is JsExternalBox) {
externrefEquals(ref, other.ref)
} else {
false
}
override fun hashCode(): Int =
externrefHashCode(ref)
}
//language=js
@JsFun("""
(() => {
const dataView = new DataView(new ArrayBuffer(8));
function numberHashCode(obj) {
if ((obj | 0) === obj) {
return obj | 0;
} else {
dataView.setFloat64(0, obj, true);
return (dataView.getInt32(0, true) * 31 | 0) + dataView.getInt32(4, true) | 0;
}
}
const hashCodes = new WeakMap();
function getObjectHashCode(obj) {
const res = hashCodes.get(obj);
if (res === undefined) {
const POW_2_32 = 4294967296;
const hash = (Math.random() * POW_2_32) | 0;
hashCodes.set(obj, hash);
return hash;
}
return res;
}
function getStringHashCode(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
hash = (hash * 31 + code) | 0;
}
return hash;
}
return (obj) => {
if (obj == null) {
return 0;
}
switch (typeof obj) {
case "object":
case "function":
return getObjectHashCode(obj);
case "number":
return numberHashCode(obj);
case "boolean":
return obj;
default:
return getStringHashCode(String(obj));
}
}
})()"""
)
private external fun externrefHashCode(ref: ExternalInterfaceType): Int
@JsFun("ref => String(ref)")
private external fun externrefToString(ref: ExternalInterfaceType): String
@JsFun("(lhs, rhs) => lhs === rhs")
private external fun externrefEquals(lhs: ExternalInterfaceType, rhs: ExternalInterfaceType): Boolean
@JsFun("(ref) => (typeof ref !== 'object' || ref === null) ? null : (externrefBoxes.get(ref) ?? null)")
private external fun getExternrefBoxOrNull(ref: ExternalInterfaceType): JsExternalBox?
@JsFun("(ref, box) => { if (typeof ref === 'object' && ref !== null) { externrefBoxes.set(ref, box); } }")
private external fun setExternrefBox(ref: ExternalInterfaceType, box: JsExternalBox)
@WasmNoOpCast
@Suppress("unused")
internal fun Any?.asWasmAnyref(): anyref =
implementedAsIntrinsic
@WasmNoOpCast
@Suppress("unused")
internal fun ExternalInterfaceType.externAsWasmAnyref(): anyref =
implementedAsIntrinsic
@WasmNoOpCast
@Suppress("unused")
internal fun Any?.asWasmExternRef(): ExternalInterfaceType =
implementedAsIntrinsic
@JsFun("(ref) => ref == null")
internal external fun isNullish(ref: ExternalInterfaceType): Boolean
internal fun externRefToAny(ref: ExternalInterfaceType): Any? {
// If ref is an instance of kotlin class -- return it cased to Any
val refAsAnyref = ref.externAsWasmAnyref()
if (wasm_ref_is_data(refAsAnyref)) {
val refAsDataRef = wasm_ref_as_data(refAsAnyref)
if (wasm_ref_test<Any>(refAsDataRef)) {
return wasm_ref_cast<Any>(refAsDataRef)
}
}
if (isNullish(ref))
return null
// If we already have a box -- return it,
// otherwise -- create a new box and remember it.
return getExternrefBoxOrNull(ref)
?: JsExternalBox(ref).also {
setExternrefBox(ref, it)
}
}
internal fun anyToExternRef(x: Any): ExternalInterfaceType {
return if (x is JsExternalBox)
x.ref
else
x.asWasmExternRef()
}
internal external fun importStringFromWasm(addr: Int): ExternalInterfaceType
@JsFun("x => x.length")
internal external fun stringLength(x: ExternalInterfaceType): Int
internal fun convertJsStringToKotlinString(x: ExternalInterfaceType): String {
val length = stringLength(x)
val addr = unsafeGetScratchRawMemory(INT_SIZE_BYTES + length * CHAR_SIZE_BYTES)
jsWriteStringIntoMemory(x, addr)
return kotlin.String(unsafeRawMemoryToCharArray(addr, length))
}
//language=js
@JsFun(
""" (str, addr) => {
const memory = new DataView(wasmInstance.exports.memory.buffer);
for (var i = 0; i < str.length; i++) {
memory.setInt16(addr + i * 2, str.charCodeAt(i), true);
}
}
"""
)
internal external fun jsWriteStringIntoMemory(str: ExternalInterfaceType, addr: Int)
internal fun kotlinToJsStringAdapter(x: String): ExternalInterfaceType {
return importStringFromWasm(exportString(x))
}
@JsFun("() => true")
internal external fun jsTrue(): ExternalInterfaceType
@JsFun("() => false")
internal external fun jsFalse(): ExternalInterfaceType
internal fun kotlinToJsBooleanAdapter(x: Boolean): ExternalInterfaceType =
if (x) jsTrue() else jsFalse()
internal fun kotlinToJsAnyAdapter(x: Any): ExternalInterfaceType =
anyToExternRef(x)
internal fun jsToKotlinAnyAdapter(x: ExternalInterfaceType): Any? =
externRefToAny(x)
internal fun jsToKotlinStringAdapter(x: ExternalInterfaceType): String =
convertJsStringToKotlinString(x)
internal fun jsToKotlinByteAdapter(x: Int): Byte = x.toByte()
internal fun jsToKotlinShortAdapter(x: Int): Short = x.toShort()
internal fun jsToKotlinCharAdapter(x: Int): Char = x.toChar()
@@ -9,6 +9,11 @@
package kotlin.wasm.internal
import kotlin.wasm.internal.reftypes.anyref
import kotlin.wasm.internal.reftypes.dataref
import kotlin.wasm.internal.reftypes.funcref
import kotlin.wasm.internal.reftypes.i31ref
@WasmOp(WasmOp.UNREACHABLE)
internal fun wasm_unreachable(): Nothing =
implementedAsIntrinsic
@@ -329,4 +334,25 @@ public external fun wasm_i64_clz(a: Long): Long
public external fun wasm_i64_popcnt(a: Long): Long
@WasmOp(WasmOp.I64_CTZ)
public external fun wasm_i64_ctz(a: Long): Long
public external fun wasm_i64_ctz(a: Long): Long
// Reference type operators
@WasmOp(WasmOp.REF_IS_FUNC)
internal external fun wasm_ref_is_func(x: anyref): Boolean
@WasmOp(WasmOp.REF_IS_DATA)
internal external fun wasm_ref_is_data(x: anyref): Boolean
@WasmOp(WasmOp.REF_IS_I31)
internal external fun wasm_ref_is_i31(x: anyref): Boolean
@WasmOp(WasmOp.REF_AS_FUNC)
internal external fun wasm_ref_as_func(x: anyref): funcref
@WasmOp(WasmOp.REF_AS_DATA)
internal external fun wasm_ref_as_data(x: anyref): dataref
@WasmOp(WasmOp.REF_AS_I31)
internal external fun wasm_ref_as_i31(x: anyref): i31ref
@@ -0,0 +1,26 @@
/*
* Copyright 2010-2021 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.
*/
@file:ExcludedFromCodegen
package kotlin.wasm.internal.reftypes
import kotlin.wasm.internal.*
// These interfaces correspond to Wasm GC reference types with the same name.
// They are not proper Kotlin interfaces and should be used with care.
//
// WARNING! Do not upcast to Any, nullable types, type parameters, etc.
// Do not use Any methods
// Do not use type operators `is`, `as`, etc.
// Do not use ==, ===
//
// Use dedicated intrinsics instead.
internal interface anyref
internal interface eqref : anyref
internal interface dataref : eqref
internal interface i31ref : eqref
internal interface funcref : anyref
@@ -216,6 +216,12 @@ internal annotation class WasmOp(val name: String) {
const val RETURN = "RETURN"
const val CALL = "CALL"
const val CALL_INDIRECT = "CALL_INDIRECT"
const val TRY = "TRY"
const val CATCH = "CATCH"
const val CATCH_ALL = "CATCH_ALL"
const val DELEGATE = "DELEGATE"
const val THROW = "THROW"
const val RETHROW = "RETHROW"
const val DROP = "DROP"
const val SELECT = "SELECT"
const val SELECT_TYPED = "SELECT_TYPED"
@@ -255,5 +261,19 @@ internal annotation class WasmOp(val name: String) {
const val REF_TEST = "REF_TEST"
const val REF_CAST = "REF_CAST"
const val BR_ON_CAST = "BR_ON_CAST"
const val BR_ON_CAST_FAIL = "BR_ON_CAST_FAIL"
const val REF_IS_FUNC = "REF_IS_FUNC"
const val REF_IS_DATA = "REF_IS_DATA"
const val REF_IS_I31 = "REF_IS_I31"
const val REF_AS_FUNC = "REF_AS_FUNC"
const val REF_AS_DATA = "REF_AS_DATA"
const val REF_AS_I31 = "REF_AS_I31"
const val BR_ON_FUNC = "BR_ON_FUNC"
const val BR_ON_DATA = "BR_ON_DATA"
const val BR_ON_I31 = "BR_ON_I31"
const val BR_ON_NON_FUNC = "BR_ON_NON_FUNC"
const val BR_ON_NON_DATA = "BR_ON_NON_DATA"
const val BR_ON_NON_I31 = "BR_ON_NON_I31"
const val GET_UNIT = "GET_UNIT"
}
}
@@ -13,4 +13,18 @@ package kotlin.js
@ExperimentalJsExport
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.FUNCTION)
public actual annotation class JsExport
public actual annotation class JsExport
/**
* Specifies JavaScript name for external and imported declarations
*/
@Retention(AnnotationRetention.BINARY)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
public actual annotation class JsName(actual val name: String)