Skip to main content

Substitution

Due to limitations of Native Image, some code needs to be changed to run in native images. This is done by substituting existing classes with new classes during native image generation. Using substitutions is the most powerful technique to support native images.

Substitutions are declared using annotations in the com.oracle.svm.core.annotate package of GraalVM SDK. These annotated classes are used by native-image tool during the generation of native images.

Examples

Source code from the GraalVM SDK is used to demonstrate the usage of substitutions.

Target Class

The first step is to identify the class to substitute. @TargetClass annotation is used to annotate a class that modifies methods or fields of another class. The class to be modified is called the original class. The table below shows attributes of @TargetClass.

AttributeTypeDefault valueDescription
valueClass<?>TargetClass.classOriginal class to substitute
classNameString""Original class substitute
classNameProviderClass<? extends Function<TargetClass, String>>NoClassNameProvider.classUse a function to provide the original class to substitute
innerClassString[]{}The suffix of the original class name when it is an inner class
onlyWithClass<?>[]TargetClass.AlwaysIncluded.classSubstitute only if all provided predicates are true. The classes must either implement BooleanSupplier or Predicate<String>

value, className and classNameProvider attributes are different ways to provide the original class to substitute.

The original class can be modified in different ways, depends on the annotations present on the class annotated with @TargetClass.

Substitution class names

You are free to use any class names for substitutions. The convention is to name the substitution class after the target class. For example, Target_java_lang_Thread is the recommended name for the java.lang.Thread class.

Alias

If no @Delete or @Substitute annotations present, the annotated class is an alias for the original class. All methods and fields for the annotated class must be annotated with @Alias, @Delete, @Substitute, or @AnnotateOriginal.

@Alias

When @Alias is present, the annotated method or field is an alias for the original element. The annotated element doesn't exist in the runtime. All usages of the annotated element reference the original element. By using @Alias, we can refer to fields and methods that are inaccessible due to Java language access control rules.

The Target_java_util_Optional class comes from GraalVM SDK. It substitutes java.util.Optional class. The value field is annotated with @Alias. This field is an alias of the private final T value field from java.util.Optional. Then this value field can be used in the toString method.

Substitution of java.util.Optional
@TargetClass(Optional.class)
final class Target_java_util_Optional<T> {
@Alias T value;

@Override
@Substitute
public String toString() {
return value != null
? "Optional[" + value + "]"
: "Optional.empty";
}
}

@Delete

When @Delete is present, the annotated element doesn't exist in the runtime.

The Target_java_net_URL class comes from GraalVM SDK. The handlers field from java.net.URL doesn't exist in the runtime.

Substitution of java.net.URL
@TargetClass(java.net.URL.class)
final class Target_java_net_URL {

@Delete private static Hashtable<?, ?> handlers;

@Substitute
private static URLStreamHandler getURLStreamHandler(String protocol) throws MalformedURLException {
return JavaNetSubstitutions.getURLStreamHandler(protocol);
}

@Substitute
@SuppressWarnings("unused")
public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) {
VMError.unsupportedFeature("Setting a custom URLStreamHandlerFactory.");
}
}

@Substitute

When @Substitute is present, the annotated element is a substitute of the same element in the target class. We already see the examples of method substitutions in the Target_java_util_Optional and Target_java_net_URL classes.

When a method is annotated with @Substitute, the method to substitute is matched by the name and the list of parameter types. We can provide a different method name using the @TargetElement annotation. However, the list of parameter types must match.

@AnnotateOriginal

Whhen @AnnotateOriginal is present, all annotations on the annotated method are also present in the original method.

The Target_java_lang_Enum class comes from GraalVM SDK. With @AnnotateOriginal, the @Uninterruptible annotation is added to the ordinal method.

Substitution of java.lang.Enum
@TargetClass(java.lang.Enum.class)
final class Target_java_lang_Enum {

@Substitute
private static Enum<?> valueOf(Class<Enum<?>> enumType, String name) {
// code omitted
}

@AnnotateOriginal
@Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
public native int ordinal();
}

Delete

If @Delete annotation is present, the annotated class and the original class don't exist.

In the code below, java.lang.ref.Finalizer class is deleted because Substrate VM does not support Finalizer references.

@TargetClass(className = "java.lang.ref.Finalizer")
@Delete
final class Target_java_lang_ref_Finalizer {
}

Replace

If @Substitute annotation is present, the annotated class replaces the original class. All methods and fields of the original class are treated as deleted by default, with the following exception:

  • A method annotated with @Substitute replaces the original method.
  • A method annotated with @KeepOriginal keeps the original method.

