From 72b021fc0b84d23d6a390637cb170c310e4f0903 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Mon, 8 Apr 2024 21:32:10 +0200 Subject: [PATCH] jso: support exporting class constructors --- .../src/main/java/org/teavm/jso/JSExport.java | 2 +- .../org/teavm/jso/impl/JSAliasRenderer.java | 78 +++++++++++++++--- .../teavm/jso/impl/JSConstructorToExpose.java | 26 ++++++ .../teavm/jso/impl/JSDependencyListener.java | 3 + .../jso/impl/JSObjectClassTransformer.java | 80 +++++++++++-------- .../jso/export/ModuleWithExportedClasses.java | 1 + .../org/teavm/jso/export/exportClasses.js | 5 ++ 7 files changed, 148 insertions(+), 47 deletions(-) create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSConstructorToExpose.java diff --git a/jso/core/src/main/java/org/teavm/jso/JSExport.java b/jso/core/src/main/java/org/teavm/jso/JSExport.java index 25e8f74f8..b751b79e8 100644 --- a/jso/core/src/main/java/org/teavm/jso/JSExport.java +++ b/jso/core/src/main/java/org/teavm/jso/JSExport.java @@ -21,6 +21,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) +@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR }) public @interface JSExport { } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java index 4b65aa810..5e56644ed 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java @@ -41,6 +41,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { private ListableClassReaderSource classSource; private JSTypeHelper typeHelper; private RenderingManager context; + private int lastExportIndex; @Override public void begin(RenderingManager context, BuildTarget buildTarget) { @@ -65,19 +66,28 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { .appendGlobal("Symbol").append("('jsoClass')").endDeclaration(); writer.append("(()").ws().append("=>").ws().append("{").softNewLine().indent(); writer.append("let c;").softNewLine(); + var exportedNamesByClass = new HashMap(); for (var className : classSource.getClassNames()) { var classReader = classSource.get(className); var hasExportedMembers = false; hasExportedMembers |= exportClassInstanceMembers(classReader); if (!className.equals(context.getEntryPoint())) { - hasExportedMembers |= exportClassStaticMembers(classReader); - if (hasExportedMembers && !typeHelper.isJavaScriptClass(className) - && !typeHelper.isJavaScriptImplementation(className)) { - exportClassFromModule(classReader); + var name = "$rt_export_class_ " + getClassAliasName(classReader) + "_" + lastExportIndex++; + hasExportedMembers |= exportClassStaticMembers(classReader, name); + if (hasExportedMembers) { + exportedNamesByClass.put(className, name); } } } 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) { @@ -125,14 +135,14 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { return true; } - private boolean exportClassStaticMembers(ClassReader classReader) { + private boolean exportClassStaticMembers(ClassReader classReader, String name) { var members = collectMembers(classReader, c -> c.hasModifier(ElementModifier.STATIC)); if (members.methods.isEmpty() && members.properties.isEmpty()) { 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()) { appendMethodAlias(aliasEntry.getKey()); @@ -175,6 +185,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { private Members collectMembers(ClassReader classReader, Predicate filter) { var methods = new HashMap(); var properties = new HashMap(); + MethodDescriptor constructor = null; for (var method : classReader.getMethods()) { if (!filter.test(method)) { continue; @@ -195,10 +206,13 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { propInfo.setter = method.getDescriptor(); break; } + case CONSTRUCTOR: + constructor = method.getDescriptor(); + break; } } } - return new Members(methods, properties); + return new Members(methods, properties, constructor); } 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(); if (name == null) { 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() { @@ -263,6 +310,11 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { 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; } @@ -354,10 +406,13 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { private static class Members { final Map methods; final Map properties; + final MethodDescriptor constructor; - Members(Map methods, Map properties) { + Members(Map methods, Map properties, + MethodDescriptor constructor) { this.methods = methods; this.properties = properties; + this.constructor = constructor; } } @@ -379,6 +434,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { private enum AliasKind { METHOD, GETTER, - SETTER + SETTER, + CONSTRUCTOR } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSConstructorToExpose.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSConstructorToExpose.java new file mode 100644 index 000000000..29a007db8 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSConstructorToExpose.java @@ -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 { +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java index cbf8bb0e9..63651175c 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java @@ -56,6 +56,9 @@ class JSDependencyListener extends AbstractDependencyListener { if (exposeAnnot == null) { exposeAnnot = method.getAnnotations().get(JSSetterToExpose.class.getName()); } + if (exposeAnnot == null) { + exposeAnnot = method.getAnnotations().get(JSConstructorToExpose.class.getName()); + } if (exposeAnnot != null) { MethodDependency methodDep = agent.linkMethod(method.getReference()); if (methodDep.getMethod() != null) { diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java index 2cb7259d5..3ee8032ff 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java @@ -241,12 +241,17 @@ class JSObjectClassTransformer implements ClassHolderTransformer { case SETTER: annotationName = JSSetterToExpose.class.getName(); break; + case CONSTRUCTOR: + annotationName = JSConstructorToExpose.class.getName(); + break; default: annotationName = JSMethodToExpose.class.getName(); break; } 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; } @@ -348,51 +353,55 @@ class JSObjectClassTransformer implements ClassHolderTransformer { private MethodExport createMethodExport(MethodReader method) { String name = null; MethodKind kind = MethodKind.METHOD; - var methodAnnot = method.getAnnotations().get(JSMethod.class.getName()); - if (methodAnnot != null) { - name = method.getName(); - var nameVal = methodAnnot.getValue("value"); - if (nameVal != null) { - String nameStr = nameVal.getString(); - if (!nameStr.isEmpty()) { - name = nameStr; - } - } + if (method.getName().equals("")) { + kind = MethodKind.CONSTRUCTOR; } else { - var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName()); - if (propertyAnnot != null) { - var nameVal = propertyAnnot.getValue("value"); + var methodAnnot = method.getAnnotations().get(JSMethod.class.getName()); + if (methodAnnot != null) { + name = method.getName(); + var nameVal = methodAnnot.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"; + } else { + var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName()); + if (propertyAnnot != null) { + var 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; } - 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); + 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); + } } } } - } - if (name == null) { - name = method.getName(); + if (name == null) { + name = method.getName(); + } } return new MethodExport(name, kind); } @@ -421,7 +430,8 @@ class JSObjectClassTransformer implements ClassHolderTransformer { enum MethodKind { METHOD, GETTER, - SETTER + SETTER, + CONSTRUCTOR } static class MethodExport { diff --git a/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClasses.java b/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClasses.java index 3ec019e38..71231ea0c 100644 --- a/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClasses.java +++ b/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClasses.java @@ -33,6 +33,7 @@ public class ModuleWithExportedClasses { public static class B { private int bar; + @JSExport public B(int bar) { this.bar = bar; } diff --git a/tests/src/test/resources/org/teavm/jso/export/exportClasses.js b/tests/src/test/resources/org/teavm/jso/export/exportClasses.js index 647bbaf70..d93f67655 100644 --- a/tests/src/test/resources/org/teavm/jso/export/exportClasses.js +++ b/tests/src/test/resources/org/teavm/jso/export/exportClasses.js @@ -21,4 +21,9 @@ export async function test() { assertEquals(true, o instanceof BB); assertEquals(false, o instanceof A); assertEquals(42, o.bar); + + let p = new BB(55); + assertEquals(true, p instanceof BB); + assertEquals(false, p instanceof A); + assertEquals(55, p.bar); } \ No newline at end of file