JS: add support for exporting Java methods as JS properties

This commit is contained in:
Alexey Andreev 2023-07-30 20:48:29 +02:00
parent 948244cbf4
commit c4c6b029e3
6 changed files with 264 additions and 25 deletions

View File

@ -17,7 +17,6 @@ package org.teavm.jso.impl;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import org.teavm.backend.javascript.codegen.SourceWriter; import org.teavm.backend.javascript.codegen.SourceWriter;
import org.teavm.backend.javascript.rendering.RenderingManager; import org.teavm.backend.javascript.rendering.RenderingManager;
import org.teavm.backend.javascript.spi.VirtualMethodContributor; import org.teavm.backend.javascript.spi.VirtualMethodContributor;
@ -54,20 +53,35 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
writer.append("var c;").softNewLine(); writer.append("var c;").softNewLine();
for (String className : classSource.getClassNames()) { for (String className : classSource.getClassNames()) {
ClassReader classReader = classSource.get(className); ClassReader classReader = classSource.get(className);
Map<MethodDescriptor, String> methods = new HashMap<>(); var methods = new HashMap<String, MethodDescriptor>();
for (MethodReader method : classReader.getMethods()) { var properties = new HashMap<String, PropertyInfo>();
String methodAlias = getPublicAlias(method); for (var method : classReader.getMethods()) {
var methodAlias = getPublicAlias(method);
if (methodAlias != null) { if (methodAlias != null) {
methods.put(method.getDescriptor(), methodAlias); switch (methodAlias.kind) {
case METHOD:
methods.put(methodAlias.name, method.getDescriptor());
break;
case GETTER: {
var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo());
propInfo.getter = method.getDescriptor();
break;
}
case SETTER: {
var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo());
propInfo.setter = method.getDescriptor();
break;
}
}
} }
} }
if (methods.isEmpty()) { if (methods.isEmpty() && properties.isEmpty()) {
continue; continue;
} }
boolean first = true; boolean first = true;
for (Map.Entry<MethodDescriptor, String> aliasEntry : methods.entrySet()) { for (var aliasEntry : methods.entrySet()) {
if (classReader.getMethod(aliasEntry.getKey()) == null) { if (classReader.getMethod(aliasEntry.getValue()) == null) {
continue; continue;
} }
if (first) { if (first) {
@ -75,12 +89,33 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
.softNewLine(); .softNewLine();
first = false; first = false;
} }
if (isKeyword(aliasEntry.getValue())) { if (isKeyword(aliasEntry.getKey())) {
writer.append("c[\"").append(aliasEntry.getValue()).append("\"]"); writer.append("c[\"").append(aliasEntry.getKey()).append("\"]");
} else { } else {
writer.append("c.").append(aliasEntry.getValue()); writer.append("c.").append(aliasEntry.getKey());
} }
writer.ws().append("=").ws().append("c.").appendMethod(aliasEntry.getKey()).append(";").softNewLine(); writer.ws().append("=").ws().append("c.").appendMethod(aliasEntry.getValue())
.append(";").softNewLine();
}
for (var aliasEntry : properties.entrySet()) {
var propInfo = aliasEntry.getValue();
if (propInfo.getter == null || classReader.getMethod(propInfo.getter) == null) {
continue;
}
if (first) {
writer.append("c").ws().append("=").ws().appendClass(className).append(".prototype;")
.softNewLine();
first = false;
}
writer.append("Object.defineProperty(c,")
.ws().append("\"").append(aliasEntry.getKey()).append("\",")
.ws().append("{").indent().softNewLine();
writer.append("get:").ws().append("c.").appendMethod(propInfo.getter);
if (propInfo.setter != null && classReader.getMethod(propInfo.setter) != null) {
writer.append(",").softNewLine();
writer.append("set:").ws().append("c.").appendMethod(propInfo.setter);
}
writer.softNewLine().outdent().append("});").softNewLine();
} }
FieldReader functorField = getFunctorField(classReader); FieldReader functorField = getFunctorField(classReader);
@ -101,9 +136,23 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
return false; return false;
} }
private String getPublicAlias(MethodReader method) { private Alias getPublicAlias(MethodReader method) {
AnnotationReader annot = method.getAnnotations().get(JSMethodToExpose.class.getName()); var annot = method.getAnnotations().get(JSMethodToExpose.class.getName());
return annot != null ? annot.getValue("name").getString() : null; if (annot != null) {
return new Alias(annot.getValue("name").getString(), AliasKind.METHOD);
}
annot = method.getAnnotations().get(JSGetterToExpose.class.getName());
if (annot != null) {
return new Alias(annot.getValue("name").getString(), AliasKind.GETTER);
}
annot = method.getAnnotations().get(JSSetterToExpose.class.getName());
if (annot != null) {
return new Alias(annot.getValue("name").getString(), AliasKind.SETTER);
}
return null;
} }
private FieldReader getFunctorField(ClassReader cls) { private FieldReader getFunctorField(ClassReader cls) {
@ -190,4 +239,25 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
MethodReader methodReader = classReader.getMethod(methodRef.getDescriptor()); MethodReader methodReader = classReader.getMethod(methodRef.getDescriptor());
return methodReader != null && getPublicAlias(methodReader) != null; return methodReader != null && getPublicAlias(methodReader) != null;
} }
static class PropertyInfo {
MethodDescriptor getter;
MethodDescriptor setter;
}
static class Alias {
final String name;
final AliasKind kind;
Alias(String name, AliasKind kind) {
this.name = name;
this.kind = kind;
}
}
enum AliasKind {
METHOD,
GETTER,
SETTER
}
} }

