Skip to content

Commit cdb7fc9

Browse files
committed
Adding the experimental option -H:Preserve
The -H:Preserve option can be used to preserve all classes for reflective access in the image for: 1. All elements in the JDK and the classapth `-H:Preserve=all` 2. A given module `-H:Preserve=module=<module-name>` 3. For the whole classpath `-H:Preserve=module=ALL-UNNAMED` 4. For a package `-H:Preserve=package=<package-name>`
1 parent 719c89f commit cdb7fc9

File tree

21 files changed

+418
-114
lines changed

21 files changed

+418
-114
lines changed

sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/RuntimeReflection.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -231,7 +231,7 @@ public static void registerAllFields(Class<?> declaringClass) {
231231
* @since 23.0
232232
*/
233233
public static void registerAllDeclaredFields(Class<?> declaringClass) {
234-
ImageSingletons.lookup(RuntimeReflectionSupport.class).registerAllDeclaredFieldsQuery(ConfigurationCondition.alwaysTrue(), declaringClass);
234+
ImageSingletons.lookup(RuntimeReflectionSupport.class).registerAllDeclaredFields(ConfigurationCondition.alwaysTrue(), declaringClass);
235235
}
236236

237237
/**

sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeReflectionSupport.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -48,7 +48,7 @@ public interface RuntimeReflectionSupport extends ReflectionRegistry {
4848

4949
void registerAllFieldsQuery(ConfigurationCondition condition, Class<?> clazz);
5050

51-
void registerAllDeclaredFieldsQuery(ConfigurationCondition condition, Class<?> clazz);
51+
void registerAllDeclaredFields(ConfigurationCondition condition, Class<?> clazz);
5252

5353
void registerAllConstructorsQuery(ConfigurationCondition condition, boolean queriedOnly, Class<?> clazz);
5454

substratevm/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This changelog summarizes major changes to GraalVM Native Image.
99
* (GR-59864) Added JVM version check to the Native Image agent. The agent will abort execution if the JVM major version does not match the version it was built with, and warn if the full JVM version is different.
1010
* (GR-59135) Verify if hosted options passed to `native-image` exist prior to starting the builder. Provide suggestions how to fix unknown options early on.
1111
* (GR-61492) The experimental JDWP option is now present in standard GraalVM builds.
12+
* (GR-54953) Add the experimental option `-H:Preserve=[all],[module=<module-name>],[path=<classpath-entry>],[package=<package-name>]` to preserve all elements in the image allowing any dynamic access such as reflection or resource access. Using `-H:Preserve=all` should make most of time images run immediately without requiring reachability metadata. Certain exceptions such as dynamic bytecode generation, multi-interface proxy classes, and foreign function calls done via Panama will not work out of the box.
1213

1314
## GraalVM for JDK 24 (Internal Version 24.2.0)
1415
* (GR-59717) Added `DuringSetupAccess.registerObjectReachabilityHandler` to allow registering a callback that is executed when an object of a specified type is marked as reachable during heap scanning.

substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ClassInclusionPolicy.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,28 @@ public void setBigBang(BigBang bb) {
5656
this.bb = bb;
5757
}
5858

59+
public static boolean isClassIncludedBase(Class<?> cls) {
60+
if (Feature.class.isAssignableFrom(cls)) {
61+
return false;
62+
}
63+
64+
if (AnnotationAccess.isAnnotationPresent(cls, TargetClass.class)) {
65+
return false;
66+
}
67+
try {
68+
Class<?> enclosingClass = cls.getEnclosingClass();
69+
return enclosingClass == null || isClassIncludedBase(enclosingClass);
70+
} catch (LinkageError e) {
71+
return true;
72+
}
73+
}
74+
5975
/**
6076
* Determine if the given class needs to be included in the image according to the policy.
6177
*/
6278
public boolean isClassIncluded(Class<?> cls) {
6379
Class<?> enclosingClass = cls.getEnclosingClass();
64-
return !Feature.class.isAssignableFrom(cls) && !AnnotationAccess.isAnnotationPresent(cls, TargetClass.class) && (enclosingClass == null || isClassIncluded(enclosingClass));
80+
return isClassIncludedBase(cls) && (enclosingClass == null || isClassIncluded(enclosingClass));
6581
}
6682

6783
/**

substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/PointsToAnalysis.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ public void onTypeInstantiated(AnalysisType type) {
666666
/* Register the type as instantiated with all its super types. */
667667

668668
assert type.isInstantiated() : type;
669-
AnalysisError.guarantee(type.isArray() || (type.isInstanceClass() && !type.isAbstract()));
669+
AnalysisError.guarantee(type.isArray() || (type.isInstanceClass() && !type.isAbstract()), "Type %s must be either an array, or a non abstract instance class", type.getName());
670670

671671
TypeState typeState = TypeState.forExactType(this, type, true);
672672
TypeState typeStateNonNull = TypeState.forExactType(this, type, false);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
package com.oracle.svm.core;
26+
27+
public record IncludeExtendedOption(String key, String value) {
28+
29+
public static final String PACKAGE_OPTION = "package";
30+
public static final String MODULE_OPTION = "module";
31+
public static final String PATH_OPTION = "path";
32+
33+
public static IncludeExtendedOption parse(String option) {
34+
String[] optionParts = SubstrateUtil.split(option, "=", 2);
35+
if (optionParts.length == 2) {
36+
return new IncludeExtendedOption(optionParts[0], optionParts[1]);
37+
} else {
38+
return new IncludeExtendedOption(option, null);
39+
}
40+
}
41+
}

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,14 @@
3535
import java.nio.file.InvalidPathException;
3636
import java.nio.file.Path;
3737
import java.nio.file.Paths;
38+
import java.util.Arrays;
39+
import java.util.HashSet;
3840
import java.util.List;
41+
import java.util.Set;
3942
import java.util.UUID;
4043
import java.util.function.Predicate;
44+
import java.util.stream.Collectors;
45+
import java.util.stream.Stream;
4146

4247
import org.graalvm.collections.EconomicMap;
4348
import org.graalvm.collections.UnmodifiableEconomicMap;
@@ -57,6 +62,7 @@
5762
import com.oracle.svm.core.option.GCOptionValue;
5863
import com.oracle.svm.core.option.HostedOptionKey;
5964
import com.oracle.svm.core.option.HostedOptionValues;
65+
import com.oracle.svm.core.option.LocatableMultiOptionValue;
6066
import com.oracle.svm.core.option.OptionMigrationMessage;
6167
import com.oracle.svm.core.option.ReplacingLocatableMultiOptionValue;
6268
import com.oracle.svm.core.option.RuntimeOptionKey;
@@ -1326,6 +1332,73 @@ public enum ReportingMode {
13261332
@Option(help = "Include all classes, methods, fields, and resources from the class path", type = OptionType.Debug) //
13271333
public static final HostedOptionKey<Boolean> IncludeAllFromClassPath = new HostedOptionKey<>(false);
13281334

1335+
@Option(help = "Preserve classes, methods, fields, and resources in the image.")//
1336+
public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> Preserve = new HostedOptionKey<>(AccumulatingLocatableMultiOptionValue.Strings.build());
1337+
1338+
public record IncludeEntry(String value, OptionKey<?> option, String optionValue, String optionOrigin) {
1339+
}
1340+
1341+
public static final Set<IncludeEntry> PRESERVED_MODULES = new HashSet<>();
1342+
public static final Set<IncludeEntry> PRESERVED_PACKAGES = new HashSet<>();
1343+
public static final Set<IncludeEntry> PRESERVED_CLASSPATH_ENTRIES = new HashSet<>();
1344+
public static boolean preserveAll;
1345+
1346+
public static final String PRESERVE_ALL = "all";
1347+
public static final String PRESERVE_NONE = "none";
1348+
1349+
public static final String PRESERVE_POSSIBLE_OPTIONS;
1350+
static {
1351+
String msg = "[" + PRESERVE_ALL + ", " + PRESERVE_NONE + ", ";
1352+
msg += Stream.of(IncludeExtendedOption.MODULE_OPTION, IncludeExtendedOption.PACKAGE_OPTION, IncludeExtendedOption.PATH_OPTION)
1353+
.map(option -> option + "=" + "<" + option + ">")
1354+
.collect(Collectors.joining(", "));
1355+
msg += "]";
1356+
PRESERVE_POSSIBLE_OPTIONS = msg;
1357+
}
1358+
1359+
public static void parsePreserveOption(EconomicMap<OptionKey<?>, Object> hostedValues) {
1360+
AccumulatingLocatableMultiOptionValue.Strings preserve = Preserve.getValue(new OptionValues(hostedValues));
1361+
Stream<LocatableMultiOptionValue.ValueWithOrigin<String>> valuesWithOrigins = preserve.getValuesWithOrigins();
1362+
valuesWithOrigins.forEach(valueWithOrigin -> {
1363+
String optionValue = valueWithOrigin.value();
1364+
String optionOrigin = valueWithOrigin.origin().toString();
1365+
1366+
var extendedOptions = Arrays.stream(valueWithOrigin.value().split(",")).toList();
1367+
for (String extendedOption : extendedOptions) {
1368+
UserError.guarantee(!extendedOption.isEmpty(), "Option %s from %s cannot be passed an empty string. The possible options are: %s",
1369+
SubstrateOptionsParser.commandArgument(Preserve, optionValue), optionOrigin, PRESERVE_POSSIBLE_OPTIONS);
1370+
if (extendedOption.equals(PRESERVE_ALL)) {
1371+
preserveAll = true;
1372+
} else if (extendedOption.equals(PRESERVE_NONE)) {
1373+
preserveAll = false;
1374+
} else {
1375+
IncludeExtendedOption subOption = IncludeExtendedOption.parse(extendedOption);
1376+
final IncludeEntry preservedEntry = new IncludeEntry(subOption.value(), Preserve, optionValue, optionOrigin);
1377+
switch (subOption.key()) {
1378+
case IncludeExtendedOption.MODULE_OPTION -> {
1379+
UserError.guarantee(subOption.value() != null, "Option %s in %s from %s requires a module name argument, e.g., %s=module-name.", subOption.key(), optionValue, optionOrigin,
1380+
subOption.key());
1381+
SubstrateOptions.PRESERVED_MODULES.add(preservedEntry);
1382+
}
1383+
case IncludeExtendedOption.PACKAGE_OPTION -> {
1384+
UserError.guarantee(subOption.value() != null, "Option %s in %s from %s requires a package name argument, e.g., %s=package-name.", subOption.key(), optionValue,
1385+
optionOrigin, subOption.key());
1386+
SubstrateOptions.PRESERVED_PACKAGES.add(preservedEntry);
1387+
}
1388+
case IncludeExtendedOption.PATH_OPTION -> {
1389+
UserError.guarantee(subOption.value() != null, "Option %s in %s from %s requires a class-path entry, e.g., %s=path/to/jar-file.", subOption.key(), optionValue,
1390+
optionOrigin, subOption.key());
1391+
SubstrateOptions.PRESERVED_CLASSPATH_ENTRIES.add(preservedEntry);
1392+
}
1393+
1394+
default -> throw UserError.abort("Unknown option %s used on %s. The possible options are: %s", subOption.key(), optionOrigin, PRESERVE_POSSIBLE_OPTIONS);
1395+
}
1396+
}
1397+
1398+
}
1399+
});
1400+
}
1401+
13291402
public static boolean includeAll() {
13301403
return IncludeAllFromModule.hasBeenSet() || IncludeAllFromPath.hasBeenSet() || IncludeAllFromPackage.hasBeenSet() || IncludeAllFromClassPath.hasBeenSet();
13311404
}

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors;
2828

29+
import java.lang.reflect.Modifier;
2930
import java.util.EnumSet;
3031
import java.util.Objects;
3132

@@ -159,7 +160,7 @@ public void registerNegativeQuery(ConfigurationCondition condition, String class
159160

160161
@Platforms(Platform.HOSTED_ONLY.class)
161162
public void registerUnsafeAllocated(ConfigurationCondition condition, Class<?> clazz) {
162-
if (!clazz.isArray()) {
163+
if (!clazz.isArray() && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) {
163164
var conditionSet = unsafeInstantiatedClasses.putIfAbsent(clazz, RuntimeConditionSet.createHosted(condition));
164165
if (conditionSet != null) {
165166
conditionSet.addCondition(condition);

substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/CmdLineOptionHandler.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@
3636
import com.oracle.svm.core.option.OptionOrigin;
3737
import com.oracle.svm.core.util.ExitStatus;
3838
import com.oracle.svm.driver.NativeImage.ArgumentQueue;
39-
import com.oracle.svm.hosted.imagelayer.LayerArchiveSupport;
40-
import com.oracle.svm.hosted.imagelayer.LayerOptionsSupport.ExtendedOption;
39+
import com.oracle.svm.core.IncludeExtendedOption;
4140
import com.oracle.svm.hosted.imagelayer.LayerOptionsSupport.LayerOption;
4241
import com.oracle.svm.util.LogUtils;
4342

@@ -146,9 +145,9 @@ private boolean consume(ArgumentQueue args, String headArg) {
146145
String layerCreateValue = headArg.substring(SubstrateOptions.LAYER_CREATE_OPTION.length());
147146
if (!layerCreateValue.isEmpty()) {
148147
LayerOption layerOption = LayerOption.parse(layerCreateValue);
149-
for (ExtendedOption option : layerOption.extendedOptions()) {
148+
for (IncludeExtendedOption option : layerOption.extendedOptions()) {
150149
switch (option.key()) {
151-
case LayerArchiveSupport.PACKAGE_OPTION -> {
150+
case IncludeExtendedOption.PACKAGE_OPTION -> {
152151
String packageName = option.value();
153152
String moduleName = nativeImage.systemPackagesToModules.get(packageName);
154153
if (moduleName != null) {

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ClassLoaderSupportImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ public void collectResources(ResourceCollector resourceCollector) {
116116

117117
/* Collect remaining resources from classpath */
118118
classLoaderSupport.classpath().stream().parallel().forEach(classpathFile -> {
119-
boolean includeCurrent = classLoaderSupport.getJavaPathsToInclude().contains(classpathFile) || classLoaderSupport.includeAllFromClassPath();
119+
boolean includeCurrent = classLoaderSupport.getPathsToInclude().contains(classpathFile) || classLoaderSupport.includeAllFromClassPath() ||
120+
classLoaderSupport.getClassPathEntriesToPreserve().contains(classpathFile) || classLoaderSupport.isEnabledDynamicAccess();
120121
try {
121122
if (Files.isDirectory(classpathFile)) {
122123
scanDirectory(classpathFile, resourceCollector, includeCurrent);
@@ -132,7 +133,8 @@ public void collectResources(ResourceCollector resourceCollector) {
132133
private void collectResourceFromModule(ResourceCollector resourceCollector, ResourceLookupInfo info) {
133134
ModuleReference moduleReference = info.resolvedModule.reference();
134135
try (ModuleReader moduleReader = moduleReference.open()) {
135-
boolean includeCurrent = classLoaderSupport.getJavaModuleNamesToInclude().contains(info.resolvedModule().name());
136+
boolean includeCurrent = classLoaderSupport.getModuleNamesToInclude().contains(info.resolvedModule().name()) ||
137+
classLoaderSupport.getModuleNamesToPreserve().contains(info.resolvedModule().name());
136138
List<ConditionalResource> resourcesFound = new ArrayList<>();
137139
moduleReader.list().forEach(resourceName -> {
138140
var conditionsWithOrigins = shouldIncludeEntry(info.module, resourceCollector, resourceName, moduleReference.location().orElse(null), includeCurrent);

0 commit comments

Comments
 (0)