From 772dd9eded260f4beaf23204ee849c95231e7ecd Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Thu, 12 Oct 2023 21:13:09 +0200 Subject: [PATCH] JS: fix returning JSO objects from Async methods Fix #805 --- jso/impl/build.gradle.kts | 1 + .../java/org/teavm/jso/impl/JSOPlugin.java | 3 + .../{AsyncCallClass.java => AsyncCaller.java} | 2 +- .../plugin/AsyncDependencyListener.java | 4 +- .../AsyncLowLevelDependencyListener.java | 2 +- .../platform/plugin/AsyncMethodGenerator.java | 20 ++-- .../platform/plugin/AsyncMethodProcessor.java | 113 +++++++++++++++++- .../src/test/java/org/teavm/vm/AsyncTest.java | 61 ++++++++++ 8 files changed, 189 insertions(+), 17 deletions(-) rename platform/src/main/java/org/teavm/platform/plugin/{AsyncCallClass.java => AsyncCaller.java} (95%) create mode 100644 tests/src/test/java/org/teavm/vm/AsyncTest.java diff --git a/jso/impl/build.gradle.kts b/jso/impl/build.gradle.kts index 4b5dea21c..5f8cd75aa 100644 --- a/jso/impl/build.gradle.kts +++ b/jso/impl/build.gradle.kts @@ -23,6 +23,7 @@ description = "implementation of JSO" dependencies { compileOnly(project(":core")) + compileOnly(project(":platform")) implementation(libs.rhino) implementation(project(":jso:core")) 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 aa5a02d93..0290b1d10 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 @@ -19,10 +19,13 @@ import org.teavm.backend.javascript.TeaVMJavaScriptHost; import org.teavm.jso.JSExceptions; import org.teavm.jso.JSObject; import org.teavm.model.MethodReference; +import org.teavm.platform.plugin.PlatformPlugin; import org.teavm.vm.TeaVMPluginUtil; +import org.teavm.vm.spi.After; import org.teavm.vm.spi.TeaVMHost; import org.teavm.vm.spi.TeaVMPlugin; +@After(PlatformPlugin.class) public class JSOPlugin implements TeaVMPlugin { @Override public void install(TeaVMHost host) { diff --git a/platform/src/main/java/org/teavm/platform/plugin/AsyncCallClass.java b/platform/src/main/java/org/teavm/platform/plugin/AsyncCaller.java similarity index 95% rename from platform/src/main/java/org/teavm/platform/plugin/AsyncCallClass.java rename to platform/src/main/java/org/teavm/platform/plugin/AsyncCaller.java index 12176889d..05f8b2beb 100644 --- a/platform/src/main/java/org/teavm/platform/plugin/AsyncCallClass.java +++ b/platform/src/main/java/org/teavm/platform/plugin/AsyncCaller.java @@ -15,6 +15,6 @@ */ package org.teavm.platform.plugin; -@interface AsyncCallClass { +@interface AsyncCaller { String value(); } diff --git a/platform/src/main/java/org/teavm/platform/plugin/AsyncDependencyListener.java b/platform/src/main/java/org/teavm/platform/plugin/AsyncDependencyListener.java index 7017448d2..865826d34 100644 --- a/platform/src/main/java/org/teavm/platform/plugin/AsyncDependencyListener.java +++ b/platform/src/main/java/org/teavm/platform/plugin/AsyncDependencyListener.java @@ -18,12 +18,12 @@ package org.teavm.platform.plugin; import org.teavm.dependency.AbstractDependencyListener; import org.teavm.dependency.DependencyAgent; import org.teavm.dependency.MethodDependency; -import org.teavm.interop.Async; public class AsyncDependencyListener extends AbstractDependencyListener { @Override public void methodReached(DependencyAgent agent, MethodDependency method) { - if (method.getMethod() != null && method.getMethod().getAnnotations().get(Async.class.getName()) != null) { + if (method.getMethod() != null && method.getMethod().getAnnotations() + .get(AsyncCaller.class.getName()) != null) { new AsyncMethodGenerator().methodReached(agent, method); } } diff --git a/platform/src/main/java/org/teavm/platform/plugin/AsyncLowLevelDependencyListener.java b/platform/src/main/java/org/teavm/platform/plugin/AsyncLowLevelDependencyListener.java index 173f9171f..438f56b65 100644 --- a/platform/src/main/java/org/teavm/platform/plugin/AsyncLowLevelDependencyListener.java +++ b/platform/src/main/java/org/teavm/platform/plugin/AsyncLowLevelDependencyListener.java @@ -68,7 +68,7 @@ public class AsyncLowLevelDependencyListener extends AbstractDependencyListener } private ClassHolder generateClassDecl(MethodReader method) { - AnnotationReader annot = method.getAnnotations().get(AsyncCallClass.class.getName()); + AnnotationReader annot = method.getAnnotations().get(AsyncCaller.class.getName()); String className = annot.getValue("value").getString(); if (!generatedClassNames.add(className)) { return null; diff --git a/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodGenerator.java b/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodGenerator.java index ef74bbed2..58f608d50 100644 --- a/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodGenerator.java +++ b/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodGenerator.java @@ -26,6 +26,7 @@ import org.teavm.dependency.DependencyPlugin; import org.teavm.dependency.MethodDependency; import org.teavm.interop.AsyncCallback; import org.teavm.model.ClassReader; +import org.teavm.model.ClassReaderSource; import org.teavm.model.ElementModifier; import org.teavm.model.MethodDescriptor; import org.teavm.model.MethodReader; @@ -38,7 +39,7 @@ public class AsyncMethodGenerator implements Generator, DependencyPlugin, Virtua @Override public void generate(GeneratorContext context, SourceWriter writer, MethodReference methodRef) throws IOException { - MethodReference asyncRef = getAsyncReference(methodRef); + MethodReference asyncRef = getAsyncReference(context.getClassSource(), methodRef); writer.append("var thread").ws().append('=').ws().append("$rt_nativeThread();").softNewLine(); writer.append("var javaThread").ws().append('=').ws().append("$rt_getThread();").softNewLine(); writer.append("if").ws().append("(thread.isResuming())").ws().append("{").indent().softNewLine(); @@ -65,7 +66,7 @@ public class AsyncMethodGenerator implements Generator, DependencyPlugin, Virtua writer.outdent().append("};").softNewLine(); writer.append("callback").ws().append("=").ws().appendMethodBody(AsyncCallbackWrapper.class, "create", AsyncCallback.class, AsyncCallbackWrapper.class).append("(callback);").softNewLine(); - writer.append("return thread.suspend(function()").ws().append("{").indent().softNewLine(); + writer.append("thread.suspend(function()").ws().append("{").indent().softNewLine(); writer.append("try").ws().append("{").indent().softNewLine(); writer.appendMethodBody(asyncRef).append('('); ClassReader cls = context.getClassSource().get(methodRef.getClassName()); @@ -81,22 +82,19 @@ public class AsyncMethodGenerator implements Generator, DependencyPlugin, Virtua .softNewLine(); writer.outdent().append("}").softNewLine(); writer.outdent().append("});").softNewLine(); + writer.append("return null;").softNewLine(); } - private MethodReference getAsyncReference(MethodReference methodRef) { - ValueType[] signature = new ValueType[methodRef.parameterCount() + 2]; - for (int i = 0; i < methodRef.parameterCount(); ++i) { - signature[i] = methodRef.getDescriptor().parameterType(i); - } - signature[methodRef.parameterCount()] = ValueType.parse(AsyncCallback.class); - signature[methodRef.parameterCount() + 1] = ValueType.VOID; - return new MethodReference(methodRef.getClassName(), methodRef.getName(), signature); + private MethodReference getAsyncReference(ClassReaderSource classSource, MethodReference methodRef) { + var method = classSource.resolve(methodRef); + var callerAnnot = method.getAnnotations().get(AsyncCaller.class.getName()); + return MethodReference.parse(callerAnnot.getValue("value").getString()); } @Override public void methodReached(DependencyAgent agent, MethodDependency method) { MethodReference ref = method.getReference(); - MethodReference asyncRef = getAsyncReference(ref); + MethodReference asyncRef = getAsyncReference(agent.getClassSource(), ref); MethodDependency asyncMethod = agent.linkMethod(asyncRef); method.addLocationListener(asyncMethod::addLocation); int paramCount = ref.parameterCount(); diff --git a/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodProcessor.java b/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodProcessor.java index 71eb68fa8..4d2f6aacd 100644 --- a/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodProcessor.java +++ b/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodProcessor.java @@ -31,6 +31,7 @@ import org.teavm.model.ElementModifier; import org.teavm.model.MethodDescriptor; import org.teavm.model.MethodHolder; import org.teavm.model.MethodReference; +import org.teavm.model.PrimitiveType; import org.teavm.model.Program; import org.teavm.model.ValueType; import org.teavm.model.Variable; @@ -52,7 +53,7 @@ public class AsyncMethodProcessor implements ClassHolderTransformer { @Override public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) { int suffix = 0; - for (MethodHolder method : cls.getMethods()) { + for (var method : List.copyOf(cls.getMethods())) { if (method.hasModifier(ElementModifier.NATIVE) && method.getAnnotations().get(Async.class.getName()) != null && method.getAnnotations().get(GeneratedBy.class.getName()) == null) { @@ -75,6 +76,8 @@ public class AsyncMethodProcessor implements ClassHolderTransformer { if (lowLevel) { generateLowLevelCall(method, suffix++); + } else { + generateCallerMethod(cls, method); } } } @@ -82,7 +85,7 @@ public class AsyncMethodProcessor implements ClassHolderTransformer { private void generateLowLevelCall(MethodHolder method, int suffix) { String className = method.getOwnerName() + "$" + method.getName() + "$" + suffix; - AnnotationHolder classNameAnnot = new AnnotationHolder(AsyncCallClass.class.getName()); + AnnotationHolder classNameAnnot = new AnnotationHolder(AsyncCaller.class.getName()); classNameAnnot.getValues().put("value", new AnnotationValue(className)); method.getAnnotations().add(classNameAnnot); @@ -186,4 +189,110 @@ public class AsyncMethodProcessor implements ClassHolderTransformer { block.add(invoke); return invoke.getReceiver(); } + + private void generateCallerMethod(ClassHolder cls, MethodHolder method) { + method.getAnnotations().remove(Async.class.getName()); + + var mappedSignature = method.getSignature(); + mappedSignature[mappedSignature.length - 1] = ValueType.object("java.lang.Object"); + var callerMethod = new MethodHolder(method.getName() + "$_asyncCall_$", mappedSignature); + var annot = new AnnotationHolder(AsyncCaller.class.getName()); + annot.getValues().put("value", new AnnotationValue(getAsyncReference(method.getReference()).toString())); + callerMethod.getAnnotations().add(annot); + callerMethod.getAnnotations().add(new AnnotationHolder(Async.class.getName())); + callerMethod.getModifiers().add(ElementModifier.NATIVE); + cls.addMethod(callerMethod); + + method.getModifiers().remove(ElementModifier.NATIVE); + var program = new Program(); + var block = program.createBasicBlock(); + var thisVar = program.createVariable(); + var call = new InvokeInstruction(); + call.setMethod(callerMethod.getReference()); + call.setType(InvocationType.SPECIAL); + if (!method.hasModifier(ElementModifier.STATIC)) { + call.setInstance(thisVar); + } else { + callerMethod.getModifiers().add(ElementModifier.STATIC); + } + var args = new Variable[method.parameterCount()]; + for (var i = 0; i < method.parameterCount(); ++i) { + args[i] = program.createVariable(); + } + call.setArguments(args); + block.add(call); + + var exit = new ExitInstruction(); + var returnType = method.getResultType(); + if (returnType instanceof ValueType.Primitive) { + call.setReceiver(program.createVariable()); + exit.setValueToReturn(unbox(call.getReceiver(), ((ValueType.Primitive) returnType).getKind(), + block, program)); + } else if (!(returnType instanceof ValueType.Void)) { + call.setReceiver(program.createVariable()); + var cast = new CastInstruction(); + cast.setValue(call.getReceiver()); + cast.setTargetType(returnType); + cast.setReceiver(program.createVariable()); + block.add(cast); + exit.setValueToReturn(cast.getReceiver()); + } + + block.add(exit); + + method.setProgram(program); + } + + private MethodReference getAsyncReference(MethodReference methodRef) { + var signature = new ValueType[methodRef.parameterCount() + 2]; + for (int i = 0; i < methodRef.parameterCount(); ++i) { + signature[i] = methodRef.getDescriptor().parameterType(i); + } + signature[methodRef.parameterCount()] = ValueType.parse(AsyncCallback.class); + signature[methodRef.parameterCount() + 1] = ValueType.VOID; + return new MethodReference(methodRef.getClassName(), methodRef.getName(), signature); + } + + private Variable unbox(Variable value, PrimitiveType type, BasicBlock block, Program program) { + var cast = new CastInstruction(); + cast.setValue(value); + cast.setReceiver(program.createVariable()); + block.add(cast); + + var call = new InvokeInstruction(); + call.setInstance(cast.getReceiver()); + call.setReceiver(program.createVariable()); + call.setType(InvocationType.VIRTUAL); + block.add(call); + + switch (type) { + case BOOLEAN: + call.setMethod(new MethodReference(Boolean.class, "booleanValue", boolean.class)); + break; + case BYTE: + call.setMethod(new MethodReference(Byte.class, "byteValue", boolean.class)); + break; + case SHORT: + call.setMethod(new MethodReference(Short.class, "shortValue", short.class)); + break; + case CHARACTER: + call.setMethod(new MethodReference(Character.class, "charValue", char.class)); + break; + case INTEGER: + call.setMethod(new MethodReference(Integer.class, "intValue", int.class)); + break; + case LONG: + call.setMethod(new MethodReference(Long.class, "longValue", int.class)); + break; + case FLOAT: + call.setMethod(new MethodReference(Float.class, "floatValue", int.class)); + break; + case DOUBLE: + call.setMethod(new MethodReference(Double.class, "doubleValue", int.class)); + break; + } + + cast.setTargetType(ValueType.object(call.getMethod().getClassName())); + return call.getReceiver(); + } } diff --git a/tests/src/test/java/org/teavm/vm/AsyncTest.java b/tests/src/test/java/org/teavm/vm/AsyncTest.java new file mode 100644 index 000000000..c62832ae4 --- /dev/null +++ b/tests/src/test/java/org/teavm/vm/AsyncTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * 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.vm; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.interop.Async; +import org.teavm.interop.AsyncCallback; +import org.teavm.jso.browser.Window; +import org.teavm.jso.core.JSString; +import org.teavm.junit.EachTestCompiledSeparately; +import org.teavm.junit.OnlyPlatform; +import org.teavm.junit.SkipJVM; +import org.teavm.junit.TeaVMTestRunner; +import org.teavm.junit.TestPlatform; + +@RunWith(TeaVMTestRunner.class) +@EachTestCompiledSeparately +@OnlyPlatform(TestPlatform.JAVASCRIPT) +@SkipJVM +public class AsyncTest { + @Test + public void primitives() { + assertEquals(23, getPrimitive()); + } + + @Async + private native int getPrimitive(); + + private void getPrimitive(AsyncCallback callback) { + Window.setTimeout(() -> callback.complete(23), 0); + } + + @Test + public void jsObjects() { + var str = getJsString(); + assertEquals(3, str.getLength()); + assertEquals("foo", str.stringValue()); + } + + @Async + private native JSString getJsString(); + + private void getJsString(AsyncCallback callback) { + Window.setTimeout(() -> callback.complete(JSString.valueOf("foo")), 0); + } +}