From a1ed797d739883c94cf43209055d35905ab4ca42 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Mon, 31 Jul 2023 20:42:09 +0200 Subject: [PATCH] JS: allow passing Object to JS methods --- .../org/teavm/jso/impl/JSClassProcessor.java | 10 +++---- .../teavm/jso/impl/JSDependencyListener.java | 3 +- .../java/org/teavm/jso/impl/JSOPlugin.java | 13 ++++++++ .../java/org/teavm/jso/impl/JSTypeHelper.java | 5 ++-- .../org/teavm/jso/impl/JSValueMarshaller.java | 21 ++++++++++++- .../java/org/teavm/jso/impl/JSWrapper.java | 26 ++++++++++++++++ .../teavm/jso/impl/JSWrapperGenerator.java | 25 ++++++++++++++-- .../org/teavm/jso/test/JSWrapperTest.java | 30 +++++++++++++++++++ .../test/java/org/teavm/tests/JSOTest.java | 15 ++++++---- 9 files changed, 130 insertions(+), 18 deletions(-) diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java index 191f26641..397875824 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java @@ -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; diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java index 90f6c79c9..d1e6d9f2f 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java @@ -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 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(); diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java index 43dd9c21e..f291b02f8 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java @@ -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); } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java index 01370ad74..5d6c759e0 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java @@ -28,7 +28,7 @@ class JSTypeHelper { private Map knownJavaScriptClasses = new HashMap<>(); private Map 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; } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java index be2136c81..1ca9f8302 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java @@ -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()); diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java index 1622d3506..9c5ab12c0 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java @@ -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; diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapperGenerator.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapperGenerator.java index 2795331c6..4653f9179 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapperGenerator.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapperGenerator.java @@ -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; + } } diff --git a/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java b/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java index b3e87e859..5817406d6 100644 --- a/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java +++ b/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java @@ -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; + } + } } diff --git a/tests/src/test/java/org/teavm/tests/JSOTest.java b/tests/src/test/java/org/teavm/tests/JSOTest.java index 9f742abc4..9ab21e624 100644 --- a/tests/src/test/java/org/teavm/tests/JSOTest.java +++ b/tests/src/test/java/org/teavm/tests/JSOTest.java @@ -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 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 { + } }