diff --git a/annotations/it/src/it/method-prefixer/invoker.properties b/annotations/it/src/it/method-prefixer/invoker.properties new file mode 100644 index 000000000..84099bc5a --- /dev/null +++ b/annotations/it/src/it/method-prefixer/invoker.properties @@ -0,0 +1 @@ +invoker.goals=test diff --git a/annotations/it/src/it/method-prefixer/pom.xml b/annotations/it/src/it/method-prefixer/pom.xml new file mode 100644 index 000000000..8592e75dc --- /dev/null +++ b/annotations/it/src/it/method-prefixer/pom.xml @@ -0,0 +1,101 @@ + + + + 4.0.0 + run.endive + + method-prefixer-endive-it + 0.0-SNAPSHOT + jar + + + @maven.compiler.release@ + + + + + run.endive + runtime + @project.version@ + + + + org.junit.jupiter + junit-jupiter-api + @junit.version@ + test + + + org.junit.jupiter + junit-jupiter-engine + @junit.version@ + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @maven-compiler-plugin.version@ + + ${maven.compiler.release} + + + + maven-resources-plugin + @maven-resources-plugin.version@ + + + copy-resources + + copy-resources + + validate + + ${basedir}/src/test/resources + + + @basedir@/../../wasm-corpus/src/main/resources/compiled + + trap.wat.wasm + + + + + + + + + run.endive + endive-compiler-maven-plugin + @project.version@ + + + compile-named + + compile + + + endive.test.NamedTrapModule + src/test/resources/trap.wat.wasm + run.endive.compiler.NameSectionMethodPrefixer + + + + compile-default + + compile + + + endive.test.DefaultTrapModule + src/test/resources/trap.wat.wasm + + + + + + + + diff --git a/annotations/it/src/it/method-prefixer/src/test/java/endive/test/MethodPrefixerTest.java b/annotations/it/src/it/method-prefixer/src/test/java/endive/test/MethodPrefixerTest.java new file mode 100644 index 000000000..5aa2346a6 --- /dev/null +++ b/annotations/it/src/it/method-prefixer/src/test/java/endive/test/MethodPrefixerTest.java @@ -0,0 +1,56 @@ +package endive.test; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import org.junit.jupiter.api.Test; +import run.endive.runtime.Instance; +import run.endive.runtime.Machine; +import run.endive.wasm.UninstantiableException; +import run.endive.wasm.WasmModule; + +/** + * The module traps in its start function, so instantiating it throws from inside the compiled code + * and the stack trace carries the generated method names. The module names its three functions + * "trap", "innerFunc" and "start". + */ +public class MethodPrefixerTest { + + @Test + public void nameSectionPrefixerNamesTheGeneratedMethods() { + var methods = methodNamesFromTrap(NamedTrapModule.load(), NamedTrapModule::create); + + assertTrue(methods.contains("trap_0"), "Expected trap_0 in: " + methods); + assertTrue(methods.contains("innerFunc_1"), "Expected innerFunc_1 in: " + methods); + assertTrue(methods.contains("start_2"), "Expected start_2 in: " + methods); + } + + @Test + public void withoutAPrefixerMethodsKeepTheDefaultNames() { + var methods = methodNamesFromTrap(DefaultTrapModule.load(), DefaultTrapModule::create); + + assertTrue(methods.contains("func_0"), "Expected func_0 in: " + methods); + assertTrue(methods.contains("func_1"), "Expected func_1 in: " + methods); + assertTrue(methods.contains("func_2"), "Expected func_2 in: " + methods); + } + + /** Instantiates the module and returns every method name on the resulting stack traces. */ + private static List methodNamesFromTrap( + WasmModule module, Function machineFactory) { + Throwable thrown = + assertThrows( + UninstantiableException.class, + () -> Instance.builder(module).withMachineFactory(machineFactory).build()); + + var methods = new ArrayList(); + for (Throwable t = thrown; t != null; t = t.getCause()) { + for (var frame : t.getStackTrace()) { + methods.add(frame.getMethodName()); + } + } + return methods; + } +} diff --git a/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java b/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java index a97b23a8b..7fb4d5dfd 100644 --- a/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java +++ b/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java @@ -73,6 +73,15 @@ public String[] getVersion() { @CommandLine.Option( order = 7, + names = "--method-prefixer", + description = + "Fully qualified name of a MethodPrefixer implementation used to name compiled" + + " methods, for example" + + " run.endive.compiler.NameSectionMethodPrefixer") + String methodPrefixer; + + @CommandLine.Option( + order = 8, names = "--module-interface", description = "Fully qualified class name for which to generate _ModuleExports and" @@ -90,6 +99,7 @@ public void run() { .withTargetWasmFolder(targetWasmFolder) .withInterpreterFallback(interpreterFallback) .withInterpretedFunctions(interpretedFunctions) + .withMethodPrefixer(methodPrefixer) .withModuleInterface(moduleInterface) .build(); diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java index 5d6a7c484..2dcd82c05 100644 --- a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java +++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java @@ -42,6 +42,11 @@ public final class Config { */ private final Set interpretedFunctions; + /** + * the fully qualified name of the MethodPrefixer implementation used to name compiled methods + */ + private final String methodPrefixer; + /** * the fully qualified name of the user's class for which to generate module interface wrappers */ @@ -60,6 +65,7 @@ private Config( Path targetWasmFolder, InterpreterFallback interpreterFallback, Set interpretedFunctions, + String methodPrefixer, String moduleInterface, List redlineTargets) { this.wasmFile = wasmFile; @@ -69,6 +75,7 @@ private Config( this.targetWasmFolder = targetWasmFolder; this.interpreterFallback = interpreterFallback; this.interpretedFunctions = interpretedFunctions; + this.methodPrefixer = methodPrefixer; this.moduleInterface = moduleInterface; this.redlineTargets = redlineTargets; } @@ -101,6 +108,10 @@ public Set interpretedFunctions() { return interpretedFunctions; } + public String methodPrefixer() { + return methodPrefixer; + } + public String moduleInterface() { return moduleInterface; } @@ -141,6 +152,7 @@ public static final class Builder { private Path targetWasmFolder; private InterpreterFallback interpreterFallback = InterpreterFallback.FAIL; private Set interpretedFunctions; + private String methodPrefixer; private String moduleInterface; private List redlineTargets = List.of(); @@ -181,6 +193,11 @@ public Builder withInterpretedFunctions(Set interpretedFunctions) { return this; } + public Builder withMethodPrefixer(String methodPrefixer) { + this.methodPrefixer = methodPrefixer; + return this; + } + public Builder withModuleInterface(String moduleInterface) { this.moduleInterface = moduleInterface; return this; @@ -200,6 +217,7 @@ public Config build() { targetWasmFolder, interpreterFallback, interpretedFunctions, + methodPrefixer, moduleInterface, redlineTargets); } diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java index 1e47938c7..04f127bca 100644 --- a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java +++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java @@ -35,6 +35,7 @@ import java.util.function.Function; import run.endive.codegen.CodegenUtils; import run.endive.codegen.ModuleInterfaceCodegen; +import run.endive.compiler.MethodPrefixer; import run.endive.compiler.internal.ByteClassCollector; import run.endive.compiler.internal.Compiler; import run.endive.runtime.CompiledModule; @@ -66,6 +67,7 @@ public Set generateResources() throws IOException { .withClassCollectorFactory(ByteClassCollector::new) .withInterpreterFallback(config.interpreterFallback()) .withInterpretedFunctions(config.interpretedFunctions()) + .withMethodPrefixer(loadMethodPrefixer(config.methodPrefixer())) .build(); var result = compiler.compile(); @@ -82,6 +84,31 @@ public Set generateResources() throws IOException { return result.interpretedFunctions(); } + /** + * Instantiates the named {@link MethodPrefixer}, or returns {@code null} when {@code className} + * is not set. The class must be on the classloader of the build tool driving the generator. + */ + private static MethodPrefixer loadMethodPrefixer(String className) { + if (className == null || className.isEmpty()) { + return null; + } + try { + return Class.forName(className) + .asSubclass(MethodPrefixer.class) + .getDeclaredConstructor() + .newInstance(); + } catch (ReflectiveOperationException | ClassCastException e) { + throw new IllegalArgumentException( + "Cannot instantiate the configured MethodPrefixer: " + + className + + ". It must implement " + + MethodPrefixer.class.getName() + + ", have a public no-argument constructor, and be on the build tool's" + + " classpath.", + e); + } + } + public void generateSources() throws IOException { var machineName = config.name() + "Machine"; var split = config.name().split("\\."); diff --git a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java index cf332d091..a0304507a 100644 --- a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java +++ b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java @@ -71,6 +71,16 @@ public class EndiveCompilerGenMojo extends AbstractMojo { @Parameter(required = false, defaultValue = "") Set interpretedFunctions; + /** + * Fully qualified name of a MethodPrefixer implementation used to name the compiled methods. + * Defaults to naming them func_0, func_1 and so on. Set it to + * run.endive.compiler.NameSectionMethodPrefixer to name them after the module's name section, + * which makes thread dumps and profiler output readable, or to your own implementation. A + * custom class must be a dependency of this plugin, not of the project. + */ + @Parameter(required = false) + String methodPrefixer; + /** * Fully qualified name of the user's class that will use the compiled module. * When set, the plugin generates _ModuleExports and _ModuleImports wrapper classes, @@ -112,6 +122,7 @@ public void execute() throws MojoExecutionException { .withTargetWasmFolder(targetWasmFolder.toPath()) .withInterpreterFallback(interpreterFallback) .withInterpretedFunctions(interpretedFunctions) + .withMethodPrefixer(methodPrefixer) .withModuleInterface(moduleInterface); if (redlineTargetsExperimental != null && !redlineTargetsExperimental.isEmpty()) { configBuilder.withRedlineTargets(redlineTargetsExperimental); diff --git a/compiler/src/main/java/run/endive/compiler/MethodPrefixer.java b/compiler/src/main/java/run/endive/compiler/MethodPrefixer.java index b45a18246..7c533d791 100644 --- a/compiler/src/main/java/run/endive/compiler/MethodPrefixer.java +++ b/compiler/src/main/java/run/endive/compiler/MethodPrefixer.java @@ -52,9 +52,6 @@ static MethodPrefixer defaultPrefixer() { * back to {@link #DEFAULT_PREFIX} for functions without one. */ static MethodPrefixer fromNameSection() { - return (funcId, module) -> { - var nameSection = module.nameSection(); - return nameSection == null ? null : nameSection.nameOfFunction(funcId); - }; + return new NameSectionMethodPrefixer(); } } diff --git a/compiler/src/main/java/run/endive/compiler/NameSectionMethodPrefixer.java b/compiler/src/main/java/run/endive/compiler/NameSectionMethodPrefixer.java new file mode 100644 index 000000000..b7e6aebe0 --- /dev/null +++ b/compiler/src/main/java/run/endive/compiler/NameSectionMethodPrefixer.java @@ -0,0 +1,16 @@ +package run.endive.compiler; + +import run.endive.wasm.WasmModule; + +/** + * A {@link MethodPrefixer} that takes each prefix from the module's name custom section, falling + * back to {@link MethodPrefixer#DEFAULT_PREFIX} for functions the section does not name. + */ +public final class NameSectionMethodPrefixer implements MethodPrefixer { + + @Override + public String getMethodPrefix(int funcId, WasmModule module) { + var nameSection = module.nameSection(); + return nameSection == null ? null : nameSection.nameOfFunction(funcId); + } +} diff --git a/docs/docs/execution/build-time-compiler.md b/docs/docs/execution/build-time-compiler.md index 183d06fa9..da8123301 100644 --- a/docs/docs/execution/build-time-compiler.md +++ b/docs/docs/execution/build-time-compiler.md @@ -37,6 +37,55 @@ WASM function size exceeds the Java method size limits and cannot be compiled to If this happens you can configure your build tool, to just issue warning messages, or to be silent. Another way to silence the message is to configure the build tool with an explicit list of functions that should be interpreted. Typically, you obtain the list of the functions by running the compiler once with `interpreterFallback` set to `WARN` +### Method Names + +By default the compiler names each generated method `func_0`, `func_1`, etc. The `methodPrefixer` +parameter takes the fully qualified name of a `MethodPrefixer` implementation that supplies a more +recognisable prefix instead, which makes thread dumps, profiler output and stack traces identify +functions by name. `NameSectionMethodPrefixer` uses the function names from the module's name +section: + +```xml + + src/main/resources/add.wasm + org.acme.wasm.Add + run.endive.compiler.NameSectionMethodPrefixer + +``` + +You can name your own implementation instead, for example one that demangles Rust or C++ symbols. +It must implement `run.endive.compiler.MethodPrefixer` and have a public no-argument constructor. +The class is loaded by the plugin, so it has to be a dependency of the plugin declaration rather +than of the project: + +```xml + + run.endive + endive-compiler-maven-plugin + + + org.acme + my-demangler + 1.0.0 + + + + +``` + +Names are resolved once, at build time. This is the only opportunity to do so: the meta Wasm module +emitted next to the compiled classes has all of its custom sections stripped, so the name section is +no longer available when the module is loaded. Resolving names here also costs nothing at runtime. + +Whatever the prefixer returns, the compiler appends `_` to produce the method name, so names +stay unique and the Wasm function index remains recoverable from any method name. Characters that are +illegal in JVM method names (`. ; [ / < >`) are replaced with underscores. The prefix is a hint for +humans; tools should use the function index instead. See +[Method Names](runtime-compiler.md#method-names) for the full contract. + +Since the name section is not retained, mapping a function index back to its original name requires +the original Wasm module, which is a build input rather than something shipped with the classes. + ## Using Maven Example configuration of the Maven plug-in: @@ -188,6 +237,14 @@ endive:compile the action to take if the compiler needs to use the interpreter because a function is too big + methodPrefixer + Fully qualified name of a MethodPrefixer implementation used to name the + compiled methods. Defaults to naming them func_0, func_1 and so on. Set + it to run.endive.compiler.NameSectionMethodPrefixer to name them after + the module's name section, which makes thread dumps and profiler output + readable, or to your own implementation. A custom class must be a + dependency of this plugin, not of the project. + moduleInterface Fully qualified name of the user's class for which to generate _ModuleExports and _ModuleImports wrapper classes. When set, eliminates