JS: add reflection support for constructors

This commit is contained in:
Alexey Andreev 2016-10-20 18:12:54 +03:00
parent c4c5635f88
commit 4171d468d4
11 changed files with 372 additions and 12 deletions

View File

@ -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);
}

View File

@ -23,6 +23,15 @@ import org.teavm.model.MethodReference;
public class ConverterInjector implements Injector {
@Override
public void generate(InjectorContext context, MethodReference methodRef) throws IOException {
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;
}
}
}

View File

@ -25,4 +25,10 @@ public interface JSClass extends PlatformClassMetadata {
@JSProperty
void setFields(JSArray<JSField> fields);
@JSProperty
JSArray<JSMethodMember> getMethods();
@JSProperty
void setMethods(JSArray<JSMethodMember> methods);
}

View File

@ -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<PlatformClass> getParameterTypes();

View File

@ -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<MethodDescriptor> 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();
}
}

View File

@ -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<T> extends TObject implements TAnnotatedElement {
private Map<TClass<?>, TAnnotation> annotationsByType;
private TField[] declaredFields;
private TField[] fields;
private TConstructor<T>[] declaredConstructors;
private static boolean reflectionInitialized;
private TClass(PlatformClass platformClass) {
@ -235,6 +240,77 @@ public class TClass<T> extends TObject implements TAnnotatedElement {
return null;
}
@InjectedBy(ClassGenerator.class)
@PluggableDependency(ClassGenerator.class)
public native <T> T newEmptyInstance();
@SuppressWarnings({ "raw", "unchecked" })
public TConstructor<?>[] getDeclaredConstructors() throws TSecurityException {
if (declaredConstructors == null) {
initReflection();
JSClass jsClass = (JSClass) getPlatformClass().getMetadata();
JSArray<JSMethodMember> 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("<init>")) {
continue;
}
PlatformSequence<PlatformClass> 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<T>(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<T> getDeclaredConstructor(TClass<?>... parameterTypes)
throws TSecurityException, TNoSuchMethodException {
for (TConstructor<?> constructor : getDeclaredConstructors()) {
if (Arrays.equals(constructor.getParameterTypes(), parameterTypes)) {
return (TConstructor<T>) constructor;
}
}
throw new TNoSuchMethodException();
}
@SuppressWarnings({ "raw", "unchecked" })
public TConstructor<T> getConstructor(TClass<?>... parameterTypes)
throws TSecurityException, TNoSuchMethodException {
for (TConstructor<?> constructor : getDeclaredConstructors()) {
if (TModifier.isPublic(constructor.getModifiers())
&& Arrays.equals(constructor.getParameterTypes(), parameterTypes)) {
return (TConstructor<T>) constructor;
}
}
throw new TNoSuchMethodException();
}
private static void getFieldsOfInterfaces(TClass<?> iface, List<TField> fields, Set<TClass<?>> visited) {
if (!visited.add(iface)) {
return;

View File

@ -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);
}
}

View File

@ -103,7 +103,8 @@ public class TConstructor<T> 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<T> extends TAccessibleObject implements TMember {
}
PlatformSequence<PlatformObject> jsArgs = Converter.arrayFromJava(initargs);
PlatformObject instance = callable.call(null, jsArgs);
PlatformObject instance = declaringClass.newEmptyInstance();
callable.call(instance, jsArgs);
return (T) Converter.toJava(instance);
}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="web" name="Web">
<configuration>
<descriptors>
<deploymentDescriptor name="web.xml" url="file://$MODULE_DIR$/src/main/webapp/WEB-INF/web.xml" />
</descriptors>
<webroots>
<root url="file://$MODULE_DIR$/target/generated/js" relative="/" />
<root url="file://$MODULE_DIR$/src/main/webapp" relative="/" />
</webroots>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/kotlin" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="PROVIDED" name="Maven: org.jetbrains.kotlin:kotlin-stdlib:1.0.3" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: org.jetbrains.kotlin:kotlin-runtime:1.0.3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.11" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="module" module-name="teavm-classlib" scope="PROVIDED" />
<orderEntry type="module" module-name="teavm-platform" scope="PROVIDED" />
<orderEntry type="module" module-name="teavm-core" scope="PROVIDED" />
<orderEntry type="module" module-name="teavm-interop" scope="PROVIDED" />
<orderEntry type="library" scope="PROVIDED" name="Maven: commons-io:commons-io:2.4" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: com.carrotsearch:hppc:0.6.1" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: org.objenesis:objenesis:2.4" level="project" />
<orderEntry type="module" module-name="teavm-jso" scope="PROVIDED" />
<orderEntry type="module" module-name="teavm-jso-impl" scope="PROVIDED" />
<orderEntry type="library" scope="PROVIDED" name="Maven: org.mozilla:rhino:1.7.7" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: org.ow2.asm:asm-debug-all:5.0.4" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: com.google.code.gson:gson:2.2.4" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: com.jcraft:jzlib:1.1.3" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: joda-time:joda-time:2.7" level="project" />
<orderEntry type="module" module-name="teavm-jso-apis" scope="PROVIDED" />
</component>
</module>

View File

@ -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<String> 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<ReflectableType> 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<ReflectableType> 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;
}
}
}

View File

@ -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;
}