Skip to content

Commit 1dc1d2f

Browse files
committed
Improve missing-registration errors for unsatisfied conditions
Thread RuntimeDynamicAccessMetadata through reflection, resource, and serialization lookup/reporting paths, and include unsatisfied runtime conditions in Missing*RegistrationError messages when matching metadata exists but conditions are not satisfied. Tighten condition-aware lookup behavior in DynamicHub/reflection paths, preserve serialization constructor-accessor metadata across image layers, add proxy interface-order hints for near matches, and fix createURLs/getResources missing-metadata reporting for unsatisfied resource include patterns (including globs). Also add a TrackConditionSatisfied testing option for condition transitions.
1 parent 2cd5581 commit 1dc1d2f

32 files changed

Lines changed: 830 additions & 242 deletions

substratevm/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ Do not run `mx` commands concurrently; parallel runs can produce misleading fail
2525

2626
## Change Hygiene
2727

28+
- Do not use lambdas in runtime code.
2829
- If you touch documented behavior, update `docs/`.
2930
- Do not commit generated output from `mxbuild/`, `svmbuild/`, `graal_dumps/`, or `sources/`.

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/MissingRegistrationUtils.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import com.oracle.svm.configure.config.ConfigurationMemberInfo;
4747
import com.oracle.svm.configure.config.ConfigurationMethod;
4848
import com.oracle.svm.configure.config.ConfigurationType;
49+
import com.oracle.svm.core.configure.RuntimeDynamicAccessMetadata;
4950
import com.oracle.svm.core.util.ExitStatus;
5051
import com.oracle.svm.shared.util.VMError;
5152
import com.oracle.svm.shared.util.StringUtil;
@@ -173,15 +174,42 @@ protected static String quote(String element) {
173174
protected static String registrationMessage(String failedAction, String elementDescriptor, String json, String accessManner, String section, String helpLink) {
174175
/* Can't use multi-line strings as they pull in format and bloat "Hello, World!" */
175176
String optionalSpace = accessManner.isEmpty() ? "" : " ";
176-
return "Cannot" + optionalSpace + accessManner + " " + failedAction + " " + elementDescriptor + ". To allow this operation, add the following to the '" + section +
177-
"' section of 'reachability-metadata.json' and rebuild the native image:" + System.lineSeparator() +
177+
return "Cannot" + optionalSpace + accessManner + " " + failedAction + " " + elementDescriptor + "." + System.lineSeparator() +
178+
System.lineSeparator() +
179+
"The matching metadata element in the '" + section + "' section of 'reachability-metadata.json' is:" + System.lineSeparator() +
178180
System.lineSeparator() +
179181
json + System.lineSeparator() +
180182
System.lineSeparator() +
181183
"The 'reachability-metadata.json' file should be located in 'META-INF/native-image/<group-id>/<artifact-id>/' of your project. For further help, see https://www.graalvm.org/latest/reference-manual/native-image/metadata/#" +
182184
helpLink;
183185
}
184186

187+
protected static String appendUnsatisfiedConditions(String message, RuntimeDynamicAccessMetadata dynamicAccessMetadata) {
188+
if (dynamicAccessMetadata == null) {
189+
return message;
190+
}
191+
String conditions = dynamicAccessMetadata.formatUnsatisfiedConditionsAsJson();
192+
if (conditions.isEmpty()) {
193+
return message;
194+
}
195+
String lineSeparator = System.lineSeparator();
196+
String paragraphSeparator = lineSeparator + lineSeparator;
197+
int splitIndex = message.indexOf(paragraphSeparator);
198+
if (splitIndex == -1) {
199+
return message + paragraphSeparator +
200+
"Reachability metadata for this access was found, but it is inactive because its runtime conditions were not satisfied." + lineSeparator +
201+
"To fix this, either change/remove the metadata condition, or make sure the condition is reached before this access." + lineSeparator +
202+
"Unsatisfied runtime conditions:" + paragraphSeparator +
203+
conditions;
204+
}
205+
return message.substring(0, splitIndex) + paragraphSeparator +
206+
"Reachability metadata for this access was found, but it is inactive because its runtime conditions were not satisfied." + lineSeparator +
207+
"To fix this, either change/remove the metadata condition, or make sure the condition is reached before this access." + lineSeparator +
208+
"Unsatisfied runtime conditions:" + paragraphSeparator +
209+
conditions + paragraphSeparator +
210+
message.substring(splitIndex + paragraphSeparator.length());
211+
}
212+
185213
protected static ConfigurationType namedConfigurationType(String typeName) {
186214
return new ConfigurationType(UnresolvedAccessCondition.unconditional(), new NamedConfigurationTypeDescriptor(typeName), true);
187215
}

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/code/RuntimeMetadataDecoderImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import com.oracle.svm.shared.singletons.MultiLayeredImageSingleton;
4848
import com.oracle.svm.core.reflect.RuntimeMetadataDecoder;
4949
import com.oracle.svm.core.reflect.target.ReflectionObjectFactory;
50+
import com.oracle.svm.core.reflect.target.Target_java_lang_reflect_AccessibleObject;
5051
import com.oracle.svm.core.reflect.target.Target_java_lang_reflect_Constructor;
5152
import com.oracle.svm.core.reflect.target.Target_java_lang_reflect_Executable;
5253
import com.oracle.svm.core.reflect.target.Target_java_lang_reflect_Field;
@@ -400,6 +401,7 @@ private static Object decodeField(UnsafeArrayTypeReader buf, Class<?> declaringC
400401
RuntimeDynamicAccessMetadata dynamicAccessMetadata = decodeDynamicAccessMetadata(buf, layerId, preserved);
401402
if (inHeap) {
402403
Field field = (Field) decodeObject(buf, layerId);
404+
SubstrateUtil.cast(field, Target_java_lang_reflect_AccessibleObject.class).dynamicAccessMetadata = RuntimeDynamicAccessMetadata.emptySet(preserved);
403405
if (publicOnly && !Modifier.isPublic(field.getModifiers())) {
404406
/*
405407
* Generate negative copy of the field. Finding a non-public field when looking for
@@ -569,6 +571,7 @@ private static Object decodeExecutable(UnsafeArrayTypeReader buf, Class<?> decla
569571
RuntimeDynamicAccessMetadata dynamicAccessMetadata = decodeDynamicAccessMetadata(buf, layerId, preserved);
570572
if (inHeap) {
571573
Executable executable = (Executable) decodeObject(buf, layerId);
574+
SubstrateUtil.cast(executable, Target_java_lang_reflect_AccessibleObject.class).dynamicAccessMetadata = RuntimeDynamicAccessMetadata.emptySet(preserved);
572575
if (publicOnly && !Modifier.isPublic(executable.getModifiers())) {
573576
/*
574577
* Generate negative copy of the executable. Finding a non-public method when

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/ConfigurationFiles.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.stream.Collectors;
3737
import java.util.stream.Stream;
3838

39+
import jdk.graal.compiler.api.replacements.Fold;
3940
import org.graalvm.nativeimage.Platform;
4041
import org.graalvm.nativeimage.Platforms;
4142

@@ -156,6 +157,18 @@ public static final class Options {
156157
@Option(help = "Testing flag: the 'typeReached' condition is always satisfied however it prints the stack trace where it would not be satisfied.")//
157158
public static final HostedOptionKey<Boolean> TrackUnsatisfiedTypeReachedConditions = new HostedOptionKey<>(false);
158159

160+
@Option(help = "Testing flag: print the stack trace when the configured 'typeReached' condition becomes satisfied. " +
161+
"Value must be a fully qualified class name or '*' to match any type.")//
162+
public static final HostedOptionKey<String> TrackConditionSatisfied = new HostedOptionKey<>(null);
163+
164+
@Option(help = "Testing flag: print concise diagnostics for reflection class-query checks, including successes and failures.")//
165+
public static final HostedOptionKey<Boolean> TrackReflectionClassQueryChecks = new HostedOptionKey<>(false);
166+
167+
@Fold
168+
public static boolean trackReflectionClassQueryChecks() {
169+
return TrackReflectionClassQueryChecks.getValue();
170+
}
171+
159172
@Option(help = "Testing flag: print 'typeReached' conditions that are used on interfaces without default methods at build time.")//
160173
public static final HostedOptionKey<Boolean> TrackTypeReachedOnInterfaces = new HostedOptionKey<>(false);
161174

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/configure/RuntimeDynamicAccessMetadata.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
package com.oracle.svm.core.configure;
2727

2828
import static com.oracle.svm.core.configure.ConfigurationFiles.Options.TrackUnsatisfiedTypeReachedConditions;
29+
import static com.oracle.svm.core.configure.ConfigurationFiles.Options.TrackConditionSatisfied;
2930

3031
import java.util.Arrays;
3132
import java.util.HashSet;
33+
import java.util.StringJoiner;
3234
import java.util.Set;
3335

3436
import org.graalvm.collections.EconomicSet;
@@ -41,6 +43,8 @@
4143
import com.oracle.svm.shared.util.LogUtils;
4244
import com.oracle.svm.shared.util.VMError;
4345

46+
import jdk.graal.compiler.api.replacements.Fold;
47+
4448
/**
4549
* The dynamic access metadata for some value that can be accessed at run time. Contains a set of
4650
* {@link #conditions} that dictate whether the value (e.g., a resource) should be accessible; also
@@ -76,8 +80,11 @@ public synchronized void addCondition(AccessCondition cnd) {
7680
TypeReachabilityCondition reachabilityCondition = (TypeReachabilityCondition) cnd;
7781
VMError.guarantee(reachabilityCondition.isRuntimeChecked(), "Only runtime conditions can be added to the ConditionalRuntimeValue.");
7882
if (satisfied) {
83+
maybeTrackConditionSatisfied(reachabilityCondition, true);
7984
return;
8085
} else if (reachabilityCondition.isAlwaysTrue()) {
86+
maybeTrackConditionSatisfied(conditions, true);
87+
8188
conditions = null;
8289
satisfied = true;
8390
return;
@@ -129,6 +136,7 @@ public boolean satisfied() {
129136
} else {
130137
for (Object condition : localConditions) {
131138
if (isSatisfied(condition)) {
139+
maybeTrackConditionSatisfied(condition);
132140
conditions = null;
133141
satisfied = result = true;
134142
break;
@@ -146,6 +154,41 @@ public boolean satisfied() {
146154
return result;
147155
}
148156

157+
private static void maybeTrackConditionSatisfied(Object condition) {
158+
maybeTrackConditionSatisfied(condition, false);
159+
}
160+
161+
private static void maybeTrackConditionSatisfied(Object condition, boolean ignoredAtBuildTime) {
162+
if (condition == null) {
163+
return;
164+
} else if (condition instanceof Object[] conditionsArray) {
165+
for (Object singleCondition : conditionsArray) {
166+
maybeTrackConditionSatisfied(singleCondition, ignoredAtBuildTime);
167+
}
168+
return;
169+
}
170+
171+
String trackedType = trackConditionSatisfied();
172+
String typeName = null;
173+
if (condition instanceof Class<?> reachedTypeCondition) {
174+
typeName = reachedTypeCondition.getTypeName();
175+
} else if (condition instanceof TypeReachabilityCondition reachedTypeCondition) {
176+
typeName = reachedTypeCondition.getType().getTypeName();
177+
}
178+
if (trackedType != null && typeName != null && ("*".equals(trackedType) || typeName.equals(trackedType)) && !"java.lang.Object".equals(typeName)) {
179+
if (ignoredAtBuildTime) {
180+
LogUtils.info("Tracked runtime condition reached at build time and ignored: type = " + typeName);
181+
} else {
182+
LogUtils.info("Tracked runtime condition reached: type = " + typeName);
183+
}
184+
}
185+
}
186+
187+
@Fold
188+
public static String trackConditionSatisfied() {
189+
return TrackConditionSatisfied.getValue();
190+
}
191+
149192
/*
150193
* Used in snippets, returns true only if the condition was already satisfied beforehand.
151194
*/
@@ -157,6 +200,70 @@ public boolean isPreserved() {
157200
return preserved;
158201
}
159202

203+
/**
204+
* Returns a user-facing description of unresolved runtime conditions.
205+
*/
206+
public String formatUnsatisfiedConditions() {
207+
Object[] localConditions = conditions;
208+
if (satisfied || localConditions == null) {
209+
return "";
210+
}
211+
212+
StringJoiner joiner = new StringJoiner(", ");
213+
for (Object condition : localConditions) {
214+
if (condition instanceof Class<?> reachedTypeCondition) {
215+
/*
216+
* java.lang.Object is always reached in practice and adds noise to diagnostics.
217+
*/
218+
if (reachedTypeCondition == Object.class) {
219+
continue;
220+
}
221+
joiner.add("typeReached(" + DynamicHub.fromClass(reachedTypeCondition).getTypeName() + ")");
222+
} else {
223+
joiner.add(String.valueOf(condition));
224+
}
225+
}
226+
return joiner.toString();
227+
}
228+
229+
/**
230+
* Returns unresolved runtime conditions formatted as JSON.
231+
*/
232+
public String formatUnsatisfiedConditionsAsJson() {
233+
Object[] localConditions = conditions;
234+
if (satisfied || localConditions == null) {
235+
return "";
236+
}
237+
238+
String lineSeparator = System.lineSeparator();
239+
StringBuilder builder = new StringBuilder();
240+
boolean wroteAny = false;
241+
for (Object condition : localConditions) {
242+
if (!(condition instanceof Class<?> reachedTypeCondition)) {
243+
continue;
244+
}
245+
/*
246+
* java.lang.Object is always reached in practice and adds noise to diagnostics.
247+
*/
248+
if (reachedTypeCondition == Object.class) {
249+
continue;
250+
}
251+
if (wroteAny) {
252+
builder.append(lineSeparator);
253+
}
254+
builder.append(" \"typeReached\": \"").append(DynamicHub.fromClass(reachedTypeCondition).getTypeName()).append("\"").append(lineSeparator);
255+
wroteAny = true;
256+
}
257+
if (!wroteAny) {
258+
return "";
259+
}
260+
/*
261+
* Remove trailing line separator to keep spacing deterministic in user-facing messages.
262+
*/
263+
builder.setLength(builder.length() - lineSeparator.length());
264+
return builder.toString();
265+
}
266+
160267
@Platforms(Platform.HOSTED_ONLY.class)
161268
public void setNotPreserved() {
162269
this.preserved = false;

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/SubstrateAllocationSnippets.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ private static DynamicHub slowPathHubOrUnsafeInstantiationError(DynamicHub hub)
391391
return hub;
392392
} else {
393393
if (MissingRegistrationUtils.throwMissingRegistrationErrors()) {
394-
MissingReflectionRegistrationUtils.reportUnsafeAllocation(DynamicHub.toClass(hub));
394+
MissingReflectionRegistrationUtils.reportUnsafeAllocation(DynamicHub.toClass(hub), hub.getUnsafeInstantiateAsInstanceMetadata());
395395
}
396396
throw new IllegalArgumentException("Type " + DynamicHub.toClass(hub).getTypeName() + " is instantiated reflectively but was never registered." +
397397
" Register the type by adding \"unsafeAllocated\" for the type in " + ConfigurationFile.REFLECTION.getFileName() + ".");

0 commit comments

Comments
 (0)