WASM: Implement wasm-native strings part 1

There are several changes here but they all required for at least one test to pass.
- Implemented String class and several utility functions using built-in CharArray
- Added new constant memory segment to hold string literals and required funcs to work with them
- Added very crude mostly incorrect rudimentary ability to pass strings back to javascript
This commit is contained in:
Igor Laevsky
2021-07-23 16:11:45 +03:00
committed by TeamCityServer
parent 9ccdffe8ad
commit 0f84525bdc
20 changed files with 229 additions and 84 deletions
@@ -32,6 +32,9 @@ class WasmSymbols(
context.module.getPackage(FqName("kotlin.wasm.internal"))
private val collectionsPackage: PackageViewDescriptor =
context.module.getPackage(StandardNames.COLLECTIONS_PACKAGE_FQ_NAME)
private val builtInsPackage: PackageViewDescriptor =
context.module.getPackage(StandardNames.BUILT_INS_PACKAGE_FQ_NAME)
override val throwNullPointerException = getInternalFunction("THROW_NPE")
override val throwISE = getInternalFunction("THROW_ISE")
@@ -77,7 +80,7 @@ class WasmSymbols(
context.irBuiltIns.charType to getInternalFunction("wasm_i32_eq"),
context.irBuiltIns.intType to getInternalFunction("wasm_i32_eq"),
context.irBuiltIns.longType to getInternalFunction("wasm_i64_eq"),
context.irBuiltIns.stringType to getInternalFunction("wasm_string_eq")
context.irBuiltIns.stringType to getFunction("wasm_string_eq", builtInsPackage)
)
val floatEqualityFunctions = mapOf(
@@ -121,7 +124,7 @@ class WasmSymbols(
val boxIntrinsic: IrSimpleFunctionSymbol = getInternalFunction("boxIntrinsic")
val unboxIntrinsic: IrSimpleFunctionSymbol = getInternalFunction("unboxIntrinsic")
val stringGetLiteral = getInternalFunction("stringLiteral")
val stringGetLiteral = getFunction("stringLiteral", builtInsPackage)
val wasmClassId = getInternalFunction("wasmClassId")
val wasmInterfaceId = getInternalFunction("wasmInterfaceId")
@@ -140,6 +143,9 @@ class WasmSymbols(
val wasmThrow = getInternalFunction("wasmThrow")
val exportStringRet = getInternalFunction("exportStringRet")
val unsafeGetScratchRawMemory = getInternalFunction("unsafeGetScratchRawMemory")
private val functionNInterfaces = (0..22).map { arity ->
getIrClass(FqName("kotlin.wasm.internal.Function$arity"))
}
@@ -173,10 +179,12 @@ class WasmSymbols(
internal fun getProperty(fqName: FqName): PropertyDescriptor =
findProperty(context.module.getPackage(fqName.parent()).memberScope, fqName.shortName()).single()
private fun getInternalFunction(name: String): IrSimpleFunctionSymbol {
val tmp = findFunctions(wasmInternalPackage.memberScope, Name.identifier(name)).single()
private fun getFunction(name: String, ownerPackage: PackageViewDescriptor): IrSimpleFunctionSymbol {
val tmp = findFunctions(ownerPackage.memberScope, Name.identifier(name)).single()
return symbolTable.referenceSimpleFunction(tmp)
}
private fun getInternalFunction(name: String) = getFunction(name, wasmInternalPackage)
private fun getIrClass(fqName: FqName): IrClassSymbol = symbolTable.referenceClass(getClass(fqName))
}
@@ -132,6 +132,15 @@ fun WasmCompiledModuleFragment.generateJs(): String {
println(value) {
console.log(">>> " + value)
},
importStringToJs(addr, wasmMem) {
const mem16 = new Uint16Array(wasmMem.buffer);
const mem32 = new Int32Array(wasmMem.buffer);
const len = mem32[addr / 4];
const str_start_addr = (addr + 4) / 2
const slice = mem16.slice(str_start_addr, str_start_addr + len);
return String.fromCharCode.apply(null, slice);
}
};
""".trimIndent()
@@ -139,5 +148,5 @@ fun WasmCompiledModuleFragment.generateJs(): String {
val jsCode =
"\nconst js_code = {${jsFuns.joinToString(",\n") { "\"" + it.importName + "\" : " + it.jsCode }}};"
return runtime + generateStringLiteralsSupport(stringLiterals) + jsCode
return runtime + jsCode
}
@@ -58,6 +58,7 @@ class BodyGenerator(val context: WasmFunctionCodegenContext) : IrElementVisitorV
is IrConstKind.Double -> body.buildConstF64(kind.valueOf(expression))
is IrConstKind.String -> {
body.buildConstI32Symbol(context.referenceStringLiteral(kind.valueOf(expression)))
body.buildConstI32(kind.valueOf(expression).length)
body.buildCall(context.referenceFunction(wasmSymbols.stringGetLiteral))
}
else -> error("Unknown constant kind")
@@ -313,6 +314,13 @@ class BodyGenerator(val context: WasmFunctionCodegenContext) : IrElementVisitorV
body.buildRefCast()
generateInstanceFieldAccess(field)
}
wasmSymbols.unsafeGetScratchRawMemory -> {
// TODO: This drops size of the allocated segment. Instead we can check that it's in bounds for better error messages.
body.buildDrop()
body.buildConstI32Symbol(context.scratchMemAddr)
}
else -> {
return false
}
@@ -358,6 +366,14 @@ class BodyGenerator(val context: WasmFunctionCodegenContext) : IrElementVisitorV
}
}
// Handle complex exported parameters.
// TODO: This should live as a separate lowering which creates separate shims for each exported function.
if (context.irFunction.isExported(context.backendContext) &&
expression.value.type == irBuiltIns.stringType) {
body.buildCall(context.referenceFunction(wasmSymbols.exportStringRet))
}
body.buildInstr(WasmOp.RETURN)
}
@@ -9,6 +9,9 @@ import org.jetbrains.kotlin.wasm.ir.WasmSymbol
// Representation of constant data in Wasm memory
internal const val CHAR_SIZE_BYTES = 2
internal const val INT_SIZE_BYTES = 4
sealed class ConstantDataElement {
abstract val sizeInBytes: Int
abstract fun dump(indent: String = "", startAddress: Int = 0): String
@@ -18,6 +21,18 @@ sealed class ConstantDataElement {
private fun addressToString(address: Int): String =
address.toString().padEnd(6, ' ')
class ConstantDataCharField(val name: String, val value: WasmSymbol<Char>) : ConstantDataElement() {
constructor(name: String, value: Char) : this(name, WasmSymbol(value))
override fun toBytes(): ByteArray = value.owner.toLittleEndianBytes()
override fun dump(indent: String, startAddress: Int): String {
return "${addressToString(startAddress)}: $indent i32 : ${value.owner} ;; $name\n"
}
override val sizeInBytes: Int = CHAR_SIZE_BYTES
}
class ConstantDataIntField(val name: String, val value: WasmSymbol<Int>) : ConstantDataElement() {
constructor(name: String, value: Int) : this(name, WasmSymbol(value))
@@ -27,7 +42,7 @@ class ConstantDataIntField(val name: String, val value: WasmSymbol<Int>) : Const
return "${addressToString(startAddress)}: $indent i32 : ${value.owner} ;; $name\n"
}
override val sizeInBytes: Int = 4
override val sizeInBytes: Int = INT_SIZE_BYTES
}
class ConstantDataIntArray(val name: String, val value: List<WasmSymbol<Int>>) : ConstantDataElement() {
@@ -40,7 +55,24 @@ class ConstantDataIntArray(val name: String, val value: List<WasmSymbol<Int>>) :
return "${addressToString(startAddress)}: $indent i32[] : ${value.map { it.owner }.toIntArray().contentToString()} ;; $name\n"
}
override val sizeInBytes: Int = value.size * 4
override val sizeInBytes: Int = value.size * INT_SIZE_BYTES
}
class ConstantDataCharArray(val name: String, val value: List<WasmSymbol<Char>>) : ConstantDataElement() {
constructor(name: String, value: CharArray) : this(name, value.map { WasmSymbol(it) })
override fun toBytes(): ByteArray {
return value
.map { it.owner.toLittleEndianBytes() }
.fold(byteArrayOf(), ByteArray::plus)
}
override fun dump(indent: String, startAddress: Int): String {
if (value.isEmpty()) return ""
return "${addressToString(startAddress)}: $indent i16[] : ${value.map { it.owner }.toCharArray().contentToString()} ;; $name\n"
}
override val sizeInBytes: Int = value.size * CHAR_SIZE_BYTES
}
class ConstantDataStruct(val name: String, val elements: List<ConstantDataElement>) : ConstantDataElement() {
@@ -67,4 +99,8 @@ fun Int.toLittleEndianBytes(): ByteArray {
return ByteArray(4) {
(this ushr (it * 8)).toByte()
}
}
fun Char.toLittleEndianBytes(): ByteArray {
return byteArrayOf((this.code and 0xFF).toByte(), (this.code and 0xFF00).toByte())
}
@@ -66,6 +66,12 @@ class DeclarationGenerator(val context: WasmModuleCodegenContext) : IrElementVis
// Generate function type
val watName = declaration.fqNameWhenAvailable.toString()
val irParameters = declaration.getEffectiveValueParameters()
// TODO: Exported types should be transformed in a separate lowering by creating shim functions for each export.
val resultType =
if (declaration.isExported(context.backendContext))
context.transformExportedResultType(declaration.returnType)
else
context.transformResultType(declaration.returnType)
val wasmFunctionType =
WasmFunctionType(
name = watName,
@@ -78,7 +84,7 @@ class DeclarationGenerator(val context: WasmModuleCodegenContext) : IrElementVis
}
},
resultTypes = listOfNotNull(
context.transformResultType(declaration.returnType).let {
resultType.let {
if (importedName != null && it is WasmRefNullType) WasmEqRef else it
}
)
@@ -82,9 +82,6 @@ class WasmTypeTransformer(
builtIns.doubleType ->
WasmF64
builtIns.stringType ->
WasmExternRef
builtIns.nothingNType ->
WasmExternRef
@@ -92,6 +89,7 @@ class WasmTypeTransformer(
builtIns.nothingType ->
WasmExternRef
// this also handles builtIns.stringType
else -> {
val klass = this.getClass()
val ic = context.backendContext.inlineClassesUtils.getInlinedClass(this)
@@ -19,6 +19,8 @@ import org.jetbrains.kotlin.wasm.ir.*
interface WasmBaseCodegenContext {
val backendContext: WasmBackendContext
val scratchMemAddr: WasmSymbol<Int>
fun referenceFunction(irFunction: IrFunctionSymbol): WasmSymbol<WasmFunction>
fun referenceGlobal(irField: IrFieldSymbol): WasmSymbol<WasmGlobal>
fun referenceGcType(irClass: IrClassSymbol): WasmSymbol<WasmTypeDeclaration>
@@ -41,6 +43,7 @@ interface WasmBaseCodegenContext {
fun transformBoxedType(irType: IrType): WasmType
fun transformValueParameterType(irValueParameter: IrValueParameter): WasmType
fun transformResultType(irType: IrType): WasmType?
fun transformExportedResultType(irType: IrType): WasmType?
fun transformBlockResultType(irType: IrType): WasmType?
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.backend.wasm.ir2wasm
import org.jetbrains.kotlin.backend.common.push
import org.jetbrains.kotlin.backend.wasm.lower.WasmSignature
import org.jetbrains.kotlin.ir.declarations.IrDeclarationWithName
import org.jetbrains.kotlin.ir.declarations.IrExternalPackageFragment
@@ -40,7 +41,6 @@ class WasmCompiledModuleFragment {
val interfaces = mutableListOf<IrClassSymbol>()
val virtualFunctions = mutableListOf<IrSimpleFunctionSymbol>()
val signatures = LinkedHashSet<WasmSignature>()
val stringLiterals = mutableListOf<String>()
val typeInfo =
ReferencableAndDefinable<IrClassSymbol, ConstantDataElement>()
@@ -73,6 +73,9 @@ class WasmCompiledModuleFragment {
var startFunction: WasmFunction? = null
val scratchMemAddr = WasmSymbol<Int>()
val scratchMemSizeInBytes = 65_536
open class ReferencableElements<Ir, Wasm : Any> {
val unbound = mutableMapOf<Ir, WasmSymbol<Wasm>>()
fun reference(ir: Ir): WasmSymbol<Wasm> {
@@ -127,12 +130,28 @@ class WasmCompiledModuleFragment {
currentDataSectionAddress += typeInfoElement.sizeInBytes
}
val stringDataSectionStart = currentDataSectionAddress
val stringDataSectionBytes = mutableListOf<Byte>()
val stringAddrs = mutableMapOf<String, Int>()
for (str in stringLiteralId.unbound.keys) {
val constData = ConstantDataCharArray("string_literal", str.toCharArray())
stringDataSectionBytes += constData.toBytes().toList()
stringAddrs[str] = currentDataSectionAddress
currentDataSectionAddress += constData.sizeInBytes
}
// Reserve some memory to pass complex exported types (like strings). It's going to be accessible through 'unsafeGetScratchRawMemory'
// runtime call from stdlib.
currentDataSectionAddress = alignUp(currentDataSectionAddress, INT_SIZE_BYTES)
scratchMemAddr.bind(currentDataSectionAddress)
currentDataSectionAddress += scratchMemSizeInBytes
bind(classIds.unbound, klassIds)
bind(referencedClassITableAddresses.unbound, interfaceTableAddresses)
bind(stringLiteralId.unbound, stringAddrs)
bindIndices(virtualFunctionId.unbound, virtualFunctions)
bindIndices(signatureId.unbound, signatures.toList())
bindIndices(interfaceId.unbound, interfaces)
bindIndices(stringLiteralId.unbound, stringLiterals)
val interfaceImplementationIds = mutableMapOf<InterfaceImplementation, Int>()
val numberOfInterfaceImpls = mutableMapOf<IrClassSymbol, Int>()
@@ -145,9 +164,9 @@ class WasmCompiledModuleFragment {
bind(referencedInterfaceImplementationId.unbound, interfaceImplementationIds)
bind(interfaceMethodTables.unbound, interfaceMethodTables.defined)
val data =
typeInfo.buildData(address = { klassIds.getValue(it) }) +
definedClassITableData.buildData(address = { interfaceTableAddresses.getValue(it) })
val data = typeInfo.buildData(address = { klassIds.getValue(it) }) +
definedClassITableData.buildData(address = { interfaceTableAddresses.getValue(it) }) +
WasmData(WasmDataMode.Active(0, stringDataSectionStart), stringDataSectionBytes.toByteArray())
val logTypeInfo = false
if (logTypeInfo) {
@@ -222,6 +241,9 @@ class WasmCompiledModuleFragment {
val memorySizeInPages = (typeInfoSize / 65_536) + 1
val memory = WasmMemory(WasmLimits(memorySizeInPages.toUInt(), memorySizeInPages.toUInt()))
// Need to export the memory in order to pass complex objects to the host language.
exports += WasmExport.Memory("memory", memory)
val importedFunctions = functions.elements.filterIsInstance<WasmFunction.Imported>()
// Sorting by depth for a valid init order
@@ -285,8 +307,12 @@ inline fun WasmCompiledModuleFragment.ReferencableAndDefinable<IrClassSymbol, Co
}
}
data class InterfaceImplementation(
val irInterface: IrClassSymbol,
val irClass: IrClassSymbol
)
)
fun alignUp(x: Int, alignment: Int): Int {
assert(alignment and (alignment - 1) == 0) { "power of 2 expected" }
return (x + alignment - 1) and (alignment - 1).inv()
}
@@ -28,6 +28,9 @@ class WasmModuleCodegenContextImpl(
private val typeTransformer =
WasmTypeTransformer(this, backendContext.irBuiltIns)
override val scratchMemAddr: WasmSymbol<Int>
get() = wasmFragment.scratchMemAddr
override fun transformType(irType: IrType): WasmType {
return with(typeTransformer) { irType.toWasmValueType() }
}
@@ -54,12 +57,18 @@ class WasmModuleCodegenContextImpl(
return with(typeTransformer) { irType.toWasmResultType() }
}
override fun transformExportedResultType(irType: IrType): WasmType? {
// Exported strings are passed as pointers to the raw memory
if (irType == backendContext.irBuiltIns.stringType)
return WasmI32
return with(typeTransformer) { irType.toWasmResultType() }
}
override fun transformBlockResultType(irType: IrType): WasmType? {
return with(typeTransformer) { irType.toWasmBlockResultType() }
}
override fun referenceStringLiteral(string: String): WasmSymbol<Int> {
wasmFragment.stringLiterals.add(string)
return wasmFragment.stringLiteralId.reference(string)
}