Skip to main content

Feature

Features allow clients to intercept the native image generation and run custom initialization code at various stages. Feature classes code is executed during native image generation.

Features are implementations of the org.graalvm.nativeimage.hosted.Feature interface. All implementation classes must have a no-argument constructor. A singleton instance for each feature is instantiated using this constructor.

Built-in Features

Built-in features provided by GraalVM are also implemented as Feature.

To use features, dependencies of GraalVM SDK need to be added to your project first.

Feature Interface

The Feature interface provides various callback handlers at different stages of native image generation. These handler methods are no-op operations in Feature interface. A Feature implementation can choose the desired methods to override.

The table below shows the methods of Feature interface. Almost all these handler methods (except cleanup) accept a single parameter access to access context information based on the stage.

MethodDescription
boolean isInConfiguration(IsInConfigurationAccess access)Check whether the feature is part of the configuration or not.
List<Class<? extends Feature>> getRequiredFeatures()Return the list of features required by this feature.
void afterRegistration(AfterRegistrationAccess access)Handler for initializations after all features have been registered and all options have been parsed
void duringSetup(DuringSetupAccess access)Handler for initializations at startup time.
void beforeAnalysis(BeforeAnalysisAccess access)Handler for initializations before the static analysis.
void duringAnalysis(DuringAnalysisAccess access)Handler for performing operations during the static analysis.
void afterAnalysis(AfterAnalysisAccess access)Handler for initializations after analysis and before universe creation.
void onAnalysisExit(OnAnalysisExitAccess access)Handler for code that needs to run after the analysis, even if an error has occurred.
void beforeUniverseBuilding(BeforeUniverseBuildingAccess access)Handler for code that needs to run before universe building, but after hosted meta-access has been created.
void beforeCompilation(BeforeCompilationAccess access)Handler for initializations before compilation.
void afterCompilation(AfterCompilationAccess access)Handler for initializations after compilation.
void afterHeapLayout(AfterHeapLayoutAccess access)Handler for initializations after the native image heap and code layout.
void beforeImageWrite(BeforeImageWriteAccess access)Handler for altering the linker command after the native image has been built and before it is written.
void afterImageWrite(AfterImageWriteAccess access)Handler for altering the image (or shared object) that the linker command produced.
void cleanup()Handler for cleanup.

The list below shows the invocation order of Feature methods:

  1. afterRegistration
  2. duringSetup
  3. beforeAnalysis
  4. duringAnalysis
  5. afterAnalysis
  6. onAnalysisExit
  7. beforeUniverseBuilding
  8. beforeCompilation
  9. afterCompilation
  10. afterHeapLayout
  11. beforeImageWrite
  12. afterImageWrite
  13. cleanup

All these Access interfaces have a super-interface org.graalvm.nativeimage.hosted.Feature.FeatureAccess. FeatureAccess provides access methods that are available for all feature methods.

MethodDescription
Class<?> findClassByName(String className)Return a class if it is present on the classpath.
List<Path> getApplicationClassPath()Return the class path of the native image that is currently built.
List<Path> getApplicationModulePath()Return the module path of the native image that is currently built.
ClassLoader getApplicationClassLoader()Return the ClassLoader that can find all classes of the class path and module path.

Write Custom Features

We can write custom features and use them in the native image generation process. The key point is finding the correct stages to add custom handlers.

A Simple Feature

The code below shows a simple feature which prints out the path of generated native image. NativeImagePathOutputFeature overrides the afterImageWrite method and uses the getImagePath method of AfterImageWriteAccess to retrieve the path.

Output path of native image
package io.vividcode.graalvm.features.examples;

import org.graalvm.nativeimage.hosted.Feature;

public class NativeImagePathOutputFeature implements Feature {

@Override
public void afterImageWrite(AfterImageWriteAccess access) {
System.out.println("Native image written to " + access.getImagePath());
}
}

Enable with --features

A feature can be enabled using the --features option. This option specifies a comma-separated list of fully qualified names of Feature implementation classes.

Enable features
native-image -cp . -H:Class=io.vividcode.graalvm.features.app.Simple \
-H:Name=app \
--features=io.vividcode.graalvm.features.examples.NativeImagePathOutputFeature

In the output of native-image, we can see the output from NativeImagePathOutputFeature.

Native image written to <path>/graalvm-examples/features/target/classes/app

Enable with @AutomaticFeature

Deprecated Automatic Features

Automatic features have been deprecated. Use --features instead.

A better choice of enabling features is using the com.oracle.svm.core.annotate.AutomaticFeature annotation. With this annotation, the feature class is unconditionally added when it's reachable on the class path.

The Feature implementation listed below uses the @AutomaticFeature annotation. It simply prints out a message in the cleanup stage. This feature doesn't need to be included in the list of --features option.

