mirror of
https://github.com/Eaglercraft-TeaVM-Fork/eagler-teavm.git
synced 2024-12-22 08:14:09 -08:00
JS: add support for exporting Java methods as JS properties
This commit is contained in:
parent
948244cbf4
commit
c4c6b029e3
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user