JS: fix returning JSO objects from Async methods

Fix #805
This commit is contained in:
Alexey Andreev 2023-10-12 21:13:09 +02:00
parent 5684c09690
commit 772dd9eded
8 changed files with 189 additions and 17 deletions

View File

@ -23,6 +23,7 @@ description = "implementation of JSO"
dependencies {
compileOnly(project(":core"))
compileOnly(project(":platform"))
implementation(libs.rhino)
implementation(project(":jso:core"))

View File

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

View File

@ -15,6 +15,6 @@
*/
package org.teavm.platform.plugin;
@interface AsyncCallClass {
@interface AsyncCaller {
String value();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Integer> 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<JSString> callback) {
Window.setTimeout(() -> callback.complete(JSString.valueOf("foo")), 0);
}
}