Skip to content

Commit ee0a8a1

Browse files
authored
[epilogue] Support logging of protobuf-serializable types (#8229)
For parity with struct-serializable types. Change struct serialization to only apply to types with a public static final <type> struct field, instead of relying only on the marker interface (which is not always followed). Doing this allows fallthrough to the protobuf handler for types with dynamic structs but static protobuf serializers.
1 parent 3dbdfa1 commit ee0a8a1

File tree

15 files changed

+344
-3
lines changed

15 files changed

+344
-3
lines changed

epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/AnnotationProcessor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
112112
new MeasureHandler(processingEnv),
113113
new PrimitiveHandler(processingEnv),
114114
new SupplierHandler(processingEnv),
115-
new StructHandler(processingEnv), // prioritize struct over sendable
115+
new StructHandler(processingEnv), // prioritize struct over sendable and protobuf
116+
new ProtobufHandler(processingEnv), // then protobuf
116117
new SendableHandler(processingEnv));
117118

118119
m_epiloguerGenerator = new EpilogueGenerator(processingEnv, customLoggers);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) FIRST and other WPILib contributors.
2+
// Open Source Software; you can modify and/or share it under the terms of
3+
// the WPILib BSD license file in the root directory of this project.
4+
5+
package edu.wpi.first.epilogue.processor;
6+
7+
import java.util.Set;
8+
import javax.annotation.processing.ProcessingEnvironment;
9+
import javax.lang.model.element.Element;
10+
import javax.lang.model.element.Modifier;
11+
import javax.lang.model.element.TypeElement;
12+
import javax.lang.model.element.VariableElement;
13+
import javax.lang.model.type.TypeMirror;
14+
import javax.lang.model.util.Elements;
15+
import javax.lang.model.util.Types;
16+
17+
/**
18+
* Supports protobuf serializable types. Protobuf-serializable types are loggable if they have a
19+
* public static final {@code proto} field of a type that inherits from {@code Protobuf}.
20+
*/
21+
public class ProtobufHandler extends ElementHandler {
22+
private final TypeMirror m_serializable;
23+
private final TypeElement m_protobufType;
24+
private final Types m_typeUtils;
25+
private final Elements m_elementUtils;
26+
27+
protected ProtobufHandler(ProcessingEnvironment processingEnv) {
28+
super(processingEnv);
29+
30+
m_serializable =
31+
processingEnv
32+
.getElementUtils()
33+
.getTypeElement("edu.wpi.first.util.protobuf.ProtobufSerializable")
34+
.asType();
35+
m_protobufType =
36+
processingEnv.getElementUtils().getTypeElement("edu.wpi.first.util.protobuf.Protobuf");
37+
m_typeUtils = processingEnv.getTypeUtils();
38+
m_elementUtils = processingEnv.getElementUtils();
39+
}
40+
41+
@Override
42+
public boolean isLoggable(Element element) {
43+
return isLoggableType(dataType(element));
44+
}
45+
46+
/**
47+
* Checks if a type is protobuf-serializable: implements the ProtobufSerializable marker interface
48+
* and has a `public static final proto` field of a type that inherits from Protobuf with a
49+
* compatible generic type bound.
50+
*
51+
* @param type The type to check
52+
* @return true if the type is protobuf-serializable, false otherwise
53+
*/
54+
public boolean isLoggableType(TypeMirror type) {
55+
var serializableType = m_typeUtils.erasure(type);
56+
var typeElement = m_elementUtils.getTypeElement(serializableType.toString());
57+
if (typeElement == null) {
58+
return false;
59+
}
60+
61+
// eg `Protobuf<Rotation2d, ?>` instead of the raw `Protobuf` type. The message type doesn't
62+
// really matter here; we can leave it as a wildcard.
63+
var sharpProtobufType =
64+
m_typeUtils.getDeclaredType(
65+
m_protobufType,
66+
typeElement.asType(), // the serializable type
67+
m_typeUtils.getWildcardType(
68+
m_elementUtils.getTypeElement("us.hebi.quickbuf.ProtoMessage").asType(), null));
69+
70+
boolean hasProto =
71+
typeElement.getEnclosedElements().stream()
72+
.filter(e -> e instanceof VariableElement)
73+
.map(e -> (VariableElement) e)
74+
.anyMatch(
75+
field -> {
76+
var nameMatch = field.getSimpleName().contentEquals("proto");
77+
var modifiersMatch =
78+
field
79+
.getModifiers()
80+
.containsAll(Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
81+
var typeMatch =
82+
m_typeUtils.isAssignable(
83+
m_typeUtils.erasure(field.asType()), sharpProtobufType);
84+
return nameMatch && modifiersMatch && typeMatch;
85+
});
86+
return m_typeUtils.isAssignable(type, m_serializable) && hasProto;
87+
}
88+
89+
public String protoAccess(TypeMirror serializableType) {
90+
var className = m_typeUtils.erasure(serializableType).toString();
91+
return className + ".proto";
92+
}
93+
94+
@Override
95+
public String logInvocation(Element element, TypeElement loggedClass) {
96+
return "backend.log(\"%s\", %s, %s)"
97+
.formatted(
98+
loggedName(element),
99+
elementAccess(element, loggedClass),
100+
protoAccess(dataType(element)));
101+
}
102+
}

epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/StructHandler.java

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,26 @@
44

55
package edu.wpi.first.epilogue.processor;
66

7+
import java.util.Set;
78
import javax.annotation.processing.ProcessingEnvironment;
89
import javax.lang.model.element.Element;
10+
import javax.lang.model.element.Modifier;
911
import javax.lang.model.element.TypeElement;
12+
import javax.lang.model.element.VariableElement;
13+
import javax.lang.model.type.ArrayType;
1014
import javax.lang.model.type.TypeMirror;
15+
import javax.lang.model.util.Elements;
1116
import javax.lang.model.util.Types;
1217

18+
/**
19+
* Supports struct serializable types. Struct-serializable types are loggable if they have a public
20+
* static final {@code struct} field of a type that inherits from {@code Struct}.
21+
*/
1322
public class StructHandler extends ElementHandler {
1423
private final TypeMirror m_serializable;
24+
private final TypeElement m_structType;
1525
private final Types m_typeUtils;
26+
private final Elements m_elementUtils;
1627

1728
protected StructHandler(ProcessingEnvironment processingEnv) {
1829
super(processingEnv);
@@ -21,16 +32,57 @@ protected StructHandler(ProcessingEnvironment processingEnv) {
2132
.getElementUtils()
2233
.getTypeElement("edu.wpi.first.util.struct.StructSerializable")
2334
.asType();
35+
m_structType =
36+
processingEnv.getElementUtils().getTypeElement("edu.wpi.first.util.struct.Struct");
2437
m_typeUtils = processingEnv.getTypeUtils();
38+
m_elementUtils = processingEnv.getElementUtils();
2539
}
2640

2741
@Override
2842
public boolean isLoggable(Element element) {
29-
return m_typeUtils.isAssignable(dataType(element), m_serializable);
43+
return isLoggableType(dataType(element));
3044
}
3145

46+
/**
47+
* Checks if a type is struct-serializable: implements the StructSerializable marker interface and
48+
* has a `public static final struct` field of a type that inherits from Struct with a compatible
49+
* generic type bound.
50+
*
51+
* @param type The type to check
52+
* @return true if the type is struct-serializable, false otherwise
53+
*/
3254
public boolean isLoggableType(TypeMirror type) {
33-
return m_typeUtils.isAssignable(type, m_serializable);
55+
TypeMirror serializableType;
56+
if (type instanceof ArrayType arr) {
57+
serializableType = arr.getComponentType();
58+
} else {
59+
serializableType = m_typeUtils.erasure(type);
60+
}
61+
var typeElement = m_elementUtils.getTypeElement(serializableType.toString());
62+
if (typeElement == null) {
63+
return false;
64+
}
65+
66+
// eg `Struct<Rotation2d>` instead of the raw `Struct` type
67+
var sharpStructType = m_typeUtils.getDeclaredType(m_structType, typeElement.asType());
68+
69+
boolean hasStruct =
70+
typeElement.getEnclosedElements().stream()
71+
.filter(e -> e instanceof VariableElement)
72+
.map(e -> (VariableElement) e)
73+
.anyMatch(
74+
field -> {
75+
var nameMatch = field.getSimpleName().contentEquals("struct");
76+
var modifiersMatch =
77+
field
78+
.getModifiers()
79+
.containsAll(Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
80+
var typeMatch =
81+
m_typeUtils.isAssignable(
82+
m_typeUtils.erasure(field.asType()), sharpStructType);
83+
return nameMatch && modifiersMatch && typeMatch;
84+
});
85+
return m_typeUtils.isAssignable(type, m_serializable) && hasStruct;
3486
}
3587

3688
public String structAccess(TypeMirror serializableType) {

epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/AnnotationProcessorTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,76 @@ public void update(EpilogueBackend backend, Example object) {
11411141
assertLoggerGenerates(source, expectedGeneratedSource);
11421142
}
11431143

1144+
@Test
1145+
void protobuf() {
1146+
String source =
1147+
"""
1148+
package edu.wpi.first.epilogue;
1149+
1150+
import edu.wpi.first.util.protobuf.Protobuf;
1151+
import edu.wpi.first.util.protobuf.ProtobufSerializable;
1152+
import java.util.List;
1153+
import us.hebi.quickbuf.*;
1154+
1155+
class ProtobufType implements ProtobufSerializable {
1156+
// Message type is necessary - Epilogue can't log with a wildcard message type in the proto
1157+
public static final Protobuf<ProtobufType, Message> proto = null; // value doesn't matter
1158+
1159+
static class Message extends ProtoMessage<Message> {
1160+
// Implement stubs for the abstract base class.
1161+
// This code never runs so actual implementations are unnecessary.
1162+
@Override
1163+
public Message copyFrom(Message other) { return null; }
1164+
@Override
1165+
public Message clear() { return null; }
1166+
@Override
1167+
public int computeSerializedSize() { return 0; }
1168+
@Override
1169+
public void writeTo(ProtoSink output) {}
1170+
@Override
1171+
public Message mergeFrom(ProtoSource input) { return null; }
1172+
@Override
1173+
public boolean equals(Object obj) { return false; }
1174+
@Override
1175+
public Message clone() { return null; }
1176+
}
1177+
}
1178+
1179+
@Logged
1180+
class Example {
1181+
ProtobufType x; // Should be logged
1182+
ProtobufType[] arr1; // Should not be logged
1183+
ProtobufType[][] arr2; // Should not be logged
1184+
List<ProtobufType> list; // Should not be logged
1185+
}
1186+
""";
1187+
1188+
String expectedGeneratedSource =
1189+
"""
1190+
package edu.wpi.first.epilogue;
1191+
1192+
import edu.wpi.first.epilogue.Logged;
1193+
import edu.wpi.first.epilogue.Epilogue;
1194+
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
1195+
import edu.wpi.first.epilogue.logging.EpilogueBackend;
1196+
1197+
public class ExampleLogger extends ClassSpecificLogger<Example> {
1198+
public ExampleLogger() {
1199+
super(Example.class);
1200+
}
1201+
1202+
@Override
1203+
public void update(EpilogueBackend backend, Example object) {
1204+
if (Epilogue.shouldLog(Logged.Importance.DEBUG)) {
1205+
backend.log("x", object.x, edu.wpi.first.epilogue.ProtobufType.proto);
1206+
}
1207+
}
1208+
}
1209+
""";
1210+
1211+
assertLoggerGenerates(source, expectedGeneratedSource);
1212+
}
1213+
11441214
@Test
11451215
void lists() {
11461216
String source =

epilogue-runtime/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ java_library(
88
"//ntcore:networktables-java",
99
"//wpiunits",
1010
"//wpiutil:wpiutil-java",
11+
"@maven//:us_hebi_quickbuf_quickbuf_runtime",
1112
],
1213
)

epilogue-runtime/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ dependencies {
1313
api(project(':ntcore'))
1414
api(project(':wpiutil'))
1515
api(project(':wpiunits'))
16+
testImplementation(project(':wpimath')) // for convenient protobuf types
1617
}

epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/EpilogueBackend.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
import edu.wpi.first.units.Measure;
88
import edu.wpi.first.units.Unit;
9+
import edu.wpi.first.util.protobuf.Protobuf;
910
import edu.wpi.first.util.struct.Struct;
1011
import java.util.Collection;
12+
import us.hebi.quickbuf.ProtoMessage;
1113

1214
/** A backend is a generic interface for Epilogue to log discrete data points. */
1315
public interface EpilogueBackend {
@@ -193,6 +195,17 @@ default <S> void log(String identifier, Collection<S> value, Struct<S> struct) {
193195
log(identifier, array, struct);
194196
}
195197

198+
/**
199+
* Logs a protobuf-serializable object.
200+
*
201+
* @param identifier the identifier of the data point
202+
* @param value the value of the data point
203+
* @param proto the protobuf to use to serialize the data
204+
* @param <P> the protobuf-serializable type
205+
* @param <M> the protobuf message type
206+
*/
207+
<P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto);
208+
196209
/**
197210
* Logs a measurement's value in terms of its base unit.
198211
*

epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/FileBackend.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,28 @@
1616
import edu.wpi.first.util.datalog.FloatLogEntry;
1717
import edu.wpi.first.util.datalog.IntegerArrayLogEntry;
1818
import edu.wpi.first.util.datalog.IntegerLogEntry;
19+
import edu.wpi.first.util.datalog.ProtobufLogEntry;
1920
import edu.wpi.first.util.datalog.RawLogEntry;
2021
import edu.wpi.first.util.datalog.StringArrayLogEntry;
2122
import edu.wpi.first.util.datalog.StringLogEntry;
2223
import edu.wpi.first.util.datalog.StructArrayLogEntry;
2324
import edu.wpi.first.util.datalog.StructLogEntry;
25+
import edu.wpi.first.util.protobuf.Protobuf;
2426
import edu.wpi.first.util.struct.Struct;
2527
import java.util.HashMap;
2628
import java.util.HashSet;
2729
import java.util.Map;
2830
import java.util.Set;
2931
import java.util.function.BiFunction;
32+
import us.hebi.quickbuf.ProtoMessage;
3033

3134
/** A backend implementation that saves information to a WPILib {@link DataLog} file on disk. */
3235
public class FileBackend implements EpilogueBackend {
3336
private final DataLog m_dataLog;
3437
private final Map<String, DataLogEntry> m_entries = new HashMap<>();
3538
private final Map<String, NestedBackend> m_subLoggers = new HashMap<>();
3639
private final Set<Struct<?>> m_seenSchemas = new HashSet<>();
40+
private final Set<Protobuf<?, ?>> m_seenProtos = new HashSet<>();
3741

3842
/**
3943
* Creates a new file-based backend.
@@ -166,4 +170,19 @@ public <S> void log(String identifier, S[] value, Struct<S> struct) {
166170

167171
((StructArrayLogEntry<S>) m_entries.get(identifier)).append(value);
168172
}
173+
174+
@Override
175+
@SuppressWarnings("unchecked")
176+
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
177+
// DataLog.addSchema has checks that we're able to skip, avoiding allocations
178+
if (m_seenProtos.add(proto)) {
179+
m_dataLog.addSchema(proto);
180+
}
181+
182+
if (!m_entries.containsKey(identifier)) {
183+
m_entries.put(identifier, ProtobufLogEntry.create(m_dataLog, identifier, proto));
184+
}
185+
186+
((ProtobufLogEntry<P>) m_entries.get(identifier)).append(value);
187+
}
169188
}

epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/LazyBackend.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
package edu.wpi.first.epilogue.logging;
66

7+
import edu.wpi.first.util.protobuf.Protobuf;
78
import edu.wpi.first.util.struct.Struct;
89
import java.util.Arrays;
910
import java.util.HashMap;
1011
import java.util.Map;
1112
import java.util.Objects;
13+
import us.hebi.quickbuf.ProtoMessage;
1214

1315
/**
1416
* A backend implementation that only logs data when it changes. Useful for keeping bandwidth and
@@ -243,4 +245,17 @@ public <S> void log(String identifier, S[] value, Struct<S> struct) {
243245
m_previousValues.put(identifier, value.clone());
244246
m_backend.log(identifier, value, struct);
245247
}
248+
249+
@Override
250+
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
251+
var previous = m_previousValues.get(identifier);
252+
253+
if (Objects.equals(previous, value)) {
254+
// no change
255+
return;
256+
}
257+
258+
m_previousValues.put(identifier, value);
259+
m_backend.log(identifier, value, proto);
260+
}
246261
}

0 commit comments

Comments
 (0)