Target Element

@TargetElement specifies additional properties for an element which is also annotated with @Alias, @Delete, @Substitute, @AnnotateOriginal, or @KeepOriginal.

The table below shows attributes of @TargetElement.

AttributeTypeDefault valueDescription
nameString""The name of the field or method in the original class
onlyWithClass<?>[]TargetClass.AlwaysIncluded.classSubstitute only if all provided predicates are true. The classes must either implement BooleanSupplier or Predicate<String>

In the code below, the getExtendedNPEMessage is a substitution of the same method in java.lang.NullPointerException, but only in JDK 17 or later.

@TargetClass(java.lang.NullPointerException.class)
final class Target_java_lang_NullPointerException {

@Substitute
@TargetElement(onlyWith = JDK17OrLater.class)
@SuppressWarnings("static-method")
private String getExtendedNPEMessage() {
return null;
}
}

Recompute Field Value

By default, field values in the native image heap of the Substrate VM are the same as in the host VM. We can recompute field values using the @RecomputeFieldValue annotation. The table below shows attributes of @RecomputeFieldValue.

AttributeTypeDefault valueDescription
kindKindKind of the recomputation
declClassClass<?>RecomputeFieldValue.classThe class parameter for the recomputation
declClassNameString""The class parameter for the recomputation
nameString""The name parameter for the recomputation
isFinalbooleanfalseWhether the value should be treated as final
disableCachingbooleanfalseWhether the caching of computed values should be disabled

Target Class

When determining the class parameter for the recomputation, the following algorithm is used:

  1. Check the declClass attribute.
  2. Check the declClassName attribute.
  3. Use the class specified by the @TargetClass annotation.

Kind

The most important attribute of @RecomputeFieldValue is kind. The type Kind is an enum with following values.

ValueDescription
NoneThe field value is not modified.
ResetThe field is rest to the default value.
NewInstanceThe field is set to an instance of declClass created by calling the default constructor.
FromAliasThe field is set to the value assigned to the @Alias field.
FieldOffsetThe int or long field is set to the offset of the field named name() of the class declClass, as it would be computed by sun.misc.Unsafe.objectFieldOffset.
ArrayBaseOffsetThe int or long field is set to the offset of the first array element of the array class declClass, as it would be computed by sun.misc.Unsafe.arrayBaseOffset(Class).
ArrayIndexScaleThe int or long field is set to the element size array class declClass, as it would be computed by sun.misc.Unsafe.arrayIndexScale(Class).
ArrayIndexShiftThe int or long field is set to the log2 of ArrayIndexScale.
AtomicFieldUpdaterOffsetSet field offsets used by java.util.concurrent.atomic.AtomicXxxFieldUpdater.
TranslateFieldOffsetThe field offset stored in this int or long field is updated. The original value must be a valid field offset in the hosted universe, and the new value is the field offset of the same field in the substrate universe.
ManualUse manual logic.
CustomUse a RecomputeFieldValue.CustomFieldValueComputer or RecomputeFieldValue.CustomFieldValueTransformer, which is specified as the target class.

Reset

In the code below, value of the otherParents field is reset to the default value.

Reset
@TargetClass(java.io.FileDescriptor.class)
final class Target_java_io_FileDescriptor {

@Alias @RecomputeFieldValue(kind = Kind.Reset)//
private List<Closeable> otherParents;
}

NewInstance

In the code below, value of the instances field is set to a new instance of ConcurrentHashMap.

NewInstance
@TargetClass(java.util.Currency.class)
final class Target_java_util_Currency {
@Alias//
@RecomputeFieldValue(kind = Kind.NewInstance, declClass = ConcurrentHashMap.class)//
private static ConcurrentMap<String, Currency> instances;
}

FromAlias

In the code below, the value of powerCache field is assigned in the static initializer of the current class.

FromAlias
@TargetClass(value = java.math.BigInteger.class)
final class Target_java_math_BigInteger {

@Alias @RecomputeFieldValue(kind = FromAlias)//
private static BigInteger[][] powerCache;

static {
/*
* BigInteger code changes the powerCache to add values lazily. We need a stable field value
* throughout native image generation, so we use our own value (which is computed by code
* duplicated from the static initializer of BigInteger).
*/
powerCache = new BigInteger[Character.MAX_RADIX + 1][];
for (int i = Character.MIN_RADIX; i <= Character.MAX_RADIX; i++) {
powerCache[i] = new BigInteger[]{BigInteger.valueOf(i)};
}
}
}

