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 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.
Method | Description |
---|---|
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:
afterRegistration
duringSetup
beforeAnalysis
duringAnalysis
afterAnalysis
onAnalysisExit
beforeUniverseBuilding
beforeCompilation
afterCompilation
afterHeapLayout
beforeImageWrite
afterImageWrite
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.
Method | Description |
---|---|
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.
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.
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
@AutomaticFeature
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.
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.
Method | Description |
---|---|
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.
Method | Description |
---|---|
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.
Method | Description |
---|---|
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
.
Method | Description |
---|---|
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
.
Method | Description |
---|---|
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.
The full source code of this example can be found on GitHub.
These code below shows the Command
and CommandFactory
interfaces and their implementations.
- Command
- CommandFactory
- Commands
- CommandFactories
package io.vividcode.graalvm.features.command;
public interface Command {
void execute();
}
package io.vividcode.graalvm.features.command;
public interface CommandFactory {
Command create();
}
package io.vividcode.graalvm.features.command;
public class Commands {
public static class Run implements Command {
@Override
public void execute() {
System.out.println("Run!");
}
}
public static class Think implements Command {
@Override
public void execute() {
System.out.println("Think...");
}
}
public static class Sleep implements Command {
@Override
public void execute() {
System.out.println("Sleep");
}
}
}
package io.vividcode.graalvm.features.command;
import io.vividcode.graalvm.features.command.Commands.Run;
import io.vividcode.graalvm.features.command.Commands.Sleep;
import io.vividcode.graalvm.features.command.Commands.Think;
public class CommandFactories {
public static class RunCommandFactory implements CommandFactory {
@Override
public Command create() {
return new Run();
}
}
public static class ThinkCommandFactory implements CommandFactory {
@Override
public Command create() {
return new Think();
}
}
public static class SleepCommandFactory implements CommandFactory {
@Override
public Command create() {
return new Sleep();
}
}
}
The code below shows the CommandExecutor
.
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.
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.
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.
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.