wasm gc: fix exporting classes to JS

This commit is contained in:
Alexey Andreev 2024-10-10 14:18:32 +02:00
parent f61d893b6d
commit 14a4a99fa5
10 changed files with 201 additions and 144 deletions

View File

@ -263,15 +263,23 @@ TeaVM.wasmGC = TeaVM.wasmGC || function() {
params.push("p" + i);
}
let paramsAsString = params.length === 0 ? "" : params.join(", ");
return new Function("rethrowJavaAsJs", "fn", `
return function(${paramsAsString}) {
try {
return fn(${paramsAsString});
} catch (e) {
rethrowJavaAsJs(e);
}
};
`)(rethrowJavaAsJs, fn);
return new Function("rethrowJavaAsJs", "fn",
`return function(${paramsAsString}) {\n` +
` try {\n` +
` return fn(${paramsAsString});\n` +
` } catch (e) {\n` +
` rethrowJavaAsJs(e);\n` +
` }\n` +
`};`
)(rethrowJavaAsJs, fn);
}
function renameConstructor(name, c) {
return new Function(
"constructor",
`return function ${name}(marker, javaObject) {\n` +
` return constructor.call(this, marker, javaObject);\n` +
`}\n`
)(c);
}
imports.teavmJso = {
emptyString: () => "",
@ -295,64 +303,41 @@ TeaVM.wasmGC = TeaVM.wasmGC || function() {
}
},
createClass(name, parent, constructor) {
name = sanitizeName(name);
name = sanitizeName(name || "JavaObject");
let action;
if (parent === null) {
let fn = new Function(
"javaObjectSymbol",
"functionsSymbol",
"wrapperCallMarker",
"constructor",
"rethrowJavaAsJs",
`let fn;
fn = function ${name}(marker, javaObject) {
if (marker === wrapperCallMarker) {
this[javaObjectSymbol] = javaObject;
this[functionsSymbol] = null;
} else if (constructor === null) {
throw new Error("This class can't be instantiated directly");
} else {
try {
return fn(wrapperCallMarker, constructor(arguments));
} catch (e) {
rethrowJavaAsJs(e);
}
}
};
let boundFn = function(javaObject) { return fn.call(this, wrapperCallMarker, javaObject); };
boundFn[wrapperCallMarker] = fn;
boundFn.prototype = fn.prototype;
return boundFn;`
);
return fn(javaObjectSymbol, functionsSymbol, wrapperCallMarkerSymbol, constructor, rethrowJavaAsJs);
action = function (javaObject) {
this[javaObjectSymbol] = javaObject;
this[functionsSymbol] = null;
};
} else {
let fn = new Function(
"parent",
"wrapperCallMarker",
"constructor",
"rethrowJavaAsJs",
`let fn
fn = function ${name}(marker, javaObject) {
if (marker === wrapperCallMarker) {
parent.call(this, javaObject);
} else if (constructor === null) {
throw new Error("This class can't be instantiated directly");
} else {
try {
return fn(wrapperCallMarker, constructor(arguments));
} catch (e) {
rethrowJavaAsJs(e);
}
}
};
fn.prototype = Object.create(parent);
fn.prototype.constructor = parent;
let boundFn = function(javaObject) { return fn.call(this, wrapperCallMarker, javaObject); };
boundFn[wrapperCallMarker] = fn;
boundFn.prototype = fn.prototype;
return fn;`
);
return fn(parent, wrapperCallMarkerSymbol, constructor, rethrowJavaAsJs);
action = function (javaObject) {
parent.call(this, javaObject);
};
fn.prototype = Object.create(parent);
fn.prototype.constructor = parent;
}
let fn = renameConstructor(name, function (marker, javaObject) {
if (marker === wrapperCallMarkerSymbol) {
action.call(this, javaObject);
} else if (constructor === null) {
throw new Error("This class can't be instantiated directly");
} else {
try {
return constructor.apply(null, arguments);
} catch (e) {
rethrowJavaAsJs(e);
}
}
});
fn.prototype = Object.create(parent || Object.prototype);
fn.prototype.constructor = fn;
let boundFn = renameConstructor(name, function(javaObject) {
return fn.call(this, wrapperCallMarkerSymbol, javaObject);
});
boundFn[wrapperCallMarkerSymbol] = fn;
boundFn.prototype = fn.prototype;
return boundFn;
},
exportClass(cls) {
return cls[wrapperCallMarkerSymbol];
@ -363,15 +348,15 @@ TeaVM.wasmGC = TeaVM.wasmGC || function() {
params.push("p" + i);
}
let paramsAsString = params.length === 0 ? "" : params.join(", ");
cls.prototype[name] = new Function("rethrowJavaAsJs", "fn", `
return function(${paramsAsString}) {
try {
return fn(${['this', params].join(", ")});
} catch (e) {
rethrowJavaAsJs(e);
}
};
`)(rethrowJavaAsJs, fn);
cls.prototype[name] = new Function("rethrowJavaAsJs", "fn",
`return function(${paramsAsString}) {\n` +
` try {\n` +
` return fn(${['this', params].join(", ")});\n` +
` } catch (e) {\n` +
` rethrowJavaAsJs(e);\n` +
` }\n` +
`};`
)(rethrowJavaAsJs, fn);
},
defineStaticMethod(cls, name, fn) {
cls[name] = defineFunction(fn);
@ -554,32 +539,33 @@ TeaVM.wasmGC = TeaVM.wasmGC || function() {
for (let i = 0; i < 32; ++i) {
let args = argumentList.length === 0 ? "" : argumentList.join(", ");
let argsAndBody = [...argumentList, "body"].join(", ");
imports.teavmJso["createFunction" + i] = new Function("wrapCallFromJavaToJs", ...argumentList, "body", `
return new Function('wrapCallFromJavaToJs', ${argsAndBody}).bind(this, wrapCallFromJavaToJs);
`).bind(null, wrapCallFromJavaToJs);
imports.teavmJso["callFunction" + i] = new Function("rethrowJsAsJava", "fn", ...argumentList, `
try {
return fn(${args});
} catch (e) {
rethrowJsAsJava(e);
}
`).bind(null, rethrowJsAsJava);
imports.teavmJso["createFunction" + i] = new Function("wrapCallFromJavaToJs", ...argumentList, "body",
`return new Function('wrapCallFromJavaToJs', ${argsAndBody}).bind(this, wrapCallFromJavaToJs);`
).bind(null, wrapCallFromJavaToJs);
imports.teavmJso["callFunction" + i] = new Function("rethrowJsAsJava", "fn", ...argumentList,
`try {\n` +
` return fn(${args});\n` +
`} catch (e) {\n` +
` rethrowJsAsJava(e);\n` +
`}`
).bind(null, rethrowJsAsJava);
imports.teavmJso["callMethod" + i] = new Function("rethrowJsAsJava", "getGlobalName", "instance",
"method", ...argumentList, `
try {
return instance !== null
? instance[method](${args})
: getGlobalName(method)(${args});
} catch (e) {
rethrowJsAsJava(e);
}`).bind(null, rethrowJsAsJava, getGlobalName);
imports.teavmJso["construct" + i] = new Function("rethrowJsAsJava", "constructor", ...argumentList, `
try {
return new constructor(${args});
} catch (e) {
rethrowJsAsJava(e);
}
`).bind(null, rethrowJsAsJava);
"method", ...argumentList,
`try {\n`+
` return instance !== null\n` +
` ? instance[method](${args})\n` +
` : getGlobalName(method)(${args});\n` +
`} catch (e) {\n` +
` rethrowJsAsJava(e);\n` +
`}`
).bind(null, rethrowJsAsJava, getGlobalName);
imports.teavmJso["construct" + i] = new Function("rethrowJsAsJava", "constructor", ...argumentList,
`try {\n` +
` return new constructor(${args});\n` +
`} catch (e) {\n` +
` rethrowJsAsJava(e);\n` +
`}`
).bind(null, rethrowJsAsJava);
imports.teavmJso["arrayOf" + i] = new Function(...argumentList, "return [" + args + "]");
let param = "p" + (i + 1);

View File

@ -139,10 +139,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
createWrapperMethod.setLevel(AccessLevel.PUBLIC);
createWrapperMethod.getModifiers().add(ElementModifier.NATIVE);
cls.addMethod(createWrapperMethod);
if (!isJavaScriptClass(cls)) {
cls.getInterfaces().add(JSMethods.JS_MARSHALLABLE);
}
cls.getInterfaces().add(JSMethods.JS_MARSHALLABLE);
}
}