FieldOffset

In the code below, value of the SYNC_STATE_FIELD_OFFSET field in the MultiThreadedMonitorSupport class is set to the field offset of the state field in the AbstractQueuedSynchronizer class.

FieldOffset
@TargetClass(MultiThreadedMonitorSupport.class)
final class Target_com_oracle_svm_core_monitor_MultiThreadedMonitorSupport {
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FieldOffset, name = "objectMonitorCondition", declClass = Target_java_util_concurrent_locks_ReentrantLock_NonfairSync.class) //
static long SYNC_MONITOR_CONDITION_FIELD_OFFSET;

@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FieldOffset, name = "state", declClass = AbstractQueuedSynchronizer.class) //
static long SYNC_STATE_FIELD_OFFSET;
}

ArrayBaseOffset and ArrayIndexShift

The code below shows an example of ArrayBaseOffset and ArrayIndexShift. The target class is the Array inner class of java.lang.invoke.VarHandleInts.

ArrayBaseOffset and ArrayIndexShift
@TargetClass(className = "java.lang.invoke.VarHandleInts", innerClass = "Array", onlyWith = JDK11OrLater.class)
final class Target_java_lang_invoke_VarHandleInts_Array {
@Alias @RecomputeFieldValue(kind = Kind.ArrayBaseOffset, declClass = int[].class) //
int abase;
@Alias @RecomputeFieldValue(kind = Kind.ArrayIndexShift, declClass = int[].class) //
int ashift;
}

AtomicFieldUpdaterOffset

AtomicFieldUpdaterOffset is specific for atomic field updaters in the java.util.concurrent.atomic package.

@TargetClass(className = "java.util.concurrent.atomic.AtomicIntegerFieldUpdater$AtomicIntegerFieldUpdaterImpl")
final class Target_java_util_concurrent_atomic_AtomicIntegerFieldUpdater_AtomicIntegerFieldUpdaterImpl {
@Alias @RecomputeFieldValue(kind = AtomicFieldUpdaterOffset) //
private long offset;

/** the same as tclass, used for checks */
@Alias private Class<?> cclass;
/** class holding the field */
@Alias private Class<?> tclass;

// simplified version of the original constructor
@SuppressWarnings("unused")
@Substitute
Target_java_util_concurrent_atomic_AtomicIntegerFieldUpdater_AtomicIntegerFieldUpdaterImpl(final Class<?> tclass,
final String fieldName, final Class<?> caller) {
final Field field;
final int modifiers;
try {
field = tclass.getDeclaredField(fieldName);
modifiers = field.getModifiers();
} catch (Exception ex) {
throw new RuntimeException(ex);
}

if (field.getType() != int.class)
throw new IllegalArgumentException("Must be integer type");

if (!Modifier.isVolatile(modifiers))
throw new IllegalArgumentException("Must be volatile type");

// access checks are disabled
this.cclass = tclass;
this.tclass = tclass;
this.offset = GraalUnsafeAccess.getUnsafe().objectFieldOffset(field);
}
}

Manual

In the code below, value of the pollingAddress field is computed using manual logic.

Manual
@Platforms(Platform.WINDOWS.class)
public final class WindowsJavaNIOSubstitutions {

@TargetClass(className = "sun.nio.fs.Cancellable")
@Platforms({Platform.WINDOWS.class})
static final class Target_sun_nio_fs_Cancellable {
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Manual)//
private long pollingAddress;
}
}

Custom

When using the Custom kind, value of the field is recomputed using an instance of CustomFieldValueComputer or CustomFieldValueTransformer interface. The class name is specified as the target class.

CustomFieldValueComputer and CustomFieldValueTransformer have a similar interface. The difference is that CustomFieldValueTransformer can access the original value of the field.

CustomFieldValueComputer
interface CustomFieldValueComputer {
Object compute(MetaAccessProvider metaAccess, ResolvedJavaField original, ResolvedJavaField annotated, Object receiver);
}
CustomFieldValueTransformer
interface CustomFieldValueTransformer {
Object transform(MetaAccessProvider metaAccess, ResolvedJavaField original, ResolvedJavaField annotated, Object receiver, Object originalValue);
}

The table below shows the list of possible parameters used in these two interfaces.

