From c4c1408160d5d27713bba18bff592fd391c57db2 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Sat, 20 Mar 2021 19:52:45 +0300 Subject: [PATCH] wasm: fix running tests, add in-browser test runners --- .../org/teavm/classlib/java/lang/TString.java | 8 + .../org/teavm/backend/wasm/WasmTarget.java | 6 + .../WasmInteropFunctionGenerator.java | 168 ++++++++++++++++++ .../backend/wasm/render/WasmCRenderer.java | 2 +- .../org/teavm/backend/wasm/wasm-runtime.js | 97 ++++++++-- .../benchmark/src/main/webapp/teavm-wasm.js | 10 +- .../java/org/teavm/junit/TeaVMTestRunner.java | 40 +++++ .../main/resources/teavm-run-test-wasm.html | 4 +- .../src/main/resources/test-server/frame.js | 17 +- 9 files changed, 324 insertions(+), 28 deletions(-) create mode 100644 core/src/main/java/org/teavm/backend/wasm/generate/WasmInteropFunctionGenerator.java diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TString.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TString.java index 06c465e34..ae628df92 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/TString.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TString.java @@ -114,6 +114,14 @@ public class TString extends TObject implements TSerializable, TComparable= characters.length) { diff --git a/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java b/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java index 8d5e302ef..35adf7aef 100644 --- a/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java +++ b/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java @@ -40,6 +40,7 @@ import org.teavm.backend.wasm.generate.WasmClassGenerator; import org.teavm.backend.wasm.generate.WasmDependencyListener; import org.teavm.backend.wasm.generate.WasmGenerationContext; import org.teavm.backend.wasm.generate.WasmGenerator; +import org.teavm.backend.wasm.generate.WasmInteropFunctionGenerator; import org.teavm.backend.wasm.generate.WasmNameProvider; import org.teavm.backend.wasm.generate.WasmSpecialFunctionGenerator; import org.teavm.backend.wasm.generate.WasmStringPool; @@ -341,6 +342,9 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost { dependencyAnalyzer.linkField(new FieldReference("java.lang.Object", "monitor")); + dependencyAnalyzer.linkMethod(new MethodReference(String.class, "allocate", int.class, String.class)) + .use(); + ClassDependency runtimeClassDep = dependencyAnalyzer.linkClass(RuntimeClass.class.getName()); ClassDependency runtimeObjectDep = dependencyAnalyzer.linkClass(RuntimeObject.class.getName()); ClassDependency runtimeArrayDep = dependencyAnalyzer.linkClass(RuntimeArray.class.getName()); @@ -431,6 +435,7 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost { WasmGenerator generator = new WasmGenerator(decompiler, classes, context, classGenerator, binaryWriter); generateMethods(classes, context, generator, classGenerator, binaryWriter, module); + new WasmInteropFunctionGenerator(classGenerator).generateFunctions(module); exceptionHandlingIntrinsic.postProcess(CallSiteDescriptor.extract(classes, classes.getClassNames())); generateIsSupertypeFunctions(tagRegistry, module, classGenerator); classGenerator.postProcess(); @@ -455,6 +460,7 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost { module.add(initFunction); module.setStartFunction(initFunction); + for (TeaVMEntryPoint entryPoint : controller.getEntryPoints().values()) { String mangledName = names.forMethod(entryPoint.getMethod()); WasmFunction function = module.getFunctions().get(mangledName); diff --git a/core/src/main/java/org/teavm/backend/wasm/generate/WasmInteropFunctionGenerator.java b/core/src/main/java/org/teavm/backend/wasm/generate/WasmInteropFunctionGenerator.java new file mode 100644 index 000000000..a4f431b33 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/generate/WasmInteropFunctionGenerator.java @@ -0,0 +1,168 @@ +/* + * Copyright 2021 konsoletyper. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.generate; + +import org.teavm.backend.wasm.model.WasmFunction; +import org.teavm.backend.wasm.model.WasmLocal; +import org.teavm.backend.wasm.model.WasmModule; +import org.teavm.backend.wasm.model.WasmType; +import org.teavm.backend.wasm.model.expression.WasmCall; +import org.teavm.backend.wasm.model.expression.WasmExpression; +import org.teavm.backend.wasm.model.expression.WasmGetLocal; +import org.teavm.backend.wasm.model.expression.WasmInt32Constant; +import org.teavm.backend.wasm.model.expression.WasmInt32Subtype; +import org.teavm.backend.wasm.model.expression.WasmIntBinary; +import org.teavm.backend.wasm.model.expression.WasmIntBinaryOperation; +import org.teavm.backend.wasm.model.expression.WasmIntType; +import org.teavm.backend.wasm.model.expression.WasmLoadInt32; +import org.teavm.backend.wasm.model.expression.WasmReturn; +import org.teavm.interop.Address; +import org.teavm.model.FieldReference; +import org.teavm.model.MethodReference; +import org.teavm.model.ValueType; +import org.teavm.runtime.Allocator; +import org.teavm.runtime.RuntimeArray; +import org.teavm.runtime.RuntimeClass; + +public class WasmInteropFunctionGenerator { + private WasmClassGenerator classGenerator; + + public WasmInteropFunctionGenerator(WasmClassGenerator classGenerator) { + this.classGenerator = classGenerator; + } + + public void generateFunctions(WasmModule module) { + module.add(allocateString()); + module.add(stringData()); + + module.add(allocateArray("teavm_allocateObjectArray", ValueType.parse(Object.class))); + module.add(allocateArray("teavm_allocateStringArray", ValueType.parse(String.class))); + module.add(allocateArray("teavm_allocateByteArray", ValueType.parse(byte.class))); + module.add(allocateArray("teavm_allocateShortArray", ValueType.parse(short.class))); + module.add(allocateArray("teavm_allocateCharArray", ValueType.parse(char.class))); + module.add(allocateArray("teavm_allocateIntArray", ValueType.parse(int.class))); + module.add(allocateArray("teavm_allocateLongArray", ValueType.parse(long.class))); + module.add(allocateArray("teavm_allocateFloatArray", ValueType.parse(float.class))); + module.add(allocateArray("teavm_allocateDoubleArray", ValueType.parse(double.class))); + module.add(arrayData("teavm_objectArrayData", 4)); + module.add(arrayData("teavm_byteArrayData", 1)); + module.add(arrayData("teavm_shortArrayData", 2)); + module.add(arrayData("teavm_charArrayData", 2)); + module.add(arrayData("teavm_intArrayData", 4)); + module.add(arrayData("teavm_longArrayData", 8)); + module.add(arrayData("teavm_floatArrayData", 4)); + module.add(arrayData("teavm_doubleArrayData", 8)); + module.add(arrayLength()); + } + + + private WasmFunction allocateString() { + WasmFunction function = new WasmFunction("teavm_allocateString"); + function.setExportName(function.getName()); + function.setResult(WasmType.INT32); + function.getParameters().add(WasmType.INT32); + + WasmLocal sizeLocal = new WasmLocal(WasmType.INT32, "size"); + function.add(sizeLocal); + + String constructorName = classGenerator.names.forMethod(new MethodReference(String.class, "allocate", + int.class, String.class)); + WasmCall constructorCall = new WasmCall(constructorName); + constructorCall.getArguments().add(new WasmGetLocal(sizeLocal)); + function.getBody().add(constructorCall); + + function.getBody().add(new WasmReturn(constructorCall)); + + return function; + } + + private WasmFunction allocateArray(String name, ValueType type) { + WasmFunction function = new WasmFunction(name); + function.setExportName(name); + function.setResult(WasmType.INT32); + function.getParameters().add(WasmType.INT32); + + WasmLocal sizeLocal = new WasmLocal(WasmType.INT32, "size"); + function.add(sizeLocal); + + int classPointer = classGenerator.getClassPointer(ValueType.arrayOf(type)); + String allocName = classGenerator.names.forMethod(new MethodReference(Allocator.class, "allocateArray", + RuntimeClass.class, int.class, Address.class)); + WasmCall call = new WasmCall(allocName); + call.getArguments().add(new WasmInt32Constant(classPointer)); + call.getArguments().add(new WasmGetLocal(sizeLocal)); + + function.getBody().add(new WasmReturn(call)); + + return function; + } + + private WasmFunction stringData() { + WasmFunction function = new WasmFunction("teavm_stringData"); + function.setExportName(function.getName()); + function.setResult(WasmType.INT32); + function.getParameters().add(WasmType.INT32); + + WasmLocal stringLocal = new WasmLocal(WasmType.INT32, "string"); + function.add(stringLocal); + + int offset = classGenerator.getFieldOffset(new FieldReference("java.lang.String", "characters")); + WasmExpression chars = new WasmLoadInt32(4, new WasmGetLocal(stringLocal), WasmInt32Subtype.INT32, offset); + + function.getBody().add(new WasmReturn(chars)); + + return function; + } + + private WasmFunction arrayData(String name, int alignment) { + WasmFunction function = new WasmFunction(name); + function.setExportName(function.getName()); + function.setResult(WasmType.INT32); + function.getParameters().add(WasmType.INT32); + + WasmLocal arrayLocal = new WasmLocal(WasmType.INT32, "array"); + function.add(arrayLocal); + + int start = WasmClassGenerator.align(classGenerator.getClassSize(RuntimeArray.class.getName()), + alignment); + WasmExpression data = new WasmIntBinary(WasmIntType.INT32, WasmIntBinaryOperation.ADD, + new WasmGetLocal(arrayLocal), new WasmInt32Constant(start)); + + function.getBody().add(new WasmReturn(data)); + + return function; + } + + private WasmFunction arrayLength() { + WasmFunction function = new WasmFunction("teavm_arrayLength"); + function.setExportName(function.getName()); + function.setResult(WasmType.INT32); + function.getParameters().add(WasmType.INT32); + + WasmLocal arrayLocal = new WasmLocal(WasmType.INT32, "array"); + function.add(arrayLocal); + + int sizeOffset = classGenerator.getFieldOffset(new FieldReference(RuntimeArray.class.getName(), "size")); + + WasmExpression ptr = new WasmIntBinary(WasmIntType.INT32, WasmIntBinaryOperation.ADD, + new WasmGetLocal(arrayLocal), new WasmInt32Constant(sizeOffset)); + WasmExpression length = new WasmLoadInt32(4, ptr, WasmInt32Subtype.INT32); + + function.getBody().add(new WasmReturn(length)); + + return function; + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/render/WasmCRenderer.java b/core/src/main/java/org/teavm/backend/wasm/render/WasmCRenderer.java index 225b060b3..4da803fa8 100644 --- a/core/src/main/java/org/teavm/backend/wasm/render/WasmCRenderer.java +++ b/core/src/main/java/org/teavm/backend/wasm/render/WasmCRenderer.java @@ -225,7 +225,7 @@ public class WasmCRenderer { renderFunctionModifiers(sb, function); sb.append(WasmCRenderingVisitor.mapType(function.getResult())).append(' '); if (function.getImportName() != null) { - sb.append(!function.getImportModule().isEmpty() + sb.append(function.getImportModule() != null && !function.getImportModule().isEmpty() ? function.getImportModule() + "_" + function.getImportName() : function.getImportName()); } else { diff --git a/core/src/main/resources/org/teavm/backend/wasm/wasm-runtime.js b/core/src/main/resources/org/teavm/backend/wasm/wasm-runtime.js index 4fbd41e57..d5ee00b3d 100644 --- a/core/src/main/resources/org/teavm/backend/wasm/wasm-runtime.js +++ b/core/src/main/resources/org/teavm/backend/wasm/wasm-runtime.js @@ -16,6 +16,12 @@ var TeaVM = TeaVM || {}; TeaVM.wasm = function() { + class JavaError extends Error { + constructor(message) { + super(message) + } + } + let lineBuffer = ""; function putwchar(charCode) { if (charCode === 10) { @@ -87,7 +93,38 @@ TeaVM.wasm = function() { }; } - function run(path, options) { + function createTeaVM(instance) { + let teavm = { + memory: instance.exports.memory, + instance, + catchException: instance.exports.teavm_catchException + } + + for (const name of ["allocateString", "stringData", "allocateObjectArray", "allocateStringArray", + "allocateByteArray", "allocateShortArray", "allocateCharArray", "allocateIntArray", + "allocateLongArray", "allocateFloatArray", "allocateDoubleArray", + "objectArrayData", "byteArrayData", "shortArrayData", "charArrayData", "intArrayData", + "longArrayData", "floatArrayData", "doubleArrayData", "arrayLength"]) { + teavm[name] = wrapExport(instance.exports["teavm_" + name], instance); + } + + 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"); + } + return result; + } + } + + function load(path, options) { if (!options) { options = {}; } @@ -105,23 +142,49 @@ TeaVM.wasm = function() { xhr.responseType = "arraybuffer"; xhr.open("GET", path); - xhr.onload = function() { - let response = xhr.response; - if (!response) { - return; - } + return new Promise((resolve, reject) => { + xhr.onload = () => { + let response = xhr.response; + if (!response) { + return; + } - WebAssembly.instantiate(response, importObj).then(function(resultObject) { - importObj.teavm.logString.memory = resultObject.instance.exports.memory; - resultObject.instance.exports.main(); - callback(resultObject); - }).catch(function(error) { - console.log("Error loading WebAssembly %o", error); - errorCallback(error); - }); - }; - xhr.send(); + 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); + }); + }; + xhr.send(); + }); } - return { importDefaults: importDefaults, run: run }; + function createMain(teavm, mainFunction) { + 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) { + 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); + for (let j = 0; j < arg.length; ++j) { + javaArgData[j] = arg.charCodeAt(j); + } + javaArgsData[i] = javaArg; + } + + resolve(wrapExport(mainFunction, teavm.instance)(javaArgs)); + }); + } + } + + return { JavaError, importDefaults, load, wrapExport, createTeaVM, createMain }; }(); diff --git a/samples/benchmark/src/main/webapp/teavm-wasm.js b/samples/benchmark/src/main/webapp/teavm-wasm.js index 45378e8d6..2b0b62cbf 100644 --- a/samples/benchmark/src/main/webapp/teavm-wasm.js +++ b/samples/benchmark/src/main/webapp/teavm-wasm.js @@ -21,12 +21,12 @@ var Benchmark = function() { this.resultTableBody = document.getElementById("result-table-body"); } Benchmark.prototype.load = function() { - TeaVM.wasm.run("teavm-wasm/classes.wasm", { + TeaVM.wasm.load("teavm-wasm/classes.wasm", { installImports: installImports.bind(this), - callback: function(result) { - this.instance = result.instance; - }.bind(this) - }); + }).then(teavm => { + this.instance = teavm; + teavm.main(); + }) }; function installImports(o) { diff --git a/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java b/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java index 8c54a4272..026d6b99e 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java +++ b/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java @@ -104,6 +104,7 @@ public class TeaVMTestRunner extends Runner implements Filterable { static final String TESTNG_PROVIDER = "org.testng.annotations.DataProvider"; private static final String PATH_PARAM = "teavm.junit.target"; private static final String JS_RUNNER = "teavm.junit.js.runner"; + private static final String WASM_RUNNER = "teavm.junit.wasm.runner"; private static final String THREAD_COUNT = "teavm.junit.threads"; private static final String JS_ENABLED = "teavm.junit.js"; static final String JS_DECODE_STACK = "teavm.junit.js.decodeStack"; @@ -191,6 +192,25 @@ public class TeaVMTestRunner extends Runner implements Filterable { if (cCommand != null) { runners.get(RunKind.C).strategy = new CRunStrategy(cCommand); } + + runStrategyName = System.getProperty(WASM_RUNNER); + if (runStrategyName != null) { + TestRunStrategy wasmRunStrategy; + switch (runStrategyName) { + case "browser": + wasmRunStrategy = new BrowserRunStrategy(outputDir, "WASM", this::customBrowser); + break; + case "browser-chrome": + wasmRunStrategy = new BrowserRunStrategy(outputDir, "WASM", this::chromeBrowser); + break; + case "browser-firefox": + wasmRunStrategy = new BrowserRunStrategy(outputDir, "WASM", this::firefoxBrowser); + break; + default: + throw new InitializationError("Unknown run strategy: " + runStrategyName); + } + runners.get(RunKind.WASM).strategy = wasmRunStrategy; + } } private Process customBrowser(String url) { @@ -479,6 +499,14 @@ public class TeaVMTestRunner extends Runner implements Filterable { File testPath = getOutputFile(outputPath, "classTest", configuration.getSuffix(), false, ".wasm"); 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("IDENTIFIER", reference.toString()); + try { + resourceToFile("teavm-run-test-wasm.html", htmlPath, properties); + } catch (IOException e) { + throw new RuntimeException(e); + } } for (TeaVMTestConfiguration configuration : getCConfigurations()) { @@ -526,6 +554,18 @@ public class TeaVMTestRunner extends Runner implements Filterable { TestRun run = prepareRun(configuration, child, compileResult, notifier, RunKind.WASM, onSuccess); if (run != null) { runs.add(run); + + File testPath = getOutputFile(outputPath, "test", configuration.getSuffix(), false, + ".wasm-runtime.js"); + File htmlPath = getOutputFile(outputPath, "test", configuration.getSuffix(), false, ".html"); + properties.put("SCRIPT", testPath.getName()); + properties.put("IDENTIFIER", ""); + + try { + resourceToFile("teavm-run-test-wasm.html", htmlPath, properties); + } catch (IOException e) { + throw new RuntimeException(e); + } } } } catch (Throwable e) { diff --git a/tools/junit/src/main/resources/teavm-run-test-wasm.html b/tools/junit/src/main/resources/teavm-run-test-wasm.html index 6b95accbc..4cf3158b7 100644 --- a/tools/junit/src/main/resources/teavm-run-test-wasm.html +++ b/tools/junit/src/main/resources/teavm-run-test-wasm.html @@ -21,9 +21,9 @@ - + \ No newline at end of file diff --git a/tools/junit/src/main/resources/test-server/frame.js b/tools/junit/src/main/resources/test-server/frame.js index 54192b2cf..4bcafb8c6 100644 --- a/tools/junit/src/main/resources/test-server/frame.js +++ b/tools/junit/src/main/resources/test-server/frame.js @@ -32,7 +32,7 @@ window.addEventListener("message", event => { case "WASM": const runtimeFile = request.file + "-runtime.js"; appendFiles([runtimeFile], 0, () => { - launchWasmTest(request.file, equest.argument, response => { + launchWasmTest(request.file, request.argument, response => { event.source.postMessage(response, "*"); }); }, error => { @@ -112,7 +112,7 @@ function launchWasmTest(path, argument, callback) { } } - TeaVM.wasm.run(path, { + TeaVM.wasm.load(path, { installImports: function(o) { o.teavm.putwchar = putwchar; }, @@ -122,7 +122,18 @@ function launchWasmTest(path, argument, callback) { errorMessage: err.message + '\n' + err.stack })); } - }); + }).then(teavm => { + teavm.main(argument ? [argument] : []); + }) + .then(() => { + callback(wrapResponse({ status: "OK" })); + }) + .catch(err => { + callback(wrapResponse({ + status: "failed", + errorMessage: err.message + '\n' + err.stack + })); + }) } function start() {