From b367a0d255c330fd1c828915f74fecdf02647c21 Mon Sep 17 00:00:00 2001 From: Alexander Gorshenev Date: Wed, 6 Sep 2017 13:53:09 +0300 Subject: [PATCH] HTML5 canvas drawing demo. --- .../kotlin/backend/konan/LinkStage.kt | 7 +- build.gradle | 4 +- .../org/jetbrains/kotlin/KonanTest.groovy | 2 +- runtime/src/launcher/cpp/launcher.cpp | 22 +++-- runtime/src/launcher/js/launcher.js | 88 ++++++++++++++----- runtime/src/main/cpp/KString.cpp | 12 +++ samples/html5Canvas/build.sh | 38 ++++++++ samples/html5Canvas/index.html | 14 +++ samples/html5Canvas/src/canvas/js/canvas.js | 33 +++++++ .../html5Canvas/src/canvas/kotlin/canvas.kt | 80 +++++++++++++++++ .../html5Canvas/src/jsinterop/js/jsinterop.js | 41 +++++++++ .../src/jsinterop/kotlin/jsinterop.kt | 61 +++++++++++++ samples/html5Canvas/src/main/kotlin/main.kt | 47 ++++++++++ 13 files changed, 414 insertions(+), 35 deletions(-) create mode 100755 samples/html5Canvas/build.sh create mode 100644 samples/html5Canvas/index.html create mode 100644 samples/html5Canvas/src/canvas/js/canvas.js create mode 100644 samples/html5Canvas/src/canvas/kotlin/canvas.kt create mode 100644 samples/html5Canvas/src/jsinterop/js/jsinterop.js create mode 100644 samples/html5Canvas/src/jsinterop/kotlin/jsinterop.kt create mode 100644 samples/html5Canvas/src/main/kotlin/main.kt diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/LinkStage.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/LinkStage.kt index 084cfc6dc1c..9c4d0006075 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/LinkStage.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/LinkStage.kt @@ -346,7 +346,12 @@ internal class LinkStage(val context: Context) { private fun JavaScriptLinker(jsFiles: List, executable: String): String { val linkedJavaScript = File("$executable.js") - linkedJavaScript.writeBytes(ByteArray(0)); + + val jsLibsExceptLauncher = jsFiles.filter { it != "launcher.js" }.map { it.removeSuffix(".js") } + + val linkerStub = "var konan = { libraries: [] };\n" + + linkedJavaScript.writeBytes(linkerStub.toByteArray()); jsFiles.forEach { linkedJavaScript.appendBytes(File(it).readBytes()) diff --git a/build.gradle b/build.gradle index d8fb604233e..b7ff31e4e53 100644 --- a/build.gradle +++ b/build.gradle @@ -260,7 +260,7 @@ targetList.each { target -> } if (target == 'wasm32') { from(project(':runtime').file('src/launcher/js')) { - into('konan/nativelib') + into('klib/stdlib/targets/wasm32/included') } } } @@ -376,4 +376,4 @@ task clean { } delete bundle.outputs.files } -} \ No newline at end of file +} diff --git a/buildSrc/plugins/src/main/groovy/org/jetbrains/kotlin/KonanTest.groovy b/buildSrc/plugins/src/main/groovy/org/jetbrains/kotlin/KonanTest.groovy index cda5728d065..2201e7603d9 100644 --- a/buildSrc/plugins/src/main/groovy/org/jetbrains/kotlin/KonanTest.groovy +++ b/buildSrc/plugins/src/main/groovy/org/jetbrains/kotlin/KonanTest.groovy @@ -282,7 +282,7 @@ fun handleExceptionContinuation(x: (Throwable) -> Unit): Continuation = ob def targetToolchain = TargetPropertiesKt.hostTargetString(properties, "targetToolchain", target) def absoluteTargetToolchain = "$dependenciesDir/$targetToolchain" def d8 = "$absoluteTargetToolchain/bin/d8" - def launcherJs = "${dist.absolutePath}/konan/nativelib/launcher.js" + def launcherJs = "${exe}.js" return [d8, '--expose-wasm', launcherJs, '--', exe] } else { return [exe] diff --git a/runtime/src/launcher/cpp/launcher.cpp b/runtime/src/launcher/cpp/launcher.cpp index 929a3557bc9..ca80467efed 100644 --- a/runtime/src/launcher/cpp/launcher.cpp +++ b/runtime/src/launcher/cpp/launcher.cpp @@ -38,6 +38,12 @@ OBJ_GETTER(setupArgs, int argc, const char** argv) { //--- main --------------------------------------------------------------------// extern "C" KInt Konan_start(const ObjHeader*); +extern "C" KInt Konan_run_start(int argc, const char** argv) { + ObjHolder args; + setupArgs(argc, argv, args.slot()); + return Konan_start(args.obj()); +} + extern "C" RUNTIME_USED int Konan_main(int argc, const char** argv) { RuntimeState* state = InitRuntime(); @@ -45,12 +51,7 @@ extern "C" RUNTIME_USED int Konan_main(int argc, const char** argv) { return 2; } - KInt exitStatus; - { - ObjHolder args; - setupArgs(argc, argv, args.slot()); - exitStatus = Konan_start(args.obj()); - } + KInt exitStatus = Konan_run_start(argc, argv); DeinitRuntime(state); @@ -63,14 +64,19 @@ extern "C" RUNTIME_USED int Konan_main(int argc, const char** argv) { extern "C" int Konan_js_arg_size(int index); extern "C" int Konan_js_fetch_arg(int index, char* ptr); -extern "C" int Konan_js_main(int argc) { +extern "C" int Konan_js_main(int argc, int memoryInit) { char** argv = (char**)konan::calloc(1, argc); for (int i = 0; i< argc; ++i) { argv[i] = (char*)konan::calloc(1, Konan_js_arg_size(i)); Konan_js_fetch_arg(i, argv[i]); } - return Konan_main(argc, (const char**)argv); + if (memoryInit) { + return Konan_main(argc, (const char**)argv); + } else { + return Konan_run_start(argc, (const char**)argv); + } } + #endif #endif diff --git a/runtime/src/launcher/js/launcher.js b/runtime/src/launcher/js/launcher.js index fa87141f1a5..7f1b2aea007 100644 --- a/runtime/src/launcher/js/launcher.js +++ b/runtime/src/launcher/js/launcher.js @@ -14,13 +14,14 @@ * limitations under the License. */ -var module; var instance; var heap; var memory; var global_arguments; var globalBase = 0; // TODO: Is there any way to obtain global_base from JavaScript? +var konanStackTop; + function isBrowser() { if (typeof window === 'undefined') { return false; @@ -29,6 +30,7 @@ function isBrowser() { }; } +var runtime; if (isBrowser()) { runtime = { print: console.log, @@ -85,6 +87,14 @@ function toString(pointer) { return string; } +function toUTF16String(pointer, size) { + var string = ''; + for (var i = pointer; i < pointer + size; i+=2) { + string += String.fromCharCode(heap[i] + heap[i+1]*256); + } + return string; +} + function int32ToHeap(value, pointer) { heap[pointer] = value & 0xff; heap[pointer+1] = (value & 0xff00) >>> 8; @@ -166,48 +176,80 @@ var konan_dependencies = { } }; -function invokeModule(buffer, args) { +function linkJavaScriptLibraries() { + konan.libraries.forEach ( function (library) { + for (var property in library) { + konan_dependencies.env[property] = library[property]; + }; + }); +}; + +function invokeModule(inst, args) { if (args.length < 1) print_usage(); global_arguments = args; - module = new WebAssembly.Module(new Uint8Array(buffer)); - module.env = {}; - module.env.memoryBase = 0; - module.env.tablebase = 0; + instance = inst; - instance = new WebAssembly.Instance(module, konan_dependencies); memory = konan_dependencies.env.memory heap = new Uint8Array(konan_dependencies.env.memory.buffer); konanStackTop = stackTop(); var exit_status = 0; + try { runGlobalInitializers(instance.exports); - exit_status = instance.exports.Konan_js_main(args.length); + if (isBrowser()) { + instance.memoryState = instance.exports.InitRuntime(); + } + exit_status = instance.exports.Konan_js_main(args.length, isBrowser() ? 0 : 1); + // TODO: so when should we deinit runtime? } catch (e) { - runtime.print("Exception executing Konan_js_main: " + e); + runtime.print("Exception executing entry point: " + e); runtime.print(e.stack); exit_status = 1; } runtime.flush(); + return exit_status; } -// Invoke from the browser. -function loadAndInvoke(filename) { - function listener () { - invokeModule(this.response, [filename]); - } - - var request = new XMLHttpRequest(); - request.addEventListener("load", listener); - request.open("GET", filename); - request.responseType = "arraybuffer"; - request.send(); +function setupModule(module) { + module.env = {}; + module.env.memoryBase = 0; + module.env.tablebase = 0; + linkJavaScriptLibraries(); } -// Invoke from d8. -if (!isBrowser()) { - quit(invokeModule(readbuffer(arguments[0]), arguments)); +// Instantiate module in Browser in a sequence of promises. +function instantiateAndRun(arraybuffer, args) { + WebAssembly.compile(arraybuffer) + .then(function(module) { + setupModule(module); + return WebAssembly.instantiate(module, konan_dependencies); + }).then(function(instance) { + return invokeModule(instance, args); + }); +} + +// Instantiate module in d8 synchronously. +function instantiateAndRunSync(arraybuffer, args) { + var module = WebAssembly.Module(arraybuffer) + setupModule(module); + var instance = WebAssembly.Instance(module, konan_dependencies); + return invokeModule(instance, args) +} + +if (isBrowser()) { + var filename = document.currentScript.getAttribute("wasm"); + fetch(filename).then( function(response) { + return response.arrayBuffer(); + }).then(function(arraybuffer) { + instantiateAndRun(arraybuffer, [filename]); + }); +} else { + // Invoke from d8. + var arrayBuffer = readbuffer(arguments[0]); + var exitStatus = instantiateAndRunSync(arrayBuffer, arguments); + quit(exitStatus); } diff --git a/runtime/src/main/cpp/KString.cpp b/runtime/src/main/cpp/KString.cpp index b5aa32fe1a0..f2491826c58 100644 --- a/runtime/src/main/cpp/KString.cpp +++ b/runtime/src/main/cpp/KString.cpp @@ -1092,4 +1092,16 @@ OBJ_GETTER(Kotlin_String_subSequence, KString thiz, KInt startIndex, KInt endInd RETURN_OBJ(result->obj()); } +const KChar* Kotlin_String_utf16pointer(KString message) { + RuntimeAssert(message->type_info() == theStringTypeInfo, "Must use a string"); + const KChar* utf16 = CharArrayAddressOfElementAt(message, 0); + return utf16; +} + +KInt Kotlin_String_utf16length(KString message) { + RuntimeAssert(message->type_info() == theStringTypeInfo, "Must use a string"); + return message->count_ * sizeof(KChar); +} + + } // extern "C" diff --git a/samples/html5Canvas/build.sh b/samples/html5Canvas/build.sh new file mode 100755 index 00000000000..384d0304d57 --- /dev/null +++ b/samples/html5Canvas/build.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +DIR=$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd ) +PATH=$DIR/../../dist/bin:$DIR/../../bin:$PATH + +if [ x$TARGET == x ]; then +case "$OSTYPE" in + darwin*) TARGET=macbook ;; + linux*) TARGET=linux ;; + *) echo "unknown: $OSTYPE" && exit 1;; +esac +fi + +var=CFLAGS_${TARGET} +CFLAGS=${!var} +var=COMPILER_ARGS_${TARGET} +COMPILER_ARGS=${!var} # add -opt for an optimized build. + +mkdir -p $DIR/build/bin + +konanc $DIR/src/jsinterop/kotlin \ + -includeBinary $DIR/src/jsinterop/js/jsinterop.js \ + -p library -o $DIR/build/jsinterop -target wasm32 || exit 1 + +konanc $DIR/src/canvas/kotlin \ + -includeBinary $DIR/src/canvas/js/canvas.js \ + -l $DIR/build/jsinterop \ + -p library -o $DIR/build/canvas -target wasm32 || exit 1 + +konanc $DIR/src/main/kotlin \ + -l $DIR/build/jsinterop -l $DIR/build/canvas \ + -o $DIR/build/bin/html5Canvas -target wasm32 || exit 1 + +echo "Artifact path is $DIR/build/bin/html5Canvas.wasm" +echo "Artifact path is $DIR/build/bin/html5Canvas.wasm.js" +echo "Check out $DIR/index.html" +echo "Serve $DIR/ by an http server for the demo" +# For example run: python -m SimpleHTTPServer 8080 diff --git a/samples/html5Canvas/index.html b/samples/html5Canvas/index.html new file mode 100644 index 00000000000..3b06c6280c0 --- /dev/null +++ b/samples/html5Canvas/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + +

Draw something using the mouse!

+ + + diff --git a/samples/html5Canvas/src/canvas/js/canvas.js b/samples/html5Canvas/src/canvas/js/canvas.js new file mode 100644 index 00000000000..22758a5d37d --- /dev/null +++ b/samples/html5Canvas/src/canvas/js/canvas.js @@ -0,0 +1,33 @@ + +konan.libraries.push ({ // This one will be auto generated. + knjs_addDocumentToArena: function(arena) { + // Should be resolved by jsinterop. + return konan_dependencies.env.Konan_js_addObjectToArena(arena, document); + }, + knjs_setInterval: function (func, interval) { + setInterval(konan_dependencies.env.Konan_js_wrapLambda(func), interval); + }, + knjs_getElementById: function (arena, obj, id, idLength) { + var name = toUTF16String(id, idLength); + var result = konan_dependencies.env.arenas[arena][obj].getElementById(name); + return konan_dependencies.env.Konan_js_addObjectToArena(arena, result); + }, + knjs_getContext: function (arena, obj, context, contextLength) { + var name = toUTF16String(context, contextLength); + var result = konan_dependencies.env.arenas[arena][obj].getContext(name); + return konan_dependencies.env.Konan_js_addObjectToArena(arena, result); + }, + knjs_getBoundingClientRect: function (arena, obj) { + var result = konan_dependencies.env.arenas[arena][obj].getBoundingClientRect(); + return konan_dependencies.env.Konan_js_addObjectToArena(arena, result); + }, + knjs_moveTo: function(arena, obj, x, y) { + konan_dependencies.env.arenas[arena][obj].moveTo(x, y); + }, + knjs_lineTo: function(arena, obj, x, y) { + konan_dependencies.env.arenas[arena][obj].lineTo(x, y); + }, + knjs_stroke: function(arena, obj) { + konan_dependencies.env.arenas[arena][obj].stroke(); + }, +}); diff --git a/samples/html5Canvas/src/canvas/kotlin/canvas.kt b/samples/html5Canvas/src/canvas/kotlin/canvas.kt new file mode 100644 index 00000000000..cb355282f3c --- /dev/null +++ b/samples/html5Canvas/src/canvas/kotlin/canvas.kt @@ -0,0 +1,80 @@ +package html5.minimal + +// This file will be auto-generated. + +import kotlinx.wasm.jsinterop.* + +@SymbolName("knjs_addDocumentToArena") +external public fun addDocumentToArena(arena: Arena): Int + +@SymbolName("knjs_getElementById") +external public fun knjs_getElementById(arena: Int, obj: Int, idPtr: Int, idLen: Int): Int; + +@SymbolName("knjs_getContext") +external public fun knjs_getContext(arena: Int, obj: Int, idPtr: Int, idLen: Int): Int; + +@SymbolName("knjs_getBoundingClientRect") +external public fun knjs_getBoundingClientRect(arena: Int, obj: Int): Int; + +@SymbolName("knjs_stroke") +external public fun knjs_stroke(arena: Int, obj: Int); + +@SymbolName("knjs_moveTo") +external public fun knjs_moveTo(arena: Int, obj: Int, x: Int, y: Int): Int; + +@SymbolName("knjs_lineTo") +external public fun knjs_lineTo(arena: Int, obj: Int, x: Int, y: Int): Int; + +@SymbolName("knjs_setInterval") +external public fun knjs_setInterval(functionIndex: Int, interval: Int); + +open class Document(arena: Int, index: Int): JsValue(arena, index) { + fun getElementById(id: String): JsValue { + val resultIndex = knjs_getElementById(this.arena, this.index, stringPointer(id), stringLengthBytes(id)) + return JsValue(this.arena, resultIndex) + } +} + +open class DOMRect(arena: Int, index: Int): JsValue(arena, index) { +} + +open class Canvas(arena: Int, index: Int): JsValue(arena, index) { + fun getContext(context: String): Context { + val resultIndex = knjs_getContext(this.arena, this.index, stringPointer(context), stringLengthBytes(context)) + return Context(this.arena, resultIndex) + } + fun getBoundingClientRect(): DOMRect { + return DOMRect(this.arena, knjs_getBoundingClientRect(this.arena, this.index)) + } +} + +// TODO: this is awful. +val JsValue.asCanvas: Canvas + get() { + return Canvas(this.arena, this.index) + } + +open class Context(arena: Int, index: Int): JsValue(arena, index) { + fun lineTo(x: Int, y: Int) { + knjs_lineTo(this.arena, this.index, x, y) + } + fun moveTo(x: Int, y: Int) { + knjs_moveTo(this.arena, this.index, x, y) + } + fun stroke() { + knjs_stroke(this.arena, this.index); + } +} +fun setInterval(lambda: (ArrayList)->Unit, interval: Int) { + knjs_setInterval(wrapFunction(lambda), interval); +} + +// TODO: The arguments are flipped to have lambda within the last position. +fun setInterval(interval: Int, lambda: (ArrayList)->Unit) + = setInterval(lambda, interval) + +class html5 { + val arena = allocateArena() + val document = Document(arena, addDocumentToArena(arena)) +} + diff --git a/samples/html5Canvas/src/jsinterop/js/jsinterop.js b/samples/html5Canvas/src/jsinterop/js/jsinterop.js new file mode 100644 index 00000000000..221b12f4efb --- /dev/null +++ b/samples/html5Canvas/src/jsinterop/js/jsinterop.js @@ -0,0 +1,41 @@ + +konan.libraries.push ({ + arenas: [], + Konan_js_allocateArena: function (array) { + konan_dependencies.env.arenas.push(array || []); + return konan_dependencies.env.arenas.length - 1; + + }, + Konan_js_freeArena: function(arenaIndex) { + var arena = konan_dependencies.env.arenas[arenaIndex]; + arena.forEach(function(element, index) { + arena[index] = null; + }); + konan_dependencies.env.arenas[arenaIndex] = null; + }, + Konan_js_addObjectToArena: function (arenaIndex, object) { + var arena = konan_dependencies.env.arenas[arenaIndex]; + arena.push(object); + return arena.length - 1; + }, + Konan_js_wrapLambda: function (index) { + return (function () { + // convert Arguments to an array + // to be provided by launcher.js + var arenaIndex = konan_dependencies.env.Konan_js_allocateArena(Array.prototype.slice.call(arguments)); + // To be provided by Kotlin runtime. + instance.exports.Konan_js_runLambda(index, arenaIndex, arguments.length); + konan_dependencies.env.Konan_js_freeArena(arenaIndex); + }); + }, + Konan_js_getInt: function(arenaIndex, objIndex, propertyNamePtr, propertyNameLength) { + // TODO: The toUTF16String() is to be resolved by launcher.js runtime. + var property = toUTF16String(propertyNamePtr, propertyNameLength); + var value = konan_dependencies.env.arenas[arenaIndex][objIndex][property]; + return value; + }, + Konan_js_setFunction: function (arena, obj, propertyName, propertyNameLength, func) { + var name = toUTF16String(propertyName, propertyNameLength); + konan_dependencies.env.arenas[arena][obj][name] = konan_dependencies.env.Konan_js_wrapLambda(func); + }, +}); diff --git a/samples/html5Canvas/src/jsinterop/kotlin/jsinterop.kt b/samples/html5Canvas/src/jsinterop/kotlin/jsinterop.kt new file mode 100644 index 00000000000..03be07b7a05 --- /dev/null +++ b/samples/html5Canvas/src/jsinterop/kotlin/jsinterop.kt @@ -0,0 +1,61 @@ +package kotlinx.wasm.jsinterop + +import konan.internal.ExportForCppRuntime +import kotlinx.cinterop.* + +typealias Arena = Int +typealias Object = Int +typealias Pointer = Int + +@SymbolName("Konan_js_allocateArena") +external public fun allocateArena(): Arena + +@SymbolName("Konan_js_freeArena") +external public fun freeArena(arena: Arena) + +@SymbolName("Kotlin_String_utf16pointer") +external public fun stringPointer(message: String): Pointer + +@SymbolName("Kotlin_String_utf16length") +external public fun stringLengthBytes(message: String): Int + +typealias KtFunction = (ArrayList)->Unit + +fun wrapFunction(func: KtFunction): Int { + val ptr: Long = StableObjPtr.create(func).value.toLong() + return ptr.toInt() // TODO: LP64 unsafe. +} + +@ExportForCppRuntime("Konan_js_runLambda") +fun runLambda(userData: Int, arena: Arena, arenaSize: Int) { + val arguments = arrayListOf() + for (i in 0..arenaSize-1) { + arguments.add(JsValue(arena, i)); + } + // TODO: LP64 unsafe: wasm32 passes Int, not Long. + val func = StableObjPtr.fromValue(userData.toLong().toCPointer()!!).get() as KtFunction + func(arguments); + + +} + +open class JsValue(val arena: Arena, val index: Object) { + fun getInt(property: String): Int { + return getInt(arena, index, stringPointer(property), stringLengthBytes(property)) + } +} + +@SymbolName("Konan_js_getInt") +external public fun getInt(arena: Arena, obj: Object, propertyPtr: Pointer, propertyLen: Int): Int; + +@SymbolName("Konan_js_setFunction") +external public fun setFunction(arena: Arena, obj: Object, propertyName: Pointer, propertyLength: Int , function: Int) + +fun setter(obj: JsValue, property: String, lambda: (arguments: ArrayList) -> Unit) { + val index = wrapFunction(lambda); + setFunction(obj.arena, obj.index, stringPointer(property), stringLengthBytes(property), index) +} + +fun JsValue.setter(property: String, lambda: (ArrayList) -> Unit) { + setter(this, property, lambda) +} diff --git a/samples/html5Canvas/src/main/kotlin/main.kt b/samples/html5Canvas/src/main/kotlin/main.kt new file mode 100644 index 00000000000..f232ec8aa69 --- /dev/null +++ b/samples/html5Canvas/src/main/kotlin/main.kt @@ -0,0 +1,47 @@ +import html5.minimal.* +import kotlinx.wasm.jsinterop.* +import konan.internal.ExportForCppRuntime + +fun main(args: Array) { + val html5 = html5() + val document = html5.document + + val canvas = document.getElementById("myCanvas").asCanvas; + val ctx = canvas.getContext("2d"); + val rect = canvas.getBoundingClientRect(); + val rectLeft = rect.getInt("left") + val rectTop = rect.getInt("top") + + var mouseX: Int = 0; + var mouseY: Int = 0; + var draw: Boolean = false; + + document.setter("onmousemove") { args: ArrayList -> + val event = args[0]; + mouseX = event.getInt("clientX") - rectLeft + mouseY = event.getInt("clientY") - rectTop + if (mouseX < 0) mouseX = 0; + if (mouseX > 639) mouseX = 639; + if (mouseY < 0) mouseY = 0; + if (mouseY > 479) mouseY = 479; + } + + document.setter("onmousedown") { + draw = true; + } + + document.setter("onmouseup") { + draw = false; + } + + setInterval(10) { + if (draw) { + ctx.lineTo(mouseX, mouseY); + ctx.stroke(); + } else { + ctx.moveTo(mouseX, mouseY); + ctx.stroke(); + } + } +} +