ParameterTypeDescription
metaAccessMetaAccessProviderThe AnalysisMetaAccess instance during the analysis or HostedMetaAccess instance after the analysis.
originalResolvedJavaFieldThe original field (if @RecomputeFieldValue is used for an @Alias field).
annotatedResolvedJavaFieldThe field annotated with @RecomputeFieldValue.
receiverObjectThe original object for instance fields, or null for static fields.
originalValueObjectThe original value of the field.

In the code below, value of the field previousKey is computed using the ServiceKeyComputer class which implements CustomFieldValueComputer.

Custom
@TargetClass(value = java.security.Provider.class)
final class Target_java_security_Provider {
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Custom, declClass = ServiceKeyComputer.class) //
private static Target_java_security_Provider_ServiceKey previousKey;
}

@Platforms(Platform.HOSTED_ONLY.class)
class ServiceKeyComputer implements RecomputeFieldValue.CustomFieldValueComputer {

@Override
public Object compute(MetaAccessProvider metaAccess, ResolvedJavaField original, ResolvedJavaField annotated, Object receiver) {
try {
// Checkstyle: stop do not use dynamic class loading
Class<?> serviceKey = Class.forName("java.security.Provider$ServiceKey");
// Checkstyle: resume
Constructor<?> constructor = ReflectionUtil.lookupConstructor(serviceKey, String.class, String.class, boolean.class);
return constructor.newInstance("", "", false);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) {
throw VMError.shouldNotReachHere(e);
}
}
}

Inject Accessor

For fields annotated with the @Alias annotation, their values can be get or set using accessors. These accessors are specified using the @InjectAccessors annotation. @InjectAccessors only has one value attribute to provide the class which provides accessors.

The accessor methods are static methods in the accessor class. The method names can be:

  • set or get for a single field
  • setFoo or getFoo for a field name foo if the accessor class can be used for multiple fields

Depending on the kind of accessor, the accessor method can have 0, 1 or 2 parameters.

AccessorParameters
get for a static fieldNo parameters
set for a static fieldOne parameter with the value to set
get for a non-static fieldOne parameter with the object to access
set for a non-static fieldTwo parameters, the first parameter is the object to access, the second parameter is the value to set

In the code below, CompletableFutureAsyncPoolAccessor is used to get the value of asyncPool or ASYNC_POOL fields. CompletableFutureAsyncPoolAccessor has only one get method to get the Executor instance.

Example of get accessor
@TargetClass(java.util.concurrent.CompletableFuture.class)
final class Target_java_util_concurrent_CompletableFuture {

@Alias @InjectAccessors(CompletableFutureAsyncPoolAccessor.class) //
@TargetElement(onlyWith = JDK8OrEarlier.class) //
private static Executor asyncPool;

@Alias @InjectAccessors(CompletableFutureAsyncPoolAccessor.class) //
@TargetElement(onlyWith = JDK11OrLater.class) //
private static Executor ASYNC_POOL;
}

class CompletableFutureAsyncPoolAccessor {
static Executor get() {
return ForkJoinPoolCommonAccessor.get();
}
}

Inject

New fields can be injected into the target class using the @Inject annotation. The annotated field must not be static. The @RecomputeFieldValue annotation is usually used with @Inject to provide the value for the new field.

In the code below, the new addedShutdownHook field is injected into the java.util.logging.LogManager class with the value of a new instance of AtomicBoolean. This new field is used in the substituted getLogManager method.

Inject
@TargetClass(value = LogManager.class)
final class Target_java_util_logging_LogManager {

@Inject @RecomputeFieldValue(kind = Kind.NewInstance, declClass = AtomicBoolean.class) private AtomicBoolean addedShutdownHook = new AtomicBoolean();

@Alias static LogManager manager;

@Alias
native void ensureLogManagerInitialized();

@Substitute
public static LogManager getLogManager() {
/* First performing logic originally in getLogManager. */
if (manager == null) {
return manager;
}
Target_java_util_logging_LogManager managerAlias = SubstrateUtil.cast(manager, Target_java_util_logging_LogManager.class);
managerAlias.ensureLogManagerInitialized();

/* Logic for adding shutdown hook. */
if (!managerAlias.addedShutdownHook.getAndSet(true)) {
/* Add a shutdown hook to close the global handlers. */
try {
Runtime.getRuntime().addShutdownHook(SubstrateUtil.cast(new Target_java_util_logging_LogManager_Cleaner(managerAlias), Thread.class));
} catch (IllegalStateException e) {
/* If the VM is already shutting down, we do not need to register shutdownHook. */
}
}

return manager;
}
}

Example

See Flyway native support for how substitutions are used.