From c48c569e9d64676bfccb667f9f7212a80f1d4992 Mon Sep 17 00:00:00 2001 From: Alexander Gorshenev Date: Wed, 25 Oct 2017 13:37:00 +0300 Subject: [PATCH] A prototype wasm32 stub generator. --- samples/html5Canvas/build.sh | 12 +- .../src/jsinterop_tool/kotlin/idl.kt | 40 +++ .../jsinterop_tool/kotlin/stubGenerator.kt | 300 ++++++++++++++++++ samples/html5Canvas/src/main/kotlin/main.kt | 2 +- 4 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 samples/html5Canvas/src/jsinterop_tool/kotlin/idl.kt create mode 100644 samples/html5Canvas/src/jsinterop_tool/kotlin/stubGenerator.kt diff --git a/samples/html5Canvas/build.sh b/samples/html5Canvas/build.sh index 26c3cb9d3ed..230878509bf 100755 --- a/samples/html5Canvas/build.sh +++ b/samples/html5Canvas/build.sh @@ -24,8 +24,16 @@ konanc $DIR/src/jsinterop/kotlin \ -includeBinary $DIR/src/jsinterop/js/jsinterop.js \ -p library -o $DIR/build/klib/jsinterop -target wasm32 || exit 1 -konanc $DIR/src/canvas/kotlin \ - -includeBinary $DIR/src/canvas/js/canvas.js \ +konanc $DIR/src/jsinterop_tool/kotlin \ + -e org.jetbrains.kotlin.konan.jsinterop.tool.main \ + -o $DIR/build/bin/generator || exit 1 + +# TODO: make a couple of args to name the result, for example. +$DIR/build/bin/generator.kexe || exit 1 + +# TODO: use the proper path and names +konanc kotlin_stubs.kt \ + -includeBinary js_stubs.js \ -l $DIR/build/klib/jsinterop \ -p library -o $DIR/build/klib/canvas -target wasm32 || exit 1 diff --git a/samples/html5Canvas/src/jsinterop_tool/kotlin/idl.kt b/samples/html5Canvas/src/jsinterop_tool/kotlin/idl.kt new file mode 100644 index 00000000000..2e0d8df0c6c --- /dev/null +++ b/samples/html5Canvas/src/jsinterop_tool/kotlin/idl.kt @@ -0,0 +1,40 @@ +package org.jetbrains.kotlin.konan.jsinterop.tool + +import org.jetbrains.kotlin.konan.jsinterop.tool.Type.* + +// This shall be an output of Web IDL parser. +val all = listOf( + Interface("Context", + Attribute("lineWidth", Integer, hasSetter = true), + Attribute("fillStyle", idlString, hasSetter = true), + Operation("lineTo", Void, Arg("x", Integer), Arg("y", Integer)), + Operation("moveTo", Void, Arg("x", Integer), Arg("y", Integer)), + Operation("beginPath", Void), + Operation("stroke", Void), + Operation("fillRect", Void, Arg("x", Integer), Arg("y", Integer), Arg("width", Integer), Arg("height", Integer)), + Operation("fillText", Void, Arg("test", idlString), Arg("x", Integer), Arg("y", Integer), Arg("maxWidth", Integer)), + Operation("fill", Void), + Operation("closePath", Void) + ), + Interface("DOMRect"), + Interface("Canvas", + Operation("getContext", InterfaceRef("Context"), Arg("context", idlString)), + Operation("getBoundingClientRect", InterfaceRef("DOMRect")) + ), + Interface("Document", + Operation("getElementById", Object, Arg("id", idlString)) + ), + Interface("Response", + Operation("json", Object) + ), + Interface("Promise", + Operation("then", InterfaceRef("Promise"), Arg("lambda", Function)) + ), + Interface("__Global", + Attribute("document", InterfaceRef("Document"), hasGetter = true), + Operation("fetch", InterfaceRef("Promise"), Arg("url", idlString)), + Operation("setInterval", Void, Arg("lambda", Function), Arg("interval", Integer)) + ) +) + + diff --git a/samples/html5Canvas/src/jsinterop_tool/kotlin/stubGenerator.kt b/samples/html5Canvas/src/jsinterop_tool/kotlin/stubGenerator.kt new file mode 100644 index 00000000000..78624b097a4 --- /dev/null +++ b/samples/html5Canvas/src/jsinterop_tool/kotlin/stubGenerator.kt @@ -0,0 +1,300 @@ +package org.jetbrains.kotlin.konan.jsinterop.tool + +import platform.posix.* + +// This is (as of now) a poor man's IDL representation. + +interface Type +interface Member + +object Void: Type +object Integer: Type +object Floating: Type +object idlString: Type +object Object: Type +object Function: Type + +data class Attribute(val name: String, val type: Type, + val hasGetter: Boolean = false, val hasSetter: Boolean = false): Member + +data class Arg(val name: String, val type: Type) + +class Operation(val name: String, val returnType: Type, vararg val args: Arg): Member + +class InterfaceRef(val name: String): Type +class Interface(val name: String, vararg val members: Member) + +fun kotlinHeader(): String { + val packageName = "html5.minimal" + return "package $packageName\n" + + "import kotlinx.wasm.jsinterop.*\n" +} + +fun Type.toKotlinType(argName: String? = null): String { + return when (this) { + is Void -> "Unit" + is Integer -> "Int" + is Floating -> "Float" + is idlString -> "String" + is Object -> "JsValue" + is Function -> "KtFunction" + is InterfaceRef -> name + else -> error("Unexpected type") + } +} + +fun Arg.wasmMapping(): String { + return when (type) { + is Void -> error("An arg can not be Void") + is Integer -> name + is Floating -> name + is idlString -> "stringPointer($name), stringLengthBytes($name)" + is Object -> TODO("implement me") + is Function -> "wrapFunction($name), ArenaManager.currentArena" + is InterfaceRef -> TODO("Implement me") + else -> error("Unexpected type") + } +} + +fun Arg.wasmArgNames(): List { + return when (type) { + is Void -> error("An arg can not be Void") + is Integer -> listOf(name) + is Floating -> listOf(name) + is idlString -> listOf("${name}Ptr", "${name}Len") + is Object -> TODO("implement me (Object)") + is Function -> listOf("${name}Index", "${name}ResultArena") + is InterfaceRef -> TODO("Implement me (InterfaceRef)") + else -> error("Unexpected type") + } +} + +fun Arg.wasmTypedMapping() + = this.wasmArgNames().map { "$it: Int" } .joinToString(", ") + +fun Type.wasmReturnMapping(value: String): String { + return when (this) { + is Void -> "" + is Integer -> value + is Floating -> value + is idlString -> TODO("Implement me") + is Object -> "JsValue(this.arena, $value)" + is Function -> TODO("Implement me") + is InterfaceRef -> "$name(ArenaManager.currentArena, $value)" + else -> error("Unexpected type") + } +} + +fun wasmFunctionName(functionName: String, interfaceName: String) + = "knjs_${interfaceName}_$functionName" + +fun wasmSetterName(propertyName: String, interfaceName: String) + = "knjs_${interfaceName}_set_$propertyName" + +fun wasmGetterName(propertyName: String, interfaceName: String) + = "knjs_${interfaceName}_get_$propertyName" + +val Operation.kotlinTypeParameters: String get() { + val lambdaRetTypes = args.filter { it.type is Function } + .map { "R${it.name}" }. joinToString(", ") + return if (lambdaRetTypes == "") "" else "<$lambdaRetTypes>" +} + + +fun Operation.generateKotlin(parentInterface: Interface): String { + val argList = args.map { + "${it.name}: ${it.type.toKotlinType(it.name)}" + }.joinToString(",") + val wasmArgList = args.map{it.wasmMapping()}.joinToString(",") + + // TODO: there can be multiple Rs. + return "\tfun $kotlinTypeParameters $name(" + + argList + + "): ${returnType.toKotlinType()} {\n" + + + "\t\tval wasmRetVal = ${wasmFunctionName(name, parentInterface.name)}(" + + "this.arena, this.index" + + (if (wasmArgList == "") "" else ", $wasmArgList") + + ")\n" + + + "\t\treturn ${returnType.wasmReturnMapping("wasmRetVal")}\n"+ + "\t}\n" +} + +fun Attribute.generateKotlinSetter(parent: Interface): String { + val kotlinType = type.toKotlinType(name) + return "\t\tset(value: $kotlinType) {\n" + + "\t\t\t${wasmSetterName(name, parent.name)}(" + + "this.arena, this.index, " + + Arg("value", type).wasmMapping() + + ")\n" + + "\t\t}\n" +} + +fun Attribute.generateKotlinGetter(parent: Interface): String { + //val kotlinType = type.toKotlinType() + return "\t\tget() {\n" + + "\t\t\tval wasmRetVal = ${wasmGetterName(name, parent.name)}(this.arena, this.index)\n" + + "\t\t\treturn ${type.wasmReturnMapping("wasmRetVal")}\n"+ + "\t\t}\n" + + "\t\n" +} + +fun Attribute.generateKotlin(parent: Interface): String { + val kotlinType = type.toKotlinType(name) + return "\tvar $name: $kotlinType\n" + + (if (hasGetter) generateKotlinGetter(parent) else "get() = error(\"There is not getter for $name\")\n") + + (if (hasSetter) generateKotlinSetter(parent) else "set(_) = error(\"There is not setter for $name\")\n") +} + +fun Operation.generateWasmStub(parent: Interface): String { + val wasmName = wasmFunctionName(this.name, parent.name) + return "@SymbolName(\"$wasmName\")\n" + + "external public fun $wasmName(arena: Int, index: Int ${args.toList().wasmTypedMapping()}): ${returnType.wasmReturnTypeMapping()}\n\n" +} +fun Attribute.generateWasmSetterStub(parent: Interface): String { + val wasmSetter = wasmSetterName(this.name, parent.name) + return "@SymbolName(\"$wasmSetter\")\n" + + "external public fun $wasmSetter(arena: Int, index: Int, ${Arg("value", this.type).wasmTypedMapping()}): Unit\n\n" +} +fun Attribute.generateWasmGetterStub(parent: Interface): String { + val wasmGetter = wasmGetterName(this.name, parent.name) + return "@SymbolName(\"$wasmGetter\")\n" + + "external public fun $wasmGetter(arena: Int, index: Int): Int\n\n" +} +fun Attribute.generateWasmStubs(parent: Interface) = + (if (hasGetter) generateWasmGetterStub(parent) else "") + + (if (hasSetter) generateWasmSetterStub(parent) else "") + +// TODO: consider using virtual mathods +fun Member.generateKotlin(parent: Interface): String { + return when (this) { + is Operation -> this.generateKotlin(parent) + is Attribute -> this.generateKotlin(parent) + else -> error("Unexpected member") + } +} + +// TODO: consider using virtual mathods +fun Member.generateWasmStub(parent: Interface) = + when (this) { + is Operation -> this.generateWasmStub(parent) + is Attribute -> this.generateWasmStubs(parent) + else -> error("Unexpected member") + + } + +fun List.wasmTypedMapping() + = this.map{ ", " + it.wasmTypedMapping() }.joinToString("") + +// TODO: more complex return types, such as returning a pair of Ints +// will require a more complex approach. +fun Type.wasmReturnTypeMapping() + = if (this == Void) "Unit" else "Int" + +fun Interface.generateKotlin(): String { + + return members.map { + it.generateWasmStub(this) + }.joinToString("") + + + "open class $name(arena: Int, index: Int): JsValue(arena, index) {\n" + + members.map { + it.generateKotlin(this) + }.joinToString("") + + "}\n" + + "val JsValue.as$name: $name\n" + + "\tget() {\n" + + "\t\treturn $name(this.arena, this.index)\n"+ + "\t}\n" +} + +fun generateKotlin(interfaces: List) = + kotlinHeader() + + interfaces.map { + it.generateKotlin() + }.joinToString("\n") + + "val html5 = __Global(ArenaManager.currentArena, -1)\n" + + "fun __Global.setInterval(interval: Int, lambda: KtFunction) = setInterval(lambda, interval)\n" + +///////////////////////////////////////////////////////// + +fun Arg.composeWasmArgs(): String { + return when (type) { + is Void -> error("An arg can not be Void") + is Integer -> "" + is Floating -> "" + is idlString -> "\t\t$name = toUTF16String(${name}Ptr, ${name}Len);\n" + is Object -> TODO("implement me") + is Function -> "\t\t$name = konan_dependencies.env.Konan_js_wrapLambda(arena, $name);\n" + + is InterfaceRef -> TODO("Implement me") + else -> error("Unexpected type") + } +} + +val Interface.receiver get() = + if (this.name == "__Global") "" else "kotlinObject(arena, obj)." + +fun Operation.generateJs(parent: Interface): String { + val wasmArgList = args.map { it.wasmArgNames() } + .flatten() + .map { ", $it" } + .joinToString("") + val argList = args.map { it.name }. joinToString(", ") + val composedArgsList = args.map { it.composeWasmArgs() }. joinToString("") + + return "\n\t${wasmFunctionName(this.name, parent.name)}: function(arena, obj$wasmArgList) {\n" + + composedArgsList + + "\t\tvar result = ${parent.receiver}$name($argList);\n" + + "\t\treturn toArena(resultArena, result);\n" + + "\t}" +} + +fun Attribute.generateJsSetter(parent: Interface): String { + val valueArg = Arg("value", type) + val wasmMapping = valueArg.wasmArgNames().joinToString(", ") + return "\n\t${wasmSetterName(name, parent.name)}: function(arena, obj, $wasmMapping) {\n" + + valueArg.composeWasmArgs() + + "\t\t${parent.receiver}$name = value;\n" + + "\t}" +} + +fun Attribute.generateJsGetter(parent: Interface): String { + return "\n\t${wasmGetterName(name, parent.name)}: function(arena, obj) {\n" + + "\t\tvar result = ${parent.receiver}$name;\n" + + "\t\treturn toArena(arena, result);\n" + + "\t}" +} + +fun Attribute.generateJs(parent: Interface) = + (if (hasGetter) generateJsGetter(parent) else "") + + (if (hasSetter) generateJsSetter(parent) else "") + +fun Member.generateJs(parent: Interface): String { + return when (this) { + is Operation -> this.generateJs(parent) + is Attribute -> this.generateJs(parent) + else -> error("Unexpected member") + } +} + +fun generateJs(interfaces: List): String = + "konan.libraries.push ({\n" + + interfaces.map { interf -> + interf.members.map { member -> + member.generateJs(interf) + } + }.flatten() .joinToString(",\n") + + "\n})\n" + +fun String.writeToFile(name: String) { + val file = fopen(name, "wb")!! + fputs(this, file) + fclose(file) +} +fun main(args: Array) { + generateKotlin(all).writeToFile("kotlin_stubs.kt") + generateJs(all).writeToFile("js_stubs.js") +} + diff --git a/samples/html5Canvas/src/main/kotlin/main.kt b/samples/html5Canvas/src/main/kotlin/main.kt index c2d67d1d0a5..ff7f30bd4d6 100644 --- a/samples/html5Canvas/src/main/kotlin/main.kt +++ b/samples/html5Canvas/src/main/kotlin/main.kt @@ -4,7 +4,7 @@ import konan.internal.ExportForCppRuntime fun main(args: Array) { - val html5 = Html5() + //val html5 = Html5() val document = html5.document val canvas = document.getElementById("myCanvas").asCanvas