[epilogue] Use reflection to access non-public superclass fields (#7996)

Co-authored-by: Sam Carlberg <sam@slfc.dev>
This commit is contained in:
Ryan Shavell
2025-08-30 23:14:41 -04:00
committed by GitHub
parent 9fd4ccf95b
commit 45db0fd45e
13 changed files with 322 additions and 545 deletions

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
@@ -52,7 +53,7 @@ public class ArrayHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
var dataType = dataType(element);
// known to be an array type (assuming isLoggable is checked first); this is a safe cast
@@ -63,13 +64,17 @@ public class ArrayHandler extends ElementHandler {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ", "
+ m_structHandler.structAccess(componentType)
+ ")";
} else {
// Primitive or string array
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}
}

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
@@ -38,7 +39,7 @@ public class CollectionHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
var dataType = dataType(element);
var componentType = ((DeclaredType) dataType).getTypeArguments().get(0);
@@ -46,12 +47,16 @@ public class CollectionHandler extends ElementHandler {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ", "
+ m_structHandler.structAccess(componentType)
+ ")";
} else {
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}
}

View File

@@ -7,6 +7,7 @@ package edu.wpi.first.epilogue.processor;
import java.util.Map;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
@@ -27,7 +28,7 @@ public class ConfiguredLoggerHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
var dataType = dataType(element);
var loggerType =
m_customLoggers.entrySet().stream()
@@ -44,7 +45,7 @@ public class ConfiguredLoggerHandler extends ElementHandler {
+ ".tryUpdate(backend.getNested(\""
+ loggedName(element)
+ "\"), "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ", Epilogue.getConfig().errorHandler)";
}
}

View File

