Skip to content
Open
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
1 change: 1 addition & 0 deletions web-image/mx.web-image/mx_web_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"RuntimeDebugChecks",
"SILENT_COMPILE",
"SourceMapSourceRoot=",
"StandaloneWasm",
"StrictWarnings",
"UnsafeErrorMessages",
"UseBinaryen",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@
import com.oracle.svm.hosted.webimage.name.WebImageNamingConvention;
import com.oracle.svm.hosted.webimage.options.WebImageOptions;
import com.oracle.svm.hosted.webimage.options.WebImageOptions.CompilerBackend;
import com.oracle.svm.hosted.webimage.wasm.WebImageWasmOptions;
import com.oracle.svm.hosted.webimage.util.BenchmarkLogger;
import com.oracle.svm.hosted.webimage.wasm.WebImageWasmLMJavaMainSupport;
import com.oracle.svm.hosted.webimage.wasmgc.WebImageWasmGCJavaMainSupport;
import com.oracle.svm.shared.option.ReplacingLocatableMultiOptionValue;
import com.oracle.svm.util.AnnotatedObjectAccess;
import com.oracle.svm.util.GuestAccess;
import com.oracle.svm.util.JVMCIReflectionUtil;
import com.oracle.svm.webimage.JSExceptionSupport;
import com.oracle.svm.webimage.WebImageJSJavaMainSupport;
import com.oracle.svm.webimage.WebImageJavaMainSupport;

Expand Down Expand Up @@ -164,6 +166,18 @@ public int build(ImageClassLoader classLoader) {
}
}

if (backend == CompilerBackend.WASMGC && Boolean.TRUE.equals(optionProvider.getHostedValues().get(WebImageOptions.StandaloneWasm))) {
// In standalone mode, stack traces require JS (genBacktrace, formatStackTrace).
// Force them off so the backtrace JSCallNodes are never emitted.
optionProvider.getHostedValues().put(JSExceptionSupport.Options.DisableStackTraces, true);

// Use smaller heap init functions to stay within Cranelift's function size limit.
// The default (100K objects) produces functions too large for wasmtime to compile.
if (!optionProvider.getHostedValues().containsKey(WebImageWasmOptions.ImageHeapObjectsPerFunction)) {
optionProvider.getHostedValues().put(WebImageWasmOptions.ImageHeapObjectsPerFunction, 1000);
}
}

if (WebImageOptions.isNativeImageBackend()) {
// The Web Image visualization should not appear in the native-image launcher
optionProvider.getHostedValues().put(VisualizationSupport.Options.Visualization, "");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,13 @@ public boolean isEnabled(CommentVerbosity required) {
}
}

/**
* Returns true if standalone WASM output is enabled (no JS interop dependencies).
*/
public static boolean isStandaloneWasm() {
return StandaloneWasm.getValue();
}

public static boolean genJSComments() {
return genJSComments(null);
}
Expand All @@ -368,6 +375,9 @@ public static boolean genJSComments(CommentVerbosity verbosity) {
return JSComments.getValue(HostedOptionValues.singleton()).isEnabled(verbosity == null ? CommentVerbosity.NORMAL : verbosity);
}

@Option(help = "Produce standalone WASM without JS interop dependencies.")//
public static final HostedOptionKey<Boolean> StandaloneWasm = new HostedOptionKey<>(false);

