jso: support exporting class constructors

This commit is contained in:
Alexey Andreev 2024-04-08 21:32:10 +02:00
parent a6fb67817c
commit 72b021fc0b
7 changed files with 148 additions and 47 deletions

View File

@ -21,6 +21,6 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) @Target({ ElementType.METHOD, ElementType.CONSTRUCTOR })
public @interface JSExport { public @interface JSExport {
} }

View File

@ -41,6 +41,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
private ListableClassReaderSource classSource; private ListableClassReaderSource classSource;
private JSTypeHelper typeHelper; private JSTypeHelper typeHelper;
private RenderingManager context; private RenderingManager context;
private int lastExportIndex;
@Override @Override
public void begin(RenderingManager context, BuildTarget buildTarget) { public void begin(RenderingManager context, BuildTarget buildTarget) {
@ -65,19 +66,28 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
.appendGlobal("Symbol").append("('jsoClass')").endDeclaration(); .appendGlobal("Symbol").append("('jsoClass')").endDeclaration();
writer.append("(()").ws().append("=>").ws().append("{").softNewLine().indent(); writer.append("(()").ws().append("=>").ws().append("{").softNewLine().indent();
writer.append("let c;").softNewLine(); writer.append("let c;").softNewLine();
var exportedNamesByClass = new HashMap<String, String>();
for (var className : classSource.getClassNames()) { for (var className : classSource.getClassNames()) {
var classReader = classSource.get(className); var classReader = classSource.get(className);
var hasExportedMembers = false; var hasExportedMembers = false;
hasExportedMembers |= exportClassInstanceMembers(classReader); hasExportedMembers |= exportClassInstanceMembers(classReader);
if (!className.equals(context.getEntryPoint())) { if (!className.equals(context.getEntryPoint())) {
hasExportedMembers |= exportClassStaticMembers(classReader); var name = "$rt_export_class_ " + getClassAliasName(classReader) + "_" + lastExportIndex++;
if (hasExportedMembers && !typeHelper.isJavaScriptClass(className) hasExportedMembers |= exportClassStaticMembers(classReader, name);
&& !typeHelper.isJavaScriptImplementation(className)) { if (hasExportedMembers) {
exportClassFromModule(classReader); exportedNamesByClass.put(className, name);
} }
} }
} }
writer.outdent().append("})();").newLine(); writer.outdent().append("})();").newLine();
for (var className : classSource.getClassNames()) {
var classReader = classSource.get(className);
var name = exportedNamesByClass.get(className);
if (name != null && !typeHelper.isJavaScriptClass(className)
&& !typeHelper.isJavaScriptImplementation(className)) {
exportClassFromModule(classReader, name);
}
}
} }
private boolean exportClassInstanceMembers(ClassReader classReader) { private boolean exportClassInstanceMembers(ClassReader classReader) {
@ -125,14 +135,14 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
return true; return true;
} }
private boolean exportClassStaticMembers(ClassReader classReader) { private boolean exportClassStaticMembers(ClassReader classReader, String name) {
var members = collectMembers(classReader, c -> c.hasModifier(ElementModifier.STATIC)); var members = collectMembers(classReader, c -> c.hasModifier(ElementModifier.STATIC));
if (members.methods.isEmpty() && members.properties.isEmpty()) { if (members.methods.isEmpty() && members.properties.isEmpty()) {
return false; return false;
} }
writer.append("c").ws().append("=").ws().appendClass(classReader.getName()).append(";").softNewLine(); writer.append("c").ws().append("=").ws().appendFunction(name).append(";").softNewLine();
for (var aliasEntry : members.methods.entrySet()) { for (var aliasEntry : members.methods.entrySet()) {
appendMethodAlias(aliasEntry.getKey()); appendMethodAlias(aliasEntry.getKey());
@ -175,6 +185,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
private Members collectMembers(ClassReader classReader, Predicate<MethodReader> filter) { private Members collectMembers(ClassReader classReader, Predicate<MethodReader> filter) {
var methods = new HashMap<String, MethodDescriptor>(); var methods = new HashMap<String, MethodDescriptor>();
var properties = new HashMap<String, PropertyInfo>(); var properties = new HashMap<String, PropertyInfo>();
MethodDescriptor constructor = null;
for (var method : classReader.getMethods()) { for (var method : classReader.getMethods()) {
if (!filter.test(method)) { if (!filter.test(method)) {
continue; continue;
@ -195,10 +206,13 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
propInfo.setter = method.getDescriptor(); propInfo.setter = method.getDescriptor();
break; break;
} }
case CONSTRUCTOR:
constructor = method.getDescriptor();
break;
} }
} }
} }
return new Members(methods, properties); return new Members(methods, properties, constructor);
} }
private void exportModule() { private void exportModule() {
@ -214,7 +228,40 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
} }
} }
private void exportClassFromModule(ClassReader cls) { private void exportClassFromModule(ClassReader cls, String functionName) {
var name = getClassAliasName(cls);
var constructors = collectMembers(cls, method -> !method.hasModifier(ElementModifier.STATIC));
var method = constructors.constructor;
writer.append("function ").appendFunction(functionName).append("(");
if (method != null) {
for (var i = 0; i < method.parameterCount(); ++i) {
if (i > 0) {
writer.append(",").ws();
}
writer.append("p" + i);
}
}
writer.append(")").ws().appendBlockStart();
if (method != null) {
writer.appendClass(cls.getName()).append(".call(this);").softNewLine();
writer.appendMethod(new MethodReference(cls.getName(), method)).append("(this");
for (var i = 0; i < method.parameterCount(); ++i) {
writer.append(",").ws().append("p" + i);
}
writer.append(");").softNewLine();
} else {
writer.append("throw new Error(\"Can't instantiate this class directly\");").softNewLine();
}
writer.outdent().append("}").append(";").softNewLine();
writer.appendFunction(functionName).append(".prototype").ws().append("=").ws()
.appendClass(cls.getName()).append(".prototype;").softNewLine();
context.exportFunction(functionName, name);
}
private String getClassAliasName(ClassReader cls) {
var name = cls.getSimpleName(); var name = cls.getSimpleName();
if (name == null) { if (name == null) {
name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1); name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1);
@ -229,7 +276,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
} }
} }
} }
context.exportClass(cls.getName(), name); return name;
} }
private boolean hasClassesToExpose() { private boolean hasClassesToExpose() {
@ -263,6 +310,11 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
return new Alias(annot.getValue("name").getString(), AliasKind.SETTER); return new Alias(annot.getValue("name").getString(), AliasKind.SETTER);
} }
annot = method.getAnnotations().get(JSConstructorToExpose.class.getName());
if (annot != null) {
return new Alias(null, AliasKind.CONSTRUCTOR);
}
return null; return null;
} }
@ -354,10 +406,13 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
private static class Members { private static class Members {
final Map<String, MethodDescriptor> methods; final Map<String, MethodDescriptor> methods;
final Map<String, PropertyInfo> properties; final Map<String, PropertyInfo> properties;
final MethodDescriptor constructor;
Members(Map<String, MethodDescriptor> methods, Map<String, PropertyInfo> properties) { Members(Map<String, MethodDescriptor> methods, Map<String, PropertyInfo> properties,
MethodDescriptor constructor) {
this.methods = methods; this.methods = methods;
this.properties = properties; this.properties = properties;
this.constructor = constructor;
} }
} }
@ -379,6 +434,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
private enum AliasKind { private enum AliasKind {
METHOD, METHOD,
GETTER, GETTER,
SETTER SETTER,
CONSTRUCTOR
} }
} }