@@ -117,9 +117,9 @@ public abstract class ElementHandler {
* @param element the element to generate the access for
* @return the generated access snippet
*/
public String elementAccess(Element element) {
public String elementAccess(Element element, TypeElement loggedClass) {
if (element instanceof VariableElement field) {
return fieldAccess(field);
return fieldAccess(field, loggedClass);
} else if (element instanceof ExecutableElement method) {
return methodAccess(method);
} else {
@@ -127,8 +127,20 @@ public abstract class ElementHandler {
}
}
private static String fieldAccess(VariableElement field) {
if (!field.getModifiers().contains(Modifier.PUBLIC)) {
private static String fieldAccess(VariableElement field, TypeElement loggedClass) {
var mods = field.getModifiers();
// To be directly accessible, the field needs to be:
// - public; or
// - protected or package-private, and declared by a superclass in the same package
// However, we can't cleanly access package information, so we'll always emit a VarHandle
// for any field declared in a superclass unless it's public and we know we can read it.
boolean isVarHandle =
field.getEnclosingElement().equals(loggedClass)
? mods.contains(Modifier.PRIVATE)
: !mods.contains(Modifier.PUBLIC);
if (isVarHandle) {
// ((com.example.Foo) $fooField.get(object))
// Extra parentheses so cast evaluates before appended methods
// (e.g. when appending .getAsDouble())
@@ -136,7 +148,7 @@ public abstract class ElementHandler {
if (type.getKind() == TypeKind.TYPEVAR) {
type = ((TypeVariable) type).getUpperBound();
}
return "((" + type.toString() + ") $" + field.getSimpleName() + ".get(object))";
return "((" + type.toString() + ") " + LoggerGenerator.varHandleName(field) + ".get(object))";
} else {
// object.fooField
return "object." + field.getSimpleName();
@@ -171,5 +183,5 @@ public abstract class ElementHandler {
* @param element the field or method element to generate the logger call for
* @return the generated log invocation
*/
public abstract String logInvocation(Element element);
public abstract String logInvocation(Element element, TypeElement loggedClass);
}

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public class EnumHandler extends ElementHandler {
@@ -27,7 +28,11 @@ public class EnumHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
public String logInvocation(Element element, TypeElement loggedClass) {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}

View File

@@ -39,7 +39,7 @@ public class LoggableHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
TypeMirror dataType = dataType(element);
var declaredType =
m_processingEnv
@@ -61,7 +61,7 @@ public class LoggableHandler extends ElementHandler {
// If there are no known loggable subtypes, return just the single logger call
if (size == 1) {
return generateLoggerCall(element, declaredType, elementAccess(element));
return generateLoggerCall(element, declaredType, elementAccess(element, loggedClass));
}
// Otherwise, generate an if-else chain to compare the element with its known loggable subtypes
@@ -73,7 +73,7 @@ public class LoggableHandler extends ElementHandler {
StringBuilder builder = new StringBuilder();
// Cache the value in a variable so it's only read once
builder.append("var %s = %s;\n".formatted(varName, elementAccess(element)));
builder.append("var %s = %s;\n".formatted(varName, elementAccess(element, loggedClass)));
for (int i = 0; i < size; i++) {
TypeElement type = loggableSubtypes.get(i);

View File

@@ -18,9 +18,11 @@ import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -185,7 +187,21 @@ public class LoggerGenerator {
var loggerFile = m_processingEnv.getFiler().createSourceFile(loggerClassName);
var varHandleFields =
loggableFields.stream().filter(e -> !e.getModifiers().contains(Modifier.PUBLIC)).toList();
loggableFields.stream()
.filter(
e -> {
if (e.getEnclosingElement().equals(clazz)) {
// The generated logger is in the same package as the logged class, so the
// only fields it can't read are private ones.
return e.getModifiers().contains(Modifier.PRIVATE);
} else {
// Logging from a superclass. Can only read public fields, unless the superclass
// is in the same package, in which case protected and package-private fields
// are also readable.
return !e.getModifiers().contains(Modifier.PUBLIC);
}
})
.toList();
boolean requiresVarHandles = !varHandleFields.isEmpty();
try (var out = new PrintWriter(loggerFile.openWriter())) {
@@ -214,41 +230,67 @@ public class LoggerGenerator {
+ "> {");
if (requiresVarHandles) {
for (var varHandleField : varHandleFields) {
for (var privateField : varHandleFields) {
// This field needs a VarHandle to access.
// Cache it in the class to avoid lookups
out.println(" private static final VarHandle $" + varHandleField.getSimpleName() + ";");
out.printf(
" // Accesses private or superclass field %s.%s%n",
privateField.getEnclosingElement(), privateField.getSimpleName());
out.printf(" private static final VarHandle %s;%n", varHandleName(privateField));
}
out.println();
}
var classReference = simpleClassName + ".class";
// Static initializer block to load VarHandles and reflection fields
if (requiresVarHandles) {
out.println(" static {");
out.println(" try {");
out.println(
" var lookup = MethodHandles.privateLookupIn("
+ classReference
+ ", MethodHandles.lookup());");
for (var varHandleField : varHandleFields) {
var fieldName = varHandleField.getSimpleName();
out.println(
" $"
+ fieldName
+ " = lookup.findVarHandle("
+ classReference
+ ", \""
+ fieldName
+ "\", "
+ m_processingEnv.getTypeUtils().erasure(varHandleField.asType())
+ ".class);");
}
out.println(" try {");
out.println(" var rootLookup = MethodHandles.lookup();");
// Group private fields by class, then generate a private lookup for each class
// and a VarHandle for each field using that lookup. Sorting and then collecting into a
// LinkedHashMap gives deterministic output ordering (mostly for tests, which check exact
// file contents, but also results in less churn when regenerating files for users who like
// to read the generated logger classes).
//
// This lets us read private fields from superclasses.
Map<Element, List<VariableElement>> privateFieldsByClass =
varHandleFields.stream()
.sorted(Comparator.comparing(e -> e.getSimpleName().toString()))
.collect(
Collectors.groupingBy(
VariableElement::getEnclosingElement,
LinkedHashMap::new,
Collectors.toList()));
privateFieldsByClass.forEach(
(enclosingClass, fields) -> {
String className = enclosingClass.toString();
String lookupName = "lookup$$" + className.replace(".", "_");
out.printf(
" var %s = MethodHandles.privateLookupIn(%s.class, rootLookup);%n",
lookupName, className);
for (var field : fields) {
var fieldname = field.getSimpleName();
out.printf(
" %s = %s.findVarHandle(%s.class, \"%s\", %s.class);%n",
varHandleName(field),
lookupName,
className,
fieldname,
m_processingEnv.getTypeUtils().erasure(field.asType()));
}
});
out.println(" } catch (ReflectiveOperationException e) {");
out.println(
" throw new RuntimeException("
+ "\"[EPILOGUE] Could not load private fields for logging!\", e);");
out.println(" }");
out.println(" }");
out.println();
}
@@ -300,7 +342,7 @@ public class LoggerGenerator {
// to be logged. For example, the sendable handler consumes all sendable types
// but does not log commands or subsystems, to prevent excessive warnings about
// unloggable commands.
var logInvocation = h.logInvocation(loggableElement);
var logInvocation = h.logInvocation(loggableElement, clazz);
if (logInvocation != null) {
out.println(logInvocation.indent(6).stripTrailing() + ";");
}
@@ -315,6 +357,18 @@ public class LoggerGenerator {
}
}
/**
* Generates the name of a VarHandle for access to the given field. The VarHandle variable's name
* is guaranteed to be unique.
*
* @param field The field to generate a VarHandle for
* @return The name of the generated VarHandle variable
*/
public static String varHandleName(VariableElement field) {
return "$%s_%s"
.formatted(field.getEnclosingElement().toString().replace(".", "_"), field.getSimpleName());
}
private void collectLoggables(
TypeElement clazz, List<VariableElement> fields, List<ExecutableElement> methods) {
var config = clazz.getAnnotation(Logged.class);

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public class MeasureHandler extends ElementHandler {
@@ -30,8 +31,12 @@ public class MeasureHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
// EpilogueBackend has builtin support for logging measures
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}

View File

@@ -16,6 +16,7 @@ import static javax.lang.model.type.TypeKind.SHORT;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public class PrimitiveHandler extends ElementHandler {
@@ -35,7 +36,11 @@ public class PrimitiveHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
public String logInvocation(Element element, TypeElement loggedClass) {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}

View File

@@ -44,7 +44,7 @@ public class SendableHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
var dataType = dataType(element);
// Do not log commands or subsystems via their sendable implementations
@@ -66,7 +66,7 @@ public class SendableHandler extends ElementHandler {
return "logSendable(backend.getNested(\""
+ loggedName(element)
+ "\"), "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ")";
}
}

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
@@ -38,11 +39,11 @@ public class StructHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ", "
+ structAccess(dataType(element))
+ ")";

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public class SupplierHandler extends ElementHandler {
@@ -42,15 +43,19 @@ public class SupplierHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
public String logInvocation(Element element, TypeElement loggedClass) {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
@Override
public String elementAccess(Element element) {
public String elementAccess(Element element, TypeElement loggedClass) {
var typeUtils = m_processingEnv.getTypeUtils();
var dataType = dataType(element);
String base = super.elementAccess(element);
String base = super.elementAccess(element, loggedClass);
if (typeUtils.isAssignable(dataType, m_booleanSupplier)) {
return base + ".getAsBoolean()";