Skip to content

HebiRobotics/native-launchers-maven-plugin

Repository files navigation

Native Launchers Maven Plugin

This plugin creates thin native launchers so that a single native shared library can be shared between multiple main methods. This significantly reduces the deployment size of multi-app bundles (e.g. CLI tools) that want to make use of GraalVM's native-image.

For a full demo project you can take a look at the sample-cli module, the Conveyor packaging configuration, and the corresponding Github Actions pipeline. Installing one of the signed pre-built packages places the launcher-hello and launcher-dir apps on the path.

How does it work?

The plugin specifies the name of the executable and the corresponding Java method, e.g.,

<launcher>
    <name>hello</name> <!-- name of the executable -->
    <mainClass>us.hebi.samples.cli.HelloWorld</mainClass> <!-- mapped Java main method -->
</launcher>

The plugin is then divided into two steps:

  1. The [gen-sources] step generates a Java file with @CEntryPoint annotated methods that result in exported symbols in the shared library. The methods handle the translation of C to Java arguments and delegate to the Java main methods. The generated class below would result in a native int run_us_hebi_samples_cli_HelloWorld_main(graal_isolatethread_t*, int, char**) method.
class NativeLaunchers {
    
  @CEntryPoint(name = "run_us_hebi_samples_cli_HelloWorld_main")
  static int run_us_hebi_samples_cli_HelloWorld_main(IsolateThread thread, int argc,  CCharPointerPointer argv) {
    try {
      String[] args = toJavaArgs(argc, argv);
      HelloWorld.main(args);
      return 0;
    } catch (Throwable t) {
      t.printStackTrace();
      return 1;
    }
  }

  private static String[] toJavaArgs(final int argc, final CCharPointerPointer argv) {
    // Java omits the name of the program argv[0]
    String[] args = new String[argc - 1];
     for (int i = 0; i < args.length; i++) {
      args[i] = CTypeConversion.toJavaString(argv.addressOf(i + 1).read());
    }
    return args;
  }
  
}
  1. The [build-launchers] step builds tiny launchers that load and call into the shared library. The loading is done dynamically, so there are no compile time dependencies and the launchers can be built independently of the native-image. The template for the native code generation is in main-dynamic.c.

A hello world app with debug info enabled (-Dlaunchers.debug) produces the following printout

bin> hello.exe arg1 arg2
[DEBUG] Running on (Windows|Linux|macOS)
[DEBUG] load library native-cli
[DEBUG] lookup symbol graal_create_isolate
[DEBUG] lookup symbol run_us_hebi_samples_cli_HelloWorld_main
[DEBUG] creating isolate thread
[DEBUG] calling run_us_hebi_samples_cli_HelloWorld_main
[DEBUG] calling us.hebi.samples.cli.HelloWorld (args: [arg1, arg2])
Hello world!

Maven Instructions

  1. add the compilation dependency for the generated graal annotations
<dependency>
    <groupId>org.graalvm.nativeimage</groupId>
    <artifactId>native-image-base</artifactId>
    <version>22.3.2</version>
    <scope>provided</scope>
</dependency>
  1. add the build plugin and define the names of the executables and the Java main methods they should map to. For a full sample configuration check sample-cli.
<plugin>
    <groupId>us.hebi.launchers</groupId>
    <artifactId>native-launchers-maven-plugin</artifactId>
    <version>0.2</version>
    <configuration>
        <outputDirectory>${graalvm.outputDir}</outputDirectory>
        <imageName>${graalvm.imageName}</imageName>
        <launchers>
            <launcher>
                <name>cli-hello</name>
                <mainClass>us.hebi.samples.cli.HelloWorld</mainClass>
            </launcher>
            <launcher>
                <name>cli-dir</name>
                <mainClass>us.hebi.samples.cli.PrintDirectoryContents</mainClass>
            </launcher>
        </launchers>
    </configuration>
    <executions>
        <execution> <!-- generate Java sources (before compilation) -->
            <id>generate-stubs</id>
            <goals>
                <goal>gen-sources</goal>
            </goals>
        </execution>
        <execution> <!-- build launchers -->
            <id>build-executables</id>
            <goals>
                <goal>build-launchers</goal>
            </goals>
        </execution>
    </executions>
</plugin> 
  1. create the GraalVM native-library with <sharedLibrary>true</sharedLibrary>. Note that the source may need additional configuration (e.g. for JNI and reflection) to be compatible with native-image.
<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <version>${graalvm.tools.version}</version>
    <extensions>true</extensions>
    <configuration>
        <outputDirectory>${outputDir}</outputDirectory>
        <imageName>${imageName}</imageName>
        <sharedLibrary>true</sharedLibrary>
        <skip>${skipNativeBuild}</skip>
        <useArgFile>false</useArgFile>
        <skipNativeTests>true</skipNativeTests>
        <verbose>true</verbose>
    </configuration>
    <executions>
        <execution>
            <id>build-native</id>
            <goals>
                <goal>compile-no-fork</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Building the source

# build everything including native image and native launchers
mvn package -Pnative

# generate launchers, but skip native image
mvn package -Pnative -Dgraalvm.skip

# build the sample project with the native image and additional debug info
mvn package -Pnative --projects sample-cli -am -Dlaunchers.debug

JavaFX Applications

JavaFX Applications work when using bellsoft's Liberica Native Image Kit distribution of GraalVM. The samples were tested on Windows with NIK23 (JDK21) - Full.

Most applications will likely need to run the tracing agent to determine used resources and reflectively accessed classes. It can be enabled by running the application with the vm option -agentlib:native-image-agent=config-output-dir=sample-cli/src/main/resources/META-INF/native-image and exploring all code paths at least once.

Known limitations

  • The @CEntryPoint annotations require a compile dependency on the graal sdk. Attempts to generate the .class file directly ended in errors due to native-image requiring a matching source file.
  • Each platform needs to be compiled individually like the Graal native-image. Static linking requires an existing native-image, and dynamic linking is not supported when cross-compiling (tested with zig 0.10.1).