View File

@ -0,0 +1,26 @@
/*
* 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 JSConstructorToExpose {
}

View File

@ -56,6 +56,9 @@ class JSDependencyListener extends AbstractDependencyListener {
if (exposeAnnot == null) { if (exposeAnnot == null) {
exposeAnnot = method.getAnnotations().get(JSSetterToExpose.class.getName()); exposeAnnot = method.getAnnotations().get(JSSetterToExpose.class.getName());
} }
if (exposeAnnot == null) {
exposeAnnot = method.getAnnotations().get(JSConstructorToExpose.class.getName());
}
if (exposeAnnot != null) { if (exposeAnnot != null) {
MethodDependency methodDep = agent.linkMethod(method.getReference()); MethodDependency methodDep = agent.linkMethod(method.getReference());
if (methodDep.getMethod() != null) { if (methodDep.getMethod() != null) {

View File

@ -241,12 +241,17 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
case SETTER: case SETTER:
annotationName = JSSetterToExpose.class.getName(); annotationName = JSSetterToExpose.class.getName();
break; break;
case CONSTRUCTOR:
annotationName = JSConstructorToExpose.class.getName();
break;
default: default:
annotationName = JSMethodToExpose.class.getName(); annotationName = JSMethodToExpose.class.getName();
break; break;
} }
var annot = new AnnotationHolder(annotationName); var annot = new AnnotationHolder(annotationName);
annot.getValues().put("name", new AnnotationValue(export.alias)); if (export.kind != MethodKind.CONSTRUCTOR) {
annot.getValues().put("name", new AnnotationValue(export.alias));
}
return annot; return annot;
} }
@ -348,51 +353,55 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
private MethodExport createMethodExport(MethodReader method) { private MethodExport createMethodExport(MethodReader method) {
String name = null; String name = null;
MethodKind kind = MethodKind.METHOD; MethodKind kind = MethodKind.METHOD;
var methodAnnot = method.getAnnotations().get(JSMethod.class.getName()); if (method.getName().equals("<init>")) {
if (methodAnnot != null) { kind = MethodKind.CONSTRUCTOR;
name = method.getName();
var nameVal = methodAnnot.getValue("value");
if (nameVal != null) {
String nameStr = nameVal.getString();
if (!nameStr.isEmpty()) {
name = nameStr;
}
}
} else { } else {
var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName()); var methodAnnot = method.getAnnotations().get(JSMethod.class.getName());
if (propertyAnnot != null) { if (methodAnnot != null) {
var nameVal = propertyAnnot.getValue("value"); name = method.getName();
var nameVal = methodAnnot.getValue("value");
if (nameVal != null) { if (nameVal != null) {
String nameStr = nameVal.getString(); String nameStr = nameVal.getString();
if (!nameStr.isEmpty()) { if (!nameStr.isEmpty()) {
name = nameStr; name = nameStr;
} }
} }
String expectedPrefix; } else {
if (method.parameterCount() == 0) { var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName());
if (method.getResultType() == ValueType.BOOLEAN) { if (propertyAnnot != null) {
expectedPrefix = "is"; var nameVal = propertyAnnot.getValue("value");
} else { if (nameVal != null) {
expectedPrefix = "get"; 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;
} }
kind = MethodKind.GETTER;
} else {
expectedPrefix = "set";
kind = MethodKind.SETTER;
}
if (name == null) { if (name == null) {
name = method.getName(); name = method.getName();
if (name.startsWith(expectedPrefix) && name.length() > expectedPrefix.length() if (name.startsWith(expectedPrefix) && name.length() > expectedPrefix.length()
&& Character.isUpperCase(name.charAt(expectedPrefix.length()))) { && Character.isUpperCase(name.charAt(expectedPrefix.length()))) {
name = Character.toLowerCase(name.charAt(expectedPrefix.length())) name = Character.toLowerCase(name.charAt(expectedPrefix.length()))
+ name.substring(expectedPrefix.length() + 1); + name.substring(expectedPrefix.length() + 1);
}
} }
} }
} }
} if (name == null) {
if (name == null) { name = method.getName();
name = method.getName(); }
} }
return new MethodExport(name, kind); return new MethodExport(name, kind);
} }
@ -421,7 +430,8 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
enum MethodKind { enum MethodKind {
METHOD, METHOD,
GETTER, GETTER,
SETTER SETTER,
CONSTRUCTOR
} }
static class MethodExport { static class MethodExport {

View File

@ -33,6 +33,7 @@ public class ModuleWithExportedClasses {
public static class B { public static class B {
private int bar; private int bar;
@JSExport
public B(int bar) { public B(int bar) {
this.bar = bar; this.bar = bar;
} }

View File

@ -21,4 +21,9 @@ export async function test() {
assertEquals(true, o instanceof BB); assertEquals(true, o instanceof BB);
assertEquals(false, o instanceof A); assertEquals(false, o instanceof A);
assertEquals(42, o.bar); assertEquals(42, o.bar);
let p = new BB(55);
assertEquals(true, p instanceof BB);
assertEquals(false, p instanceof A);
assertEquals(55, p.bar);
} }