Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ public Builder withInterpretedFunctions(Set<Integer> interpretedFunctions) {
return this;
}

/**
* Sets the {@link MethodPrefixer} used to name the compiled methods. Defaults to
* {@link MethodPrefixer#defaultPrefixer()}.
*/
public Builder withMethodPrefixer(MethodPrefixer methodPrefixer) {
compilerBuilder.withMethodPrefixer(methodPrefixer);
return this;
}

public Builder withCache(Cache cache) {
this.cache = cache;
return this;
Expand Down
60 changes: 60 additions & 0 deletions compiler/src/main/java/run/endive/compiler/MethodPrefixer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package run.endive.compiler;

import run.endive.wasm.WasmModule;

/**
* Supplies the human readable prefix used when naming the JVM method compiled for a WASM function.
*
* <p>The compiler derives every method name as {@code <sanitized prefix>_<funcId>}. The prefixer
* only controls the prefix; the compiler owns the rest of the name. That split keeps two
* invariants that the rest of the compiler and any external tooling can rely on, regardless of
* what a prefixer returns:
*
* <ul>
* <li>method names are unique, because the function id is unique
* <li>the function id can always be recovered by parsing the {@code _<funcId>} suffix
* </ul>
*
* <p>Characters that are illegal in a JVM method name ({@code . ; [ / < >}, see
* <a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.2.2">JVM Spec
* §4.2.2</a>) are replaced with {@code _}. A prefixer that needs to preserve the original name
* exactly can avoid the substitution by encoding those characters itself, for example by
* percent-encoding them.
*
* <p>The prefix is a hint for humans reading a thread dump or a profile. Tooling should never
* parse it; it should use the function id instead.
*/
@FunctionalInterface
public interface MethodPrefixer {

/**
* The prefix used when no prefixer is configured, and the fallback whenever a prefixer returns
* {@code null}, an empty string, or a string that sanitizes to nothing.
*/
String DEFAULT_PREFIX = "func";

/**
* Returns the prefix for the method compiled for {@code funcId}, or {@code null} to use
* {@link #DEFAULT_PREFIX}.
*
* @param funcId the WASM function index, covering imported and defined functions
* @param module the module being compiled
*/
String getMethodPrefix(int funcId, WasmModule module);

/** Returns the default prefixer, naming every method {@value #DEFAULT_PREFIX}. */
static MethodPrefixer defaultPrefixer() {
return (funcId, module) -> DEFAULT_PREFIX;
}

/**
* Returns a prefixer that uses the function name from the module's name custom section, falling
* 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);
};
}
}
82 changes: 63 additions & 19 deletions compiler/src/main/java/run/endive/compiler/internal/Compiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.InstructionAdapter;
import run.endive.compiler.InterpreterFallback;
import run.endive.compiler.MethodPrefixer;
import run.endive.runtime.CallResult;
import run.endive.runtime.Instance;
import run.endive.runtime.Machine;
Expand Down Expand Up @@ -161,6 +162,7 @@ public final class Compiler {
private final boolean[] tailCallTypes;
private final boolean moduleHasTailCalls;
private final boolean moduleHasObjectRefs;
private final String[] methodNames;
private boolean useBridgeClasses;
private IntFunction<String> callIndirectClassResolver;

Expand All @@ -170,7 +172,8 @@ private Compiler(
int maxFunctionsPerClass,
InterpreterFallback interpreterFallback,
Set<Integer> interpretedFunctions,
Supplier<ClassCollector> classCollectorFactory) {
Supplier<ClassCollector> classCollectorFactory,
MethodPrefixer methodPrefixer) {
this.className = requireNonNull(className, "className");
this.module = requireNonNull(module, "module");
this.analyzer = new WasmAnalyzer(module);
Expand Down Expand Up @@ -202,6 +205,30 @@ private Compiler(
this.functionTypes.stream()
.anyMatch(ft -> ft.hasObjectRefParams() || ft.hasObjectRefReturns());
this.maxFunctionsPerClass = maxFunctionsPerClass;
// Resolve every method name up front, so that the prefixer is consulted exactly once per
// function and definitions and call sites cannot disagree.
var prefixer = requireNonNullElse(methodPrefixer, MethodPrefixer.defaultPrefixer());
this.methodNames = new String[this.functionTypes.size()];
for (int funcId = 0; funcId < this.methodNames.length; funcId++) {
this.methodNames[funcId] = methodNameForFunc(funcId, prefixer, module);
}
}

private String methodName(int funcId) {
return methodNames[funcId];
}

/**
* Returns the id of the WASM function compiled into {@code methodName}, or {@code -1} when no
* function was compiled under that name.
*/
private int funcIdForMethodName(String methodName) {
for (int funcId = 0; funcId < methodNames.length; funcId++) {
if (methodNames[funcId].equals(methodName)) {
return funcId;
}
}
return -1;
}

private Set<Integer> collectCallRefTypeIds() {
Expand Down Expand Up @@ -229,6 +256,7 @@ public static final class Builder {
private InterpreterFallback interpreterFallback;
private Set<Integer> interpretedFunctions;
private Supplier<ClassCollector> classCollectorFactory;
private MethodPrefixer methodPrefixer;

private Builder(WasmModule module) {
this.module = module;
Expand Down Expand Up @@ -259,6 +287,11 @@ public Builder withClassCollectorFactory(Supplier<ClassCollector> classCollector
return this;
}

public Builder withMethodPrefixer(MethodPrefixer methodPrefixer) {
this.methodPrefixer = methodPrefixer;
return this;
}

public Compiler build() {
var className = this.className;
if (className == null) {
Expand All @@ -280,7 +313,8 @@ public Compiler build() {
maxFunctionsPerClass,
interpreterFallback,
interpretedFunctions,
classCollectorFactory);
classCollectorFactory,
methodPrefixer);
}
}

Expand Down Expand Up @@ -351,10 +385,8 @@ private void compileExtraClasses() {
break;
} catch (MethodTooLargeException e) {
String methodName = e.getMethodName();
if (methodName.startsWith("func_")) {
// Add the method to interpreted function list... and try again.
var funcId = Integer.parseInt(methodName.substring("func_".length()));

int funcId = funcIdForMethodName(methodName);
if (funcId >= 0) {
String functionDescription = "WASM function index: " + funcId;
if (module.nameSection() != null) {
String name = module.nameSection().nameOfFunction(funcId);
Expand Down Expand Up @@ -496,7 +528,7 @@ private Consumer<ClassVisitor> emitFunctionGroup(int start, int end, String inte
if (i < functionImports) {
emitFunction(
classWriter,
methodNameForFunc(funcId),
methodName(funcId),
methodTypeFor(type),
true,
asm -> compileHostFunction(funcId, type, asm));
Expand All @@ -507,7 +539,7 @@ private Consumer<ClassVisitor> emitFunctionGroup(int start, int end, String inte

emitFunction(
classWriter,
methodNameForFunc(funcId),
methodName(funcId),
methodTypeFor(type),
true,
asm ->
Expand All @@ -533,7 +565,7 @@ private Consumer<ClassVisitor> emitFunctionGroup(int start, int end, String inte
}
}
} catch (MethodTooLargeException e) {
throw handleMethodTooLarge(e, module);
throw handleMethodTooLarge(e);
}
}
};
Expand Down Expand Up @@ -666,7 +698,7 @@ private byte[] compileClass() {
try {
return binaryWriter.toByteArray();
} catch (MethodTooLargeException e) {
throw handleMethodTooLarge(e, module);
throw handleMethodTooLarge(e);
}
}

Expand All @@ -686,11 +718,10 @@ private boolean isFuncTypeMatch(int expectedTypeId, int funcIdx, FunctionType ex
return expectedType.equals(functionTypes.get(funcIdx));
}

private static RuntimeException handleMethodTooLarge(
MethodTooLargeException e, WasmModule module) {
private RuntimeException handleMethodTooLarge(MethodTooLargeException e) {
String name = e.getMethodName();
if (name.startsWith("func_") && module.nameSection() != null) {
int funcId = Integer.parseInt(name.split("_", -1)[1]);
int funcId = funcIdForMethodName(name);
if (funcId >= 0 && module.nameSection() != null) {
String function = module.nameSection().nameOfFunction(funcId);
if (function != null) {
name += " (" + function + ")";
Expand Down Expand Up @@ -1392,7 +1423,10 @@ private void compileCallFunction(int funcId, FunctionType type, InstructionAdapt
asm.load(0, OBJECT_TYPE);

emitInvokeFunction(
asm, internalClassName(classNameForFuncGroup(className, funcId)), funcId, type);
asm,
internalClassName(classNameForFuncGroup(className, funcId)),
methodName(funcId),
type);

// box the result into long[]
Class<?> returnType = jvmReturnType(type);
Expand Down Expand Up @@ -1482,7 +1516,10 @@ private void compileCallWithRefsFunction(
asm.load(0, OBJECT_TYPE);

emitInvokeFunction(
asm, internalClassName(classNameForFuncGroup(className, funcId)), funcId, type);
asm,
internalClassName(classNameForFuncGroup(className, funcId)),
methodName(funcId),
type);

// Build CallResult from the function's JVM return value
Class<?> returnType = jvmReturnType(type);
Expand Down Expand Up @@ -1681,7 +1718,10 @@ private void compileCallIndirect(
// return func_0(a, b, memory, callerInstance);
asm.mark(labels[i]);
emitInvokeFunction(
asm, classNameForFuncGroup(internalClassName, keys[i]), keys[i], type);
asm,
classNameForFuncGroup(internalClassName, keys[i]),
methodName(keys[i]),
type);
asm.areturn(getType(jvmReturnType(type)));
}

Expand Down Expand Up @@ -1829,7 +1869,10 @@ private void compileCallIndirectApply(
// return func_0(a, b, memory, callerInstance);
asm.mark(labels[i]);
emitInvokeFunction(
asm, classNameForFuncGroup(internalClassName, keys[i]), keys[i], type);
asm,
classNameForFuncGroup(internalClassName, keys[i]),
methodName(keys[i]),
type);
asm.areturn(getType(jvmReturnType(type)));
asm.areturn(OBJECT_TYPE);
}
Expand Down Expand Up @@ -2105,7 +2148,8 @@ private void compileFunction(
tailCallFunctions,
tailCallTypes,
useBridgeClasses ? callIndirectClassResolver : typeId -> internalClassName,
analysis.maxTempSlots());
analysis.maxTempSlots(),
this::methodName);

int localsCount = type.params().size();
if (hasTooManyParameters(type)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import run.endive.compiler.MethodPrefixer;
import run.endive.runtime.Instance;
import run.endive.runtime.Memory;
import run.endive.wasm.WasmModule;
Expand Down Expand Up @@ -282,11 +283,14 @@ public static void emitInvokeVirtual(MethodVisitor asm, Method method) {
}

public static void emitInvokeFunction(
MethodVisitor asm, String internalClassName, int funcId, FunctionType functionType) {
MethodVisitor asm,
String internalClassName,
String methodName,
FunctionType functionType) {
asm.visitMethodInsn(
Opcodes.INVOKESTATIC,
internalClassName,
methodNameForFunc(funcId),
methodName,
methodTypeFor(functionType).toMethodDescriptorString(),
false);
}
Expand All @@ -298,8 +302,51 @@ public static String valueMethodName(List<ValType> types) {
.collect(joining("_"));
}

public static String methodNameForFunc(int funcId) {
return "func_" + funcId;
/**
* Builds the JVM method name for a WASM function as {@code <sanitized prefix>_<funcId>}. The
* prefixer only supplies the prefix, so the {@code _<funcId>} suffix always makes the name
* unique and keeps the function id recoverable via {@link #extractFuncId(String)}.
*/
public static String methodNameForFunc(int funcId, MethodPrefixer prefixer, WasmModule module) {
String prefix = prefixer == null ? null : prefixer.getMethodPrefix(funcId, module);
if (prefix != null) {
prefix = sanitizeWasmName(prefix);
}
if (prefix == null || prefix.isEmpty()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe more correct to accept empty prefix?

prefix = MethodPrefixer.DEFAULT_PREFIX;
}
return prefix + "_" + funcId;
}

static String sanitizeWasmName(String name) {
StringBuilder sb = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
// see https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.2.2 for
// reference
if (c == '.' || c == ';' || c == '[' || c == '/' || c == '<' || c == '>') {
sb.append('_');
} else {
sb.append(c);
}
}
return sb.toString();
}

/**
* Returns the WASM function id parsed from the {@code _<funcId>} suffix of a compiled method
* name, or {@code -1} when the name does not end in one.
*/
static int extractFuncId(String methodName) {
int lastUnderscore = methodName.lastIndexOf('_');
if (lastUnderscore < 0) {
return -1;
}
try {
return Integer.parseInt(methodName.substring(lastUnderscore + 1));
} catch (NumberFormatException e) {
return -1;
}
}

static String callMethodName(int funcId) {
Expand Down
Loading
Loading