wasm: trying to implement coroutines

This commit is contained in:
Alexey Andreev 2021-03-21 22:13:09 +03:00
parent c4c1408160
commit bd53c1a5a2
6 changed files with 206 additions and 64 deletions

View File

@ -27,9 +27,11 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.teavm.ast.InvocationExpr;
import org.teavm.ast.decompilation.Decompiler;
import org.teavm.backend.lowlevel.analyze.LowLevelInliningFilterFactory;
import org.teavm.backend.lowlevel.dependency.StringsDependencyListener;
@ -69,8 +71,10 @@ import org.teavm.backend.wasm.intrinsics.RuntimeClassIntrinsic;
import org.teavm.backend.wasm.intrinsics.ShadowStackIntrinsic;
import org.teavm.backend.wasm.intrinsics.StructureIntrinsic;
import org.teavm.backend.wasm.intrinsics.WasmHeapIntrinsic;
import org.teavm.backend.wasm.intrinsics.WasmIntrinsic;
import org.teavm.backend.wasm.intrinsics.WasmIntrinsicFactory;
import org.teavm.backend.wasm.intrinsics.WasmIntrinsicFactoryContext;
import org.teavm.backend.wasm.intrinsics.WasmIntrinsicManager;
import org.teavm.backend.wasm.intrinsics.WasmRuntimeIntrinsic;
import org.teavm.backend.wasm.model.WasmFunction;
import org.teavm.backend.wasm.model.WasmLocal;
@ -92,6 +96,7 @@ import org.teavm.backend.wasm.model.expression.WasmLoadInt32;
import org.teavm.backend.wasm.model.expression.WasmReturn;
import org.teavm.backend.wasm.model.expression.WasmSetLocal;
import org.teavm.backend.wasm.model.expression.WasmStoreInt32;
import org.teavm.backend.wasm.model.expression.WasmUnreachable;
import org.teavm.backend.wasm.optimization.UnusedFunctionElimination;
import org.teavm.backend.wasm.render.WasmBinaryRenderer;
import org.teavm.backend.wasm.render.WasmBinaryVersion;
@ -150,7 +155,9 @@ import org.teavm.model.transformation.BoundCheckInsertion;
import org.teavm.model.transformation.ClassPatch;
import org.teavm.model.transformation.NullCheckInsertion;
import org.teavm.runtime.Allocator;
import org.teavm.runtime.EventQueue;
import org.teavm.runtime.ExceptionHandling;
import org.teavm.runtime.Fiber;
import org.teavm.runtime.RuntimeArray;
import org.teavm.runtime.RuntimeClass;
import org.teavm.runtime.RuntimeObject;
@ -354,6 +361,22 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
}
}
dependencyAnalyzer.linkMethod(new MethodReference(Fiber.class, "isResuming", boolean.class)).use();
dependencyAnalyzer.linkMethod(new MethodReference(Fiber.class, "isSuspending", boolean.class)).use();
dependencyAnalyzer.linkMethod(new MethodReference(Fiber.class, "current", Fiber.class)).use();
dependencyAnalyzer.linkMethod(new MethodReference(Fiber.class, "startMain", String[].class, void.class)).use();
dependencyAnalyzer.linkMethod(new MethodReference(EventQueue.class, "processSingle", void.class)).use();
dependencyAnalyzer.linkMethod(new MethodReference(EventQueue.class, "isStopped", boolean.class)).use();
dependencyAnalyzer.linkMethod(new MethodReference(Thread.class, "setCurrentThread", Thread.class,
void.class)).use();
ClassReader fiberClass = dependencyAnalyzer.getClassSource().get(Fiber.class.getName());
for (MethodReader method : fiberClass.getMethods()) {
if (method.getName().startsWith("pop") || method.getName().equals("push")) {
dependencyAnalyzer.linkMethod(method.getReference()).use();
}
}
dependencyAnalyzer.addDependencyListener(new StringsDependencyListener());
}
@ -417,6 +440,7 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
context.addIntrinsic(new MemoryTraceIntrinsic());
}
context.addIntrinsic(new WasmHeapIntrinsic());
context.addIntrinsic(new FiberIntrinsic());
IntrinsicFactoryContext intrinsicFactoryContext = new IntrinsicFactoryContext();
for (WasmIntrinsicFactory additionalIntrinsicFactory : additionalIntrinsics) {
@ -459,15 +483,7 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
generateInitFunction(classes, initFunction, names, binaryWriter.getAddress());
module.add(initFunction);
module.setStartFunction(initFunction);
for (TeaVMEntryPoint entryPoint : controller.getEntryPoints().values()) {
String mangledName = names.forMethod(entryPoint.getMethod());
WasmFunction function = module.getFunctions().get(mangledName);
if (function != null) {
function.setExportName(entryPoint.getPublicName());
}
}
module.add(createStartFunction(names));
for (String functionName : classGenerator.getFunctionTable()) {
WasmFunction function = module.getFunctions().get(functionName);
@ -503,6 +519,22 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
emitRuntime(buildTarget, getBaseName(outputName) + ".wasm-runtime.js");
}
private WasmFunction createStartFunction(NameProvider names) {
WasmFunction function = new WasmFunction("teavm_start");
function.setExportName("start");
function.getParameters().add(WasmType.INT32);
WasmLocal local = new WasmLocal(WasmType.INT32, "args");
function.add(local);
WasmCall call = new WasmCall(names.forMethod(new MethodReference(Fiber.class, "startMain", String[].class,
void.class)));
call.getArguments().add(new WasmGetLocal(local));
function.getBody().add(call);
return function;
}
private class IntrinsicFactoryContext implements WasmIntrinsicFactoryContext {
@Override
public ClassReaderSource getClassSource() {
@ -967,4 +999,53 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
return classSource;
}
}
class FiberIntrinsic implements WasmIntrinsic {
@Override
public boolean isApplicable(MethodReference methodReference) {
if (!methodReference.getClassName().equals(Fiber.class.getName())) {
return false;
}
switch (methodReference.getName()) {
case "runMain":
case "setCurrentThread":
return true;
default:
return false;
}
}
@Override
public WasmExpression apply(InvocationExpr invocation, WasmIntrinsicManager manager) {
switch (invocation.getMethod().getName()) {
case "runMain": {
Iterator<? extends TeaVMEntryPoint> entryPointIter = controller.getEntryPoints().values()
.iterator();
if (entryPointIter.hasNext()) {
TeaVMEntryPoint entryPoint = entryPointIter.next();
String name = manager.getNames().forMethod(entryPoint.getMethod());
WasmCall call = new WasmCall(name);
call.getArguments().add(manager.generate(invocation.getArguments().get(0)));
call.setLocation(invocation.getLocation());
return call;
} else {
WasmUnreachable unreachable = new WasmUnreachable();
unreachable.setLocation(invocation.getLocation());
return unreachable;
}
}
case "setCurrentThread": {
String name = manager.getNames().forMethod(new MethodReference(Thread.class,
"setCurrentThread", Thread.class, void.class));
WasmCall call = new WasmCall(name);
call.getArguments().add(manager.generate(invocation.getArguments().get(0)));
call.setLocation(invocation.getLocation());
return call;
}
default:
throw new IllegalArgumentException();
}
}
}
}

