invocation (not yet with arguments)

This commit is contained in:
Sander Hautvast 2023-07-28 18:21:35 +02:00
parent 7bc4b96fd7
commit 93cd1a081a
17 changed files with 499 additions and 55 deletions

View file

@ -12,3 +12,9 @@ __nl.sander.reflective.compare.Compare__
__nl.sander.reflective.tomap.ToMap__
* turn any bean/record into a Map<String, Object>
Now working on capabilities that mimick java.lang.reflect
* not going to create something like setAccessible(true), since that's likely impossible without jdk support, and probably not wanted either
* I do plan to substitute java.lang.reflect.Array, because of it's VERY poor performance
* a read model for methods, fields etc
* invocation capabilities

View file

@ -0,0 +1,6 @@
package com.github.shautvast.reflective;
public abstract class AbstractInvoker {
public abstract Object invoke(Object instance, Object... arguments);
}

View file

@ -1,6 +0,0 @@
package com.github.shautvast.reflective;
public abstract class AbstractMetaClassFactory {
public abstract MetaClass create();
}

View file

@ -0,0 +1,54 @@
package com.github.shautvast.reflective;
import com.github.shautvast.reflective.java.ASM;
import com.github.shautvast.reflective.java.ByteClassLoader;
import com.github.shautvast.reflective.java.Java;
import com.github.shautvast.rusty.Result;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.UUID;
import static com.github.shautvast.rusty.Result.err;
import static com.github.shautvast.rusty.Result.ok;
import static org.objectweb.asm.Opcodes.*;
public class InvokerFactory {
public static final String SUPER = Java.internalName(AbstractInvoker.class);
public static Result<AbstractInvoker> of(MetaMethod m) {
ClassNode classNode = ASM.createDefaultClassNode("Invoker" + UUID.randomUUID(), SUPER);
MethodNode invokerMethod = new MethodNode(ACC_PUBLIC,
"invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", null, null);
invokerMethod.instructions.add(new VarInsnNode(ALOAD, 1));
invokerMethod.instructions.add(new TypeInsnNode(CHECKCAST, m.getMetaClass().getRawName()));
invokerMethod.instructions.add(new MethodInsnNode(INVOKEVIRTUAL, m.getMetaClass().getRawName(), m.getName(), m.getDescriptor()));
invokerMethod.instructions.add(new InsnNode(ARETURN));
classNode.methods.add(invokerMethod);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classNode.accept(classWriter);
byte[] byteArray = classWriter.toByteArray();
try (FileOutputStream out = new FileOutputStream("C.class")) {
out.write(byteArray);
} catch (IOException e) {
e.printStackTrace();
}
ByteClassLoader.INSTANCE.addClass(classNode.name, byteArray);
try {
return ok((AbstractInvoker) ByteClassLoader.INSTANCE.loadClass(classNode.name).getConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException |
ClassNotFoundException e) {
return err(e);
}
}
}

View file

@ -1,17 +1,25 @@
package com.github.shautvast.reflective;
import com.github.shautvast.reflective.java.Java;
import java.util.*;
public class MetaClass {
private final String name;
private final String rawName;
private final Map<String, MetaField> fields;
private final Map<String, MetaMethod> methods;
private final Set<MetaMethod> constructors;
private final Class<?> javaClass;
public MetaClass(String name) {
this.name = name;
public MetaClass(Class<?> javaClass, String rawName) {
this.javaClass = javaClass;
this.name = Java.externalName(rawName);
this.rawName = rawName;
this.fields = new HashMap<>();
this.methods = new HashMap<>();
this.constructors = new HashSet<>();
}
public String getName() {
@ -26,11 +34,31 @@ public class MetaClass {
return Set.copyOf(methods.values());
}
void addField(int access, String name, String descriptor) {
fields.put(name, new MetaField(name));
public Optional<MetaMethod> getMethod(String name) {
return Optional.ofNullable(methods.get(name));
}
public void addMethod(int access, String methodname, String descriptor) {
methods.put(methodname, new MetaMethod(methodname));
public Set<MetaMethod> getConstructors() {
return constructors;
}
void addField(int access, String name, String descriptor) {
fields.put(name, new MetaField(name, access)); //ASM access same as reflect modifiers?
}
public void addMethod(int access, String name, String descriptor) {
methods.put(name, new MetaMethod(this, name, access, descriptor));
}
public void addConstructor(int access, String methodname, String descriptor) {
}
public Class<?> getJavaClass() {
return javaClass;
}
public String getRawName() {
return rawName;
}
}

View file

@ -5,26 +5,24 @@ import com.github.shautvast.reflective.java.Java;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.tree.ClassNode;
import static org.objectweb.asm.Opcodes.ASM9;
public class MetaClassFactory extends ClassVisitor {
public static final String SUPER = Java.internalName(AbstractComparator.class);
private boolean isRecord = false;
final ClassNode classNode = new ClassNode();
final Class<?> javaClass;
private MetaClass metaClassToBuild;
public MetaClassFactory() {
public MetaClassFactory(Class<?>javaClass) {
super(ASM9);
this.javaClass = javaClass;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
metaClassToBuild = new MetaClass(Java.externalName(name));
metaClassToBuild = new MetaClass(javaClass, name);
}
@Override
@ -36,7 +34,11 @@ public class MetaClassFactory extends ClassVisitor {
@Override
public MethodVisitor visitMethod(int access, String methodname,
String descriptor, String signature, String[] exceptions) {
if (!Java.isConstructor(methodname)) {
metaClassToBuild.addMethod(access, methodname, descriptor);
} else {
metaClassToBuild.addConstructor(access, methodname, descriptor);
}
return null;
}

View file

@ -3,8 +3,18 @@ package com.github.shautvast.reflective;
public class MetaField {
private final String name;
private final int modifiers;
public MetaField(String name) {
public MetaField(String name, int modifiers) {
this.name = name;
this.modifiers = modifiers;
}
public String getName() {
return name;
}
public int getModifiers() {
return modifiers;
}
}

View file

@ -1,9 +1,88 @@
package com.github.shautvast.reflective;
public class MetaMethod {
private final String name;
import com.github.shautvast.reflective.java.Java;
import com.github.shautvast.rusty.Result;
public MetaMethod(String methodname) {
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import static com.github.shautvast.rusty.Result.err;
import static com.github.shautvast.rusty.Result.ok;
public class MetaMethod {
private final MetaClass metaClass;
private final String name;
private final int modifiers;
private final String descriptor;
private List<Class<?>> parameters = new LinkedList<>();
private Class<?> returnParameter;
private final AbstractInvoker invoker = InvokerFactory.of(this).unwrapOr(() -> null);
public MetaMethod(MetaClass metaClass, String methodname, int modifiers, String descriptor) {
this.metaClass = metaClass;
this.name = methodname;
this.modifiers = modifiers;
this.descriptor = descriptor;
getParameters(descriptor);
}
public String getName() {
return name;
}
public MetaClass getMetaClass() {
return metaClass;
}
public int getModifiers() {
return modifiers;
}
public String getDescriptor() {
return descriptor;
}
public List<Class<?>> getParameters() {
return parameters;
}
public Class<?> getReturnParameter() {
return returnParameter;
}
@SuppressWarnings("unchecked")
public <T> Result<T> invoke(Object instance, Object... arguments) {
if (instance.getClass() != metaClass.getJavaClass()) {
return Result.err("instance type not of " + metaClass.getJavaClass());
}
try {
T invoke = (T) invoker.invoke(instance, arguments);
return ok(invoke);
} catch (Exception e) {
return err(e);
}
}
private void getParameters(String descriptor) {
String[] split = descriptor.split("[()]");
String parms = split[1];
StringBuilder buf = new StringBuilder();
for (int i = 0; i < parms.length(); i++) {
String t = Character.toString(parms.charAt(i));
if (!"L".equals(t) && buf.length() == 0) {
this.parameters.add(Java.getClassFromDescriptor(t));
}
if (";".equals(t)) {
buf.append(t);
this.parameters.add(Java.getClassFromDescriptor(buf.toString()));
buf = new StringBuilder();
} else {
buf.append(t);
}
}
this.parameters = Collections.unmodifiableList(this.parameters); //effectively final
this.returnParameter = Java.getClassFromDescriptor(split[2]);
}
}

View file

@ -13,7 +13,7 @@ public class Reflective {
try {
ClassReader cr = Java.getClassReader(type);
MetaClassFactory factory = new MetaClassFactory();
MetaClassFactory factory = new MetaClassFactory(type);
cr.accept(factory, ClassReader.SKIP_FRAMES);
return factory.getMetaClass();
} catch (Exception e) {

View file

@ -1,5 +1,6 @@
package com.github.shautvast.reflective.compare;
import com.github.shautvast.reflective.java.ASM;
import com.github.shautvast.reflective.java.Java;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
@ -20,10 +21,8 @@ class ComparatorFactory extends ClassVisitor {
super(ASM9);
}
final ClassNode classNode = new ClassNode();
private String classToMap;
ClassNode classNode;
private MethodNode compareMethod;
private int localVarIndex = 0;
@ -34,15 +33,7 @@ class ComparatorFactory extends ClassVisitor {
isRecord = true;
}
this.classToMap = name;
classNode.name = "Apple" + UUID.randomUUID();
classNode.superName = SUPER;
classNode.version = V11;
classNode.access = ACC_PUBLIC;
MethodNode constructor = new MethodNode(ACC_PUBLIC, Java.INIT, Java.ZERO_ARGS_VOID, null, null);
constructor.instructions.add(new VarInsnNode(ALOAD, 0));
constructor.instructions.add(new MethodInsnNode(INVOKESPECIAL, SUPER, Java.INIT, Java.ZERO_ARGS_VOID));
constructor.instructions.add(new InsnNode(RETURN));
classNode.methods.add(constructor);
classNode = ASM.createDefaultClassNode("Apple" + UUID.randomUUID(), SUPER);
compareMethod = new MethodNode(ACC_PUBLIC,
"compare", "(Ljava/lang/Object;Ljava/lang/Object;)L" + Java.internalName(Result.class) + ";", null, null);

View file

@ -0,0 +1,25 @@
package com.github.shautvast.reflective.java;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;
import static org.objectweb.asm.Opcodes.*;
public class ASM {
private ASM() {
}
public static ClassNode createDefaultClassNode(String name, String superClass) {
ClassNode classNode = new ClassNode(Opcodes.ASM9);
classNode.name = name;
classNode.superName = superClass;
classNode.version = V11;
classNode.access = ACC_PUBLIC;
MethodNode constructor = new MethodNode(ACC_PUBLIC, Java.INIT, Java.ZERO_ARGS_VOID, null, null);
constructor.instructions.add(new VarInsnNode(ALOAD, 0));
constructor.instructions.add(new MethodInsnNode(INVOKESPECIAL, superClass, Java.INIT, Java.ZERO_ARGS_VOID));
constructor.instructions.add(new InsnNode(RETURN));
classNode.methods.add(constructor);
return classNode;
}
}

View file

@ -4,6 +4,8 @@ import org.objectweb.asm.ClassReader;
import java.io.IOException;
import static java.lang.reflect.Array.newInstance;
/*
* common utils not for external use
*/
@ -21,8 +23,8 @@ public class Java {
return internalName(type.getName());
}
public static String externalName(String internalName){
return internalName.replaceAll("/",".");
public static String externalName(String internalName) {
return internalName.replaceAll("/", ".");
}
public static boolean hasArgs(String desc) {
@ -56,4 +58,87 @@ public class Java {
throw new ClassNotFoundException(type.getName());
}
}
public static boolean isConstructor(String methodname) {
return INIT.equals(methodname);
}
public static Class<?> getTypeFrom(String typedesc) {
switch (typedesc) {
case "B":
return byte.class;
case "I":
return int.class;
case "J":
return long.class;
case "Z":
return boolean.class;
case "C":
return char.class;
case "S":
return short.class;
case "F":
return float.class;
case "D":
return double.class;
default:
try {
if (typedesc.startsWith("L")) {
return Class.forName(externalName(typedesc.substring(1)));
}
if (typedesc.startsWith("[") && !typedesc.endsWith(";")) {
return Class.forName(externalName(typedesc.substring(1)));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
throw new RuntimeException();
}
public static Class<?> getClassFromDescriptor(String descriptor) {
int arrayDims = 0;
while (descriptor.startsWith("[")) {
arrayDims++;
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 {
char typeChar = descriptor.charAt(0);
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;
default:
throw new RuntimeException(new ClassNotFoundException("unknown descriptor: " + descriptor)); //must not happen
}
}
}
}

View file

@ -1,5 +1,6 @@
package com.github.shautvast.reflective.tomap;
import com.github.shautvast.reflective.java.ASM;
import com.github.shautvast.reflective.java.Java;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
@ -13,10 +14,9 @@ import static org.objectweb.asm.Opcodes.*;
class ToMapFactory extends ClassVisitor {
public static final String SUPER_NAME = Java.internalName(AbstractToMap.class.getName());
private boolean isRecord = false;
final ClassNode classNode = new ClassNode();
private String classToMap;
ClassNode classNode;
private MethodNode mappifyMethod;
@ -30,15 +30,7 @@ class ToMapFactory extends ClassVisitor {
isRecord = true;
}
this.classToMap = name;
classNode.name = "ToMap" + UUID.randomUUID();
classNode.superName = SUPER_NAME;
classNode.version = V11;
classNode.access = ACC_PUBLIC;
MethodNode constructor = new MethodNode(ACC_PUBLIC, Java.INIT, Java.ZERO_ARGS_VOID, null, null);
constructor.instructions.add(new VarInsnNode(ALOAD, 0));
constructor.instructions.add(new MethodInsnNode(INVOKESPECIAL, SUPER_NAME, Java.INIT, Java.ZERO_ARGS_VOID));
constructor.instructions.add(new InsnNode(RETURN));
classNode.methods.add(constructor);
classNode = ASM.createDefaultClassNode("ToMap" + UUID.randomUUID(), SUPER_NAME);
mappifyMethod = new MethodNode(ACC_PUBLIC,
"toMap", "(Ljava/lang/Object;)Ljava/util/Map;", null, null);
@ -66,11 +58,11 @@ class ToMapFactory extends ClassVisitor {
add(new VarInsnNode(ALOAD, 1));
add(new TypeInsnNode(CHECKCAST, Java.internalName(classToMap)));
add(new MethodInsnNode(INVOKEVIRTUAL, classToMap, getterMethodName, "()" + returnType));
add(new MethodInsnNode(INVOKEVIRTUAL, classNode.name, "add", "(Ljava/util/HashMap;Ljava/lang/String;"+translate(returnType)+")V"));
add(new MethodInsnNode(INVOKEVIRTUAL, classNode.name, "add", "(Ljava/util/HashMap;Ljava/lang/String;" + translate(returnType) + ")V"));
}
private String translate(String typeDesc){
if (typeDesc.startsWith("L")){
private String translate(String typeDesc) {
if (typeDesc.startsWith("L")) {
return Java.OBJECT;
} else {
return typeDesc;

View file

@ -0,0 +1,22 @@
package com.github.shautvast.rusty;
public class Panic extends RuntimeException{
public Panic() {
}
public Panic(String message) {
super(message);
}
public Panic(String message, Throwable cause) {
super(message, cause);
}
public Panic(Throwable cause) {
super(cause);
}
public Panic(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View file

@ -0,0 +1,68 @@
package com.github.shautvast.rusty;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* attempted rust-style exception handling
*
* @param <T> type of expected result
*/
public class Result<T> {
private final T value;
private final Panic error;
private Result(T value, Throwable error) {
this.value = value;
this.error = new Panic(error);
}
private Result(T value) {
this.value = value;
this.error = null;
}
public static <T> Result<T> err(Throwable error) {
return new Result<>(null, new Panic(error));
}
public static <T> Result<T> err(String error) {
return new Result<>(null, new Panic(error));
}
public static <T> Result<T> ok(T value) {
return new Result<>(value);
}
public T unwrap() {
if (error == null) {
return value;
} else {
throw error;
}
}
public T expect(String failedExpectation) {
if (error == null) {
return value;
} else {
throw new Panic(failedExpectation);
}
}
public T unwrapOr(Supplier<T> valueSupplier) {
if (error == null) {
return value;
} else {
return valueSupplier.get();
}
}
public <R> Result<R> map(Function<T, R> mappingFunction) {
if (error == null) {
return new Result<>(mappingFunction.apply(value), null);
} else {
return new Result<>(null, error);
}
}
}

View file

@ -0,0 +1,8 @@
package com.github.shautvast.reflective;
public class DummyInvoker extends AbstractInvoker {
public Object invoke(Object instance, Object... arguments) {
return ((ReflectiveTest.Dummy) instance).getName();
}
}

View file

@ -1,13 +1,87 @@
package com.github.shautvast.reflective;
import com.github.shautvast.rusty.Panic;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
public class ReflectiveTest {
@Test
void test(){
assertEquals("java.lang.String",Reflective.getMetaForClass(String.class).getName());
void testMethods() {
Dummy dummy = new Dummy("bar");
MetaClass metaDummy = Reflective.getMetaForClass(dummy.getClass());
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();
assertFalse(methods.isEmpty());
assertEquals(4, methods.size());
MetaMethod equals = metaDummy.getMethod("equals").orElseGet(Assertions::fail);
assertEquals(List.of(Object.class), equals.getParameters());
assertEquals(boolean.class, equals.getReturnParameter());
assertTrue(Modifier.isPublic(equals.getModifiers()));
MetaMethod hashCode = metaDummy.getMethod("hashCode").orElseGet(Assertions::fail);
assertEquals(List.of(), hashCode.getParameters());
assertEquals(int.class, hashCode.getReturnParameter());
assertTrue(Modifier.isPublic(hashCode.getModifiers()));
MetaMethod getName = metaDummy.getMethod("getName").orElseGet(Assertions::fail);
assertEquals(List.of(), getName.getParameters());
assertEquals(String.class, getName.getReturnParameter());
assertTrue(Modifier.isPublic(getName.getModifiers()));
MetaMethod privateMethod = metaDummy.getMethod("privateMethod").orElseGet(Assertions::fail);
assertEquals(List.of(), privateMethod.getParameters());
assertEquals(String[].class, privateMethod.getReturnParameter());
assertTrue(Modifier.isPrivate(privateMethod.getModifiers()));
}
@Test
void testInvocation() {
Dummy dummy = new Dummy("bar");
MetaMethod getName = Reflective.getMetaForClass(dummy.getClass()).getMethod("getName").orElseGet(Assertions::fail);
assertThrows(Panic.class, () -> getName.invoke("foo").unwrap());
assertEquals("bar", getName.invoke(dummy).unwrap());
}
public static class Dummy {
private final String name;
public Dummy(String name) {
this.name = name;
}
public String getName() {
return privateMethod()[0];
}
@Override
public boolean equals(Object obj) {
return obj instanceof Dummy;
}
@Override
public int hashCode() {
return 6;
}
private String[] privateMethod() {
return new String[]{name, "bar"};
}
}
}