Feature with @AutomaticFeature
package io.vividcode.graalvm.features.examples;

import com.oracle.svm.core.annotate.AutomaticFeature;
import org.graalvm.nativeimage.hosted.Feature;

@AutomaticFeature
public class NoOpCleanupFeature implements Feature {

@Override
public void cleanup() {
System.out.println("Cleanup");
}
}

When running native-image using the same options, the output shows that two features are enabled.

 2 user-provided feature(s)
- io.vividcode.graalvm.features.examples.NativeImagePathOutputFeature
- io.vividcode.graalvm.features.examples.NoOpCleanupFeature

Provide Runtime Configurations

The usual approach to provide build configurations is using JSON files. JSON files are easy to write and use. However, they are static files and cannot meet the requirements for some complex usage scenarios.

GraalVM SDK has the API to provide runtime configurations using Java code. This is designed to be used with custom features.

Class Initialization

The org.graalvm.nativeimage.hosted.RuntimeClassInitialization class provides methods that can be called during native image generation to configure class initialization behavior. These methods are static.

MethodDescription
void initializeAtRunTime(Class<?>... classes)Registers the provided classes, and all of their subclasses, for class initialization at runtime.
void initializeAtBuildTime(Class<?>... classes)Registers the provided classes as eagerly initialized during image-build time.
void initializeAtRunTime(String... packages)Registers all classes in provided packages, and all of their subclasses, for class initialization at runtime.
void initializeAtBuildTime(String... packages)Registers all classes in provided packages as eagerly initialized during image-build time.

Reflection

The org.graalvm.nativeimage.hosted.RuntimeReflection class provides methods that can be called during native image generation to register classes, methods, and fields for reflection at runtime. These methods are static.

MethodDescription
void register(Class<?>... classes)Makes the provided classes available for reflection at run time.
void register(Executable... methods)Makes the provided methods available for reflection at run time.
void registerAsQueried(Executable... methods)Makes the provided methods available for reflection queries at run time.
void register(Field... fields)Makes the provided fields available for reflection at run time.
void registerForReflectiveInstantiation(Class<?>... classes)Makes the provided classes available for reflective instantiation by Class.newInstance.

Serialization

The org.graalvm.nativeimage.hosted.RuntimeSerialization class provides methods that can be called before and during analysis, to register classes for serialization at image runtime. These methods are static.

MethodDescription
void register(Class<?>... classes)Makes the provided classes available for serialization at runtime.
void registerWithTargetConstructorClass(Class<?> clazz, Class<?> customTargetConstructorClazz)Makes the provided class available for serialization at runtime but uses the provided customTargetConstructorClazz for deserialization.

Resources

To add resources in features, we need to get an instance of the com.oracle.svm.core.configure.ResourcesRegistry interface using the lookup method of ImageSingletons first, then use methods of ResourcesRegistry to add resources.

The following table shows methods of ResourcesRegistry.

MethodDescription
void addResources(ConfigurationCondition condition, String pattern)Add resources.
void ignoreResources(ConfigurationCondition condition, String pattern)Ignore resources.
void addResourceBundles(ConfigurationCondition condition, String name)Add resource bundles.
void addResourceBundles(ConfigurationCondition condition, String basename, Collection<Locale> locales)Add resource bundles.
void addClassBasedResourceBundle(ConfigurationCondition condition, String basename, String className)Add class based resource bundles.

Dynamic Proxy

To add dynamic proxies in features, we need to get an instance of the com.oracle.svm.core.jdk.proxy.DynamicProxyRegistry interface using the lookup method of ImageSingletons first, then use the addProxyClass method to register dynamic proxies.

The following table shows methods of DynamicProxyRegistry.

MethodDescription
void addProxyClass(Class<?>... interfaces)Add dynamic proxy classes.

Summary

To summaries, there are two patterns of providing runtime configurations:

  • Use static methods provided by some utility classes.
  • Use ImageSingletons to get an instance first, then use the instance.

Actually, those utility classes also use ImageSingletons under the hood. For example, the support of reflection is provided by the org.graalvm.nativeimage.impl.RuntimeReflectionSupport interface. Below is the implementation of the register method of RuntimeReflection. It also uses ImageSingletons to lookup an instance of RuntimeReflectionSupport first, then uses the register method of RuntimeReflectionSupport.

public static void register(Class<?>... classes) {
ImageSingletons.lookup(RuntimeReflectionSupport.class).register(ConfigurationCondition.alwaysTrue(), classes);
}

Example

In this example, there are several commands which can be executed by an executor. Each command has a type. Command objects are created by their factories.

note

The full source code of this example can be found on GitHub.

