HTML5 canvas drawing demo.

This commit is contained in:
Alexander Gorshenev
2017-09-06 13:53:09 +03:00
committed by alexander-gorshenev
parent ce3ed52705
commit b367a0d255
13 changed files with 414 additions and 35 deletions
@@ -346,7 +346,12 @@ internal class LinkStage(val context: Context) {
private fun JavaScriptLinker(jsFiles: List<String>, executable: String): String { private fun JavaScriptLinker(jsFiles: List<String>, executable: String): String {
val linkedJavaScript = File("$executable.js") 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 { jsFiles.forEach {
linkedJavaScript.appendBytes(File(it).readBytes()) linkedJavaScript.appendBytes(File(it).readBytes())
+1 -1
View File
@@ -260,7 +260,7 @@ targetList.each { target ->
} }
if (target == 'wasm32') { if (target == 'wasm32') {
from(project(':runtime').file('src/launcher/js')) { from(project(':runtime').file('src/launcher/js')) {
into('konan/nativelib') into('klib/stdlib/targets/wasm32/included')
} }
} }
} }
@@ -282,7 +282,7 @@ fun handleExceptionContinuation(x: (Throwable) -> Unit): Continuation<Any?> = ob
def targetToolchain = TargetPropertiesKt.hostTargetString(properties, "targetToolchain", target) def targetToolchain = TargetPropertiesKt.hostTargetString(properties, "targetToolchain", target)
def absoluteTargetToolchain = "$dependenciesDir/$targetToolchain" def absoluteTargetToolchain = "$dependenciesDir/$targetToolchain"
def d8 = "$absoluteTargetToolchain/bin/d8" def d8 = "$absoluteTargetToolchain/bin/d8"
def launcherJs = "${dist.absolutePath}/konan/nativelib/launcher.js" def launcherJs = "${exe}.js"
return [d8, '--expose-wasm', launcherJs, '--', exe] return [d8, '--expose-wasm', launcherJs, '--', exe]
} else { } else {
return [exe] return [exe]
+14 -8
View File
@@ -38,6 +38,12 @@ OBJ_GETTER(setupArgs, int argc, const char** argv) {
//--- main --------------------------------------------------------------------// //--- main --------------------------------------------------------------------//
extern "C" KInt Konan_start(const ObjHeader*); 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) { extern "C" RUNTIME_USED int Konan_main(int argc, const char** argv) {
RuntimeState* state = InitRuntime(); RuntimeState* state = InitRuntime();
@@ -45,12 +51,7 @@ extern "C" RUNTIME_USED int Konan_main(int argc, const char** argv) {
return 2; return 2;
} }
KInt exitStatus; KInt exitStatus = Konan_run_start(argc, argv);
{
ObjHolder args;
setupArgs(argc, argv, args.slot());
exitStatus = Konan_start(args.obj());
}
DeinitRuntime(state); 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_arg_size(int index);
extern "C" int Konan_js_fetch_arg(int index, char* ptr); 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); char** argv = (char**)konan::calloc(1, argc);
for (int i = 0; i< argc; ++i) { for (int i = 0; i< argc; ++i) {
argv[i] = (char*)konan::calloc(1, Konan_js_arg_size(i)); argv[i] = (char*)konan::calloc(1, Konan_js_arg_size(i));
Konan_js_fetch_arg(i, argv[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
#endif #endif
+65 -23
View File
@@ -14,13 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
var module;
var instance; var instance;
var heap; var heap;
var memory; var memory;
var global_arguments; var global_arguments;
var globalBase = 0; // TODO: Is there any way to obtain global_base from JavaScript? var globalBase = 0; // TODO: Is there any way to obtain global_base from JavaScript?
var konanStackTop;
function isBrowser() { function isBrowser() {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return false; return false;
@@ -29,6 +30,7 @@ function isBrowser() {
}; };
} }
var runtime;
if (isBrowser()) { if (isBrowser()) {
runtime = { runtime = {
print: console.log, print: console.log,
@@ -85,6 +87,14 @@ function toString(pointer) {
return string; 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) { function int32ToHeap(value, pointer) {
heap[pointer] = value & 0xff; heap[pointer] = value & 0xff;
heap[pointer+1] = (value & 0xff00) >>> 8; 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(); if (args.length < 1) print_usage();
global_arguments = args; global_arguments = args;
module = new WebAssembly.Module(new Uint8Array(buffer)); instance = inst;
module.env = {};
module.env.memoryBase = 0;
module.env.tablebase = 0;
instance = new WebAssembly.Instance(module, konan_dependencies);
memory = konan_dependencies.env.memory memory = konan_dependencies.env.memory
heap = new Uint8Array(konan_dependencies.env.memory.buffer); heap = new Uint8Array(konan_dependencies.env.memory.buffer);
konanStackTop = stackTop(); konanStackTop = stackTop();
var exit_status = 0; var exit_status = 0;
try { try {
runGlobalInitializers(instance.exports); 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) { } catch (e) {
runtime.print("Exception executing Konan_js_main: " + e); runtime.print("Exception executing entry point: " + e);
runtime.print(e.stack); runtime.print(e.stack);
exit_status = 1; exit_status = 1;
} }
runtime.flush(); runtime.flush();
return exit_status; return exit_status;
} }
// Invoke from the browser. function setupModule(module) {
function loadAndInvoke(filename) { module.env = {};
function listener () { module.env.memoryBase = 0;
invokeModule(this.response, [filename]); module.env.tablebase = 0;
} linkJavaScriptLibraries();
var request = new XMLHttpRequest();
request.addEventListener("load", listener);
request.open("GET", filename);
request.responseType = "arraybuffer";
request.send();
} }
// Invoke from d8. // Instantiate module in Browser in a sequence of promises.
if (!isBrowser()) { function instantiateAndRun(arraybuffer, args) {
quit(invokeModule(readbuffer(arguments[0]), arguments)); 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);
} }
+12
View File
@@ -1092,4 +1092,16 @@ OBJ_GETTER(Kotlin_String_subSequence, KString thiz, KInt startIndex, KInt endInd
RETURN_OBJ(result->obj()); 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" } // extern "C"
+38
View File
@@ -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
+14
View File
@@ -0,0 +1,14 @@
<html>
<head>
<meta charset="utf-8">
<script src="build/bin/html5Canvas.wasm.js" wasm="build/bin//html5Canvas.wasm"> </script>
</head>
<body>
<canvas id="myCanvas" width="640" height="480" style="border:1px solid #000000;">
</canvas>
<p>Draw something using the mouse!</p>
</body>
</html>
@@ -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();
},
});
@@ -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<JsValue>)->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<JsValue>)->Unit)
= setInterval(lambda, interval)
class html5 {
val arena = allocateArena()
val document = Document(arena, addDocumentToArena(arena))
}
@@ -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);
},
});
@@ -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<JsValue>)->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<JsValue>()
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<JsValue>) -> Unit) {
val index = wrapFunction(lambda);
setFunction(obj.arena, obj.index, stringPointer(property), stringLengthBytes(property), index)
}
fun JsValue.setter(property: String, lambda: (ArrayList<JsValue>) -> Unit) {
setter(this, property, lambda)
}
@@ -0,0 +1,47 @@
import html5.minimal.*
import kotlinx.wasm.jsinterop.*
import konan.internal.ExportForCppRuntime
fun main(args: Array<String>) {
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<JsValue> ->
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();
}
}
}