View File

@ -701,7 +701,7 @@ class WasmCRenderingVisitor implements WasmExpressionVisitor {
StringBuilder sb = new StringBuilder();
if (expression.isImported()) {
sb.append(!function.getImportModule().isEmpty()
sb.append(function.getImportModule() != null && !function.getImportModule().isEmpty()
? function.getImportModule() + "_" + function.getImportName()
: function.getImportName());
} else {

View File

@ -17,10 +17,9 @@ package org.teavm.runtime;
import java.util.Arrays;
import org.teavm.backend.c.intrinsic.RuntimeInclude;
import org.teavm.interop.Export;
import org.teavm.interop.Import;
import org.teavm.interop.Platforms;
import org.teavm.interop.StaticInit;
import org.teavm.interop.UnsupportedOn;
@StaticInit
public final class EventQueue {
@ -72,6 +71,31 @@ public final class EventQueue {
}
}
@Export(name = "teavm_processQueue")
public static long processSingle() {
if (size == 0) {
return -1;
}
Node node = data[0];
long currentTime = System.currentTimeMillis();
if (node.time <= System.currentTimeMillis()) {
remove(0);
node.event.run();
if (size == 0) {
return -1;
}
return Math.max(0, node.time - currentTime);
} else {
return node.time - currentTime;
}
}
@Export(name = "teavm_stopped")
public static boolean isStopped() {
return finished;
}
public static void stop() {
finished = true;
}
@ -129,12 +153,10 @@ public final class EventQueue {
@Import(name = "teavm_waitFor")
@RuntimeInclude("fiber.h")
@UnsupportedOn(Platforms.WEBASSEMBLY)
private static native void waitFor(long time);
@Import(name = "teavm_interrupt")
@Import(name = "teavm_interrupt", module = "teavm")
@RuntimeInclude("fiber.h")
@UnsupportedOn(Platforms.WEBASSEMBLY)
private static native void interrupt();
private static void ensureCapacity(int capacity) {

View File

@ -43,19 +43,46 @@ TeaVM.wasm = function() {
function getNativeOffset(instant) {
return new Date(instant).getTimezoneOffset();
}
function logString(string) {
var memory = new DataView(logString.memory.buffer);
var arrayPtr = memory.getUint32(string + 8, true);
var length = memory.getUint32(arrayPtr + 8, true);
for (var i = 0; i < length; ++i) {
putwchar(memory.getUint16(i * 2 + arrayPtr + 12, true));
function logString(string, controller) {
let instance = controller.instance;
let memory = instance.exports.memory.buffer;
let arrayPtr = instance.exports.teavm_stringData(string);
let length = instance.exports.teavm_arrayLength(arrayPtr);
let arrayData = new DataView(memory, instance.exports.teavm_charArrayData(arrayPtr), length * 2);
for (let i = 0; i < length; ++i) {
putwchar(arrayData.memory.getUint16(i * 2, true));
}
}
function logInt(i) {
lineBuffer += i.toString();
}
function interrupt(controller) {
if (controller.timer !== null) {
clearTimeout(controller.timer);
controller.timer = null;
}
controller.timer = setTimeout(() => process(controller), 0);
}
function process(controller) {
let result = controller.instance.exports.teavm_processQueue();
if (!controller.complete) {
if (controller.instance.exports.teavm_stopped()) {
controller.complete = true;
controller.resolve();
}
}
if (result >= 0) {
controller.timer = setTimeout(() => process(controller), result)
}
}
function importDefaults(obj) {
function defaults(obj) {
let controller = {};
controller.instance = null;
controller.timer = null;
controller.resolve = null;
controller.reject = null;
controller.complete = false;
obj.teavm = {
currentTimeMillis: currentTimeMillis,
nanoTime: function() { return performance.now(); },
@ -67,9 +94,10 @@ TeaVM.wasm = function() {
towlower: towlower,
towupper: towupper,
getNativeOffset: getNativeOffset,
logString: logString,
logString: string => logString(string, controller),
logInt: logInt,
logOutOfMemory: function() { console.log("Out of memory") }
logOutOfMemory: () => console.log("Out of memory"),
teavm_interrupt: () => interrupt(controller)
};
obj.teavmMath = Math;
@ -91,6 +119,8 @@ TeaVM.wasm = function() {
gcCompleted: function() {},
init: function(maxHeap) {}
};
return controller;
}
function createTeaVM(instance) {
@ -109,35 +139,29 @@ TeaVM.wasm = function() {
}
teavm.main = createMain(teavm, instance.exports.main);
return teavm;
}
function wrapExport(fn, instance) {
return function() {
let result = fn.apply(this, arguments);
let ex = instance.exports.teavm_catchException();
if (ex !== 0) {
throw new JavaError("Uncaught exception occurred in java");
let ex = catchException(instance);
if (ex !== null) {
throw ex;
}
return result;
}
}
function catchException(instance) {
let ex = instance.exports.teavm_catchException();
if (ex !== 0) {
return new JavaError("Uncaught exception occurred in Java");
}
return null;
}
function load(path, options) {
if (!options) {
options = {};
}
let callback = typeof options.callback !== "undefined" ? options.callback : function() {};
let errorCallback = typeof options.errorCallback !== "undefined" ? options.errorCallback : function() {};
let importObj = {};
importDefaults(importObj);
if (typeof options.installImports !== "undefined") {
options.installImports(importObj);
}
let xhr = new XMLHttpRequest();
xhr.responseType = "arraybuffer";
xhr.open("GET", path);
@ -146,45 +170,61 @@ TeaVM.wasm = function() {
xhr.onload = () => {
let response = xhr.response;
if (!response) {
reject("Error loading Wasm data")
return;
}
WebAssembly.instantiate(response, importObj).then(resultObject => {
importObj.teavm.logString.memory = resultObject.instance.exports.memory;
let teavm = createTeaVM(resultObject.instance);
teavm.main = createMain(teavm, wrapExport, resultObject.instance.exports.main);
resolve(teavm);
}).catch(error => {
reject(error);
});
resolve(response);
};
xhr.send();
}).then(data => create(data, options));
}
function create(data, options) {
if (!options) {
options = {};
}
const importObj = {};
const controller = defaults(importObj);
if (typeof options.installImports !== "undefined") {
options.installImports(importObj);
}
return WebAssembly.instantiate(data, importObj).then(resultObject => {
controller.instance = resultObject.instance;
let teavm = createTeaVM(resultObject.instance);
teavm.main = createMain(teavm, controller);
return teavm;
});
}
function createMain(teavm, mainFunction) {
function createMain(teavm, controller) {
return function(args) {
if (typeof args === "undefined") {
args = [];
}
return new Promise(resolve => {
let javaArgs = teavm.allocateStringArray(mainArgs.length);
let javaArgsData = new Uint32Array(teavm.memory, teavm.objectArrayData(javaArgs), args.length);
for (let i = 0; i < mainArgs.length; ++i) {
return new Promise((resolve, reject) => {
let javaArgs = teavm.allocateStringArray(args.length);
let javaArgsData = new DataView(teavm.memory.buffer, teavm.objectArrayData(javaArgs), args.length * 4);
for (let i = 0; i < args.length; ++i) {
let arg = args[i];
let javaArg = teavm.allocateString(arg.length);
let javaArgAddress = teavm.objectArrayData(teavm.stringData(javaArg));
let javaArgData = new Uint16Array(teavm.memory, javaArgAddress, arg.length);
let javaArgData = new DataView(teavm.memory.buffer, javaArgAddress, arg.length * 2);
for (let j = 0; j < arg.length; ++j) {
javaArgData[j] = arg.charCodeAt(j);
javaArgData.setUint16(j * 2, arg.charCodeAt(j), true);
}
javaArgsData[i] = javaArg;
javaArgsData.setInt32(i * 4, javaArg, true);
}
resolve(wrapExport(mainFunction, teavm.instance)(javaArgs));
controller.resolve = resolve;
controller.reject = reject;
wrapExport(teavm.instance.exports.start, teavm.instance)(javaArgs);
process(controller);
});
}
}
return { JavaError, importDefaults, load, wrapExport, createTeaVM, createMain };
return { JavaError, load, create };
}();

View File

@ -500,7 +500,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
runs.add(createTestRun(configuration, testPath, child, RunKind.WASM, reference.toString(),
notifier, onSuccess));
File htmlPath = getOutputFile(outputPathForMethod, "test-wasm", configuration.getSuffix(), false, ".html");
properties.put("SCRIPT", "../" + testPath.getName() + "-runtime.js");
properties.put("SCRIPT", "../" + testPath.getName());
properties.put("IDENTIFIER", reference.toString());
try {
resourceToFile("teavm-run-test-wasm.html", htmlPath, properties);
@ -555,8 +555,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
if (run != null) {
runs.add(run);
File testPath = getOutputFile(outputPath, "test", configuration.getSuffix(), false,
".wasm-runtime.js");
File testPath = getOutputFile(outputPath, "test", configuration.getSuffix(), false, ".wasm");
File htmlPath = getOutputFile(outputPath, "test", configuration.getSuffix(), false, ".html");
properties.put("SCRIPT", testPath.getName());
properties.put("IDENTIFIER", "");

View File

@ -21,9 +21,9 @@
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
<script type="text/javascript" src="${SCRIPT}"></script>
<script type="text/javascript" src="${SCRIPT}-runtime.js"></script>
<script type="text/javascript">
TeaVM.wasm.load("test.wasm").then(teavm => teavm.main(["${IDENTIFIER}"]));
TeaVM.wasm.load("${SCRIPT}").then(teavm => teavm.main(["${IDENTIFIER}"]));
</script>
</body>
</html>