Skip to content

Issues related to boot module layer synthesizing with closed-world approach #3733

@ivan-ristovic

Description

@ivan-ristovic

Describe the issue

NI boot module layer currently only contains modules which contents are reachable after analysis. This behavior follows the closed-world assumption and is naturally translated to module system as well. However, in the case of modules, it can in some cases cause unintended results. This issue shows a simple example, with the goal to open a discussion about potential changes in the current closed-world approach when it comes to modules.

Describe GraalVM and your environment:

Steps to reproduce the issue

A simple example can be a modularized application with two modules - one of which will be a "library" module exporting a package that will be used by the "main" module.

The "library" module can be defined as:

module core.app {
        exports core.util;
}

It contains a class core.util.WorkerUtil with the following definition:

package core.util;

public class WorkerUtil {
        public static void doSomething() {
                System.out.println("WorkerUtil working...");
        }
}

The "main" module can be defined as:

module main.app {
        requires core.app;
}

Our main method, contained in main.app module, can be defined as:

package example.app;

import java.lang.ModuleLayer;
import core.util.WorkerUtil;

public class AppMain {

        // This function can also be in another module
        public static void useLibrary() {
                WorkerUtil.doSomething();
        }
        
        public static void main(String[] args) {
                useLibrary();
                assert ModuleLayer.boot().modules()
                        .stream()
                        .anyMatch(m -> m.getName().equals("core.app"));
        }
}

We can build an image of this application (in this example all jar files that are created by javac are located in pkg directory):

$ USE_NATIVE_IMAGE_JAVA_PLATFORM_MODULE_SYSTEM=true native-image -ea -p pkg -m main.app/example.app.AppMain

We can now verify that the assertion will hold by running the image. If we do not invoke the useLibrary() method, the entire core.app module will not be included in the image, as types from that module are not used. Re-running native-image in the same way as above (however this time not invoking useLibrary() method), and invoking the generated executable will lead to an AssertionError:

Exception in thread "main" java.lang.AssertionError
	at example.app.AppMain.main(AppMain.java:14)

This way modules can unintentionally break dependant modules simply by removing dependencies.

More details

For this example, the size of an executable which only contains only reachable modules (14 in this case): 11481768 B (~11M)

For this example, the size of an executable which contains all modules on the module path (26 in this case): 11485864 B (~11M)

From here, we can see that the difference is 4096 B, which averages to ~341 B per module (this value correlates to the number of packages contained in the module).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions