Skip to content

Commit 3926d85

Browse files
committed
added ClassAccess utility for reflective field and method access
1 parent a31ee3b commit 3926d85

5 files changed

Lines changed: 675 additions & 0 deletions

File tree

foundry-core/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ plugins {
55

66
dependencies {
77
implementation(libs.guava)
8+
implementation(libs.asm)
9+
implementation(libs.asm.commons)
810
}
911

1012
publishing {
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
package org.machinemc.foundry.util;
2+
3+
import org.objectweb.asm.ClassWriter;
4+
import org.objectweb.asm.MethodVisitor;
5+
import org.objectweb.asm.Opcodes;
6+
import org.objectweb.asm.Type;
7+
import org.objectweb.asm.commons.GeneratorAdapter;
8+
import org.objectweb.asm.commons.Method;
9+
import org.objectweb.asm.signature.SignatureWriter;
10+
11+
import java.lang.invoke.MethodHandles;
12+
import java.lang.reflect.Constructor;
13+
import java.lang.reflect.Field;
14+
import java.lang.reflect.InvocationTargetException;
15+
import java.lang.reflect.Modifier;
16+
import java.util.Arrays;
17+
import java.util.Map;
18+
import java.util.concurrent.ConcurrentHashMap;
19+
20+
public final class ClassAccess {
21+
22+
private static final Map<FieldAccessKey<?>, FieldAccess<?, ?>> fieldCache = new ConcurrentHashMap<>();
23+
private static final Map<MethodAccessKey<?>, MethodAccess<?, ?>> methodCache = new ConcurrentHashMap<>();
24+
25+
26+
private ClassAccess() {}
27+
28+
public static <T> T getField(Object source, String name) throws NoSuchFieldException {
29+
//noinspection unchecked,rawtypes
30+
return (T) ((FieldAccess) field(source.getClass(), name)).get(source);
31+
}
32+
33+
public static <T> void setField(Object source, String name, T value) throws NoSuchFieldException {
34+
//noinspection unchecked,rawtypes
35+
((FieldAccess) field(source.getClass(), name)).set(source, value);
36+
}
37+
38+
public static <T> T getFieldStatic(Class<?> source, String name) throws NoSuchFieldException {
39+
// noinspection unchecked
40+
return (T) field(source, name).get(null);
41+
}
42+
43+
public static <T> void setFieldStatic(Class<?> source, String name, T value) throws NoSuchFieldException {
44+
field(source, name).set(null, value);
45+
}
46+
47+
public static <T> T invokeMethod(Object source, String name) throws NoSuchMethodException {
48+
return invokeMethod(source, name, new Class[0], new Object[0]);
49+
}
50+
51+
public static <T> T invokeMethod(Object source, String name, Object... arguments) throws NoSuchMethodException {
52+
Class<?>[] parameters = parameters(arguments);
53+
return invokeMethod(source, name, parameters, arguments);
54+
}
55+
56+
public static <T> T invokeMethod(Object source, String name, Class<?>[] parameters, Object... arguments) throws NoSuchMethodException {
57+
//noinspection unchecked,rawtypes
58+
return (T) ((MethodAccess) method(source.getClass(), name, parameters)).invoke(source, arguments);
59+
}
60+
61+
public static <T> T invokeStatic(Class<?> source, String name) throws NoSuchMethodException {
62+
return invokeStatic(source, name, new Class[0], new Object[0]);
63+
}
64+
65+
public static <T> T invokeStatic(Class<?> source, String name, Object... arguments) throws NoSuchMethodException {
66+
Class<?>[] parameters = parameters(arguments);
67+
return invokeStatic(source, name, parameters, arguments);
68+
}
69+
70+
public static <T> T invokeStatic(Class<?> source, String name, Class<?>[] parameters, Object... arguments) throws NoSuchMethodException {
71+
// noinspection unchecked
72+
return (T) method(source, name, parameters).invoke(null, arguments);
73+
}
74+
75+
private static Class<?>[] parameters(Object... arguments) {
76+
return Arrays.stream(arguments)
77+
.peek(arg -> {
78+
if (arg == null)
79+
throw new IllegalArgumentException("To invoke a method with null arguments you must specify the parameter types." +
80+
" Call #invokeMethod(Object, String, Class[], Object[]) instead.");
81+
})
82+
.map(Object::getClass)
83+
.toArray(Class[]::new);
84+
}
85+
86+
public static <S> S invokeConstructor(Class<S> source) throws NoSuchMethodException {
87+
return invokeConstructor(source, new Class[0], new Object[0]);
88+
}
89+
90+
public static <S> S invokeConstructor(Class<S> source, Object... arguments) throws NoSuchMethodException {
91+
Class<?>[] parameters = Arrays.stream(arguments)
92+
.peek(arg -> {
93+
if (arg == null)
94+
throw new IllegalArgumentException("To invoke a constructor with null arguments you must specify the parameter types." +
95+
" Call #invokeConstructor(Object, Class[], Object[]) instead.");
96+
})
97+
.map(Object::getClass)
98+
.toArray(Class[]::new);
99+
return invokeConstructor(source, parameters, arguments);
100+
}
101+
102+
public static <S> S invokeConstructor(Class<S> source, Class<?>[] parameters, Object... arguments) throws NoSuchMethodException {
103+
//noinspection unchecked
104+
return (S) method(source, "<init>", parameters).invoke(null, arguments);
105+
}
106+
107+
public static <S, T> FieldAccess<S, T> field(Class<S> source, String name) throws NoSuchFieldException {
108+
return fieldAccess(FieldAccessKey.of(source, name));
109+
}
110+
111+
private static <S, T> FieldAccess<S, T> fieldAccess(FieldAccessKey<S> key) {
112+
//noinspection unchecked
113+
return (FieldAccess<S, T>) fieldCache.computeIfAbsent(key, k -> {
114+
Class<?> aClass = defineHiddenIn(key.source(), generateFieldAccess(key));
115+
try {
116+
return (FieldAccess<?, ?>) aClass.getDeclaredConstructor().newInstance();
117+
} catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
118+
throw new RuntimeException(e); // Should not happen
119+
}
120+
});
121+
}
122+
123+
public static <S, T> MethodAccess<S, T> method(Class<S> source, String name, Class<?>... parameters) throws NoSuchMethodException {
124+
return methodAccess(MethodAccessKey.of(source, name, parameters));
125+
}
126+
127+
private static <S, T> MethodAccess<S, T> methodAccess(MethodAccessKey<S> key) {
128+
//noinspection unchecked
129+
return (MethodAccess<S, T>) methodCache.computeIfAbsent(key, k -> {
130+
Class<?> aClass = defineHiddenIn(key.source(), generateMethodAccess(key));
131+
try {
132+
return (MethodAccess<?, ?>) aClass.getDeclaredConstructor().newInstance();
133+
} catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
134+
throw new RuntimeException(e); // Should not happen
135+
}
136+
});
137+
}
138+
139+
private static Class<?> defineHiddenIn(Class<?> host, byte[] bytes) {
140+
try {
141+
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(host, MethodHandles.lookup())
142+
.defineHiddenClass(bytes, true, MethodHandles.Lookup.ClassOption.NESTMATE);
143+
return lookup.lookupClass();
144+
} catch (IllegalAccessException e) {
145+
throw new RuntimeException(e); // Should not happen
146+
}
147+
}
148+
149+
private static byte[] generateFieldAccess(FieldAccessKey<?> key) {
150+
Type objectT = Type.getType(Object.class);
151+
Type fieldAccessT = Type.getType(FieldAccess.class);
152+
Type sourceT = Type.getType(key.source());
153+
Type fieldT = Type.getType(key.type());
154+
Class<?> boxedField = TypeUtils.box(key.type());
155+
Type boxedFieldT = boxedField != null ? Type.getType(boxedField) : fieldT;
156+
157+
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
158+
159+
cw.visit(
160+
Opcodes.V25,
161+
Opcodes.ACC_PUBLIC,
162+
sourceT.getInternalName() + "$Field$" + key.hashCode(),
163+
accessSignature(fieldAccessT, sourceT, boxedFieldT),
164+
Type.getInternalName(Object.class),
165+
new String[] {fieldAccessT.getInternalName()}
166+
);
167+
168+
defineConstructor(cw);
169+
170+
String getterDesc = Type.getMethodDescriptor(objectT, objectT);
171+
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "get", getterDesc, null, null);
172+
GeneratorAdapter ga = new GeneratorAdapter(mv, Opcodes.ACC_PUBLIC, "get", getterDesc);
173+
174+
ga.visitCode();
175+
if (!key.isStatic()) {
176+
ga.loadArg(0);
177+
ga.checkCast(sourceT);
178+
ga.getField(sourceT, key.name(), fieldT);
179+
} else {
180+
ga.getStatic(sourceT, key.name(), fieldT);
181+
}
182+
ga.box(fieldT);
183+
ga.returnValue();
184+
ga.endMethod();
185+
186+
String setterDesc = Type.getMethodDescriptor(Type.getType(void.class), objectT, objectT);
187+
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "set", setterDesc, null, null);
188+
ga = new GeneratorAdapter(mv, Opcodes.ACC_PUBLIC, "set", setterDesc);
189+
190+
ga.visitCode();
191+
if (key.constant()) {
192+
ga.throwException(Type.getType(IllegalAccessException.class), "Cannot modify final field " + key.readable());
193+
} else {
194+
if (!key.isStatic()) {
195+
ga.loadArg(0);
196+
ga.checkCast(sourceT);
197+
ga.loadArg(1);
198+
ga.unbox(fieldT);
199+
ga.putField(sourceT, key.name(), fieldT);
200+
} else {
201+
ga.loadArg(1);
202+
ga.unbox(fieldT);
203+
ga.putStatic(sourceT, key.name(), fieldT);
204+
}
205+
ga.returnValue();
206+
}
207+
ga.endMethod();
208+
209+
cw.visitEnd();
210+
return cw.toByteArray();
211+
}
212+
213+
private static byte[] generateMethodAccess(MethodAccessKey<?> key) {
214+
Type objectT = Type.getType(Object.class);
215+
Type methodAccessT = Type.getType(MethodAccess.class);
216+
Type sourceT = Type.getType(key.source());
217+
Type returnTypeT = Type.getType(key.returnType());
218+
Class<?> boxedReturnType = TypeUtils.box(key.returnType());
219+
Type boxedReturnTypeT = boxedReturnType != null ? Type.getType(boxedReturnType) : returnTypeT;
220+
Type[] parametersT = Arrays.stream(key.parameters()).map(Type::getType).toArray(Type[]::new);
221+
222+
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
223+
224+
cw.visit(
225+
Opcodes.V25,
226+
Opcodes.ACC_PUBLIC,
227+
sourceT.getInternalName() + "$Method$" + key.hashCode(),
228+
accessSignature(methodAccessT, sourceT, boxedReturnTypeT),
229+
Type.getInternalName(Object.class),
230+
new String[] {methodAccessT.getInternalName()}
231+
);
232+
233+
defineConstructor(cw);
234+
235+
String invokeDesk = Type.getMethodDescriptor(objectT, objectT, Type.getType(Object[].class));
236+
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_VARARGS, "invoke", invokeDesk, null, null);
237+
GeneratorAdapter ga = new GeneratorAdapter(mv, Opcodes.ACC_PUBLIC, "invoke", invokeDesk);
238+
239+
ga.visitCode();
240+
if (key.constructor()) {
241+
ga.newInstance(sourceT);
242+
ga.dup();
243+
loadArgs(ga, key.parameters());
244+
ga.invokeConstructor(sourceT, new Method(key.name(), Type.VOID_TYPE, parametersT));
245+
} else if (key.isStatic()) {
246+
loadArgs(ga, key.parameters());
247+
ga.invokeStatic(sourceT, new Method(key.name(), returnTypeT, parametersT));
248+
249+
if (returnTypeT.equals(Type.VOID_TYPE)) {
250+
ga.push((String) null);
251+
} else if (isPrimitive(returnTypeT)) {
252+
ga.box(returnTypeT);
253+
}
254+
} else {
255+
ga.loadArg(0);
256+
ga.checkCast(sourceT);
257+
loadArgs(ga, key.parameters());
258+
ga.invokeVirtual(sourceT, new Method(key.name(), returnTypeT, parametersT));
259+
260+
if (returnTypeT.equals(Type.VOID_TYPE)) {
261+
ga.push((String) null);
262+
} else if (isPrimitive(returnTypeT)) {
263+
ga.box(returnTypeT);
264+
}
265+
}
266+
ga.returnValue();
267+
ga.endMethod();
268+
269+
cw.visitEnd();
270+
return cw.toByteArray();
271+
}
272+
273+
private static boolean isPrimitive(Type type) {
274+
return type.getSort() != Type.OBJECT && type.getSort() != Type.ARRAY;
275+
}
276+
277+
private static void loadArgs(GeneratorAdapter ga, Class<?>[] parameters) {
278+
for (int i = 0; i < parameters.length; i++) {
279+
Type paramT = Type.getType(parameters[i]);
280+
ga.loadArg(1);
281+
ga.push(i);
282+
ga.arrayLoad(Type.getType(Object.class));
283+
if (!isPrimitive(paramT)) {
284+
ga.checkCast(paramT);
285+
} else {
286+
ga.unbox(paramT);
287+
}
288+
}
289+
}
290+
291+
private static void defineConstructor(ClassWriter cw) {
292+
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
293+
294+
mv.visitCode();
295+
mv.visitVarInsn(Opcodes.ALOAD, 0);
296+
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V", false);
297+
mv.visitInsn(Opcodes.RETURN);
298+
mv.visitMaxs(0, 0);
299+
mv.visitEnd();
300+
}
301+
302+
private static String accessSignature(Type interfaceType, Type source, Type type) {
303+
SignatureWriter signature = new SignatureWriter();
304+
305+
signature.visitSuperclass();
306+
signature.visitClassType(Type.getInternalName(Object.class));
307+
signature.visitEnd();
308+
309+
signature.visitInterface();
310+
signature.visitClassType(interfaceType.getInternalName());
311+
signature.visitTypeArgument(SignatureWriter.INSTANCEOF).visitClassType(source.getInternalName());
312+
signature.visitEnd();
313+
314+
signature.visitTypeArgument(SignatureWriter.INSTANCEOF).visitClassType(type.getInternalName());
315+
signature.visitEnd();
316+
317+
signature.visitEnd();
318+
return signature.toString();
319+
}
320+
321+
public interface FieldAccess<S, T> {
322+
T get(S source);
323+
void set(S source, T value);
324+
}
325+
326+
@FunctionalInterface
327+
public interface MethodAccess<S, T> {
328+
T invoke(S source, Object... args);
329+
}
330+
331+
private record FieldAccessKey<S>(Class<? extends S> source, String name, Class<?> type, boolean constant, boolean isStatic) {
332+
333+
public String readable() {
334+
return (isStatic ? "static " : "") + type.getName() + " " + source.getName() + "." + name;
335+
}
336+
337+
public static <S> FieldAccessKey<S> of(Class<? extends S> source, String name) throws NoSuchFieldException {
338+
Field field = source.getDeclaredField(name);
339+
int modifiers = field.getModifiers();
340+
return new FieldAccessKey<>(source, name, field.getType(), (modifiers & Modifier.FINAL) != 0, (modifiers & Modifier.STATIC) != 0);
341+
}
342+
343+
}
344+
345+
private record MethodAccessKey<S>(Class<? extends S> source, String name, Class<?> returnType, Class<?>[] parameters, boolean isStatic) {
346+
347+
public static <S> MethodAccessKey<S> of(Class<? extends S> source, String name, Class<?>[] parameters) throws NoSuchMethodException {
348+
if ("<init>".equals(name)) {
349+
Constructor<?> constructor = source.getDeclaredConstructor(parameters);
350+
return new MethodAccessKey<>(source, name, source, constructor.getParameterTypes(), false);
351+
}
352+
java.lang.reflect.Method method = source.getDeclaredMethod(name, parameters);
353+
return new MethodAccessKey<>(source, name, method.getReturnType(), method.getParameterTypes(), (method.getModifiers() & Modifier.STATIC) != 0);
354+
}
355+
356+
public boolean constructor() {
357+
return name.equals("<init>");
358+
}
359+
360+
@Override
361+
public boolean equals(Object o) {
362+
if (!(o instanceof MethodAccessKey<?>(Class<?> source1, String name1, Class<?> type, Class<?>[] parameters1, boolean aStatic)))
363+
return false;
364+
365+
return isStatic == aStatic && name.equals(name1) && returnType.equals(type) && Arrays.equals(parameters, parameters1) && source.equals(source1);
366+
}
367+
368+
@Override
369+
public int hashCode() {
370+
return Arrays.deepHashCode(new Object[]{source, name, returnType, parameters, isStatic});
371+
}
372+
373+
}
374+
375+
}

foundry-core/src/main/java/org/machinemc/foundry/util/TypeUtils.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ public final class TypeUtils {
3333
private TypeUtils() {
3434
}
3535

36+
public static Class<?> box(Class<?> primitive) {
37+
return PRIMITIVE_WRAPPERS.get(primitive);
38+
}
39+
40+
public static Class<?> unbox(Class<?> boxed) {
41+
return PRIMITIVE_WRAPPERS.inverse().get(boxed);
42+
}
43+
3644
/**
3745
* Checks if a variable of an {@code expected} type can be assigned a value of an
3846
* {@code actual} type, following Java's assignment and subtyping rules.

0 commit comments

Comments
 (0)