These code below shows the Command and CommandFactory interfaces and their implementations.

package io.vividcode.graalvm.features.command;

public interface Command {

void execute();
}

The code below shows the CommandExecutor.

CommandExecutor using reflections
package io.vividcode.graalvm.features.command;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CommandExecutor {

private final Map<String, CommandFactory> commandFactories = new HashMap<>();

public CommandExecutor() {
InputStream inputStream = CommandExecutor.class.getResourceAsStream("/commands.txt");
if (inputStream != null) {
new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
.lines()
.forEach(
line -> {
String[] parts = line.split("=");
if (parts.length == 2) {
try {
Class<?> clazz =
Class.forName(
parts[1], true, Thread.currentThread().getContextClassLoader());
commandFactories.put(
parts[0], (CommandFactory) clazz.getDeclaredConstructor().newInstance());
System.out.println("Added command factory " + parts[1]);
} catch (Exception e) {
System.err.println("Failed to create command factory " + parts[1]);
}
}
});
}
}

public void execute(List<String> commands) {
commands.forEach(
command -> {
CommandFactory commandFactory = commandFactories.get(command);
if (commandFactory != null) {
commandFactory.create().execute();
}
});
}
}

The execute method accepts a list of command types and execute them. The mapping between a command's type and the class name of its factory is stored in the commands.txt resource. Reflection API is used to load the command factory class and create a new instance of it.

The code below shows the content of commands.txt. Each line is a mapping between a command type and its factory class.

commands.txt
run=io.vividcode.graalvm.features.command.CommandFactories$RunCommandFactory
think=io.vividcode.graalvm.features.command.CommandFactories$ThinkCommandFactory
sleep=io.vividcode.graalvm.features.command.CommandFactories$SleepCommandFactory

In the constructor of CommandExecutor, the content of commands.txt resource is parsed. For each command type, its factory class is loaded and a factory instance is instantiated. These factory instances are put into the commandFactories map. In the execute method, for each command type, the create method of its factory instance is invoked to create a command object first, then the execute method of the command object is invoked to execute this command.

The usage of Reflection API needs to be registered. This is done by creating a new feature called CommandReflectionFeature. In the registerClass method, a class and its no-arg constructor is registered for reflection using RuntimeReflection. In the beforeAnalysis method, command factories are registered using the registerClass method.

Feature to register reflections
package io.vividcode.graalvm.features.command;

import com.oracle.svm.core.annotate.AutomaticFeature;
import io.vividcode.graalvm.features.command.CommandFactories.RunCommandFactory;
import io.vividcode.graalvm.features.command.CommandFactories.SleepCommandFactory;
import io.vividcode.graalvm.features.command.CommandFactories.ThinkCommandFactory;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;

@AutomaticFeature
public class CommandReflectionFeature implements Feature {

@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
registerClass(RunCommandFactory.class);
registerClass(ThinkCommandFactory.class);
registerClass(SleepCommandFactory.class);
}

private void registerClass(Class<?> clazz) {
RuntimeReflection.register(clazz);
RuntimeReflection.registerForReflectiveInstantiation(clazz);
}
}

For the usage of commands.txt resource, the CommandResourceFeature feature uses the addResources method of ResourcesRegistry to register it.

Feature to add resources
package io.vividcode.graalvm.features.command;

import com.oracle.svm.core.annotate.AutomaticFeature;
import com.oracle.svm.core.configure.ResourcesRegistry;
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.impl.ConfigurationCondition;

@AutomaticFeature
public class CommandResourceFeature implements Feature {

@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
ResourcesRegistry registry = ImageSingletons.lookup(ResourcesRegistry.class);
registry.addResources(ConfigurationCondition.alwaysTrue(), ".*\\.txt");
}
}

With these features, we don't need to use configuration files anymore. We can simply run native-image without the H:ResourceConfigurationFiles or -H:ReflectionConfigurationFiles option.

Advanced Features

Feature Inclusion

If a feature should only be enabled under certain conditions, the isInConfiguration method of Feature should be implemented. This is typically done by checking native-image options or checking the context information using the IsInConfigurationAccess object.

VM inspection is implemented using the com.oracle.svm.core.VMInspection feature. In the isInConfiguration method, the AllowVMInspection and DumpThreadStacksOnSignal options are checked to determine whether this feature should be included.

JavaFX is enabled using the com.oracle.svm.hosted.javafx.JavaFXFeature feature. In the isInConfiguration method, the findClassByName method of FeatureAccess is used to check whether the javafx.application.Application class can be found. If this class is found, then this feature should be included.

Feature Dependencies

A feature can declare its dependencies to other features by implementing the getRequiredFeatures method. As long as the dependency chain is non-cyclic, all required features are processed before this feature.