From 4171d468d4dac053c5f27238e11f2f8d0b1cc26a Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Thu, 20 Oct 2016 18:12:54 +0300 Subject: [PATCH] JS: add reflection support for constructors --- .../impl/ReflectionDependencyListener.java | 4 +- .../impl/reflection/ConverterInjector.java | 11 +- .../classlib/impl/reflection/JSClass.java | 6 + ...JSConstructor.java => JSMethodMember.java} | 2 +- .../classlib/java/lang/ClassGenerator.java | 96 ++++++++++++++- .../org/teavm/classlib/java/lang/TClass.java | 76 ++++++++++++ .../java/lang/TNoSuchMethodException.java | 25 ++++ .../java/lang/reflect/TConstructor.java | 6 +- samples/kotlin/teavm-samples-kotlin.iml | 45 +++++++ .../java/lang/reflect/ConstructorTest.java | 111 ++++++++++++++++++ .../java/org/teavm/junit/TeaVMTestRunner.java | 2 + 11 files changed, 372 insertions(+), 12 deletions(-) rename classlib/src/main/java/org/teavm/classlib/impl/reflection/{JSConstructor.java => JSMethodMember.java} (94%) create mode 100644 classlib/src/main/java/org/teavm/classlib/java/lang/TNoSuchMethodException.java create mode 100644 samples/kotlin/teavm-samples-kotlin.iml create mode 100644 tests/src/test/java/org/teavm/classlib/java/lang/reflect/ConstructorTest.java diff --git a/classlib/src/main/java/org/teavm/classlib/impl/ReflectionDependencyListener.java b/classlib/src/main/java/org/teavm/classlib/impl/ReflectionDependencyListener.java index 8dbdf1f3d..d9f60a034 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/ReflectionDependencyListener.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/ReflectionDependencyListener.java @@ -161,9 +161,9 @@ public class ReflectionDependencyListener extends AbstractDependencyListener { ClassReader cls = agent.getClassSource().get(reflectedType.getName()); for (MethodDescriptor methodDescriptor : accessibleMethods) { MethodReader calledMethod = cls.getMethod(methodDescriptor); - MethodDependency calledMethodDep = agent.linkMethod(method.getReference(), location); + MethodDependency calledMethodDep = agent.linkMethod(calledMethod.getReference(), location); calledMethodDep.use(); - for (int i = 0; i < methodDescriptor.parameterCount(); ++i) { + for (int i = 0; i < calledMethod.parameterCount(); ++i) { propagateSet(agent, methodDescriptor.parameterType(i), method.getVariable(1).getArrayItem(), calledMethodDep.getVariable(i + 1), location); } diff --git a/classlib/src/main/java/org/teavm/classlib/impl/reflection/ConverterInjector.java b/classlib/src/main/java/org/teavm/classlib/impl/reflection/ConverterInjector.java index 6895e5aae..9c59dec44 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/reflection/ConverterInjector.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/reflection/ConverterInjector.java @@ -23,6 +23,15 @@ import org.teavm.model.MethodReference; public class ConverterInjector implements Injector { @Override public void generate(InjectorContext context, MethodReference methodRef) throws IOException { - context.writeExpr(context.getArgument(0)); + switch (methodRef.getName()) { + case "toJava": + case "fromJava": + context.writeExpr(context.getArgument(0)); + break; + case "arrayFromJava": + context.writeExpr(context.getArgument(0)); + context.getWriter().append(".data"); + break; + } } } diff --git a/classlib/src/main/java/org/teavm/classlib/impl/reflection/JSClass.java b/classlib/src/main/java/org/teavm/classlib/impl/reflection/JSClass.java index 022095bd7..e884e7db1 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/reflection/JSClass.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/reflection/JSClass.java @@ -25,4 +25,10 @@ public interface JSClass extends PlatformClassMetadata { @JSProperty void setFields(JSArray fields); + + @JSProperty + JSArray getMethods(); + + @JSProperty + void setMethods(JSArray methods); } diff --git a/classlib/src/main/java/org/teavm/classlib/impl/reflection/JSConstructor.java b/classlib/src/main/java/org/teavm/classlib/impl/reflection/JSMethodMember.java similarity index 94% rename from classlib/src/main/java/org/teavm/classlib/impl/reflection/JSConstructor.java rename to classlib/src/main/java/org/teavm/classlib/impl/reflection/JSMethodMember.java index a85c5f92a..13669bca4 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/reflection/JSConstructor.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/reflection/JSMethodMember.java @@ -19,7 +19,7 @@ import org.teavm.jso.JSProperty; import org.teavm.platform.PlatformClass; import org.teavm.platform.PlatformSequence; -public interface JSConstructor extends JSMember { +public interface JSMethodMember extends JSMember { @JSProperty PlatformSequence getParameterTypes(); diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/ClassGenerator.java b/classlib/src/main/java/org/teavm/classlib/java/lang/ClassGenerator.java index 21274678a..4631029d4 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/ClassGenerator.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/ClassGenerator.java @@ -18,18 +18,31 @@ package org.teavm.classlib.java.lang; import java.io.IOException; import java.util.Set; import org.teavm.backend.javascript.codegen.SourceWriter; +import org.teavm.backend.javascript.rendering.Precedence; import org.teavm.backend.javascript.rendering.RenderingUtil; import org.teavm.backend.javascript.spi.Generator; import org.teavm.backend.javascript.spi.GeneratorContext; +import org.teavm.backend.javascript.spi.Injector; +import org.teavm.backend.javascript.spi.InjectorContext; import org.teavm.classlib.impl.ReflectionDependencyListener; +import org.teavm.dependency.DependencyAgent; +import org.teavm.dependency.DependencyPlugin; +import org.teavm.dependency.MethodDependency; +import org.teavm.model.CallLocation; import org.teavm.model.ClassReader; import org.teavm.model.ElementModifier; import org.teavm.model.FieldReader; +import org.teavm.model.FieldReference; import org.teavm.model.MemberReader; +import org.teavm.model.MethodDescriptor; +import org.teavm.model.MethodReader; import org.teavm.model.MethodReference; import org.teavm.model.ValueType; -public class ClassGenerator implements Generator { +public class ClassGenerator implements Generator, Injector, DependencyPlugin { + private static final FieldReference platformClassField = + new FieldReference(Class.class.getName(), "platformClass"); + @Override public void generate(GeneratorContext context, SourceWriter writer, MethodReference methodRef) throws IOException { switch (methodRef.getName()) { @@ -39,6 +52,27 @@ public class ClassGenerator implements Generator { } } + @Override + public void generate(InjectorContext context, MethodReference methodRef) throws IOException { + switch (methodRef.getName()) { + case "newEmptyInstance": + context.getWriter().append("new "); + context.writeExpr(context.getArgument(0), Precedence.MEMBER_ACCESS); + context.getWriter().append('.').appendField(platformClassField); + context.getWriter().append("()"); + break; + } + } + + @Override + public void methodReached(DependencyAgent agent, MethodDependency method, CallLocation location) { + switch (method.getReference().getName()) { + case "newEmptyInstance": + method.getVariable(0).getClassValueNode().connect(method.getResult()); + break; + } + } + private void generateCreateMetadata(GeneratorContext context, SourceWriter writer) throws IOException { ReflectionDependencyListener reflection = context.getService(ReflectionDependencyListener.class); for (String className : reflection.getClassesWithReflectableFields()) { @@ -83,10 +117,32 @@ public class ClassGenerator implements Generator { private void generateCreateMethodsForClass(GeneratorContext context, SourceWriter writer, String className) throws IOException { ReflectionDependencyListener reflection = context.getService(ReflectionDependencyListener.class); + Set accessibleMethods = reflection.getAccessibleMethods(className); ClassReader cls = context.getClassSource().get(className); writer.appendClass(className).append(".$meta.methods").ws().append('=').ws().append('[').indent(); + generateCreateMembers(writer, cls.getMethods(), method -> { + appendProperty(writer, "parameterTypes", false, () -> { + writer.append('['); + for (int i = 0; i < method.parameterCount(); ++i) { + if (i > 0) { + writer.append(',').ws(); + } + writer.append(context.typeToClassString(method.parameterType(i))); + } + writer.append(']'); + }); + + appendProperty(writer, "callable", false, () -> { + if (accessibleMethods != null && accessibleMethods.contains(method.getDescriptor())) { + renderCallable(writer, method); + } else { + writer.append("null"); + } + }); + }); + writer.outdent().append("];").softNewLine(); } @@ -109,8 +165,6 @@ public class ClassGenerator implements Generator { renderer.render(member); writer.outdent().softNewLine().append("}"); } - - writer.outdent().append("];").softNewLine(); } private void appendProperty(SourceWriter writer, String name, boolean first, Fragment value) throws IOException { @@ -140,9 +194,39 @@ public class ClassGenerator implements Generator { writer.outdent().append("}"); } - private void initClass(SourceWriter writer, FieldReader field) throws IOException { - if (field.hasModifier(ElementModifier.STATIC)) { - writer.appendClass(field.getOwnerName()).append("_$callClinit();").softNewLine(); + private void renderCallable(SourceWriter writer, MethodReader method) throws IOException { + writer.append("function(obj,").ws().append("args)").ws().append("{").indent().softNewLine(); + + initClass(writer, method); + + if (method.getResultType() != ValueType.VOID) { + writer.append("return "); + } + if (method.hasModifier(ElementModifier.STATIC)) { + writer.appendMethodBody(method.getReference()); + } else { + writer.append("obj.").appendMethod(method.getDescriptor()); + } + + writer.append('('); + for (int i = 0; i < method.parameterCount(); ++i) { + if (i > 0) { + writer.append(',').ws(); + } + int index = i; + unboxIfNecessary(writer, method.parameterType(i), () -> writer.append("args[" + index + "]")); + } + writer.append(");").softNewLine(); + + if (method.getResultType() == ValueType.VOID) { + writer.append("return null;").softNewLine(); + } + writer.outdent().append("}"); + } + + private void initClass(SourceWriter writer, MemberReader member) throws IOException { + if (member.hasModifier(ElementModifier.STATIC)) { + writer.appendClass(member.getOwnerName()).append("_$callClinit();").softNewLine(); } } diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TClass.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TClass.java index 8511ddba6..c56e30321 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/TClass.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TClass.java @@ -25,17 +25,21 @@ import java.util.List; import java.util.Map; import java.util.Set; import org.teavm.backend.javascript.spi.GeneratedBy; +import org.teavm.backend.javascript.spi.InjectedBy; import org.teavm.classlib.impl.DeclaringClassMetadataGenerator; import org.teavm.classlib.impl.reflection.Flags; import org.teavm.classlib.impl.reflection.JSClass; import org.teavm.classlib.impl.reflection.JSField; +import org.teavm.classlib.impl.reflection.JSMethodMember; import org.teavm.classlib.java.lang.annotation.TAnnotation; import org.teavm.classlib.java.lang.reflect.TAnnotatedElement; import org.teavm.dependency.PluggableDependency; import org.teavm.interop.Address; import org.teavm.interop.DelegateTo; +import org.teavm.classlib.java.lang.reflect.TConstructor; import org.teavm.classlib.java.lang.reflect.TField; import org.teavm.classlib.java.lang.reflect.TModifier; +import org.teavm.dependency.PluggableDependency; import org.teavm.jso.core.JSArray; import org.teavm.platform.Platform; import org.teavm.platform.PlatformClass; @@ -53,6 +57,7 @@ public class TClass extends TObject implements TAnnotatedElement { private Map, TAnnotation> annotationsByType; private TField[] declaredFields; private TField[] fields; + private TConstructor[] declaredConstructors; private static boolean reflectionInitialized; private TClass(PlatformClass platformClass) { @@ -235,6 +240,77 @@ public class TClass extends TObject implements TAnnotatedElement { return null; } + @InjectedBy(ClassGenerator.class) + @PluggableDependency(ClassGenerator.class) + public native T newEmptyInstance(); + + @SuppressWarnings({ "raw", "unchecked" }) + public TConstructor[] getDeclaredConstructors() throws TSecurityException { + if (declaredConstructors == null) { + initReflection(); + JSClass jsClass = (JSClass) getPlatformClass().getMetadata(); + JSArray jsMethods = jsClass.getMethods(); + declaredConstructors = new TConstructor[jsMethods.getLength()]; + int count = 0; + for (int i = 0; i < jsMethods.getLength(); ++i) { + JSMethodMember jsMethod = jsMethods.get(i); + if (!jsMethod.getName().equals("")) { + continue; + } + PlatformSequence jsParameterTypes = jsMethod.getParameterTypes(); + TClass[] parameterTypes = new TClass[jsParameterTypes.getLength()]; + for (int j = 0; j < parameterTypes.length; ++j) { + parameterTypes[j] = getClass(jsParameterTypes.get(j)); + } + declaredConstructors[count++] = new TConstructor(this, jsMethod.getName(), jsMethod.getModifiers(), + jsMethod.getAccessLevel(), parameterTypes, jsMethod.getCallable()); + } + declaredConstructors = Arrays.copyOf(declaredConstructors, count); + } + return declaredConstructors; + } + + public TConstructor[] getConstructors() throws TSecurityException { + TConstructor[] declaredConstructors = getDeclaredConstructors(); + TConstructor[] constructors = new TConstructor[declaredConstructors.length]; + + int sz = 0; + for (TConstructor constructor : declaredConstructors) { + if (TModifier.isPublic(constructor.getModifiers())) { + constructors[sz++] = constructor; + } + } + + if (sz < constructors.length) { + constructors = Arrays.copyOf(constructors, sz); + } + + return constructors; + } + + @SuppressWarnings({ "raw", "unchecked" }) + public TConstructor getDeclaredConstructor(TClass... parameterTypes) + throws TSecurityException, TNoSuchMethodException { + for (TConstructor constructor : getDeclaredConstructors()) { + if (Arrays.equals(constructor.getParameterTypes(), parameterTypes)) { + return (TConstructor) constructor; + } + } + throw new TNoSuchMethodException(); + } + + @SuppressWarnings({ "raw", "unchecked" }) + public TConstructor getConstructor(TClass... parameterTypes) + throws TSecurityException, TNoSuchMethodException { + for (TConstructor constructor : getDeclaredConstructors()) { + if (TModifier.isPublic(constructor.getModifiers()) + && Arrays.equals(constructor.getParameterTypes(), parameterTypes)) { + return (TConstructor) constructor; + } + } + throw new TNoSuchMethodException(); + } + private static void getFieldsOfInterfaces(TClass iface, List fields, Set> visited) { if (!visited.add(iface)) { return; diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TNoSuchMethodException.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TNoSuchMethodException.java new file mode 100644 index 000000000..2cd78206a --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TNoSuchMethodException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016 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.classlib.java.lang; + +public class TNoSuchMethodException extends TReflectiveOperationException { + public TNoSuchMethodException() { + } + + public TNoSuchMethodException(TString message) { + super(message); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/reflect/TConstructor.java b/classlib/src/main/java/org/teavm/classlib/java/lang/reflect/TConstructor.java index ccaa674ac..5c3189769 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/reflect/TConstructor.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/reflect/TConstructor.java @@ -103,7 +103,8 @@ public class TConstructor extends TAccessibleObject implements TMember { throw new TIllegalArgumentException(); } for (int i = 0; i < initargs.length; ++i) { - if (initargs[i] != null && !parameterTypes[i].isInstance((TObject) initargs[i])) { + if (!parameterTypes[i].isPrimitive() && initargs[i] != null + && !parameterTypes[i].isInstance((TObject) initargs[i])) { throw new TIllegalArgumentException(); } if (parameterTypes[i].isPrimitive() && initargs[i] == null) { @@ -112,7 +113,8 @@ public class TConstructor extends TAccessibleObject implements TMember { } PlatformSequence jsArgs = Converter.arrayFromJava(initargs); - PlatformObject instance = callable.call(null, jsArgs); + PlatformObject instance = declaringClass.newEmptyInstance(); + callable.call(instance, jsArgs); return (T) Converter.toJava(instance); } diff --git a/samples/kotlin/teavm-samples-kotlin.iml b/samples/kotlin/teavm-samples-kotlin.iml new file mode 100644 index 000000000..d6d03f110 --- /dev/null +++ b/samples/kotlin/teavm-samples-kotlin.iml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/src/test/java/org/teavm/classlib/java/lang/reflect/ConstructorTest.java b/tests/src/test/java/org/teavm/classlib/java/lang/reflect/ConstructorTest.java new file mode 100644 index 000000000..b81a46a3d --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/lang/reflect/ConstructorTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2016 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.classlib.java.lang.reflect; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.classlib.support.Reflectable; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +public class ConstructorTest { + @Test + public void constructorsEnumerated() { + callConstructors(); + + assertEquals("" + + "ConstructorTest$ReflectableType(int,java.lang.Object);" + + "ConstructorTest$ReflectableType(java.lang.String);" + + "protected ConstructorTest$ReflectableType();" + + "public ConstructorTest$ReflectableType(int);", + collectConstructors(ReflectableType.class.getDeclaredConstructors())); + } + + @Test + public void publicConstructorsEnumerated() { + callConstructors(); + + assertEquals("public ConstructorTest$ReflectableType(int);", + collectConstructors(ReflectableType.class.getConstructors())); + } + + private void callConstructors() { + new ReflectableType(); + new ReflectableType(1); + new ReflectableType("2"); + new ReflectableType(1, "2"); + } + + private String collectConstructors(Constructor[] constructors) { + List lines = new ArrayList<>(); + for (Constructor constructor : constructors) { + lines.add(constructor + ";"); + } + StringBuilder sb = new StringBuilder(); + Collections.sort(lines); + for (String line : lines) { + sb.append(line); + } + return sb.toString().replace("org.teavm.classlib.java.lang.reflect.", ""); + } + + @Test + public void newInstance() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, + InstantiationException { + Constructor constructor = ReflectableType.class.getDeclaredConstructor(); + ReflectableType instance = constructor.newInstance(); + assertEquals(0, instance.a); + assertNull(instance.b); + } + + @Test + public void newInstance2() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, + InstantiationException { + Constructor constructor = ReflectableType.class + .getDeclaredConstructor(int.class, Object.class); + ReflectableType instance = constructor.newInstance(23, "42"); + assertEquals(23, instance.a); + assertEquals("42", instance.b); + } + + static class ReflectableType { + public int a; + public Object b; + + @Reflectable protected ReflectableType() { + } + + @Reflectable public ReflectableType(int a) { + this.a = a; + } + + @Reflectable ReflectableType(String b) { + this.b = b; + } + + @Reflectable ReflectableType(int a, Object b) { + this.a = a; + this.b = b; + } + } +} diff --git a/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java b/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java index e58c1f592..906cd1a11 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java +++ b/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java @@ -302,6 +302,8 @@ public class TeaVMTestRunner extends Runner implements Filterable { compileResult = compileTest(child, configuration); } catch (Exception e) { notifier.fireTestFailure(new Failure(description, e)); + notifier.fireTestFinished(description); + latch.countDown(); return null; }