wasm: fix running tests, add in-browser test runners

This commit is contained in:
Alexey Andreev 2021-03-20 19:52:45 +03:00
parent b2b7a603b4
commit c4c1408160
9 changed files with 324 additions and 28 deletions

View File

@ -114,6 +114,14 @@ public class TString extends TObject implements TSerializable, TComparable<TStri
this(sb.buffer, 0, sb.length());
}
private TString(int length) {
this.characters = new char[length];
}
private static TString allocate(int size) {
return new TString(size);
}
@Override
public char charAt(int index) {
if (index < 0 || index >= characters.length) {

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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 };
}();

View File

@ -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) {

View File

@ -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<CTarget> 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) {

View File

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

View File

@ -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() {