@Option(help = "Determine if the Web Image compilation should be silent and not dump info")//
public static final OptionKey<Boolean> SILENT_COMPILE = new OptionKey<>(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,129 @@ public class WasmImports {

public static final WasmImportForeignCallDescriptor PROXY_CHAR_ARRAY = new WasmImportForeignCallDescriptor(MODULE_CONVERT, "proxyCharArray", WasmExtern.class, new Class<?>[]{char[].class},
"Creates a JS proxy around a char array");

/**
* Module name for WASI snapshot preview1 imports.
*/
public static final String MODULE_WASI = "wasi_snapshot_preview1";

/**
* WASI fd_write: write bytes to a file descriptor via iovec.
* <p>
* Signature: {@code fd_write(fd: i32, iovs: i32, iovs_len: i32, nwritten: i32) -> i32}
* <p>
* The caller must set up a ciovec structure in linear memory at {@code iovs}:
* {@code { buf: i32, buf_len: i32 }}.
* The number of bytes written is stored at the {@code nwritten} pointer.
*/
public static final ImportDescriptor.Function wasiFdWrite = new ImportDescriptor.Function(MODULE_WASI, "fd_write",
TypeUse.withResult(i32, i32, i32, i32, i32), "WASI fd_write");

/**
* WASI proc_exit: terminate the process with an exit code.
* <p>
* Signature: {@code proc_exit(code: i32)}
*/
public static final ImportDescriptor.Function wasiProcExit = new ImportDescriptor.Function(MODULE_WASI, "proc_exit",
TypeUse.withoutResult(i32), "WASI proc_exit");

/**
* Simple host print: write raw bytes to a file descriptor.
* <p>
* Signature: {@code host_print(fd: i32, ptr: i32, len: i32)}
* <p>
* This is a simpler alternative to WASI fd_write that doesn't require iovec setup.
* Used by the WasmLM backend when targeting standalone WASM without JS.
*/
public static final ImportDescriptor.Function hostPrintBytes = new ImportDescriptor.Function(MODULE_IO, "host_print_bytes",
TypeUse.withoutResult(i32, i32, i32), "Host: print raw bytes to fd");

/**
* Simple host print for 2-byte chars.
* <p>
* Signature: {@code host_print_chars(fd: i32, ptr: i32, num_chars: i32)}
*/
public static final ImportDescriptor.Function hostPrintChars = new ImportDescriptor.Function(MODULE_IO, "host_print_chars",
TypeUse.withoutResult(i32, i32, i32), "Host: print 2-byte chars to fd");

/**
* Print a single character to a file descriptor.
* <p>
* Signature: {@code print_char(fd: i32, char_code: i32)}
* <p>
* Used by the WasmGC standalone backend where GC-managed arrays cannot be passed
* as linear memory pointers. Characters are sent one at a time.
*/
public static final ImportDescriptor.Function printChar = new ImportDescriptor.Function(MODULE_IO, "print_char",
TypeUse.withoutResult(i32, i32), "Print single char: (fd, char_code)");

/**
* Print characters from a linear memory buffer.
* <p>
* Signature: {@code print_buffer(fd: i32, ptr: i32, num_chars: i32)}
* <p>
* Reads {@code num_chars} 16-bit characters starting at byte offset {@code ptr}
* in the module's linear memory. Used for batch printing in standalone WasmGC mode.
*/
public static final ImportDescriptor.Function printBuffer = new ImportDescriptor.Function(MODULE_IO, "print_buffer",
TypeUse.withoutResult(i32, i32, i32), "Print chars from linear memory buffer: (fd, ptr, num_chars)");

/**
* Host flush: flush a file descriptor.
* <p>
* Signature: {@code host_flush(fd: i32)}
*/
public static final ImportDescriptor.Function hostFlush = new ImportDescriptor.Function(MODULE_IO, "host_flush",
TypeUse.withoutResult(i32), "Host: flush fd");

/**
* Host time: get current time in milliseconds.
* <p>
* Signature: {@code host_time_ms() -> f64}
*/
public static final ImportDescriptor.Function hostTimeMs = new ImportDescriptor.Function(MODULE_IO, "host_time_ms",
TypeUse.withResult(f64), "Host: current time in ms");

/**
* Component-model-compatible import descriptors for standalone WASM output.
* <p>
* These use fully-qualified module names (e.g. {@code graalvm:standalone/io@0.1.0})
* and kebab-case function names (e.g. {@code print-char}) as required by the
* WebAssembly Component Model specification.
* <p>
* Use these when targeting {@code wasm-tools component new} wrapping.
*/
public static final class Component {
public static final String WIT_PACKAGE = "graalvm:standalone";
public static final String WIT_VERSION = "0.1.0";

public static final String COMPONENT_COMPAT = WIT_PACKAGE + "/compat@" + WIT_VERSION;
public static final String COMPONENT_IO = WIT_PACKAGE + "/io@" + WIT_VERSION;
public static final String COMPONENT_WASI = WIT_PACKAGE + "/wasi@" + WIT_VERSION;

// Compat math functions (names are already kebab-compatible)
public static final ImportDescriptor.Function F32Rem = new ImportDescriptor.Function(COMPONENT_COMPAT, "f32rem", TypeUse.forBinary(f32, f32, f32), "JVM FREM Instruction");
public static final ImportDescriptor.Function F64Rem = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64rem", TypeUse.forBinary(f64, f64, f64), "JVM DREM Instruction");
public static final ImportDescriptor.Function F64Log = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64log", TypeUse.forUnary(f64, f64), "Math.log");
public static final ImportDescriptor.Function F64Log10 = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64log10", TypeUse.forUnary(f64, f64), "Math.log10");
public static final ImportDescriptor.Function F64Sin = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64sin", TypeUse.forUnary(f64, f64), "Math.sin");
public static final ImportDescriptor.Function F64Cos = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64cos", TypeUse.forUnary(f64, f64), "Math.cos");
public static final ImportDescriptor.Function F64Tan = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64tan", TypeUse.forUnary(f64, f64), "Math.tan");
public static final ImportDescriptor.Function F64Tanh = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64tanh", TypeUse.forUnary(f64, f64), "Math.tanh");
public static final ImportDescriptor.Function F64Exp = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64exp", TypeUse.forUnary(f64, f64), "Math.exp");
public static final ImportDescriptor.Function F64Pow = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64pow", TypeUse.forBinary(f64, f64, f64), "Math.pow");
public static final ImportDescriptor.Function F64Cbrt = new ImportDescriptor.Function(COMPONENT_COMPAT, "f64cbrt", TypeUse.forBinary(f64, f64, f64), "Math.cbrt");

// IO functions (kebab-case)
public static final ImportDescriptor.Function printChar = new ImportDescriptor.Function(COMPONENT_IO, "print-char",
TypeUse.withoutResult(i32, i32), "Print single char: (fd, char_code)");
public static final ImportDescriptor.Function printBuffer = new ImportDescriptor.Function(COMPONENT_IO, "print-buffer",
TypeUse.withoutResult(i32, i32, i32), "Print chars from linear memory buffer: (fd, ptr, num_chars)");
public static final ImportDescriptor.Function hostTimeMs = new ImportDescriptor.Function(COMPONENT_IO, "host-time-ms",
TypeUse.withResult(f64), "Host: current time in ms");

// WASI (kebab-case)
public static final ImportDescriptor.Function procExit = new ImportDescriptor.Function(COMPONENT_WASI, "proc-exit",
TypeUse.withoutResult(i32), "WASI proc_exit");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.SequencedMap;
import java.util.Set;

import com.oracle.svm.hosted.webimage.wasm.ast.id.WasmId;
import com.oracle.svm.hosted.webimage.wasmgc.ast.RecursiveGroup;
Expand Down Expand Up @@ -65,6 +67,16 @@ public class WasmModule {

protected final ActiveData activeData = new ActiveData();

/**
* Functions that need to be declared in a declarative element segment.
* <p>
* Per the WebAssembly spec, any function referenced by {@code ref.func} outside of an
* active/passive element segment must be declared in a declarative element segment.
* This is required for validation by strict validators like {@code wasm-tools validate}
* and {@code wasmtime}.
*/
protected final Set<WasmId.Func> declarativeFuncRefs = new LinkedHashSet<>();

protected StartFunction startFunction = null;

public void addFunction(Function fun) {
Expand Down Expand Up @@ -161,6 +173,17 @@ public void addActiveData(long offset, byte[] data) {
activeData.addData(offset, data);
}

/**
* Declares a function reference for a declarative element segment.
*/
public void addDeclarativeFuncRef(WasmId.Func func) {
declarativeFuncRefs.add(func);
}

public Set<WasmId.Func> getDeclarativeFuncRefs() {
return Collections.unmodifiableSet(declarativeFuncRefs);
}

public void constructActiveDataSegments() {
// Limit the number of data segments so that we don't exceet MAX_DATA_SEGMENTS.
activeData.constructDataSegments(MAX_DATA_SEGMENTS - this.dataSegments.size()).forEach(this::addData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public class WasmIdFactory {
private final Set<TempLocal> temporaryVariables = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Set<Table> tables = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final ConcurrentMap<ImportDescriptor.Function, WasmId.FunctionImport> functionImports = new ConcurrentHashMap<>();
private final ConcurrentMap<ImportDescriptor.Function, ImportDescriptor.Function> importRemappings = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer, WasmId.Memory> memories = new ConcurrentHashMap<>();
private final Set<Tag> tags = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Set<Global> globals = Collections.newSetFromMap(new ConcurrentHashMap<>());
Expand Down Expand Up @@ -204,7 +205,20 @@ public Table newTable() {
}

public WasmId.FunctionImport forFunctionImport(ImportDescriptor.Function wasmImport) {
return createForKey(wasmImport, functionImports, WasmId.FunctionImport::new);
ImportDescriptor.Function resolved = importRemappings.getOrDefault(wasmImport, wasmImport);
return createForKey(resolved, functionImports, WasmId.FunctionImport::new);
}

/**
* Registers an import remapping. When {@link #forFunctionImport(ImportDescriptor.Function)} is
* called with {@code from}, the import will instead be created with {@code to}.
* <p>
* This is used for standalone WASM output where import module names and function names
* need to follow the WebAssembly Component Model naming convention.
*/
public void addImportRemapping(ImportDescriptor.Function from, ImportDescriptor.Function to) {
assert assertNotFrozen();
importRemappings.put(from, to);
}

public WasmId.Memory forMemory(int num) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashSet;
import java.util.Set;

import com.oracle.svm.hosted.webimage.options.WebImageOptions;
import com.oracle.svm.hosted.webimage.wasm.WebImageWasmOptions;
Expand Down Expand Up @@ -375,17 +377,81 @@ private void printExtensionSuffix(WasmUtil.Extension extension) {
@Override
@SuppressWarnings("try")
public void visitModule(WasmModule m) {
collectDeclarativeFuncRefs(m);

parenOpen("module");
space();
try (var ignored = new Indenter()) {
super.visitModule(m);
emitDeclarativeFuncRefs(m);
}
newline();

print(')');
newline();
}

/**
* Emits declarative element segments for all functions referenced by {@code ref.func}.
* <p>
* In WAT: {@code (elem declare func $f1 $f2 ...)}
*/
private void emitDeclarativeFuncRefs(WasmModule m) {
Set<WasmId.Func> funcRefs = m.getDeclarativeFuncRefs();
if (funcRefs.isEmpty()) {
return;
}

newline();
newline();
printComment("Declarative element segment for ref.func declarations");
newline();
parenOpen("elem declare func");
for (WasmId.Func func : funcRefs) {
space();
printId(func);
}
parenClose();
}

/**
* Scans all functions and globals for {@code ref.func} instructions and registers them
* as declarative function references in the module.
* <p>
* Per the WebAssembly spec, functions referenced by {@code ref.func} outside of active
* or passive element segments must be declared in a declarative element segment
* ({@code (elem declare func ...)}).
*/
private void collectDeclarativeFuncRefs(WasmModule m) {
Set<WasmId.Func> funcRefs = new LinkedHashSet<>();

// Collect from function bodies
RefFuncCollector collector = new RefFuncCollector(funcRefs);
for (Function func : m.getFunctions()) {
collector.visitFunction(func);
}

// Collect from global initializers
for (Global global : m.getGlobals().sequencedValues()) {
collector.visitInstruction(global.init);
}

// Collect from table element initializers
for (Table table : m.getTables()) {
if (table.elements != null) {
for (Instruction elem : table.elements) {
collector.visitInstruction(elem);
}
}
}

// Functions already in active table elements don't need declarative declaration,
// but including them is harmless and simpler than filtering.
for (WasmId.Func func : funcRefs) {
m.addDeclarativeFuncRef(func);
}
}

@Override
public void visitModuleField(ModuleField f) {
newline();
Expand Down Expand Up @@ -1483,4 +1549,22 @@ public void visitAnyExternConversion(Instruction.AnyExternConversion inst) {
}
newline();
}

/**
* Visitor that collects all function IDs referenced by {@code ref.func} instructions.
*/
private static class RefFuncCollector extends WasmVisitor {

private final Set<WasmId.Func> funcRefs;

RefFuncCollector(Set<WasmId.Func> funcRefs) {
this.funcRefs = funcRefs;
}

@Override
public void visitRefFunc(Instruction.RefFunc inst) {
funcRefs.add(inst.func);
super.visitRefFunc(inst);
}
}
}
Loading