From 931f0f1f4a1e21aed9166dcecf6f259b65de03c4 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Mon, 15 Apr 2024 20:53:24 +0200 Subject: [PATCH] jso: support varargs in exported classes and modules --- .../impl/AnnotationVirtualMethods.java | 8 +- .../backend/javascript/JavaScriptTarget.java | 39 +++++--- .../javascript/TeaVMJavaScriptHost.java | 6 +- .../rendering/MethodBodyRenderer.java | 8 +- .../javascript/rendering/Renderer.java | 40 ++++++--- .../rendering/RenderingContext.java | 10 ++- ...ontributor.java => MethodContributor.java} | 4 +- ...ext.java => MethodContributorContext.java} | 2 +- .../org/teavm/model/ClassReaderSource.java | 8 ++ .../org/teavm/backend/javascript/runtime.js | 10 ++- .../src/main/java/org/teavm/jso/impl/JS.java | 4 + .../org/teavm/jso/impl/JSAliasRenderer.java | 8 +- .../jso/impl/JSExportedMethodAsFunction.java | 34 +++++++ .../java/org/teavm/jso/impl/JSMethods.java | 2 + .../org/teavm/jso/impl/JSNativeInjector.java | 6 ++ .../java/org/teavm/jso/impl/JSOPlugin.java | 1 + .../jso/impl/JSObjectClassTransformer.java | 65 +++++++++++--- .../java/org/teavm/jso/impl/JSTypeHelper.java | 2 - .../java/org/teavm/jso/impl/JSVararg.java | 26 ++++++ .../platform/plugin/AsyncMethodGenerator.java | 8 +- .../java/org/teavm/jso/export/ExportTest.java | 6 ++ .../teavm/jso/export/ModuleWithVararg.java | 90 +++++++++++++++++++ .../resources/org/teavm/jso/export/varargs.js | 28 ++++++ 23 files changed, 354 insertions(+), 61 deletions(-) rename core/src/main/java/org/teavm/backend/javascript/spi/{VirtualMethodContributor.java => MethodContributor.java} (83%) rename core/src/main/java/org/teavm/backend/javascript/spi/{VirtualMethodContributorContext.java => MethodContributorContext.java} (93%) create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSExportedMethodAsFunction.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSVararg.java create mode 100644 tests/src/test/java/org/teavm/jso/export/ModuleWithVararg.java create mode 100644 tests/src/test/resources/org/teavm/jso/export/varargs.js diff --git a/classlib/src/main/java/org/teavm/classlib/impl/AnnotationVirtualMethods.java b/classlib/src/main/java/org/teavm/classlib/impl/AnnotationVirtualMethods.java index a9a88548a..cac2cf05a 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/AnnotationVirtualMethods.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/AnnotationVirtualMethods.java @@ -15,15 +15,15 @@ */ package org.teavm.classlib.impl; -import org.teavm.backend.javascript.spi.VirtualMethodContributor; -import org.teavm.backend.javascript.spi.VirtualMethodContributorContext; +import org.teavm.backend.javascript.spi.MethodContributor; +import org.teavm.backend.javascript.spi.MethodContributorContext; import org.teavm.model.ClassReader; import org.teavm.model.MethodReference; import org.teavm.platform.PlatformAnnotationProvider; -public class AnnotationVirtualMethods implements VirtualMethodContributor { +public class AnnotationVirtualMethods implements MethodContributor { @Override - public boolean isVirtual(VirtualMethodContributorContext context, MethodReference methodRef) { + public boolean isContributing(MethodContributorContext context, MethodReference methodRef) { ClassReader cls = context.getClassSource().get(methodRef.getClassName()); if (cls == null || !cls.getInterfaces().contains(PlatformAnnotationProvider.class.getName())) { return false; diff --git a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java index fbf89a94f..6efb07223 100644 --- a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java +++ b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java @@ -60,8 +60,8 @@ import org.teavm.backend.javascript.spi.GeneratedBy; import org.teavm.backend.javascript.spi.Generator; import org.teavm.backend.javascript.spi.InjectedBy; import org.teavm.backend.javascript.spi.Injector; -import org.teavm.backend.javascript.spi.VirtualMethodContributor; -import org.teavm.backend.javascript.spi.VirtualMethodContributorContext; +import org.teavm.backend.javascript.spi.MethodContributor; +import org.teavm.backend.javascript.spi.MethodContributorContext; import org.teavm.backend.javascript.templating.JavaScriptTemplateFactory; import org.teavm.cache.EmptyMethodNodeCache; import org.teavm.cache.MethodNodeCache; @@ -122,7 +122,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { private DebugInformationEmitter debugEmitter; private MethodNodeCache astCache = EmptyMethodNodeCache.INSTANCE; private final Set asyncMethods = new HashSet<>(); - private List customVirtualMethods = new ArrayList<>(); + private List customVirtualMethods = new ArrayList<>(); + private List forcedFunctionMethods = new ArrayList<>(); private boolean strict; private BoundCheckInsertion boundCheckInsertion = new BoundCheckInsertion(); private NullCheckInsertion nullCheckInsertion = new NullCheckInsertion(NullCheckFilter.EMPTY); @@ -366,11 +367,13 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { debugEmitterToUse = new DummyDebugInformationEmitter(); } - var virtualMethodContributorContext = new VirtualMethodContributorContextImpl(classes); + var methodContributorContext = new MethodContributorContextImpl(classes); RenderingContext renderingContext = new RenderingContext(debugEmitterToUse, controller.getUnprocessedClassSource(), classes, controller.getClassLoader(), controller.getServices(), controller.getProperties(), naming, - controller.getDependencyInfo(), m -> isVirtual(virtualMethodContributorContext, m), + controller.getDependencyInfo(), + m -> isVirtual(methodContributorContext, m), + m -> isForcedFunction(methodContributorContext, m), controller.getClassInitializerInfo(), strict ) { @Override @@ -804,31 +807,45 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { } @Override - public void addVirtualMethods(VirtualMethodContributor virtualMethods) { + public void addVirtualMethods(MethodContributor virtualMethods) { customVirtualMethods.add(virtualMethods); } + @Override + public void addForcedFunctionMethods(MethodContributor forcedFunctionMethods) { + this.forcedFunctionMethods.add(forcedFunctionMethods); + } + @Override public boolean isAsyncSupported() { return true; } - private boolean isVirtual(VirtualMethodContributorContext context, MethodReference method) { + private boolean isVirtual(MethodContributorContext context, MethodReference method) { if (controller.isVirtual(method)) { return true; } - for (VirtualMethodContributor predicate : customVirtualMethods) { - if (predicate.isVirtual(context, method)) { + for (MethodContributor predicate : customVirtualMethods) { + if (predicate.isContributing(context, method)) { return true; } } return false; } - static class VirtualMethodContributorContextImpl implements VirtualMethodContributorContext { + private boolean isForcedFunction(MethodContributorContext context, MethodReference method) { + for (var predicate : forcedFunctionMethods) { + if (predicate.isContributing(context, method)) { + return true; + } + } + return false; + } + + private static class MethodContributorContextImpl implements MethodContributorContext { private ClassReaderSource classSource; - VirtualMethodContributorContextImpl(ClassReaderSource classSource) { + MethodContributorContextImpl(ClassReaderSource classSource) { this.classSource = classSource; } diff --git a/core/src/main/java/org/teavm/backend/javascript/TeaVMJavaScriptHost.java b/core/src/main/java/org/teavm/backend/javascript/TeaVMJavaScriptHost.java index d7684fee5..3c272b0ce 100644 --- a/core/src/main/java/org/teavm/backend/javascript/TeaVMJavaScriptHost.java +++ b/core/src/main/java/org/teavm/backend/javascript/TeaVMJavaScriptHost.java @@ -18,7 +18,7 @@ package org.teavm.backend.javascript; import java.util.function.Function; import org.teavm.backend.javascript.spi.Generator; import org.teavm.backend.javascript.spi.Injector; -import org.teavm.backend.javascript.spi.VirtualMethodContributor; +import org.teavm.backend.javascript.spi.MethodContributor; import org.teavm.model.MethodReference; import org.teavm.vm.spi.RendererListener; import org.teavm.vm.spi.TeaVMHostExtension; @@ -34,5 +34,7 @@ public interface TeaVMJavaScriptHost extends TeaVMHostExtension { void add(RendererListener listener); - void addVirtualMethods(VirtualMethodContributor virtualMethods); + void addVirtualMethods(MethodContributor virtualMethods); + + void addForcedFunctionMethods(MethodContributor forcedFunctionMethods); } diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/MethodBodyRenderer.java b/core/src/main/java/org/teavm/backend/javascript/rendering/MethodBodyRenderer.java index 0c48aba77..7a1712be5 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/MethodBodyRenderer.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/MethodBodyRenderer.java @@ -93,12 +93,16 @@ public class MethodBodyRenderer implements MethodNodeVisitor, GeneratorContext { } public void renderParameters(MethodReference reference, Set modifiers) { + renderParameters(reference, modifiers, false); + } + + public void renderParameters(MethodReference reference, Set modifiers, boolean forceParentheses) { int startParam = 0; if (modifiers.contains(ElementModifier.STATIC)) { startParam = 1; } var count = reference.parameterCount() - startParam + 1; - if (count != 1) { + if (count != 1 || forceParentheses) { writer.append("("); } for (int i = startParam; i <= reference.parameterCount(); ++i) { @@ -107,7 +111,7 @@ public class MethodBodyRenderer implements MethodNodeVisitor, GeneratorContext { } writer.append(statementRenderer.variableName(i)); } - if (count != 1) { + if (count != 1 || forceParentheses) { writer.append(")"); } } diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java b/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java index f281068c0..9fe0c84b6 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java @@ -372,8 +372,13 @@ public class Renderer implements RenderingManager { if (!filterMethod(method)) { continue; } - writer.startVariableDeclaration(); - renderBody(method, decompiler); + var isFunction = context.isForcedFunction(method.getReference()); + if (isFunction) { + writer.startFunctionDeclaration(); + } else { + writer.startVariableDeclaration(); + } + renderBody(method, decompiler, isFunction); writer.endDeclaration(); if (needsInitializers && !method.hasModifier(ElementModifier.STATIC) && method.getName().equals("")) { @@ -381,7 +386,6 @@ public class Renderer implements RenderingManager { } } - writer.emitClass(null); } @@ -781,7 +785,12 @@ public class Renderer implements RenderingManager { } private void emitVirtualFunctionWrapper(MethodReference method) { - if (method.parameterCount() <= 4) { + var forced = context.isForcedFunction(method); + if (forced) { + writer.appendFunction("$rt_wrapFunctionVararg").append("(").appendMethod(method).append(")"); + return; + } + if (method.parameterCount() <= 4 && !forced) { writer.appendFunction("$rt_wrapFunction" + method.parameterCount()); writer.append("(").appendMethod(method).append(")"); return; @@ -810,22 +819,22 @@ public class Renderer implements RenderingManager { writer.append(");").ws().append("}"); } - private void renderBody(MethodHolder method, Decompiler decompiler) { + private void renderBody(MethodHolder method, Decompiler decompiler, boolean isFunction) { MethodReference ref = method.getReference(); writer.emitMethod(ref.getDescriptor()); writer.appendMethod(ref); if (method.hasModifier(ElementModifier.NATIVE)) { - renderNativeBody(method, classSource); + renderNativeBody(method, classSource, isFunction); } else { - renderRegularBody(method, decompiler); + renderRegularBody(method, decompiler, isFunction); } writer.outdent().append("}"); writer.emitMethod(null); } - private void renderNativeBody(MethodHolder method, ClassReaderSource classes) { + private void renderNativeBody(MethodHolder method, ClassReaderSource classes, boolean isFunction) { var reference = method.getReference(); var generator = generators.get(reference); if (generator == null) { @@ -841,7 +850,7 @@ public class Renderer implements RenderingManager { } var async = asyncMethods.contains(reference); - renderMethodPrologue(reference, method.getModifiers()); + renderMethodPrologue(reference, method.getModifiers(), isFunction); methodBodyRenderer.renderNative(generator, async, reference); threadLibraryUsed |= methodBodyRenderer.isThreadLibraryUsed(); } @@ -895,7 +904,7 @@ public class Renderer implements RenderingManager { } } - private void renderRegularBody(MethodHolder method, Decompiler decompiler) { + private void renderRegularBody(MethodHolder method, Decompiler decompiler, boolean isFunction) { MethodReference reference = method.getReference(); MethodNode node; var async = asyncMethods.contains(reference); @@ -907,14 +916,17 @@ public class Renderer implements RenderingManager { } methodBodyRenderer.setCurrentMethod(node); - renderMethodPrologue(method.getReference(), method.getModifiers()); + renderMethodPrologue(method.getReference(), method.getModifiers(), isFunction); methodBodyRenderer.render(node, async); threadLibraryUsed |= methodBodyRenderer.isThreadLibraryUsed(); } - private void renderMethodPrologue(MethodReference reference, Set modifier) { - methodBodyRenderer.renderParameters(reference, modifier); - writer.sameLineWs().append("=>").ws().append("{").indent().softNewLine(); + private void renderMethodPrologue(MethodReference reference, Set modifier, boolean isFunction) { + methodBodyRenderer.renderParameters(reference, modifier, isFunction); + if (!isFunction) { + writer.sameLineWs().append("=>"); + } + writer.ws().append("{").indent().softNewLine(); } private AstCacheEntry decompileRegular(Decompiler decompiler, MethodHolder method) { diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingContext.java b/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingContext.java index aecb9649c..a222dd22f 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingContext.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingContext.java @@ -51,6 +51,7 @@ public abstract class RenderingContext { private NamingStrategy naming; private DependencyInfo dependencyInfo; private Predicate virtualPredicate; + private Predicate forcedFunctionPredicate; private final Map stringPoolMap = new HashMap<>(); private final List stringPool = new ArrayList<>(); private final List readonlyStringPool = Collections.unmodifiableList(stringPool); @@ -63,7 +64,9 @@ public abstract class RenderingContext { ClassReaderSource initialClassSource, ListableClassReaderSource classSource, ClassLoader classLoader, ServiceRepository services, Properties properties, NamingStrategy naming, DependencyInfo dependencyInfo, - Predicate virtualPredicate, ClassInitializerInfo classInitializerInfo, + Predicate virtualPredicate, + Predicate forcedFunctionPredicate, + ClassInitializerInfo classInitializerInfo, boolean strict) { this.debugEmitter = debugEmitter; this.initialClassSource = initialClassSource; @@ -74,6 +77,7 @@ public abstract class RenderingContext { this.naming = naming; this.dependencyInfo = dependencyInfo; this.virtualPredicate = virtualPredicate; + this.forcedFunctionPredicate = forcedFunctionPredicate; this.classInitializerInfo = classInitializerInfo; this.strict = strict; } @@ -118,6 +122,10 @@ public abstract class RenderingContext { return virtualPredicate.test(method); } + public boolean isForcedFunction(MethodReference method) { + return forcedFunctionPredicate.test(method); + } + public boolean isDynamicInitializer(String className) { return classInitializerInfo.isDynamicInitializer(className); } diff --git a/core/src/main/java/org/teavm/backend/javascript/spi/VirtualMethodContributor.java b/core/src/main/java/org/teavm/backend/javascript/spi/MethodContributor.java similarity index 83% rename from core/src/main/java/org/teavm/backend/javascript/spi/VirtualMethodContributor.java rename to core/src/main/java/org/teavm/backend/javascript/spi/MethodContributor.java index 1b3cabe68..99c72098b 100644 --- a/core/src/main/java/org/teavm/backend/javascript/spi/VirtualMethodContributor.java +++ b/core/src/main/java/org/teavm/backend/javascript/spi/MethodContributor.java @@ -17,6 +17,6 @@ package org.teavm.backend.javascript.spi; import org.teavm.model.MethodReference; -public interface VirtualMethodContributor { - boolean isVirtual(VirtualMethodContributorContext context, MethodReference methodRef); +public interface MethodContributor { + boolean isContributing(MethodContributorContext context, MethodReference methodRef); } diff --git a/core/src/main/java/org/teavm/backend/javascript/spi/VirtualMethodContributorContext.java b/core/src/main/java/org/teavm/backend/javascript/spi/MethodContributorContext.java similarity index 93% rename from core/src/main/java/org/teavm/backend/javascript/spi/VirtualMethodContributorContext.java rename to core/src/main/java/org/teavm/backend/javascript/spi/MethodContributorContext.java index 8e042fc63..6267500ae 100644 --- a/core/src/main/java/org/teavm/backend/javascript/spi/VirtualMethodContributorContext.java +++ b/core/src/main/java/org/teavm/backend/javascript/spi/MethodContributorContext.java @@ -17,6 +17,6 @@ package org.teavm.backend.javascript.spi; import org.teavm.model.ClassReaderSource; -public interface VirtualMethodContributorContext { +public interface MethodContributorContext { ClassReaderSource getClassSource(); } diff --git a/core/src/main/java/org/teavm/model/ClassReaderSource.java b/core/src/main/java/org/teavm/model/ClassReaderSource.java index 980f8b832..1cf6a136d 100644 --- a/core/src/main/java/org/teavm/model/ClassReaderSource.java +++ b/core/src/main/java/org/teavm/model/ClassReaderSource.java @@ -119,4 +119,12 @@ public interface ClassReaderSource { default Optional isSuperType(String superType, String subType) { return ClassReaderSourceHelper.isSuperType(this, superType, subType); } + + default MethodReader getMethod(MethodReference ref) { + var cls = get(ref.getClassName()); + if (cls == null) { + return null; + } + return cls.getMethod(ref.getDescriptor()); + } } diff --git a/core/src/main/resources/org/teavm/backend/javascript/runtime.js b/core/src/main/resources/org/teavm/backend/javascript/runtime.js index 180c84bd1..073016546 100644 --- a/core/src/main/resources/org/teavm/backend/javascript/runtime.js +++ b/core/src/main/resources/org/teavm/backend/javascript/runtime.js @@ -40,6 +40,12 @@ let $rt_wrapFunction3 = f => function(p1, p2, p3) { let $rt_wrapFunction4 = f => function(p1, p2, p3, p4) { return f(this, p1, p2, p3, p4); } +let $rt_wrapFunctionVararg = f => function() { + let array = new teavm_globals.Array(); + array.push(this); + Array.prototype.push.apply(array, arguments); + return f.apply(null, array); +} let $rt_threadStarter = f => function() { let args = teavm_globals.Array.prototype.slice.apply(arguments); $rt_startThread(function() { @@ -110,4 +116,6 @@ let $rt_setThread = t => { let $rt_apply = (instance, method, args) => instance[method].apply(instance, args); -let $rt_apply_topLevel = (method, args) => method.apply(null, args); \ No newline at end of file +let $rt_apply_topLevel = (method, args) => method.apply(null, args); + +let $rt_skip = (array, count) => count === 0 ? array : Array.prototype.slice.call(array, count); \ No newline at end of file diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JS.java b/jso/impl/src/main/java/org/teavm/jso/impl/JS.java index 16ffc22c3..8c35d51f9 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JS.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JS.java @@ -716,4 +716,8 @@ final class JS { @InjectedBy(JSNativeInjector.class) @NoSideEffects public static native JSObject throwCCEIfFalse(boolean value, JSObject o); + + @InjectedBy(JSNativeInjector.class) + @NoSideEffects + public static native JSObject argumentsBeginningAt(int index); } 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 5e56644ed..939dcd27d 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 @@ -20,8 +20,8 @@ import java.util.Map; import java.util.function.Predicate; import org.teavm.backend.javascript.codegen.SourceWriter; import org.teavm.backend.javascript.rendering.RenderingManager; -import org.teavm.backend.javascript.spi.VirtualMethodContributor; -import org.teavm.backend.javascript.spi.VirtualMethodContributorContext; +import org.teavm.backend.javascript.spi.MethodContributor; +import org.teavm.backend.javascript.spi.MethodContributorContext; import org.teavm.jso.JSClass; import org.teavm.model.AnnotationReader; import org.teavm.model.ClassReader; @@ -35,7 +35,7 @@ import org.teavm.model.MethodReference; import org.teavm.vm.BuildTarget; import org.teavm.vm.spi.RendererListener; -class JSAliasRenderer implements RendererListener, VirtualMethodContributor { +class JSAliasRenderer implements RendererListener, MethodContributor { private static String variableChars = "abcdefghijklmnopqrstuvwxyz"; private SourceWriter writer; private ListableClassReaderSource classSource; @@ -389,7 +389,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { } @Override - public boolean isVirtual(VirtualMethodContributorContext context, MethodReference methodRef) { + public boolean isContributing(MethodContributorContext context, MethodReference methodRef) { ClassReader classReader = context.getClassSource().get(methodRef.getClassName()); if (classReader == null) { return false; diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSExportedMethodAsFunction.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSExportedMethodAsFunction.java new file mode 100644 index 000000000..932b793f8 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSExportedMethodAsFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 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 org.teavm.backend.javascript.spi.MethodContributor; +import org.teavm.backend.javascript.spi.MethodContributorContext; +import org.teavm.model.MethodReference; + +public class JSExportedMethodAsFunction implements MethodContributor { + @Override + public boolean isContributing(MethodContributorContext context, MethodReference methodRef) { + var method = context.getClassSource().getMethod(methodRef); + if (method != null) { + if (method.getAnnotations().get(JSVararg.class.getName()) != null) { + return true; + } + } + + return false; + } +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java index 1fd64968a..aad726d40 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java @@ -131,6 +131,8 @@ final class JSMethods { JSObject.class, boolean.class); public static final MethodReference THROW_CCE_IF_FALSE = new MethodReference(JS.class, "throwCCEIfFalse", boolean.class, JSObject.class, JSObject.class); + public static final MethodReference ARGUMENTS_BEGINNING_AT = new MethodReference(JS.class, + "argumentsBeginningAt", int.class, JSObject.class); public static final ValueType JS_OBJECT = ValueType.object(JSObject.class.getName()); public static final ValueType OBJECT = ValueType.object("java.lang.Object"); diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java index 4bf1cc02a..f3a30337d 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java @@ -237,6 +237,12 @@ public class JSNativeInjector implements Injector, DependencyPlugin { writer.append(")"); break; } + case "argumentsBeginningAt": { + writer.appendFunction("$rt_skip").append("(arguments,").ws(); + context.writeExpr(context.getArgument(0), Precedence.min()); + writer.append(")"); + break; + } default: if (methodRef.getName().startsWith("unwrap")) { diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java index 2b80a86bd..1828b8f61 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java @@ -48,6 +48,7 @@ public class JSOPlugin implements TeaVMPlugin { jsHost.addInjectorProvider(new GeneratorAnnotationInstaller<>(new JSBodyGenerator(), DynamicInjector.class.getName())); jsHost.addVirtualMethods(aliasRenderer); + jsHost.addForcedFunctionMethods(new JSExportedMethodAsFunction()); JSExceptionsGenerator exceptionsGenerator = new JSExceptionsGenerator(); jsHost.add(new MethodReference(JSExceptions.class, "getJavaException", JSObject.class, Throwable.class), 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 3ee8032ff..3137a7ba0 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 @@ -49,6 +49,7 @@ import org.teavm.model.Program; import org.teavm.model.ValueType; import org.teavm.model.Variable; import org.teavm.model.instructions.ExitInstruction; +import org.teavm.model.instructions.IntegerConstantInstruction; import org.teavm.model.instructions.InvocationType; import org.teavm.model.instructions.InvokeInstruction; @@ -111,13 +112,18 @@ class JSObjectClassTransformer implements ClassHolderTransformer { private void exposeMethods(ClassHolder classHolder, ExposedClass classToExpose, Diagnostics diagnostics, MethodReference functorMethod) { int index = 0; - for (MethodDescriptor method : classToExpose.methods.keySet()) { + for (var entry : classToExpose.methods.entrySet()) { + var method = entry.getKey(); + var export = entry.getValue(); MethodReference methodRef = new MethodReference(classHolder.getName(), method); CallLocation callLocation = new CallLocation(methodRef); - ValueType[] exportedMethodSignature = Arrays.stream(method.getSignature()) - .map(type -> ValueType.object(JSObject.class.getName())) - .toArray(ValueType[]::new); + var paramCount = method.parameterCount(); + if (export.vararg) { + --paramCount; + } + var exportedMethodSignature = new ValueType[paramCount + 1]; + Arrays.fill(exportedMethodSignature, JSMethods.JS_OBJECT); MethodDescriptor exportedMethodDesc = new MethodDescriptor(method.getName() + "$exported$" + index++, exportedMethodSignature); MethodHolder exportedMethod = new MethodHolder(exportedMethodDesc); @@ -134,10 +140,15 @@ class JSObjectClassTransformer implements ClassHolderTransformer { for (int i = 0; i < method.parameterCount(); ++i) { variablesToPass[i] = program.createVariable(); } + if (export.vararg) { + transformVarargParam(variablesToPass, program, marshallInstructions, exportedMethod, 1); + } for (int i = 0; i < method.parameterCount(); ++i) { + var byRef = i == method.parameterCount() - 1 && export.vararg + && typeHelper.isSupportedByRefType(method.parameterType(i)); variablesToPass[i] = marshaller.unwrapReturnValue(callLocation, variablesToPass[i], - method.parameterType(i), false, true); + method.parameterType(i), byRef, true); } basicBlock.addAll(marshallInstructions); @@ -161,8 +172,6 @@ class JSObjectClassTransformer implements ClassHolderTransformer { basicBlock.add(exit); classHolder.addMethod(exportedMethod); - - var export = classToExpose.methods.get(method); exportedMethod.getAnnotations().add(createExportAnnotation(export)); if (methodRef.equals(functorMethod)) { @@ -179,10 +188,14 @@ class JSObjectClassTransformer implements ClassHolderTransformer { continue; } + var paramCount = method.parameterCount(); + var vararg = method.hasModifier(ElementModifier.VARARGS); + if (vararg) { + --paramCount; + } var callLocation = new CallLocation(method.getReference()); - var exportedMethodSignature = Arrays.stream(method.getSignature()) - .map(type -> ValueType.object(JSObject.class.getName())) - .toArray(ValueType[]::new); + var exportedMethodSignature = new ValueType[paramCount + 1]; + Arrays.fill(exportedMethodSignature, JSMethods.JS_OBJECT); var exportedMethodDesc = new MethodDescriptor(method.getName() + "$exported$" + index++, exportedMethodSignature); var exportedMethod = new MethodHolder(exportedMethodDesc); @@ -200,10 +213,15 @@ class JSObjectClassTransformer implements ClassHolderTransformer { for (int i = 0; i < method.parameterCount(); ++i) { variablesToPass[i] = program.createVariable(); } + if (vararg) { + transformVarargParam(variablesToPass, program, marshallInstructions, exportedMethod, 0); + } for (int i = 0; i < method.parameterCount(); ++i) { + var byRef = i == method.parameterCount() - 1 && vararg + && typeHelper.isSupportedByRefType(method.parameterType(i)); variablesToPass[i] = marshaller.unwrapReturnValue(callLocation, variablesToPass[i], - method.parameterType(i), false, true); + method.parameterType(i), byRef, true); } basicBlock.addAll(marshallInstructions); @@ -232,6 +250,25 @@ class JSObjectClassTransformer implements ClassHolderTransformer { } } + private void transformVarargParam(Variable[] variablesToPass, Program program, + List instructions, MethodHolder method, int additionalSkip) { + var last = variablesToPass.length - 1; + + var lastConstant = new IntegerConstantInstruction(); + lastConstant.setReceiver(program.createVariable()); + lastConstant.setConstant(last + additionalSkip); + instructions.add(lastConstant); + + var extractVarargs = new InvokeInstruction(); + extractVarargs.setType(InvocationType.SPECIAL); + extractVarargs.setMethod(JSMethods.ARGUMENTS_BEGINNING_AT); + extractVarargs.setArguments(lastConstant.getReceiver()); + extractVarargs.setReceiver(variablesToPass[last]); + instructions.add(extractVarargs); + + method.getAnnotations().add(new AnnotationHolder(JSVararg.class.getName())); + } + private AnnotationHolder createExportAnnotation(MethodExport export) { String annotationName; switch (export.kind) { @@ -403,7 +440,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer { name = method.getName(); } } - return new MethodExport(name, kind); + return new MethodExport(name, kind, method.hasModifier(ElementModifier.VARARGS)); } private void addFunctorField(ClassHolder cls, MethodReference method) { @@ -437,10 +474,12 @@ class JSObjectClassTransformer implements ClassHolderTransformer { static class MethodExport { final String alias; final MethodKind kind; + boolean vararg; - MethodExport(String alias, MethodKind kind) { + MethodExport(String alias, MethodKind kind, boolean vararg) { this.alias = alias; this.kind = kind; + this.vararg = vararg; } } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java index 5d497d52c..8aaf41d15 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java @@ -121,8 +121,6 @@ class JSTypeHelper { default: return false; } - } else if (itemType instanceof ValueType.Object) { - return isJavaScriptClass(((ValueType.Object) itemType).getClassName()); } else { return false; } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSVararg.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSVararg.java new file mode 100644 index 000000000..4a573b9a5 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSVararg.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 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 JSVararg { +} diff --git a/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodGenerator.java b/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodGenerator.java index abd2cbfd3..04dd02aa1 100644 --- a/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodGenerator.java +++ b/platform/src/main/java/org/teavm/platform/plugin/AsyncMethodGenerator.java @@ -18,8 +18,8 @@ package org.teavm.platform.plugin; import org.teavm.backend.javascript.codegen.SourceWriter; import org.teavm.backend.javascript.spi.Generator; import org.teavm.backend.javascript.spi.GeneratorContext; -import org.teavm.backend.javascript.spi.VirtualMethodContributor; -import org.teavm.backend.javascript.spi.VirtualMethodContributorContext; +import org.teavm.backend.javascript.spi.MethodContributor; +import org.teavm.backend.javascript.spi.MethodContributorContext; import org.teavm.backend.javascript.templating.JavaScriptTemplate; import org.teavm.backend.javascript.templating.JavaScriptTemplateFactory; import org.teavm.dependency.DependencyAgent; @@ -34,7 +34,7 @@ import org.teavm.model.MethodReader; import org.teavm.model.MethodReference; import org.teavm.model.ValueType; -public class AsyncMethodGenerator implements Generator, DependencyPlugin, VirtualMethodContributor { +public class AsyncMethodGenerator implements Generator, DependencyPlugin, MethodContributor { private static final MethodDescriptor completeMethod = new MethodDescriptor("complete", Object.class, void.class); private static final MethodDescriptor errorMethod = new MethodDescriptor("error", Throwable.class, void.class); private JavaScriptTemplate template; @@ -101,7 +101,7 @@ public class AsyncMethodGenerator implements Generator, DependencyPlugin, Virtua } @Override - public boolean isVirtual(VirtualMethodContributorContext context, MethodReference methodRef) { + public boolean isContributing(MethodContributorContext context, MethodReference methodRef) { ClassReader cls = context.getClassSource().get(methodRef.getClassName()); if (cls == null) { return false; diff --git a/tests/src/test/java/org/teavm/jso/export/ExportTest.java b/tests/src/test/java/org/teavm/jso/export/ExportTest.java index eee99995d..9cc4de567 100644 --- a/tests/src/test/java/org/teavm/jso/export/ExportTest.java +++ b/tests/src/test/java/org/teavm/jso/export/ExportTest.java @@ -82,6 +82,11 @@ public class ExportTest { testExport("exportClasses", ModuleWithExportedClasses.class); } + @Test + public void varargs() { + testExport("varargs", ModuleWithVararg.class); + } + private void testExport(String name, Class moduleClass) { if (!Boolean.parseBoolean(System.getProperty("teavm.junit.js", "true"))) { return; @@ -89,6 +94,7 @@ public class ExportTest { try { var jsTarget = new JavaScriptTarget(); jsTarget.setModuleType(JSModuleType.ES2015); + jsTarget.setObfuscated(false); var teavm = new TeaVMBuilder(jsTarget).build(); var outputDir = new File(targetFile, name); teavm.installPlugins(); diff --git a/tests/src/test/java/org/teavm/jso/export/ModuleWithVararg.java b/tests/src/test/java/org/teavm/jso/export/ModuleWithVararg.java new file mode 100644 index 000000000..7c94bcd1f --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/export/ModuleWithVararg.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 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.export; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.teavm.jso.JSExport; +import org.teavm.jso.JSExportClasses; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; +import org.testng.util.Strings; + +@JSExportClasses(ModuleWithVararg.A.class) +public class ModuleWithVararg { + private ModuleWithVararg() { + } + + @JSExport + public static String strings(String... values) { + var sb = new StringBuilder(); + sb.append("strings: "); + sb.append(Strings.join(", ", values)); + return sb.toString(); + } + + @JSExport + public static String prefixStrings(int a, String... values) { + var sb = new StringBuilder(); + sb.append("strings(").append(a).append("): "); + sb.append(Strings.join(", ", values)); + return sb.toString(); + } + + @JSExport + public static String ints(int... values) { + var sb = new StringBuilder(); + sb.append("ints: "); + sb.append(IntStream.of(values).mapToObj(Integer::toString).collect(Collectors.joining(", "))); + return sb.toString(); + } + + @JSExport + public static String prefixInts(String prefix, int... values) { + var sb = new StringBuilder(); + sb.append("ints(").append(prefix).append("): "); + sb.append(IntStream.of(values).mapToObj(Integer::toString).collect(Collectors.joining(", "))); + return sb.toString(); + } + + @JSExport + public static String objects(ObjectWithString... values) { + var sb = new StringBuilder(); + sb.append("objects: "); + sb.append(Stream.of(values).map(v -> v.getStringValue()).collect(Collectors.joining(", "))); + return sb.toString(); + } + + public interface ObjectWithString extends JSObject { + @JSProperty + String getStringValue(); + } + + public static class A { + @JSExport + public A() { + } + + @JSExport + public String strings(String... values) { + var sb = new StringBuilder(); + sb.append("A.strings: "); + sb.append(Strings.join(", ", values)); + return sb.toString(); + } + } +} diff --git a/tests/src/test/resources/org/teavm/jso/export/varargs.js b/tests/src/test/resources/org/teavm/jso/export/varargs.js new file mode 100644 index 000000000..588490c9f --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/export/varargs.js @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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. + */ + +import * as varargs from '/tests/varargs/test.js'; + +export async function test() { + assertEquals("strings: a, b", varargs.strings("a", "b")); + assertEquals("strings(23): a, b", varargs.prefixStrings(23, "a", "b")); + assertEquals("ints: 23, 42", varargs.ints(23, 42)); + assertEquals("ints(*): 23, 42", varargs.prefixInts("*", 23, 42)); + assertEquals("objects: a, b", varargs.objects({ stringValue: "a" }, { stringValue: "b" })); + + let obj = new varargs.A(); + assertEquals("A.strings: a, b", obj.strings("a", "b")); +}