View File

@ -48,6 +48,12 @@ class JSDependencyListener extends AbstractDependencyListener {
ClassReader cls = agent.getClassSource().get(className); ClassReader cls = agent.getClassSource().get(className);
for (MethodReader method : cls.getMethods()) { for (MethodReader method : cls.getMethods()) {
AnnotationReader exposeAnnot = method.getAnnotations().get(JSMethodToExpose.class.getName()); AnnotationReader exposeAnnot = method.getAnnotations().get(JSMethodToExpose.class.getName());
if (exposeAnnot == null) {
exposeAnnot = method.getAnnotations().get(JSGetterToExpose.class.getName());
}
if (exposeAnnot == null) {
exposeAnnot = method.getAnnotations().get(JSSetterToExpose.class.getName());
}
if (exposeAnnot != null) { if (exposeAnnot != null) {
MethodDependency methodDep = agent.linkMethod(method.getReference()); MethodDependency methodDep = agent.linkMethod(method.getReference());
methodDep.getVariable(0).propagate(agent.getType(className)); methodDep.getVariable(0).propagate(agent.getType(className));

View File

@ -0,0 +1,27 @@
/*
* Copyright 2017 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.jso.impl;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@interface JSGetterToExpose {
String name();
}

View File

@ -25,6 +25,7 @@ import java.util.Set;
import org.teavm.diagnostics.Diagnostics; import org.teavm.diagnostics.Diagnostics;
import org.teavm.jso.JSMethod; import org.teavm.jso.JSMethod;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.model.AccessLevel; import org.teavm.model.AccessLevel;
import org.teavm.model.AnnotationHolder; import org.teavm.model.AnnotationHolder;
import org.teavm.model.AnnotationReader; import org.teavm.model.AnnotationReader;
@ -154,9 +155,21 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
classHolder.addMethod(exportedMethod); classHolder.addMethod(exportedMethod);
String publicAlias = classToExpose.methods.get(method); var export = classToExpose.methods.get(method);
AnnotationHolder annot = new AnnotationHolder(JSMethodToExpose.class.getName()); String annotationName;
annot.getValues().put("name", new AnnotationValue(publicAlias)); switch (export.kind) {
case GETTER:
annotationName = JSGetterToExpose.class.getName();
break;
case SETTER:
annotationName = JSSetterToExpose.class.getName();
break;
default:
annotationName = JSMethodToExpose.class.getName();
break;
}
AnnotationHolder annot = new AnnotationHolder(annotationName);
annot.getValues().put("name", new AnnotationValue(export.alias));
exportedMethod.getAnnotations().add(annot); exportedMethod.getAnnotations().add(annot);
if (methodRef.equals(functorMethod)) { if (methodRef.equals(functorMethod)) {
@ -189,8 +202,8 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
} }
if (cls.getParent() != null) { if (cls.getParent() != null) {
ExposedClass parent = getExposedClass(cls.getParent()); ExposedClass parent = getExposedClass(cls.getParent());
exposedCls.inheritedMethods.putAll(parent.inheritedMethods); exposedCls.inheritedMethods.addAll(parent.inheritedMethods);
exposedCls.inheritedMethods.putAll(parent.methods); exposedCls.inheritedMethods.addAll(parent.methods.keySet());
exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces); exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces);
} }
addInterfaces(exposedCls, cls); addInterfaces(exposedCls, cls);
@ -213,10 +226,12 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
|| (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) { || (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) {
continue; continue;
} }
if (!exposedCls.inheritedMethods.containsKey(method.getDescriptor())) { if (!exposedCls.inheritedMethods.contains(method.getDescriptor())) {
String name = method.getName(); String name = null;
MethodKind kind = MethodKind.METHOD;
AnnotationReader methodAnnot = method.getAnnotations().get(JSMethod.class.getName()); AnnotationReader methodAnnot = method.getAnnotations().get(JSMethod.class.getName());
if (methodAnnot != null) { if (methodAnnot != null) {
name = method.getName();
AnnotationValue nameVal = methodAnnot.getValue("value"); AnnotationValue nameVal = methodAnnot.getValue("value");
if (nameVal != null) { if (nameVal != null) {
String nameStr = nameVal.getString(); String nameStr = nameVal.getString();
@ -224,8 +239,43 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
name = nameStr; name = nameStr;
} }
} }
} else {
var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName());
if (propertyAnnot != null) {
AnnotationValue nameVal = propertyAnnot.getValue("value");
if (nameVal != null) {
String nameStr = nameVal.getString();
if (!nameStr.isEmpty()) {
name = nameStr;
}
}
String expectedPrefix;
if (method.parameterCount() == 0) {
if (method.getResultType() == ValueType.BOOLEAN) {
expectedPrefix = "is";
} else {
expectedPrefix = "get";
}
kind = MethodKind.GETTER;
} else {
expectedPrefix = "set";
kind = MethodKind.SETTER;
}
if (name == null) {
name = method.getName();
if (name.startsWith(expectedPrefix) && name.length() > expectedPrefix.length()
&& Character.isUpperCase(name.charAt(expectedPrefix.length()))) {
name = Character.toLowerCase(name.charAt(expectedPrefix.length()))
+ name.substring(expectedPrefix.length() + 1);
}
}
}
} }
exposedCls.methods.put(method.getDescriptor(), name); if (name == null) {
name = method.getName();
}
exposedCls.methods.put(method.getDescriptor(), new MethodExport(name, kind));
} }
} }
} }
@ -256,8 +306,24 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
} }
static class ExposedClass { static class ExposedClass {
Map<MethodDescriptor, String> inheritedMethods = new HashMap<>(); Set<MethodDescriptor> inheritedMethods = new HashSet<>();
Map<MethodDescriptor, String> methods = new HashMap<>(); Map<MethodDescriptor, MethodExport> methods = new HashMap<>();
Set<String> implementedInterfaces = new HashSet<>(); Set<String> implementedInterfaces = new HashSet<>();
} }
enum MethodKind {
METHOD,
GETTER,
SETTER
}
static class MethodExport {
final String alias;
final MethodKind kind;
MethodExport(String alias, MethodKind kind) {
this.alias = alias;
this.kind = kind;
}
}
} }

View File

@ -0,0 +1,27 @@
/*
* Copyright 2017 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.jso.impl;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@interface JSSetterToExpose {
String name();
}

View File

@ -20,10 +20,13 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.teavm.jso.JSBody; import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.junit.SkipJVM; import org.teavm.junit.SkipJVM;
import org.teavm.junit.TeaVMTestRunner; import org.teavm.junit.TeaVMTestRunner;
import org.teavm.junit.WholeClassCompilation;
@RunWith(TeaVMTestRunner.class) @RunWith(TeaVMTestRunner.class)
@WholeClassCompilation
@SkipJVM @SkipJVM
public class ExportClass { public class ExportClass {
@Test @Test
@ -32,9 +35,24 @@ public class ExportClass {
assertEquals("[OK]", callIFromJs(new DerivedSimpleClass())); assertEquals("[OK]", callIFromJs(new DerivedSimpleClass()));
} }
@Test
public void classWithPropertiesExported() {
var o = new ClassWithProperty("q");
assertEquals("q", extractFoo(o));
setFoo(o);
assertEquals("w", o.fooValue);
}
@JSBody(params = "a", script = "return a.foo('OK');") @JSBody(params = "a", script = "return a.foo('OK');")
private static native String callIFromJs(I a); private static native String callIFromJs(I a);
@JSBody(params = "a", script = "return a.foo;")
private static native String extractFoo(J a);
@JSBody(params = "a", script = "a.foo = 'w';")
private static native String setFoo(J a);
interface I extends JSObject { interface I extends JSObject {
String foo(String a); String foo(String a);
} }
@ -53,4 +71,29 @@ public class ExportClass {
} }
} }
interface J extends JSObject {
@JSProperty
String getFoo();
@JSProperty
void setFoo(String value);
}
static class ClassWithProperty implements J {
String fooValue;
ClassWithProperty(String fooValue) {
this.fooValue = fooValue;
}
@Override
public String getFoo() {
return fooValue;
}
@Override
public void setFoo(String value) {
fooValue = value;
}
}
} }