Compare commits

...

10 commits

Author SHA1 Message Date
Shautvast
fbfd8d5e93 added the getters 2023-09-03 11:29:04 +02:00
Shautvast
021304a5d8 reshuffling methods, adding docs 2023-09-02 17:27:42 +02:00
Shautvast
207d91cd6c completed the ArrayFactory Setters 2023-09-02 16:56:11 +02:00
Shautvast
31f2b7c0f5 completed the ArrayFactory 2023-09-02 13:34:02 +02:00
Shautvast
7ccf137898 v1.2.1 bugfix for methods in different classes with the same name 2023-08-22 19:23:00 +02:00
Shautvast
86aff15ee5 v1.2.0 2023-08-22 17:54:40 +02:00
Shautvast
7de5019a4f fixed bug with primitive arguments 2023-08-22 17:54:13 +02:00
Shautvast
1a09f3e687 wip Array Factory 2023-08-22 15:29:34 +02:00
Shautvast
58c2afcce0 wip Array Factory 2023-08-22 15:28:23 +02:00
Shautvast
bba00f1cbd test for exceptions 2023-07-31 15:39:46 +02:00
26 changed files with 997 additions and 135 deletions

13
pom.xml
View file

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.github.shautvast</groupId> <groupId>com.github.shautvast</groupId>
<artifactId>reflective</artifactId> <artifactId>reflective</artifactId>
<version>1.0.0</version> <version>1.2.1</version>
<name>reflective</name> <name>reflective</name>
<description>Reflective utils that don't use java.lang.reflect</description> <description>Reflective utils that don't use java.lang.reflect</description>
@ -24,6 +24,17 @@
<artifactId>asm-tree</artifactId> <artifactId>asm-tree</artifactId>
<version>9.4</version> <version>9.4</version>
</dependency> </dependency>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<distributionManagement> <distributionManagement>

View file

@ -7,13 +7,15 @@ import com.github.shautvast.rusty.Result;
import org.objectweb.asm.ClassWriter; import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.*; import org.objectweb.asm.tree.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import static com.github.shautvast.reflective.java.Java.*;
import static com.github.shautvast.reflective.java.Java.DOUBLE; import static com.github.shautvast.reflective.java.Java.DOUBLE;
import static com.github.shautvast.reflective.java.Java.FLOAT; import static com.github.shautvast.reflective.java.Java.FLOAT;
import static com.github.shautvast.reflective.java.Java.INTEGER; import static com.github.shautvast.reflective.java.Java.INTEGER;
import static com.github.shautvast.reflective.java.Java.LONG; import static com.github.shautvast.reflective.java.Java.LONG;
import static com.github.shautvast.reflective.java.Java.*;
import static com.github.shautvast.rusty.Result.err; import static com.github.shautvast.rusty.Result.err;
import static com.github.shautvast.rusty.Result.ok; import static com.github.shautvast.rusty.Result.ok;
import static org.objectweb.asm.Opcodes.*; import static org.objectweb.asm.Opcodes.*;
@ -32,7 +34,7 @@ public class InvokerFactory {
*/ */
public static Result<AbstractInvoker> of(MetaMethod method) { public static Result<AbstractInvoker> of(MetaMethod method) {
// new ASM ClassNode with default constructor // new ASM ClassNode with default constructor
String className = "Invoker" + method.getName() + method.getDescriptor().replaceAll("[()/;\\[]", ""); String className = "Invoker" + method.getMetaClass().getJavaClass().getSimpleName() + method.getName() + method.getDescriptor().replaceAll("[()/;\\[]", "");
if (ByteClassLoader.INSTANCE.isDefined(className)) { if (ByteClassLoader.INSTANCE.isDefined(className)) {
return getInvoker(className); return getInvoker(className);
} }
@ -58,12 +60,14 @@ public class InvokerFactory {
} }
// put argument on the stack // put argument on the stack
insns.add(new InsnNode(AALOAD)); insns.add(new InsnNode(AALOAD));
insns.add(new TypeInsnNode(CHECKCAST, method.getParameters().get(i).getDescriptor())); String type = internalName(mapPrimitiveToWrapper(method.getParameters().get(i).getType()).getName());
insns.add(new TypeInsnNode(CHECKCAST, type));
unwrapPrimitive(method.getParameters().get(i), insns);
} }
// call the method // call the method
insns.add(new MethodInsnNode(INVOKEVIRTUAL, method.getMetaClass().getRawName(), method.getName(), method.getDescriptor())); insns.add(new MethodInsnNode(INVOKEVIRTUAL, method.getMetaClass().getRawName(), method.getName(), method.getDescriptor()));
// if the returned argument is primitive, wrap it as an Object // if the returned argument is primitive, wrap it as an Object
wrapAnyPrimitivesForReturnObject(method, invokerMethod); wrapPrimitive(method.getReturnParameter(), invokerMethod.instructions);
// return the object on the stack // return the object on the stack
insns.add(new InsnNode(ARETURN)); insns.add(new InsnNode(ARETURN));
@ -75,6 +79,12 @@ public class InvokerFactory {
classNode.accept(classWriter); classNode.accept(classWriter);
byte[] byteArray = classWriter.toByteArray(); byte[] byteArray = classWriter.toByteArray();
try (FileOutputStream out = new FileOutputStream(method.getMetaClass().getName() + "_" + method.getName() + ".class")) {
out.write(byteArray);
} catch (IOException e) {
e.printStackTrace();
}
// load it into the JVM // load it into the JVM
ByteClassLoader.INSTANCE.addClass(className, byteArray); ByteClassLoader.INSTANCE.addClass(className, byteArray);
return getInvoker(classNode.name); return getInvoker(classNode.name);
@ -89,12 +99,39 @@ public class InvokerFactory {
} }
} }
private static void unwrapPrimitive(Parameter<?> parameter, InsnList insns) {
switch (parameter.getDescriptor()) {
case "B":
insns.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B"));
break;
case "S":
insns.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S"));
break;
case "I":
insns.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I"));
break;
case "Z":
insns.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z"));
break;
case "J":
insns.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J"));
break;
case "F":
insns.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F"));
break;
case "D":
insns.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D"));
break;
case "C":
insns.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C"));
}
}
/* /*
* wrap primitives in their wrapper * wrap primitives in their wrapper
*/ */
private static void wrapAnyPrimitivesForReturnObject(MetaMethod m, MethodNode invokerMethod) { private static void wrapPrimitive(Parameter<?> parameter, InsnList insns) {
InsnList insns = invokerMethod.instructions; switch (parameter.getDescriptor()) {
switch (m.getReturnParameter().getDescriptor()) {
case "V": case "V":
insns.add(new InsnNode(ACONST_NULL)); insns.add(new InsnNode(ACONST_NULL));
break; break;
@ -120,10 +157,32 @@ public class InvokerFactory {
insns.add(new MethodInsnNode(INVOKESTATIC, SHORT, VALUEOF, "(S)Ljava/lang/Short;")); insns.add(new MethodInsnNode(INVOKESTATIC, SHORT, VALUEOF, "(S)Ljava/lang/Short;"));
break; break;
case "B": case "B":
insns.add(new MethodInsnNode(INVOKESTATIC, "java/lang/Byte", VALUEOF, "(B)Ljava/lang/Byte;")); insns.add(new MethodInsnNode(INVOKESTATIC, BYTE, VALUEOF, "(B)Ljava/lang/Byte;"));
break; break;
} }
} }
private static Class<?> mapPrimitiveToWrapper(Class<?> type) {
if (type == int.class) {
return Integer.class;
} else if (type == byte.class) {
return Byte.class;
} else if (type == short.class) {
return Short.class;
} else if (type == long.class) {
return Long.class;
} else if (type == float.class) {
return Float.class;
} else if (type == double.class) {
return Double.class;
} else if (type == char.class) {
return Character.class;
} else if (type == boolean.class) {
return Boolean.class;
} else {
return type;
}
}
} }

