JS: allow passing Object to JS methods

This commit is contained in:
Alexey Andreev 2023-07-31 20:42:09 +02:00
parent 059281a25c
commit a1ed797d73
9 changed files with 130 additions and 18 deletions

View File

@ -171,8 +171,7 @@ class JSClassProcessor {
callerMethod.getModifiers().add(ElementModifier.STATIC);
Program program = ProgramUtils.copy(method.getProgram());
program.createVariable();
InstructionVariableMapper variableMapper = new InstructionVariableMapper(var ->
program.variableAt(var.getIndex() + 1));
var variableMapper = new InstructionVariableMapper(var -> program.variableAt(var.getIndex() + 1));
for (int i = program.variableCount() - 1; i > 0; --i) {
program.variableAt(i).setDebugName(program.variableAt(i - 1).getDebugName());
program.variableAt(i).setLabel(program.variableAt(i - 1).getLabel());
@ -241,12 +240,12 @@ class JSClassProcessor {
processIsInstance((IsInstanceInstruction) insn);
} else if (insn instanceof InvokeInstruction) {
var invoke = (InvokeInstruction) insn;
processInvokeArgs(invoke);
var method = getMethod(invoke.getMethod().getClassName(), invoke.getMethod().getDescriptor());
if (method == null) {
continue;
}
processInvokeArgs(invoke, method);
var callLocation = new CallLocation(methodToProcess.getReference(), insn.getLocation());
replacement.clear();
if (processInvocation(method, callLocation, invoke, methodToProcess)) {
@ -272,8 +271,9 @@ class JSClassProcessor {
}
}
private void processInvokeArgs(InvokeInstruction invoke) {
if (typeHelper.isJavaScriptClass(invoke.getMethod().getClassName())) {
private void processInvokeArgs(InvokeInstruction invoke, MethodReader methodToInvoke) {
if (typeHelper.isJavaScriptClass(invoke.getMethod().getClassName())
|| methodToInvoke.getAnnotations().get(JSBody.class.getName()) != null) {
return;
}
Variable[] newArgs = null;

View File

@ -15,7 +15,6 @@
*/
package org.teavm.jso.impl;
import java.util.Set;
import org.teavm.dependency.AbstractDependencyListener;
import org.teavm.dependency.DependencyAgent;
import org.teavm.dependency.MethodDependency;
@ -35,7 +34,7 @@ class JSDependencyListener extends AbstractDependencyListener {
@Override
public void methodReached(DependencyAgent agent, MethodDependency method) {
MethodReference ref = method.getReference();
Set<MethodReference> callbackMethods = repository.callbackMethods.get(ref);
var callbackMethods = repository.callbackMethods.get(ref);
if (callbackMethods != null) {
for (MethodReference callbackMethod : callbackMethods) {
agent.linkMethod(callbackMethod).addLocation(new CallLocation(ref)).use();

View File

@ -55,14 +55,27 @@ public class JSOPlugin implements TeaVMPlugin {
var wrapperGenerator = new JSWrapperGenerator();
jsHost.add(new MethodReference(JSWrapper.class, "directJavaToJs", Object.class, JSObject.class),
wrapperGenerator);
jsHost.add(new MethodReference(JSWrapper.class, "directJsToJava", JSObject.class, Object.class),
wrapperGenerator);
jsHost.add(new MethodReference(JSWrapper.class, "dependencyJavaToJs", Object.class, JSObject.class),
wrapperGenerator);
jsHost.add(new MethodReference(JSWrapper.class, "dependencyJsToJava", JSObject.class, Object.class),
wrapperGenerator);
jsHost.add(new MethodReference(JSWrapper.class, "isJava", Object.class, boolean.class),
wrapperGenerator);
jsHost.add(new MethodReference(JSWrapper.class, "isJava", JSObject.class, boolean.class),
wrapperGenerator);
jsHost.add(new MethodReference(JSWrapper.class, "wrapperToJs", JSWrapper.class, JSObject.class),
wrapperGenerator);
jsHost.add(new MethodReference(JSWrapper.class, "jsToWrapper", JSObject.class, JSWrapper.class),
wrapperGenerator);
host.add(new MethodReference(JSWrapper.class, "jsToWrapper", JSObject.class, JSWrapper.class),
wrapperGenerator);
host.add(new MethodReference(JSWrapper.class, "dependencyJavaToJs", Object.class, JSObject.class),
wrapperGenerator);
host.add(new MethodReference(JSWrapper.class, "dependencyJsToJava", JSObject.class, Object.class),
wrapperGenerator);
TeaVMPluginUtil.handleNatives(host, JS.class);
}

View File

@ -28,7 +28,7 @@ class JSTypeHelper {
private Map<String, Boolean> knownJavaScriptClasses = new HashMap<>();
private Map<String, Boolean> knownJavaScriptImplementations = new HashMap<>();
public JSTypeHelper(ClassReaderSource classSource) {
JSTypeHelper(ClassReaderSource classSource) {
this.classSource = classSource;
knownJavaScriptClasses.put(JSObject.class.getName(), true);
}
@ -91,7 +91,8 @@ class JSTypeHelper {
return isSupportedType(((ValueType.Array) type).getItemType());
} else if (type instanceof ValueType.Object) {
String typeName = ((ValueType.Object) type).getClassName();
return typeName.equals("java.lang.String") || isJavaScriptClass(typeName);
return typeName.equals("java.lang.String") || typeName.equals("java.lang.Object")
|| isJavaScriptClass(typeName);
} else {
return false;
}

View File

@ -107,6 +107,16 @@ class JSValueMarshaller {
if (type instanceof ValueType.Object) {
String className = ((ValueType.Object) type).getClassName();
if (className.equals("java.lang.Object")) {
var unwrapNative = new InvokeInstruction();
unwrapNative.setLocation(location);
unwrapNative.setType(InvocationType.SPECIAL);
unwrapNative.setMethod(new MethodReference(JSWrapper.class, "javaToJs", Object.class, JSObject.class));
unwrapNative.setArguments(var);
unwrapNative.setReceiver(program.createVariable());
replacement.add(unwrapNative);
return unwrapNative.getReceiver();
}
if (!className.equals("java.lang.String")) {
return var;
}
@ -282,7 +292,16 @@ class JSValueMarshaller {
}
} else if (type instanceof ValueType.Object) {
String className = ((ValueType.Object) type).getClassName();
if (className.equals(JSObject.class.getName())) {
if (className.equals(Object.class.getName())) {
var wrapNative = new InvokeInstruction();
wrapNative.setLocation(location.getSourceLocation());
wrapNative.setType(InvocationType.SPECIAL);
wrapNative.setMethod(new MethodReference(JSWrapper.class, "jsToJava", JSObject.class, Object.class));
wrapNative.setArguments(var);
wrapNative.setReceiver(program.createVariable());
replacement.add(wrapNative);
return wrapNative.getReceiver();
} else if (className.equals(JSObject.class.getName())) {
return var;
} else if (className.equals("java.lang.String")) {
return unwrap(var, "unwrapString", JSMethods.JS_OBJECT, stringType, location.getSourceLocation());

View File

@ -109,6 +109,15 @@ public final class JSWrapper {
@NoSideEffects
public static native JSObject directJavaToJs(Object obj);
@NoSideEffects
public static native Object directJsToJava(JSObject obj);
@NoSideEffects
public static native JSObject dependencyJavaToJs(Object obj);
@NoSideEffects
public static native Object dependencyJsToJava(JSObject obj);
@NoSideEffects
private static native JSObject wrapperToJs(JSWrapper obj);
@ -118,6 +127,9 @@ public final class JSWrapper {
@NoSideEffects
public static native boolean isJava(Object obj);
@NoSideEffects
public static native boolean isJava(JSObject obj);
public static JSObject unwrap(Object o) {
if (o == null) {
return null;
@ -132,6 +144,20 @@ public final class JSWrapper {
return isJava(o) ? unwrap(o) : directJavaToJs(o);
}
public static JSObject javaToJs(Object o) {
if (o == null) {
return null;
}
return isJava(o) && o instanceof JSWrapper ? unwrap(o) : dependencyJavaToJs(o);
}
public static Object jsToJava(JSObject o) {
if (o == null) {
return null;
}
return !isJava(o) ? wrap(directJsToJava(o)) : dependencyJsToJava(o);
}
public static boolean isJs(Object o) {
if (o == null) {
return false;

View File

@ -19,15 +19,21 @@ import java.io.IOException;
import org.teavm.backend.javascript.spi.Injector;
import org.teavm.backend.javascript.spi.InjectorContext;
import org.teavm.dependency.DependencyAgent;
import org.teavm.dependency.DependencyNode;
import org.teavm.dependency.DependencyPlugin;
import org.teavm.dependency.MethodDependency;
import org.teavm.model.MethodReference;
public class JSWrapperGenerator implements Injector, DependencyPlugin {
private DependencyNode externalClassesNode;
@Override
public void generate(InjectorContext context, MethodReference methodRef) throws IOException {
switch (methodRef.getName()) {
case "directJavaToJs":
case "directJsToJava":
case "dependencyJavaToJs":
case "dependencyJsToJava":
case "wrapperToJs":
case "jsToWrapper":
context.writeExpr(context.getArgument(0));
@ -41,8 +47,23 @@ public class JSWrapperGenerator implements Injector, DependencyPlugin {
@Override
public void methodReached(DependencyAgent agent, MethodDependency method) {
if (method.getMethod().getName().equals("jsToWrapper")) {
method.getResult().propagate(agent.getType(JSWrapper.class.getName()));
switch (method.getMethod().getName()) {
case "jsToWrapper":
method.getResult().propagate(agent.getType(JSWrapper.class.getName()));
break;
case "dependencyJavaToJs":
method.getVariable(1).connect(getExternalClassesNode(agent));
break;
case "dependencyJsToJava":
getExternalClassesNode(agent).connect(method.getResult());
break;
}
}
private DependencyNode getExternalClassesNode(DependencyAgent agent) {
if (externalClassesNode == null) {
externalClassesNode = agent.createNode();
}
return externalClassesNode;
}
}

View File

@ -229,9 +229,39 @@ public class JSWrapperTest {
assertEquals("org.teavm.jso.impl.JSWrapper", field1.getClass().getName());
}
@Test
public void passJavaToJS() {
var a = processObject(new A(23));
assertTrue(a instanceof A);
assertEquals(23, ((A) a).getX());
a = processObject(JSString.valueOf("qwe"));
assertTrue(a instanceof JSString);
assertEquals("qwe", ((JSString) a).stringValue());
a = processObject(JSNumber.valueOf(23));
assertTrue(a instanceof JSString);
assertEquals("number", ((JSString) a).stringValue());
}
@JSBody(script = "return null;")
private static native JSObject jsNull();
@JSBody(params = "o", script = "return o === null;")
private static native boolean isNull(JSObject o);
@JSBody(params = "o", script = "return typeof o === 'number' ? 'number' : o;")
private static native Object processObject(Object o);
static class A {
private int x;
A(int x) {
this.x = x;
}
int getX() {
return x;
}
}
}

View File

@ -41,15 +41,15 @@ public class JSOTest {
assertNotNull(foundProblem);
Object[] params = foundProblem.getParams();
assertThat(params[0], is(new MethodReference(JSOTest.class, "jsBodyWithWrongParameter",
Object.class, void.class)));
A.class, void.class)));
}
private static void callJSBodyWithWrongParameter() {
jsBodyWithWrongParameter(23);
jsBodyWithWrongParameter(new A());
}
@JSBody(params = "param", script = "alert(param.toString());")
private static native void jsBodyWithWrongParameter(Object param);
private static native void jsBodyWithWrongParameter(A param);
@Test
public void reportsAboutWrongNonStaticJSBody() {
@ -69,7 +69,7 @@ public class JSOTest {
new JSOTest().wrongNonStaticJSBody();
}
@JSBody(params = {}, script = "alert(this.toString());")
@JSBody(script = "alert(this.toString());")
private native void wrongNonStaticJSBody();
@Test
@ -83,7 +83,7 @@ public class JSOTest {
assertNotNull(foundProblem);
Object[] params = foundProblem.getParams();
assertThat(params[0], is(new MethodReference(JSOTest.class, "jsBodyWithWrongReturningType", String.class,
Object.class)));
A.class)));
}
private static void callJSBodyWithWrongReturningType() {
@ -91,7 +91,7 @@ public class JSOTest {
}
@JSBody(params = "value", script = "return value;")
private static native Object jsBodyWithWrongReturningType(String value);
private static native A jsBodyWithWrongReturningType(String value);
private List<Problem> build(String methodName) {
TeaVM vm = new TeaVMBuilder(new JavaScriptTarget()).build();
@ -101,4 +101,7 @@ public class JSOTest {
vm.build(name -> new ByteArrayOutputStream(), "tmp");
return vm.getProblemProvider().getSevereProblems();
}
public static class A {
}
}