View File

@ -66,6 +66,8 @@ final class WasmGCJSRuntime {
@Import(name = "charAt", module = "teavmJso")
static native char charAt(JSObject str, int index);
static native JSObject wrapObject(Object obj);
static Throwable wrapException(JSObject obj) {
return new WasmGCExceptionWrapper(obj);
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 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.jso.impl.wasmgc;
import org.teavm.ast.InvocationExpr;
import org.teavm.backend.wasm.intrinsics.gc.WasmGCIntrinsic;
import org.teavm.backend.wasm.intrinsics.gc.WasmGCIntrinsicContext;
import org.teavm.backend.wasm.model.expression.WasmCall;
import org.teavm.backend.wasm.model.expression.WasmExpression;
import org.teavm.backend.wasm.model.expression.WasmGetGlobal;
class WasmGCJSRuntimeIntrinsic implements WasmGCIntrinsic {
private WasmGCJsoCommonGenerator commonGen;
WasmGCJSRuntimeIntrinsic(WasmGCJsoCommonGenerator commonGen) {
this.commonGen = commonGen;
}
@Override
public WasmExpression apply(InvocationExpr invocation, WasmGCIntrinsicContext context) {
var jsoContext = WasmGCJsoContext.wrap(context);
var wrapperClass = commonGen.getDefaultWrapperClass(jsoContext);
var wrapperFunction = commonGen.javaObjectToJSFunction(jsoContext);
return new WasmCall(wrapperFunction, context.generate(invocation.getArguments().get(0)),
new WasmGetGlobal(wrapperClass));
}
}

View File

@ -21,7 +21,6 @@ import org.teavm.backend.wasm.intrinsics.gc.WasmGCIntrinsicContext;
import org.teavm.backend.wasm.model.WasmFunction;
import org.teavm.backend.wasm.model.WasmType;
import org.teavm.backend.wasm.model.expression.WasmCall;
import org.teavm.backend.wasm.model.expression.WasmCast;
import org.teavm.backend.wasm.model.expression.WasmExpression;
import org.teavm.backend.wasm.model.expression.WasmExternConversion;
import org.teavm.backend.wasm.model.expression.WasmExternConversionType;
@ -41,17 +40,6 @@ class WasmGCJSWrapperIntrinsic implements WasmGCIntrinsic {
var function = getWrapFunction(context);
return new WasmCall(function, context.generate(invocation.getArguments().get(0)));
}
case "dependencyJavaToJs":
case "directJavaToJs":
return new WasmExternConversion(WasmExternConversionType.ANY_TO_EXTERN,
context.generate(invocation.getArguments().get(0)));
case "dependencyJsToJava":
case "directJsToJava": {
var any = new WasmExternConversion(WasmExternConversionType.EXTERN_TO_ANY,
context.generate(invocation.getArguments().get(0)));
var objectType = context.typeMapper().mapType(ValueType.parse(Object.class));
return new WasmCast(any, (WasmType.Reference) objectType);
}
case "isJava": {
var convert = new WasmExternConversion(WasmExternConversionType.EXTERN_TO_ANY,
context.generate(invocation.getArguments().get(0)));

View File

@ -25,6 +25,7 @@ import org.teavm.model.ClassHolderTransformerContext;
import org.teavm.model.ElementModifier;
import org.teavm.model.MethodDescriptor;
import org.teavm.model.MethodHolder;
import org.teavm.model.ValueType;
import org.teavm.model.emit.ProgramEmitter;
class WasmGCJSWrapperTransformer implements ClassHolderTransformer {
@ -33,6 +34,12 @@ class WasmGCJSWrapperTransformer implements ClassHolderTransformer {
if (cls.getName().equals(JSWrapper.class.getName())) {
transformMarshallMethod(cls.getMethod(new MethodDescriptor("marshallJavaToJs", Object.class,
JSObject.class)), context);
transformDirectJavaToJs(cls.getMethod(new MethodDescriptor("directJavaToJs", Object.class,
JSObject.class)), context);
transformDirectJavaToJs(cls.getMethod(new MethodDescriptor("dependencyJavaToJs", Object.class,
JSObject.class)), context);
transformDirectJsToJava(cls.getMethod(new MethodDescriptor("dependencyJsToJava", JSObject.class,
Object.class)), context);
transformWrapMethod(cls.getMethod(new MethodDescriptor("wrap", JSObject.class, Object.class)));
transformIsJava(cls.getMethod(new MethodDescriptor("isJava", Object.class, boolean.class)), context);
addCreateWrapperMethod(cls, context);
@ -43,9 +50,26 @@ class WasmGCJSWrapperTransformer implements ClassHolderTransformer {
method.getModifiers().remove(ElementModifier.NATIVE);
var pe = ProgramEmitter.create(method, context.getHierarchy());
var obj = pe.var(1, Object.class);
pe.when(obj.instanceOf(ValueType.parse(JSMarshallable.class)).isFalse()).thenDo(() -> {
pe.invoke(WasmGCJSRuntime.class, "wrapObject", JSObject.class, obj).returnValue();
});
obj.cast(JSMarshallable.class).invokeVirtual("marshallToJs", JSObject.class).returnValue();
}
private void transformDirectJavaToJs(MethodHolder method, ClassHolderTransformerContext context) {
method.getModifiers().remove(ElementModifier.NATIVE);
var pe = ProgramEmitter.create(method, context.getHierarchy());
var obj = pe.var(1, Object.class);
pe.invoke(JSWrapper.class, "marshallJavaToJs", JSObject.class, obj).returnValue();
}
private void transformDirectJsToJava(MethodHolder method, ClassHolderTransformerContext context) {
method.getModifiers().remove(ElementModifier.NATIVE);
var pe = ProgramEmitter.create(method, context.getHierarchy());
var obj = pe.var(1, JSObject.class);
pe.invoke(JSWrapper.class, "unmarshallJavaFromJs", Object.class, obj).returnValue();
}
private void transformWrapMethod(MethodHolder method) {
method.getModifiers().add(ElementModifier.NATIVE);
method.setProgram(null);

View File

@ -57,15 +57,10 @@ public final class WasmGCJso {
var wrapperIntrinsic = new WasmGCJSWrapperIntrinsic();
wasmGCHost.addIntrinsic(new MethodReference(JSWrapper.class, "wrap", JSObject.class, Object.class),
wrapperIntrinsic);
wasmGCHost.addIntrinsic(new MethodReference(JSWrapper.class, "dependencyJavaToJs", Object.class,
JSObject.class), wrapperIntrinsic);
wasmGCHost.addIntrinsic(new MethodReference(JSWrapper.class, "directJavaToJs", Object.class, JSObject.class),
wrapperIntrinsic);
wasmGCHost.addIntrinsic(new MethodReference(JSWrapper.class, "dependencyJsToJava", JSObject.class,
Object.class), wrapperIntrinsic);
wasmGCHost.addIntrinsic(new MethodReference(JSWrapper.class, "directJsToJava", JSObject.class, Object.class),
wrapperIntrinsic);
wasmGCHost.addIntrinsic(new MethodReference(JSWrapper.class, "isJava", JSObject.class, boolean.class),
wrapperIntrinsic);
wasmGCHost.addIntrinsic(new MethodReference(WasmGCJSRuntime.class, "wrapObject", Object.class,
JSObject.class), new WasmGCJSRuntimeIntrinsic(commonGen));
}
}

View File

@ -63,6 +63,8 @@ class WasmGCJsoCommonGenerator {
private WasmFunction definePropertyFunction;
private WasmFunction defineStaticPropertyFunction;
private WasmFunction exportClassFunction;
private WasmFunction javaObjectToJSFunction;
private WasmGlobal defaultWrapperClass;
private Map<String, WasmGlobal> definedClasses = new HashMap<>();
WasmGCJsoCommonGenerator(WasmGCJSFunctions jsFunctions) {
@ -198,6 +200,23 @@ class WasmGCJsoCommonGenerator {
return new WasmGetGlobal(global);
}
WasmGlobal getDefaultWrapperClass(WasmGCJsoContext context) {
if (defaultWrapperClass == null) {
var name = context.names().topLevel("teavm.js@defaultWrapperClass");
defaultWrapperClass = new WasmGlobal(name, WasmType.Reference.EXTERN,
new WasmNullConstant(WasmType.Reference.EXTERN));
context.module().globals.add(defaultWrapperClass);
addInitializerPart(context, initializer -> {
var createClass = new WasmCall(createClassFunction(context),
new WasmNullConstant(WasmType.Reference.EXTERN),
new WasmNullConstant(WasmType.Reference.EXTERN),
new WasmNullConstant(WasmType.Reference.FUNC));
initializer.getBody().add(new WasmSetGlobal(defaultWrapperClass, createClass));
});
}
return defaultWrapperClass;
}
WasmGlobal getDefinedClass(WasmGCJsoContext context, String className) {
return definedClasses.computeIfAbsent(className, n -> defineClass(context, n));
}
@ -216,8 +235,6 @@ class WasmGCJsoCommonGenerator {
defineProperties(context, members, cls, global, expressions);
var staticMembers = AliasCollector.collectMembers(cls, AliasCollector::isStaticMember);
defineStaticMethods(context, staticMembers, cls, global, expressions, isModule);
defineStaticProperties(context, staticMembers, cls, global, expressions);
var simpleName = className.substring(className.lastIndexOf('.') + 1);
var javaClassName = context.strings().getStringConstant(simpleName);
@ -233,6 +250,7 @@ class WasmGCJsoCommonGenerator {
WasmExpression constructor;
if (members.constructor != null) {
var function = context.functions().forStaticMethod(members.constructor);
function.setReferenced(true);
constructor = new WasmFunctionReference(function);
needsExport = true;
} else {
@ -240,10 +258,14 @@ class WasmGCJsoCommonGenerator {
}
var createClass = new WasmCall(createClassFunction(context), jsClassName, jsExportedParent, constructor);
expressions.add(0, new WasmSetGlobal(global, createClass));
var globalForStatic = global;
if (needsExport) {
exportClass(context, cls, global, expressions);
globalForStatic = exportClass(context, cls, global, expressions);
}
defineStaticMethods(context, staticMembers, cls, globalForStatic, expressions, isModule);
defineStaticProperties(context, staticMembers, cls, globalForStatic, expressions);
context.addToInitializer(f -> f.getBody().addAll(expressions));
return global;
}
@ -340,7 +362,7 @@ class WasmGCJsoCommonGenerator {
}
}
private void exportClass(WasmGCJsoContext context, ClassReader cls, WasmGlobal global,
private WasmGlobal exportClass(WasmGCJsoContext context, ClassReader cls, WasmGlobal global,
List<WasmExpression> expressions) {
var exportName = getClassAliasName(cls);
var globalName = context.names().topLevel("teavm.js.export.class@" + exportName);
@ -351,6 +373,8 @@ class WasmGCJsoCommonGenerator {
var exported = new WasmCall(exportClassFunction(context), new WasmGetGlobal(global));
expressions.add(new WasmSetGlobal(exportGlobal, exported));
return exportGlobal;
}
private String parentExportedClass(WasmGCJsoContext context, String className) {
@ -453,6 +477,18 @@ class WasmGCJsoCommonGenerator {
return exportClassFunction;
}
WasmFunction javaObjectToJSFunction(WasmGCJsoContext context) {
if (javaObjectToJSFunction == null) {
javaObjectToJSFunction = new WasmFunction(context.functionTypes().of(WasmType.Reference.EXTERN,
context.typeMapper().mapType(ValueType.parse(Object.class)), WasmType.Reference.EXTERN));
javaObjectToJSFunction.setName(context.names().topLevel("teavm.jso@javaObjectToJS"));
javaObjectToJSFunction.setImportName("javaObjectToJS");
javaObjectToJSFunction.setImportModule("teavmJso");
context.module().functions.add(javaObjectToJSFunction);
}
return javaObjectToJSFunction;
}
private String getClassAliasName(ClassReader cls) {
var name = cls.getSimpleName();
if (name == null) {

View File

@ -19,7 +19,6 @@ import org.teavm.backend.wasm.generators.gc.WasmGCCustomGenerator;
import org.teavm.backend.wasm.generators.gc.WasmGCCustomGeneratorContext;
import org.teavm.backend.wasm.model.WasmFunction;
import org.teavm.backend.wasm.model.WasmLocal;
import org.teavm.backend.wasm.model.WasmType;
import org.teavm.backend.wasm.model.expression.WasmCall;
import org.teavm.backend.wasm.model.expression.WasmGetGlobal;
import org.teavm.backend.wasm.model.expression.WasmGetLocal;
@ -28,7 +27,6 @@ import org.teavm.model.ValueType;
class WasmGCMarshallMethodGenerator implements WasmGCCustomGenerator {
private WasmGCJsoCommonGenerator commonGen;
private WasmFunction javaObjectToJSFunction;
WasmGCMarshallMethodGenerator(WasmGCJsoCommonGenerator commonGen) {
this.commonGen = commonGen;
@ -42,21 +40,8 @@ class WasmGCMarshallMethodGenerator implements WasmGCCustomGenerator {
function.add(thisLocal);
var jsClassGlobal = commonGen.getDefinedClass(jsoContext, method.getClassName());
var wrapperFunction = javaObjectToJSFunction(context);
var wrapperFunction = commonGen.javaObjectToJSFunction(jsoContext);
function.getBody().add(new WasmCall(wrapperFunction, new WasmGetLocal(thisLocal),
new WasmGetGlobal(jsClassGlobal)));
}
private WasmFunction javaObjectToJSFunction(WasmGCCustomGeneratorContext context) {
if (javaObjectToJSFunction == null) {
javaObjectToJSFunction = new WasmFunction(context.functionTypes().of(WasmType.Reference.EXTERN,
context.typeMapper().mapType(ValueType.parse(Object.class)), WasmType.Reference.EXTERN));
javaObjectToJSFunction.setName(context.names().topLevel("teavm.jso@javaObjectToJS"));
javaObjectToJSFunction.setImportName("javaObjectToJS");
javaObjectToJSFunction.setImportModule("teavmJso");
context.module().functions.add(javaObjectToJSFunction);
}
return javaObjectToJSFunction;
}
}

View File

@ -112,14 +112,18 @@ public class ExportTest {
@Test
public void varargs() {
testExport("varargs", ModuleWithVararg.class);
testExport("varargs", ModuleWithVararg.class, true);
}
private void testExport(String name, Class<?> moduleClass) {
testExport(name, moduleClass, false);
}
private void testExport(String name, Class<?> moduleClass, boolean skipWasmGC) {
if (jsNeeded) {
testExportJs(name, moduleClass);
}
if (wasmGCNeeded) {
if (wasmGCNeeded && !skipWasmGC) {
testExportWasmGC(name, moduleClass);
}
}