View file

@ -1,28 +0,0 @@
package com.github.shautvast.reflective;
import com.github.shautvast.reflective.MetaClass;
import com.github.shautvast.reflective.Reflective;
public class Main {
public static void main(String[] args) {
Dummy dummy = new Dummy();
dummy.setName("foo");
MetaClass metaDummy = Reflective.getMetaClass(dummy.getClass());
metaDummy.getMethod("setName")
.ifPresent(m -> m.invoke(dummy, "bar"));
System.out.println(dummy.getName()); // prints "bar"
}
public static class Dummy {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View file

@ -34,6 +34,7 @@ public class MetaClass {
return Set.copyOf(methods.values()); return Set.copyOf(methods.values());
} }
// should account for overloading!
public Optional<MetaMethod> getMethod(String name) { public Optional<MetaMethod> getMethod(String name) {
return Optional.ofNullable(methods.get(name)); return Optional.ofNullable(methods.get(name));
} }
@ -43,7 +44,7 @@ public class MetaClass {
} }
void addField(int access, String name, String descriptor) { void addField(int access, String name, String descriptor) {
fields.put(name, new MetaField(name, access)); //ASM access same as reflect modifiers? fields.put(name, new MetaField(name, access, Java.toClass(descriptor))); //ASM access same as reflect modifiers?
} }
public void addMethod(int access, String name, String descriptor) { public void addMethod(int access, String name, String descriptor) {
@ -61,4 +62,6 @@ public class MetaClass {
public String getRawName() { public String getRawName() {
return rawName; return rawName;
} }
} }

View file

@ -5,9 +5,12 @@ public class MetaField {
private final String name; private final String name;
private final int modifiers; private final int modifiers;
public MetaField(String name, int modifiers) { private final Class<?> type;
public MetaField(String name, int modifiers, Class<?> type) {
this.name = name; this.name = name;
this.modifiers = modifiers; this.modifiers = modifiers;
this.type = type;
} }
public String getName() { public String getName() {

View file

@ -78,13 +78,13 @@ public class MetaMethod {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
for (int i = 0; i < parms.length(); i++) { for (int i = 0; i < parms.length(); i++) {
String t = Character.toString(parms.charAt(i)); String t = Character.toString(parms.charAt(i));
if (!"L".equals(t) && buf.length() == 0) { if (!"[".equals(t) && !"L".equals(t) && buf.length() == 0) {
mutableParams.add(new Parameter<>(Java.getClassFromDescriptor(t), t)); mutableParams.add(new Parameter<>(Java.toClass(t), t));
} }
if (";".equals(t)) { if (";".equals(t)) {
buf.append(t); buf.append(t);
String desc = buf.toString(); String desc = buf.toString();
mutableParams.add(new Parameter<>(Java.getClassFromDescriptor(desc), correct(desc))); mutableParams.add(new Parameter<>(Java.toClass(desc), correct(desc)));
buf = new StringBuilder(); buf = new StringBuilder();
} else { } else {
buf.append(t); buf.append(t);
@ -93,7 +93,7 @@ public class MetaMethod {
String returnDesc = split[2]; String returnDesc = split[2];
this.returnParameter = new Parameter<>( this.returnParameter = new Parameter<>(
Java.getClassFromDescriptor(returnDesc), Java.toClass(returnDesc),
correct(returnDesc)); correct(returnDesc));
return Collections.unmodifiableList(mutableParams); return Collections.unmodifiableList(mutableParams);

View file

@ -5,6 +5,9 @@ public class Parameter<T> {
private final String descriptor; private final String descriptor;
public Parameter(Class<T> type, String descriptor) { public Parameter(Class<T> type, String descriptor) {
if (type ==null){
throw new IllegalArgumentException("Type cannot be null");
}
this.type = type; this.type = type;
this.descriptor = descriptor; this.descriptor = descriptor;
} }
@ -17,10 +20,6 @@ public class Parameter<T> {
return type == Void.class; return type == Void.class;
} }
public boolean isPrimitive(){
return !descriptor.startsWith("L");
}
public String getDescriptor() { public String getDescriptor() {
return descriptor; return descriptor;
} }

View file

@ -0,0 +1,240 @@
package com.github.shautvast.reflective.array;
import com.github.shautvast.reflective.array.base.*;
import java.lang.reflect.Array;
/**
* Public interface for dynamically working with arrays (create, set and get operations)
* <p>
* Drop in replacement for java.lang.reflect.Array
*/
public final class ArrayFactory {
private ArrayFactory() {
}
/**
* Creates a new array of the specified type and dimensions
*
* @param elementType the array element type
* @param dimensions array of ints, ie {10} means 1 dimension, length 10. {10,20} means 2 dimensions, size 10 by 20
* @return an object that you can cast to the expected array type
*/
public static Object newInstance(Class<?> elementType, int... dimensions) {
return ArrayHandlerFactory.getCreatorInstance(elementType, dimensions).newInstance();
}
/**
* Sets an Object value on an array
*
* @param array the array on which the value is set
* @param index the array index
* @param value the value to set
*/
public static void set(Object array, int index, Object value) {
ArrayHandlerFactory.getAccessorInstance(ObjectArrayAccessor.class, typeChecked(array)).set(array, index, value);
}
/**
* Gets a Object value from an array
*
* @param array the array to access
* @param index
* @return the value of the array at the index
*/
public static Object get(Object array, int index) {
return ArrayHandlerFactory.getAccessorInstance(ObjectArrayAccessor.class, typeChecked(array)).get(array, index);
}
/**
* Sets an int value on an array
*
* @param array the array on which the value is set
* @param index the array index
* @param value the value to set
*/
public static void setInt(Object array, int index, int value) {
ArrayHandlerFactory.getAccessorInstance(IntArrayAccessor.class, typeChecked(array)).set(array, index, value);
}
/**
* Gets an int value from an array
*
* @param array the array to access
* @param index
* @return the value of the array at the index
*/
public static int getInt(Object array, int index) {
return ArrayHandlerFactory.getAccessorInstance(IntArrayAccessor.class, typeChecked(array)).get(array, index);
}
/**
* Sets a byte value on an array
*
* @param array the array on which the value is set
* @param index the array index
* @param value the value to set
*/
public static void setByte(Object array, int index, byte value) {
ArrayHandlerFactory.getAccessorInstance(ByteArrayAccessor.class, typeChecked(array)).set(array, index, value);
}
/**
* Gets a byte value from an array
*
* @param array the array to access
* @param index
* @return the value of the array at the index
*/
public static byte getByte(Object array, int index) {
return ArrayHandlerFactory.getAccessorInstance(ByteArrayAccessor.class, typeChecked(array)).get(array, index);
}
/**
* Sets a short value on an array
*
* @param array the array on which the value is set
* @param index the array index
* @param value the value to set
*/
public static void setShort(Object array, int index, short value) {
ArrayHandlerFactory.getAccessorInstance(ShortArrayAccessor.class, typeChecked(array)).set(array, index, value);
}
/**
* Gets a short value from an array
*
* @param array the array to access
* @param index
* @return the value of the array at the index
*/
public static short getShort(Object array, int index) {
return ArrayHandlerFactory.getAccessorInstance(ShortArrayAccessor.class, typeChecked(array)).get(array, index);
}
/**
* Sets a long value on an array
*
* @param array the array on which the value is set
* @param index the array index
* @param value the value to set
*/
public static void setLong(Object array, int index, long value) {
ArrayHandlerFactory.getAccessorInstance(LongArrayAccessor.class, typeChecked(array)).set(array, index, value);
}
/**
* Gets a long value from an array
*
* @param array the array to access
* @param index
* @return the value of the array at the index
*/
public static long getLong(Object array, int index) {
return ArrayHandlerFactory.getAccessorInstance(LongArrayAccessor.class, typeChecked(array)).get(array, index);
}
/**
* Sets a byte value on an array
*
* @param array the array on which the value is set
* @param index the array index
* @param value the value to set
*/
public static void setFloat(Object array, int index, float value) {
ArrayHandlerFactory.getAccessorInstance(FloatArrayAccessor.class, typeChecked(array)).set(array, index, value);
}
/**
* Gets a float value from an array
*
* @param array the array to access
* @param index
* @return the value of the array at the index
*/
public static float getFloat(Object array, int index) {
return ArrayHandlerFactory.getAccessorInstance(FloatArrayAccessor.class, typeChecked(array)).get(array, index);
}
/**
* Sets a double value on an array
*
* @param array the array on which the value is set
* @param index the array index
* @param value the value to set
*/
public static void setDouble(Object array, int index, double value) {
ArrayHandlerFactory.getAccessorInstance(DoubleArrayAccessor.class, typeChecked(array)).set(array, index, value);
}
/**
* Gets a double value from an array
*
* @param array the array to access
* @param index
* @return the value of the array at the index
*/
public static double getDouble(Object array, int index) {
return ArrayHandlerFactory.getAccessorInstance(DoubleArrayAccessor.class, typeChecked(array)).get(array, index);
}
/**
* Sets a char value on an array
*
* @param array the array on which the value is set
* @param index the array index
* @param value the value to set
*/
public static void setChar(Object array, int index, char value) {
ArrayHandlerFactory.getAccessorInstance(CharArrayAccessor.class, typeChecked(array)).set(array, index, value);
}
/**
* Gets a char value from an array
*
* @param array the array to access
* @param index
* @return the value of the array at the index
*/
public static char getChar(Object array, int index) {
return ArrayHandlerFactory.getAccessorInstance(CharArrayAccessor.class, typeChecked(array)).get(array, index);
}
/**
* Sets a boolean value on an array
*
* @param array the array on which the value is set
* @param index the array index
* @param value the value to set
*/
public static void setBoolean(Object array, int index, boolean value) {
ArrayHandlerFactory.getAccessorInstance(BooleanArrayAccessor.class, typeChecked(array)).set(array, index, value);
}
/**
* Gets a boolean value from an array
*
* @param array the array to access
* @param index
* @return the value of the array at the index
*/
public static boolean getBoolean(Object array, int index) {
return ArrayHandlerFactory.getAccessorInstance(BooleanArrayAccessor.class, typeChecked(array)).get(array, index);
}
/*
* Only checks if object is an array, not the type of that array. TODO
*/
private static Class<?> typeChecked(Object array) {
if (array == null) {
throw new NullPointerException("Argument is null");
}
Class<?> arrayType = array.getClass();
if (!arrayType.isArray()) {
throw new IllegalArgumentException("Argument is not an array");
}
return arrayType;
}
}

View file

@ -0,0 +1,207 @@
package com.github.shautvast.reflective.array;
import com.github.shautvast.reflective.array.base.ArrayCreator;
import com.github.shautvast.reflective.array.base.ArrayAccessor;
import com.github.shautvast.reflective.array.base.ObjectArrayAccessor;
import com.github.shautvast.reflective.java.ASM;
import com.github.shautvast.reflective.java.ByteClassLoader;
import com.github.shautvast.reflective.java.Java;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static org.objectweb.asm.Opcodes.*;
class ArrayHandlerFactory {
/* cache for the compiled creator classes */
private static final Map<String, ArrayCreator> creatorCache = new ConcurrentHashMap<>();
/* Cache for the compiled accessor classes.
* The outer Map contains the ArrayAccessor type (some primitive or Object Accessor)
* That maps to the concrete calculated ArrayAccessor instance name which maps to the instance itself.
* TODO see if this can be optimized
*/
private static final Map<Class<? extends ArrayAccessor>, Map<String, Object>> accessorCache = new ConcurrentHashMap<>();
/*
* generic method for creating array accessors (primitives and objects)
*/
@SuppressWarnings("unchecked")
static <T extends ArrayAccessor> T getAccessorInstance(Class<T> accessorBaseType, Class<?> arrayType) {
String arrayTypeName = Java.internalName(arrayType);
String syntheticClassName = "com/shautvast/reflective/array/ArrayAccessor_"
+ Java.javaName(arrayTypeName) + Java.getNumDimensions(arrayType);
return (T) accessorCache.computeIfAbsent(accessorBaseType, k -> new ConcurrentHashMap<>()).
computeIfAbsent(syntheticClassName,
k -> ArrayHandlerFactory.createSyntheticArrayAccessor(accessorBaseType, arrayTypeName, syntheticClassName));
}
/* creates an instance of an ArrayCreator of the specified type */
static ArrayCreator getCreatorInstance(Class<?> elementType, int... dimensions) {
String elementTypeName = Java.internalName(elementType);
String syntheticClassName = "com/shautvast/reflective/array/ArrayCreator_"
+ Java.javaName(elementTypeName) + dimensions.length;
return creatorCache.computeIfAbsent(syntheticClassName,
k -> ArrayHandlerFactory.createSyntheticArrayCreator(elementTypeName, syntheticClassName, dimensions));
}
/* Creates the ASM ClassNode for an ArrayCreator */
static ArrayCreator createSyntheticArrayCreator(String elementType, String name, int... dimensions) {
ClassNode classNode = ASM.createDefaultClassNode(name, Java.internalName(ArrayCreator.class));
classNode.methods.add(createNewInstanceMethodNode(elementType, dimensions));
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classNode.accept(classWriter);
byte[] byteArray = classWriter.toByteArray();
// try (FileOutputStream out = new FileOutputStream(name + ".class")) {
// out.write(byteArray);
// } catch (IOException e) {
// e.printStackTrace();
// }
ByteClassLoader.INSTANCE.addClass(classNode.name, byteArray);
try {
return (ArrayCreator) (ByteClassLoader.INSTANCE.loadClass(classNode.name).getConstructor().newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* Creates the ASM ClassNode for an ArrayAccessor */
static <T> T createSyntheticArrayAccessor(Class<T> accessorType, String arrayType, String name) {
ClassNode classNode = ASM.createDefaultClassNode(name, Java.internalName(accessorType));
classNode.methods.add(createSetMethodNode(accessorType, arrayType));
classNode.methods.add(createGetMethodNode(accessorType, arrayType));
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classNode.accept(classWriter);
byte[] byteArray = classWriter.toByteArray();
try (FileOutputStream out = new FileOutputStream("A.class")) {
out.write(byteArray);
} catch (IOException e) {
e.printStackTrace();
}
ByteClassLoader.INSTANCE.addClass(classNode.name, byteArray);
try {
return accessorType.cast(ByteClassLoader.INSTANCE.loadClass(classNode.name).getConstructor().newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/* Creates the newInstance method for ArrayCreator classes */
private static MethodNode createNewInstanceMethodNode(String componentType, int[] dimensions) {
MethodNode methodNode = new MethodNode(ACC_PUBLIC,
"newInstance", "()Ljava/lang/Object;", null, null);
InsnList insns = methodNode.instructions;
Arrays.stream(dimensions).forEach(d -> insns.add(new LdcInsnNode(d)));
insns.add(new MultiANewArrayInsnNode(Java.createArrayType(componentType, dimensions), dimensions.length));
insns.add(new InsnNode(ARETURN));
return methodNode;
}
/* Creates the set method for ArrayAccessor classes */
private static MethodNode createSetMethodNode(Class<?> accessorType, String arrayType) {
String elementType;
if (accessorType == ObjectArrayAccessor.class) {
elementType = "Ljava/lang/Object;";
} else {
elementType = arrayType.substring(1);
}
MethodNode methodNode = new MethodNode(ACC_PUBLIC,
"set", "(Ljava/lang/Object;I" + elementType + ")V", null, null);
int[] opcodes = arrayStoreInstructions(elementType);
InsnList insns = methodNode.instructions;
insns.add(new VarInsnNode(ALOAD, 1));
insns.add(new TypeInsnNode(CHECKCAST, arrayType));
insns.add(new VarInsnNode(ILOAD, 2));
insns.add(new VarInsnNode(opcodes[0], 3));
insns.add(new InsnNode(opcodes[1]));
insns.add(new InsnNode(RETURN));
return methodNode;
}
private static MethodNode createGetMethodNode(Class<?> accessorType, String arrayType) {
String elementType;
if (accessorType == ObjectArrayAccessor.class) {
elementType = "Ljava/lang/Object;";
} else {
elementType = arrayType.substring(1);
}
MethodNode methodNode = new MethodNode(ACC_PUBLIC,
"get", "(Ljava/lang/Object;I)" + elementType, null, null);
InsnList insns = methodNode.instructions;
int[] opcodes = arrayLoadAndReturnInstructions(elementType);
insns.add(new VarInsnNode(ALOAD, 1));
insns.add(new TypeInsnNode(CHECKCAST, arrayType));
insns.add(new VarInsnNode(ILOAD, 2));
insns.add(new InsnNode(opcodes[0]));
insns.add(new InsnNode(opcodes[1]));
return methodNode;
}
/*
* gets the pair of appropriate load (type) and store (array of type) instructions
*/
private static int[] arrayStoreInstructions(String type) {
switch (type) {
case "B":
case "Z":
return new int[]{ILOAD, BASTORE};
case "S":
return new int[]{ILOAD, SASTORE};
case "I":
return new int[]{ILOAD, IASTORE};
case "J":
return new int[]{LLOAD, LASTORE};
case "F":
return new int[]{FLOAD, FASTORE};
case "D":
return new int[]{DLOAD, DASTORE};
case "C":
return new int[]{ILOAD, CASTORE};
default:
return new int[]{ALOAD, AASTORE};
}
}
/*
* gets the pair of appropriate load (type) and store (array of type) instructions
*/
private static int[] arrayLoadAndReturnInstructions(String type) {
switch (type) {
case "B":
case "Z":
return new int[]{BALOAD, IRETURN};
case "I":
return new int[]{IALOAD, IRETURN};
case "S":
return new int[]{SALOAD, IRETURN};
case "C":
return new int[]{CALOAD, IRETURN};
case "J":
return new int[]{LALOAD, LRETURN};
case "F":
return new int[]{FALOAD, FRETURN};
case "D":
return new int[]{DALOAD, DRETURN};
default:
return new int[]{AALOAD, ARETURN};
}
}
}

View file

@ -0,0 +1,7 @@
package com.github.shautvast.reflective.array.base;
/**
* Empty interface because you can't be generic over primitives in Java
*/
public interface ArrayAccessor {
}

View file

@ -0,0 +1,6 @@
package com.github.shautvast.reflective.array.base;
public abstract class ArrayCreator {
public abstract Object newInstance();
}

View file

@ -0,0 +1,6 @@
package com.github.shautvast.reflective.array.base;
public abstract class BooleanArrayAccessor implements ArrayAccessor {
public abstract void set(Object array, int index, boolean value);
public abstract boolean get(Object array, int index);
}

View file

@ -0,0 +1,8 @@
package com.github.shautvast.reflective.array.base;
public abstract class ByteArrayAccessor implements ArrayAccessor {
public abstract void set(Object array, int index, byte value);
public abstract byte get(Object array, int index);
}

View file

@ -0,0 +1,7 @@
package com.github.shautvast.reflective.array.base;
public abstract class CharArrayAccessor implements ArrayAccessor {
public abstract void set(Object array, int index, char value);
public abstract char get(Object array, int index);
}

View file

@ -0,0 +1,7 @@
package com.github.shautvast.reflective.array.base;
public abstract class DoubleArrayAccessor implements ArrayAccessor {
public abstract void set(Object array, int index, double value);
public abstract double get(Object array, int index);
}

View file

@ -0,0 +1,8 @@
package com.github.shautvast.reflective.array.base;
public abstract class FloatArrayAccessor implements ArrayAccessor {
public abstract void set(Object array, int index, float value);
public abstract float get(Object array, int index);
}

View file

@ -0,0 +1,7 @@
package com.github.shautvast.reflective.array.base;
public abstract class IntArrayAccessor implements ArrayAccessor {
public abstract void set(Object array, int index, int value);
public abstract int get(Object array, int index);
}

View file

@ -0,0 +1,8 @@
package com.github.shautvast.reflective.array.base;
public abstract class LongArrayAccessor implements ArrayAccessor {
public abstract void set(Object array, int index, long value);
public abstract long get(Object array, int index);
}

View file

@ -0,0 +1,6 @@
package com.github.shautvast.reflective.array.base;
public abstract class ObjectArrayAccessor implements ArrayAccessor {
public abstract void set(Object array, int index, Object value);
public abstract Object get(Object array, int index);
}

View file

@ -0,0 +1,8 @@
package com.github.shautvast.reflective.array.base;
public abstract class ShortArrayAccessor implements ArrayAccessor {
public abstract void set(Object array, int index, short value);
public abstract short get(Object array, int index);
}

View file

@ -23,7 +23,7 @@ public class ByteClassLoader extends ClassLoader {
} }
public void addClass(String name, byte[] bytecode) { public void addClass(String name, byte[] bytecode) {
Class<?> classDef = defineClass(name, bytecode, 0, bytecode.length); Class<?> classDef = defineClass(null, bytecode, 0, bytecode.length);
classes.put(name, classDef); classes.put(name, classDef);
} }

View file

@ -4,8 +4,6 @@ import org.objectweb.asm.ClassReader;
import java.io.IOException; import java.io.IOException;
import static java.lang.reflect.Array.newInstance;
/* /*
* common utils not for external use * common utils not for external use
*/ */
@ -22,6 +20,7 @@ public class Java {
public static final String DOUBLE = "java/lang/Double"; public static final String DOUBLE = "java/lang/Double";
public static final String CHAR = "java/lang/Character"; public static final String CHAR = "java/lang/Character";
public static final String SHORT = "java/lang/Short"; public static final String SHORT = "java/lang/Short";
public static final String BYTE = "java/lang/Byte";
public static String internalName(String className) { public static String internalName(String className) {
return className.replaceAll("\\.", "/"); return className.replaceAll("\\.", "/");
@ -106,49 +105,116 @@ public class Java {
throw new RuntimeException(); throw new RuntimeException();
} }
public static Class<?> getClassFromDescriptor(String descriptor) { public static Class<?> toClass(String descriptor) {
int arrayDims = 0; try {
while (descriptor.startsWith("[")) { if (descriptor.length() == 1) {
arrayDims++; return mapIfPrimitive(descriptor.charAt(0));
descriptor = descriptor.substring(1);
} //could be cheaper
if (descriptor.startsWith("L") && descriptor.endsWith(";")) {
try {
String className = descriptor.substring(1, descriptor.length() - 1).replaceAll("/", ".");
Class<?> clazz = Class.forName(className);
if (arrayDims > 0) {
clazz = newInstance(clazz, new int[arrayDims]).getClass();
}
return clazz;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e); // not supposed to happen
} }
} else { if (!descriptor.startsWith("[") && descriptor.endsWith(";")) {
char typeChar = descriptor.charAt(0); descriptor = descriptor.substring(0, descriptor.length() - 1);
boolean isArray = arrayDims != 0;
switch (typeChar) {
case 'B':
return isArray ? newInstance(byte.class, new int[arrayDims]).getClass() : byte.class;
case 'C':
return isArray ? newInstance(char.class, new int[arrayDims]).getClass() : char.class;
case 'D':
return isArray ? newInstance(double.class, new int[arrayDims]).getClass() : double.class;
case 'F':
return isArray ? newInstance(float.class, new int[arrayDims]).getClass() : float.class;
case 'I':
return isArray ? newInstance(int.class, new int[arrayDims]).getClass() : int.class;
case 'J':
return isArray ? newInstance(long.class, new int[arrayDims]).getClass() : long.class;
case 'S':
return isArray ? newInstance(short.class, new int[arrayDims]).getClass() : short.class;
case 'Z':
return isArray ? newInstance(boolean.class, new int[arrayDims]).getClass() : boolean.class;
case 'V':
return Void.class;
default:
throw new RuntimeException(new ClassNotFoundException("unknown descriptor: " + descriptor)); //must not happen
} }
int i = 0;
while (descriptor.charAt(i) == '[' && i < 256) {
i++;
}
assert i < 256;
String dims = descriptor.substring(0, i);
String type = descriptor.substring(i);
if (i == 0 && type.startsWith("L")) {
type = type.substring(1);
}
type = Java.externalName(type);
Class<?> aClass = Class.forName(dims + type);
return aClass;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} }
} }
private static Class<?> mapIfPrimitive(char descriptor) {
switch (descriptor) {
case 'B':
return byte.class;
case 'S':
return short.class;
case 'I':
return int.class;
case 'J':
return long.class;
case 'F':
return float.class;
case 'D':
return double.class;
case 'C':
return char.class;
case 'Z':
return boolean.class;
case 'V':
return Void.class;
default:
return null;
}
}
/* strips all disallowed characters from a classname */
public static String javaName(String arrayTypeName) {
return arrayTypeName
.replaceAll("[/.\\[;]", "")
.toLowerCase();
}
public static String mapPrimitiveOrArrayName(String type) {
switch (type) {
case "byte":
return "B";
case "short":
return "S";
case "int":
return "I";
case "long":
return "J";
case "float":
return "F";
case "double":
return "D";
case "char":
return "C";
case "boolean":
return "Z";
default:
if (type.startsWith("[") || type.contains("/")) {
return type;
} else {
throw new IllegalArgumentException(type + "?");
}
}
}
public static int getNumDimensions(Class<?> arrayType) {
return getNumDimensions(arrayType.getName());
}
public static int getNumDimensions(String arrayName) {
int i = 0;
while (arrayName.charAt(i) == '[') {
i++;
}
return i;
}
public static String createArrayType(String componentType, int[] dimensions) {
StringBuilder s = new StringBuilder();
s.append("[".repeat(dimensions.length));
boolean isObject = !componentType.startsWith("[") && componentType.contains("/");
if (isObject) {
s.append("L");
s.append(componentType);
} else {
s.append(Java.mapPrimitiveOrArrayName(componentType));
}
if (isObject) {
s.append(";");
}
return s.toString();
}
} }

View file

@ -65,4 +65,8 @@ public class Result<T> {
return new Result<>(null, error); return new Result<>(null, error);
} }
} }
public boolean isOk() {
return error == null;
}
} }

View file

@ -1,9 +0,0 @@
package com.github.shautvast.reflective;
public class DummyInvoker extends AbstractInvoker {
public Object invoke(Object instance, Object... arguments) {
((ReflectiveTest.Dummy) instance).setName((String)arguments[0]);
return null;
}
}

View file

@ -1,11 +1,14 @@
package com.github.shautvast.reflective; package com.github.shautvast.reflective;
import com.github.shautvast.rusty.Panic; import com.github.shautvast.rusty.Panic;
import com.github.shautvast.rusty.Result;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -15,17 +18,13 @@ public class ReflectiveTest {
@Test @Test
void testMethods() { void testMethods() {
Dummy dummy = new Dummy("bar"); Dummy dummy = getDummy();
MetaClass metaDummy = Reflective.getMetaClass(dummy.getClass()); MetaClass metaDummy = Reflective.getMetaClass(dummy.getClass());
assertEquals("com.github.shautvast.reflective.ReflectiveTest$Dummy", metaDummy.getName()); assertEquals("com.github.shautvast.reflective.ReflectiveTest$Dummy", metaDummy.getName());
Iterator<MetaField> fields = metaDummy.getFields().iterator();
assertTrue(fields.hasNext());
assertEquals("name", fields.next().getName());
Set<MetaMethod> methods = metaDummy.getMethods(); Set<MetaMethod> methods = metaDummy.getMethods();
assertFalse(methods.isEmpty()); assertFalse(methods.isEmpty());
assertEquals(5, methods.size()); assertEquals(22, methods.size());
MetaMethod equals = metaDummy.getMethod("equals").orElseGet(Assertions::fail); MetaMethod equals = metaDummy.getMethod("equals").orElseGet(Assertions::fail);
assertEquals(boolean.class, equals.getReturnParameter().getType()); assertEquals(boolean.class, equals.getReturnParameter().getType());
@ -36,10 +35,10 @@ public class ReflectiveTest {
assertEquals(int.class, hashCode.getReturnParameter().getType()); assertEquals(int.class, hashCode.getReturnParameter().getType());
assertTrue(Modifier.isPublic(hashCode.getModifiers())); assertTrue(Modifier.isPublic(hashCode.getModifiers()));
MetaMethod getName = metaDummy.getMethod("getName").orElseGet(Assertions::fail); MetaMethod getStringValue = metaDummy.getMethod("getStringValue").orElseGet(Assertions::fail);
assertEquals(List.of(), getName.getParameters()); assertEquals(List.of(), getStringValue.getParameters());
assertEquals(String.class, getName.getReturnParameter().getType()); assertEquals(String.class, getStringValue.getReturnParameter().getType());
assertTrue(Modifier.isPublic(getName.getModifiers())); assertTrue(Modifier.isPublic(getStringValue.getModifiers()));
MetaMethod privateMethod = metaDummy.getMethod("privateMethod").orElseGet(Assertions::fail); MetaMethod privateMethod = metaDummy.getMethod("privateMethod").orElseGet(Assertions::fail);
assertEquals(List.of(), privateMethod.getParameters()); assertEquals(List.of(), privateMethod.getParameters());
@ -50,39 +49,89 @@ public class ReflectiveTest {
@Test @Test
void testInvokeGetter() { void testInvokeGetter() {
Dummy dummy = new Dummy("bar"); Dummy dummy = getDummy();
MetaMethod getName = Reflective.getMetaClass(dummy.getClass()).getMethod("getName").orElseGet(Assertions::fail); MetaMethod getStringValue = Reflective.getMetaClass(dummy.getClass()).getMethod("getStringValue").orElseGet(Assertions::fail);
assertThrows(Panic.class, () -> getName.invoke("foo").unwrap()); // passing "foo" as the instance is not allowed
assertEquals("bar", getName.invoke(dummy).unwrap()); assertThrows(Panic.class, () -> getStringValue.invoke("foo").unwrap());
// we should pass a valid dummy instance
assertEquals("don't panic!", getStringValue.invoke(dummy).unwrap());
} }
@Test @Test
void testInvokeSetter() { void testInvokeSetters() {
Dummy dummy = new Dummy("bar"); Dummy dummy = getDummy();
MetaClass metaForClass = Reflective.getMetaClass(dummy.getClass()); MetaClass metaForClass = Reflective.getMetaClass(dummy.getClass());
MetaMethod setName = metaForClass.getMethod("setName").orElseGet(Assertions::fail);
assertEquals("bar", dummy.getName()); // before invoke MetaMethod setByte = metaForClass.getMethod("setByteValue").orElseGet(Assertions::fail);
setName.invoke(dummy, "foo"); setByte.invoke(dummy, (byte) -42).unwrap();
assertEquals("foo", dummy.getName()); // after invoke assertEquals((byte) -42, dummy.getByteValue());
MetaMethod setShort = metaForClass.getMethod("setShortValue").orElseGet(Assertions::fail);
setShort.invoke(dummy, (short) -43).unwrap();
assertEquals((short) -43, dummy.getShortValue());
MetaMethod setInt = metaForClass.getMethod("setIntValue").orElseGet(Assertions::fail);
setInt.invoke(dummy, -44).unwrap();
assertEquals(-44, dummy.getIntValue());
MetaMethod setLongValue = metaForClass.getMethod("setLongValue").orElseGet(Assertions::fail);
setLongValue.invoke(dummy, -45L).unwrap();
assertEquals(-45L, dummy.getLongValue());
MetaMethod setFloat = metaForClass.getMethod("setFloatValue").orElseGet(Assertions::fail);
setFloat.invoke(dummy, -46.0F).unwrap();
assertEquals(-46.0F, dummy.getFloatValue());
MetaMethod setDouble = metaForClass.getMethod("setDoubleValue").orElseGet(Assertions::fail);
setDouble.invoke(dummy, -47.0).unwrap();
assertEquals(-47.0, dummy.getDoubleValue());
MetaMethod setChar = metaForClass.getMethod("setCharValue").orElseGet(Assertions::fail);
setChar.invoke(dummy, '-').unwrap();
assertEquals('-', dummy.getCharValue());
MetaMethod setBoolean = metaForClass.getMethod("setBooleanValue").orElseGet(Assertions::fail);
setBoolean.invoke(dummy, false).unwrap();
assertFalse(dummy.isBooleanValue());
MetaMethod setString = metaForClass.getMethod("setStringValue").orElseGet(Assertions::fail);
setString.invoke(dummy, "panic!").unwrap();
assertEquals("panic!", dummy.getStringValue());
} }
@Test
void testInvocationExceptionHappened() {
Dummy dummy = getDummy();
MetaClass metaForClass = Reflective.getMetaClass(dummy.getClass());
MetaMethod throwEx = metaForClass.getMethod("throwEx").orElseGet(Assertions::fail);
Result<Void> result = throwEx.invoke(dummy, "foo");
assertFalse(result.isOk());
}
private static Dummy getDummy() {
return new Dummy((byte) 42, (short) 43,
44, 45, 46.0F, 47.0,
'D', true, "don't panic!",
new byte[1][1], new String[]{"Please don't touch that button"});
}
@Getter
@Setter
@AllArgsConstructor
public static class Dummy { public static class Dummy {
private String name; private byte byteValue;
private short shortValue;
public Dummy(String name) { private int intValue;
this.name = name; private long longValue;
} private float floatValue;
private double doubleValue;
public String getName() { private char charValue;
return privateMethod()[0]; private boolean booleanValue;
} private String stringValue;
private byte[][] byteArrayValue;
public void setName(String name) { private String[] stringArrayValue;
this.name = name;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
@ -94,8 +143,14 @@ public class ReflectiveTest {
return 6; return 6;
} }
@SuppressWarnings("unused")
private String[] privateMethod() { private String[] privateMethod() {
return new String[]{name, "bar"}; return new String[1];
}
@SuppressWarnings("unused")
public void throwEx() {
throw new RuntimeException("ex");
} }
} }
} }

View file

@ -0,0 +1,174 @@
package com.github.shautvast.reflective.array;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class ArrayFactoryTest {
@Test
void testCreate1DimShortArray() {
Object o = ArrayFactory.newInstance(short.class, 1);
assertTrue(o instanceof short[]);
}
@Test
void testCreate2DimIntArray() {
int[][] ints = (int[][]) ArrayFactory.newInstance(int[].class, 1);
assertEquals(1, ints.length);
ints[0] = new int[1]; // will fail if array is not correctly created
assertEquals(1, ints[0].length);
}
@Test
void testCreate3DimStringArray() {
String[][][] array = (String[][][]) ArrayFactory.newInstance(String.class, 6, 7, 8);
assertEquals(6, array.length);
assertEquals(7, array[0].length);
assertEquals(8, array[0][0].length);
}
@Test
void testSetObject() {
String[] strings = new String[1];
ArrayFactory.set(strings, 0, "helloworld");
assertArrayEquals(new String[]{"helloworld"}, strings);
}
@Test
void testSetInt() {
int[] ints = new int[1];
ArrayFactory.setInt(ints, 0, 11);
assertArrayEquals(new int[]{11}, ints);
}
@Test
void testSetByte() {
byte[] bytes = new byte[1];
ArrayFactory.setByte(bytes, 0, (byte) 11);
assertArrayEquals(new byte[]{11}, bytes);
}
@Test
void testSetShort() {
short[] shorts = new short[1];
ArrayFactory.setShort(shorts, 0, (short) 11);
assertArrayEquals(new short[]{11}, shorts);
}
@Test
void testSetLong() {
long[] longs = new long[1];
ArrayFactory.setLong(longs, 0, 11L);
assertArrayEquals(new long[]{11}, longs);
}
@Test
void testSetFloat() {
float[] floats = new float[1];
ArrayFactory.setFloat(floats, 0, 11.1F);
assertArrayEquals(new float[]{11.1F}, floats);
}
@Test
void testSetDouble() {
double[] doubles = new double[1];
ArrayFactory.setDouble(doubles, 0, 11.1D);
assertArrayEquals(new double[]{11.1D}, doubles);
}
@Test
void testSetBoolean() {
boolean[] booleans = new boolean[]{false};
ArrayFactory.setBoolean(booleans, 0, true);
assertArrayEquals(new boolean[]{true}, booleans);
}
@Test
void testSetChar() {
char[] chars = new char[]{'C'};
ArrayFactory.setChar(chars, 0, 'D');
assertArrayEquals(new char[]{'D'}, chars);
}
@Test
void testGetFromObjectArray() {
String[] strings = new String[]{"helloworld"};
Object o = ArrayFactory.get(strings, 0);
assertTrue(o instanceof String);
String string = (String) o;
assertEquals("helloworld", string);
}
@Test
void testGetFromByteArray() {
byte[] bytes = new byte[]{17};
byte b = ArrayFactory.getByte(bytes, 0);
assertEquals(17, b);
}
@Test
void testGetFromShortArray() {
short[] shorts = new short[]{17};
short s = ArrayFactory.getShort(shorts, 0);
assertEquals(17, s);
}
@Test
void testGetFromIntArray() {
int[] ints = new int[]{17};
int i = ArrayFactory.getInt(ints, 0);
assertEquals(17, i);
}
@Test
void testGetFromLongArray() {
long[] longs = new long[]{17};
long l = ArrayFactory.getLong(longs, 0);
assertEquals(17, l);
}
@Test
void testGetFromCharArray() {
char[] chars = new char[]{17};
char c = ArrayFactory.getChar(chars, 0);
assertEquals(17, c);
}
@Test
void testGetFromBooleanArray() {
boolean[] booleans = new boolean[]{true};
boolean b = ArrayFactory.getBoolean(booleans, 0);
assertEquals(true, b);
}
@Test
void testGetFromFloatArray() {
float[] floats = new float[]{17.5F};
float f = ArrayFactory.getFloat(floats, 0);
assertEquals(17.5, f);
}
@Test
void testGetFromDoubleArray() {
double[] doubles = new double[]{17.5F};
double d = ArrayFactory.getDouble(doubles, 0);
assertEquals(17.5, d);
}
@Test
void arrayIndexOutOfBoundsException() {
double[] doubles = new double[]{};
assertThrows(ArrayIndexOutOfBoundsException.class, () -> ArrayFactory.getDouble(doubles, 0));
}
@Test
void notAnArray() {
assertThrows(IllegalArgumentException.class, () -> ArrayFactory.get("foo", 0));
}
@Test
void arrayIsNull() {
assertThrows(NullPointerException.class, () -> ArrayFactory.get(null, 0));
}
}