diff --git a/web-image/mx.web-image/mx_web_image.py b/web-image/mx.web-image/mx_web_image.py index 844f7656b92c..438be37745dc 100644 --- a/web-image/mx.web-image/mx_web_image.py +++ b/web-image/mx.web-image/mx_web_image.py @@ -104,6 +104,7 @@ "RuntimeDebugChecks", "SILENT_COMPILE", "SourceMapSourceRoot=", + "StandaloneWasm", "StrictWarnings", "UnsafeErrorMessages", "UseBinaryen", diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java index 8ae01a93f106..4223b543af82 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java @@ -50,6 +50,7 @@ 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; @@ -57,6 +58,7 @@ 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; @@ -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, ""); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/options/WebImageOptions.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/options/WebImageOptions.java index d43ab24abb83..22c59b1f14ae 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/options/WebImageOptions.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/options/WebImageOptions.java @@ -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); } @@ -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 StandaloneWasm = new HostedOptionKey<>(false); + @Option(help = "Determine if the Web Image compilation should be silent and not dump info")// public static final OptionKey SILENT_COMPILE = new OptionKey<>(false); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/WasmImports.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/WasmImports.java index befd0374e052..ae964c1d05c7 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/WasmImports.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/WasmImports.java @@ -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. + *

+ * Signature: {@code fd_write(fd: i32, iovs: i32, iovs_len: i32, nwritten: i32) -> i32} + *

+ * 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. + *

+ * 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. + *

+ * Signature: {@code host_print(fd: i32, ptr: i32, len: i32)} + *

+ * 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. + *

+ * 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. + *

+ * Signature: {@code print_char(fd: i32, char_code: i32)} + *

+ * 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. + *

+ * Signature: {@code print_buffer(fd: i32, ptr: i32, num_chars: i32)} + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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"); + } } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/WasmModule.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/WasmModule.java index 7c046706ebd6..901545e3bdf6 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/WasmModule.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/WasmModule.java @@ -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; @@ -65,6 +67,16 @@ public class WasmModule { protected final ActiveData activeData = new ActiveData(); + /** + * Functions that need to be declared in a declarative element segment. + *

+ * 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 declarativeFuncRefs = new LinkedHashSet<>(); + protected StartFunction startFunction = null; public void addFunction(Function fun) { @@ -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 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); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/id/WasmIdFactory.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/id/WasmIdFactory.java index 984a95db3df3..bd454d70888a 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/id/WasmIdFactory.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/id/WasmIdFactory.java @@ -86,6 +86,7 @@ public class WasmIdFactory { private final Set temporaryVariables = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set tables = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final ConcurrentMap functionImports = new ConcurrentHashMap<>(); + private final ConcurrentMap importRemappings = new ConcurrentHashMap<>(); private final ConcurrentMap memories = new ConcurrentHashMap<>(); private final Set tags = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set globals = Collections.newSetFromMap(new ConcurrentHashMap<>()); @@ -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}. + *

+ * 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) { diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmPrinter.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmPrinter.java index e984ae8009d6..48ef1d1a37b6 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmPrinter.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmPrinter.java @@ -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; @@ -375,10 +377,13 @@ 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(); @@ -386,6 +391,67 @@ public void visitModule(WasmModule m) { newline(); } + /** + * Emits declarative element segments for all functions referenced by {@code ref.func}. + *

+ * In WAT: {@code (elem declare func $f1 $f2 ...)} + */ + private void emitDeclarativeFuncRefs(WasmModule m) { + Set 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. + *

+ * 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 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(); @@ -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 funcRefs; + + RefFuncCollector(Set funcRefs) { + this.funcRefs = funcRefs; + } + + @Override + public void visitRefFunc(Instruction.RefFunc inst) { + funcRefs.add(inst.func); + super.visitRefFunc(inst); + } + } } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmAssembler.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmAssembler.java index 8cc2c456fdb3..ae227aec442c 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmAssembler.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmAssembler.java @@ -47,6 +47,7 @@ import com.oracle.svm.core.util.UserError; import com.oracle.svm.hosted.c.codegen.CCompilerInvoker; import com.oracle.svm.hosted.c.util.FileUtils; +import com.oracle.svm.hosted.webimage.options.WebImageOptions; import com.oracle.svm.hosted.webimage.wasm.WebImageWasmOptions; import com.oracle.svm.shared.option.HostedOptionKey; import com.oracle.svm.shared.option.SubstrateOptionsParser; @@ -125,7 +126,12 @@ public static void install() { Path tempDirectory = ImageSingletons.lookup(TemporaryBuildDirectoryProvider.class).getTemporaryBuildDirectory(); WasmAssembler assembler; - if (BinaryenCompat.usesBinaryen()) { + if (WebImageOptions.isStandaloneWasm() && !BinaryenCompat.usesBinaryen()) { + // Standalone WasmGC output defaults to wasm-tools for assembly since + // wasm-as (Binaryen) and wat2wasm (wabt) don't fully support WasmGC. + // Users can override with -H:+UseBinaryen if their version supports GC. + assembler = new WasmAssembler.WasmTools(tempDirectory); + } else if (BinaryenCompat.usesBinaryen()) { assembler = new WasmAssembler.Binaryen(tempDirectory); } else { assembler = new WasmAssembler.Wat2Wasm(tempDirectory); @@ -517,6 +523,90 @@ protected String getVerificationFile() { } } + /** + * Uses {@code wasm-tools parse} from the wasm-tools project. + *

+ * This assembler supports WasmGC and all modern WASM proposals, making it the + * preferred choice for standalone WasmGC output. + */ + @SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class, other = Disallowed.class) + public static class WasmTools extends WasmAssembler { + + protected WasmTools(Path tempDirectory) { + super(tempDirectory); + } + + @Override + protected String getProjectName() { + return "wasm-tools"; + } + + @Override + protected String getURL() { + return "https://github.com/bytecodealliance/wasm-tools"; + } + + @Override + protected String getMinimumVersion() { + return "1.200.0"; + } + + @Override + protected String getExecutable() { + return "wasm-tools"; + } + + @Override + protected HostedOptionKey getPathOption() { + // Reuse the wat2wasm path option for custom paths + return Options.Wat2WasmPath; + } + + @Override + protected String getDebugNamesFlag() { + return ""; + } + + @Override + protected List getOutputFlags(Path wasmPath) { + return List.of("-o", wasmPath.toString()); + } + + @Override + protected List getExtraFlags() { + return Collections.emptyList(); + } + + @Override + protected String getVerificationFile() { + return "verify-wasm-tools.wat"; + } + + @Override + protected List getVersionInfoOptions() { + return List.of("--version"); + } + + /** + * wasm-tools uses subcommands: {@code wasm-tools parse -o }. + */ + @Override + public RunResult runAssembler(Path watPath, Path wasmPath) throws IOException, InterruptedException { + Path executable = assemblerInfo.assemblerPath; + + List args = new ArrayList<>(); + args.add("parse"); + if (wasmPath != null) { + args.add("-o"); + args.add(wasmPath.toString()); + } + if (watPath != null) { + args.add(watPath.toString()); + } + return runCommand(executable, args); + } + } + public record AssemblerInfo(Path assemblerPath, String versionString) { } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wasm-tools.wat b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wasm-tools.wat new file mode 100644 index 000000000000..3c736ab490b2 --- /dev/null +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wasm-tools.wat @@ -0,0 +1,12 @@ +(; + Wasm text file to verify the wasm-tools assembler works correctly. + + Uses WasmGC features (struct types, ref types). +;) +(module + (type $point (struct (field $x i32) (field $y i32))) + (func $main (export "main") (result i32) + (struct.get $point $x + (struct.new $point (i32.const 42) (i32.const 7))) + ) +) diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/id/GCKnownIds.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/id/GCKnownIds.java index 505fa8424930..b0a934c96d14 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/id/GCKnownIds.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/id/GCKnownIds.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Locale; +import com.oracle.svm.hosted.webimage.options.WebImageOptions; import com.oracle.svm.hosted.webimage.wasm.ast.Export; import com.oracle.svm.hosted.webimage.wasm.ast.id.KnownIds; import com.oracle.svm.hosted.webimage.wasm.ast.id.WasmId; @@ -181,6 +182,8 @@ public class GCKnownIds extends KnownIds { public final WasmGCJSBodyTemplates.ExtractJSValue extractJSValueTemplate; public final WasmGCJSBodyTemplates.IsJavaObject isJavaObjectTemplate; + public final WasmGCFunctionTemplates.StandalonePrintChars standalonePrintCharsTemplate; + private final List functionExports = new ArrayList<>(); public GCKnownIds(WasmIdFactory idFactory) { @@ -253,10 +256,16 @@ public GCKnownIds(WasmIdFactory idFactory) { this.extractJSValueTemplate = new WasmGCJSBodyTemplates.ExtractJSValue(idFactory); this.isJavaObjectTemplate = new WasmGCJSBodyTemplates.IsJavaObject(idFactory); + this.standalonePrintCharsTemplate = new WasmGCFunctionTemplates.StandalonePrintChars(idFactory); + this.functionExports.add(Export.forFunction(unsafeCreateTemplate.requestFunctionId(), "unsafe.create", "Create uninitialized instance of given class")); - this.functionExports.add(Export.forFunction(wrapExternTemplate.requestFunctionId(), "extern.wrap", "Wrap externref in WasmExtern")); - this.functionExports.add(Export.forFunction(toExternTemplate.requestFunctionId(), "extern.unwrap", "Unwrap Java object to externref")); - this.functionExports.add(Export.forFunction(toExternTemplate.requestFunctionId(), "extern.isjavaobject", "Check if reference is a Java Object")); + if (!WebImageOptions.isStandaloneWasm()) { + // In standalone mode, extern.wrap/unwrap use externref which is not component-model + // compatible. Skip these exports since they are only used for JS interop. + this.functionExports.add(Export.forFunction(wrapExternTemplate.requestFunctionId(), "extern.wrap", "Wrap externref in WasmExtern")); + this.functionExports.add(Export.forFunction(toExternTemplate.requestFunctionId(), "extern.unwrap", "Unwrap Java object to externref")); + this.functionExports.add(Export.forFunction(toExternTemplate.requestFunctionId(), "extern.isjavaobject", "Check if reference is a Java Object")); + } this.functionExports.add(Export.forFunction(arrayLengthTemplate.requestFunctionId(), "array.length", "Length of a Java array")); this.functionExports.add(Export.forFunction(arrayCreateTemplate.requestFunctionId(char.class), "array.char.create", "Create char array")); this.functionExports.add(Export.forFunction(arrayCreateTemplate.requestFunctionId(String.class), "array.string.create", "Create String array")); @@ -305,7 +314,8 @@ public List> getLateFunctionTemplates() { fillHeapObjectTemplate, fillHeapArrayTemplate, objectCloneTemplate, - arrayCloneTemplate); + arrayCloneTemplate, + standalonePrintCharsTemplate); } @Override diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/visitors/WasmGCElementCreator.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/visitors/WasmGCElementCreator.java index 8e2a87ab1df8..ce4ada121a08 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/visitors/WasmGCElementCreator.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/visitors/WasmGCElementCreator.java @@ -52,6 +52,7 @@ import com.oracle.svm.hosted.webimage.wasmgc.ast.id.GCKnownIds; import com.oracle.svm.hosted.webimage.wasmgc.ast.id.WebImageWasmGCIds; import com.oracle.svm.hosted.webimage.wasmgc.codegen.WasmGCCloneSupport; +import com.oracle.svm.hosted.webimage.options.WebImageOptions; import com.oracle.svm.hosted.webimage.wasmgc.codegen.WebImageWasmGCProviders; import com.oracle.svm.hosted.webimage.wasmgc.types.WasmRefType; import com.oracle.svm.webimage.wasm.types.WasmPackedType; @@ -172,9 +173,12 @@ protected void registerNewJavaStructType(WebImageWasmGCIds.JavaStruct javaStruct fields.addAll(getExtraHubFields()); } - // The WasmExtern class gets an additional non-java field holding the externref + // The WasmExtern class gets an additional non-java field holding the externref. + // In standalone mode, externref is not available (no JS host), so use a dummy i32 field + // to preserve struct layout without introducing externref into the module. if (javaType.equals(wasmProviders.getMetaAccess().lookupJavaType(WasmExtern.class))) { - fields.add(new StructType.Field(knownIds.embedderField, FieldType.immutable(WasmRefType.EXTERNREF), "Internal field holding a reference to an embedder object")); + WasmStorageType fieldType = WebImageOptions.isStandaloneWasm() ? WasmPrimitiveType.i32 : WasmRefType.EXTERNREF; + fields.add(new StructType.Field(knownIds.embedderField, FieldType.immutable(fieldType), "Internal field holding a reference to an embedder object")); } boolean isFinal = false; diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WasmGCFunctionTemplates.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WasmGCFunctionTemplates.java index 8e194ef89eaa..f1f988169d44 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WasmGCFunctionTemplates.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WasmGCFunctionTemplates.java @@ -39,6 +39,8 @@ import com.oracle.svm.hosted.meta.HostedMethod; import com.oracle.svm.hosted.meta.HostedType; import com.oracle.svm.hosted.webimage.name.WebImageNamingConvention; +import com.oracle.svm.hosted.webimage.options.WebImageOptions; +import com.oracle.svm.hosted.webimage.wasm.WasmImports; import com.oracle.svm.hosted.webimage.wasm.ast.Function; import com.oracle.svm.hosted.webimage.wasm.ast.FunctionTypeDescriptor; import com.oracle.svm.hosted.webimage.wasm.ast.Instruction; @@ -113,6 +115,16 @@ protected String getFunctionName() { @Override protected Function createFunction(Context ctxt) { WebImageWasmGCProviders providers = (WebImageWasmGCProviders) ctxt.getProviders(); + + if (WebImageOptions.isStandaloneWasm()) { + // In standalone mode, produce a stub that returns i32(0) instead of externref. + // This function is dead code but must still be valid. + Function f = ctxt.createFunction(TypeUse.forUnary(WasmPrimitiveType.i32, providers.util().getJavaLangObjectType()), + "Stub: extern.unwrap not available in standalone mode"); + f.getInstructions().add(Instruction.Const.forInt(0)); + return f; + } + ResolvedJavaType wasmExternType = providers.getMetaAccess().lookupJavaType(WasmExtern.class); WasmId.StructType wasmExternId = idFactory.newJavaStruct(wasmExternType); WasmRefType wasmExternRef = wasmExternId.asNonNull(); @@ -161,9 +173,19 @@ protected String getFunctionName() { protected Function createFunction(Context ctxt) { WebImageWasmGCProviders providers = (WebImageWasmGCProviders) ctxt.getProviders(); WasmGCUtil util = providers.util(); + WasmRefType javaLangObjectType = util.getJavaLangObjectType(); + + if (WebImageOptions.isStandaloneWasm()) { + // In standalone mode, produce a stub that takes i32 and returns null. + // This function is dead code but must still be valid. + Function f = ctxt.createFunction(TypeUse.forUnary(javaLangObjectType, WasmPrimitiveType.i32), + "Stub: extern.wrap not available in standalone mode"); + f.getInstructions().add(new Instruction.RefNull(javaLangObjectType)); + return f; + } + ResolvedJavaType wasmExternType = providers.getMetaAccess().lookupJavaType(WasmExtern.class); WasmId.StructType wasmExternId = idFactory.newJavaStruct(wasmExternType); - WasmRefType javaLangObjectType = util.getJavaLangObjectType(); JavaConstant hubConstant = providers.getConstantReflection().asJavaClass(providers.getMetaAccess().lookupJavaType(WasmExtern.class)); @@ -1136,4 +1158,131 @@ protected Function createFunction(Context context) { return f; } } + + /** + * Function that prints a GC-managed char array to a file descriptor using batch I/O + * via a linear memory transfer buffer. + *

+ * This is used in standalone WASM mode where GC arrays cannot be passed as linear memory + * pointers to host functions. Characters are copied from the GC array into a 1-page (64KB) + * linear memory buffer, then flushed to the host via {@code io.print_buffer(fd, ptr, count)}. + *

+ * For arrays larger than the buffer (32K chars), the copy+flush is repeated in chunks. + * This is 10-100x faster than per-character {@code print_char} for long strings. + *

+ * Generates (simplified): + * + *

{@code
+     * (func $standalone.printChars (param $fd i32) (param $array (ref null $charArrayStruct))
+     *   (local $i i32) (local $len i32) (local $chunk i32)
+     *   (local.set $len (array.len ...))
+     *   (block $done (loop $outer
+     *     (br_if $done (i32.ge_u $i $len))
+     *     ;; chunk = min(len - i, 32768)
+     *     ;; inner loop: copy chars to linear memory at offset j*2
+     *     (block $inner_done (loop $inner
+     *       (br_if $inner_done (i32.ge_u $j $chunk))
+     *       (i32.store16 offset=0 (i32.shl $j 1) (array.get_u ... (i32.add $i $j)))
+     *       (local.set $j (i32.add $j 1))
+     *       (br $inner)))
+     *     (call $io.print_buffer $fd (i32.const 0) $chunk)
+     *     (local.set $i (i32.add $i $chunk))
+     *     (br $outer))))
+     * }
+ */ + public static class StandalonePrintChars extends WasmFunctionTemplate.Singleton { + + /** Max chars per batch (1 page = 64KB, 2 bytes per char = 32K chars). */ + private static final int BUFFER_CHARS = 32768; + + public StandalonePrintChars(WasmIdFactory idFactory) { + super(idFactory, true); + } + + @Override + protected String getFunctionName() { + return "standalone.printChars"; + } + + @Override + protected Function createFunction(Context ctxt) { + WebImageWasmGCProviders providers = (WebImageWasmGCProviders) ctxt.getProviders(); + GCKnownIds knownIds = providers.knownIds(); + + WasmValType charArrayStructType = knownIds.getArrayStructType(JavaKind.Char).asNullable(); + + Function f = ctxt.createFunction( + TypeUse.withoutResult(WasmPrimitiveType.i32, charArrayStructType), + "Print char array to fd via linear memory batch buffer"); + Instructions instructions = f.getInstructions(); + + WasmId.Local fdParam = f.getParam(0); + WasmId.Local arrayParam = f.getParam(1); + WasmId.Local srcIndex = idFactory.newTemporaryVariable(WasmPrimitiveType.i32); // position in source array + WasmId.Local arrayLength = idFactory.newTemporaryVariable(WasmPrimitiveType.i32); + WasmId.Local chunkSize = idFactory.newTemporaryVariable(WasmPrimitiveType.i32); // chars in current chunk + WasmId.Local bufIndex = idFactory.newTemporaryVariable(WasmPrimitiveType.i32); // position within chunk + + WasmId.Func printBufferImport = idFactory.forFunctionImport(WasmImports.printBuffer); + + // len = array.length + instructions.add(arrayLength.setter(providers.builder().getArrayLength(arrayParam.getter()))); + + // Outer loop: process chunks + WasmId.Label doneLabel = idFactory.newInternalLabel("done"); + WasmId.Label outerLabel = idFactory.newInternalLabel("outer"); + Instruction.Block outerBlock = new Instruction.Block(doneLabel); + instructions.add(outerBlock); + Instruction.Loop outerLoop = new Instruction.Loop(outerLabel); + outerBlock.instructions.add(outerLoop); + + // Break if srcIndex >= arrayLength + outerLoop.instructions.add(new Instruction.Break(doneLabel, + Binary.Op.I32GeU.create(srcIndex.getter(), arrayLength.getter()))); + + // chunkSize = min(arrayLength - srcIndex, BUFFER_CHARS) + Instruction remaining = Binary.Op.I32Sub.create(arrayLength.getter(), srcIndex.getter()); + Instruction.If chunkIf = new Instruction.If(null, + Binary.Op.I32LtU.create(remaining, Instruction.Const.forInt(BUFFER_CHARS))); + chunkIf.thenInstructions.add(chunkSize.setter(Binary.Op.I32Sub.create(arrayLength.getter(), srcIndex.getter()))); + chunkIf.elseInstructions.add(chunkSize.setter(Instruction.Const.forInt(BUFFER_CHARS))); + outerLoop.instructions.add(chunkIf); + + // Reset buffer index + outerLoop.instructions.add(bufIndex.setter(Instruction.Const.forInt(0))); + + // Inner loop: copy chars to linear memory + WasmId.Label innerDoneLabel = idFactory.newInternalLabel("innerDone"); + WasmId.Label innerLabel = idFactory.newInternalLabel("inner"); + Instruction.Block innerBlock = new Instruction.Block(innerDoneLabel); + outerLoop.instructions.add(innerBlock); + Instruction.Loop innerLoop = new Instruction.Loop(innerLabel); + innerBlock.instructions.add(innerLoop); + + // Break inner if bufIndex >= chunkSize + innerLoop.instructions.add(new Instruction.Break(innerDoneLabel, + Binary.Op.I32GeU.create(bufIndex.getter(), chunkSize.getter()))); + + // i32.store16 at byte offset (bufIndex * 2), value = array[srcIndex + bufIndex] + Instruction srcArrayIndex = Binary.Op.I32Add.create(srcIndex.getter(), bufIndex.getter()); + Instruction charValue = providers.builder().getArrayElement(arrayParam.getter(), srcArrayIndex, JavaKind.Char); + Instruction byteOffset = Binary.Op.I32Shl.create(bufIndex.getter(), Instruction.Const.forInt(1)); + // Store as i32 with memoryWidth=16 (i32.store16) + innerLoop.instructions.add(new Instruction.Store(WasmPrimitiveType.i32, 0, charValue, byteOffset, 16)); + + // bufIndex++ + innerLoop.instructions.add(bufIndex.setter(Binary.Op.I32Add.create(bufIndex.getter(), Instruction.Const.forInt(1)))); + innerLoop.instructions.add(new Instruction.Break(innerLabel)); + + // After inner loop: call print_buffer(fd, 0, chunkSize) + outerLoop.instructions.add(new Instruction.Call(printBufferImport, + fdParam.getter(), Instruction.Const.forInt(0), chunkSize.getter())); + + // srcIndex += chunkSize + outerLoop.instructions.add(srcIndex.setter(Binary.Op.I32Add.create(srcIndex.getter(), chunkSize.getter()))); + outerLoop.instructions.add(new Instruction.Break(outerLabel)); + + return f; + } + } } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCCodeGen.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCCodeGen.java index e99e26aa1614..2d4eb60796b2 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCCodeGen.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCCodeGen.java @@ -41,11 +41,16 @@ import com.oracle.svm.hosted.webimage.WebImageHostedConfiguration; import com.oracle.svm.hosted.webimage.codegen.LowerableResource; import com.oracle.svm.hosted.webimage.codegen.LowerableResources; +import com.oracle.svm.hosted.webimage.options.WebImageOptions; +import com.oracle.svm.hosted.webimage.util.metrics.ImageMetricsCollector; import com.oracle.svm.hosted.webimage.codegen.WebImageProviders; import com.oracle.svm.hosted.webimage.js.JSBody; import com.oracle.svm.hosted.webimage.js.JSKeyword; import com.oracle.svm.hosted.webimage.wasm.WasmJSCounterparts; +import com.oracle.svm.hosted.webimage.wasm.ast.Export; import com.oracle.svm.hosted.webimage.wasm.ast.Instruction; +import com.oracle.svm.hosted.webimage.wasm.ast.Limit; +import com.oracle.svm.hosted.webimage.wasm.ast.Memory; import com.oracle.svm.hosted.webimage.wasm.ast.id.WasmId; import com.oracle.svm.hosted.webimage.wasm.ast.visitors.WasmElementCreator; import com.oracle.svm.hosted.webimage.wasm.ast.visitors.WasmRelocationVisitor; @@ -90,6 +95,14 @@ protected void writeImageHeap() { protected void genWasmModule() { super.genWasmModule(); + if (WebImageOptions.isStandaloneWasm()) { + // Add a 1-page (64KB) linear memory for batch printing buffer. + // WasmGC modules can have both GC types and linear memory. + WasmId.Memory memId = getProviders().idFactory().forMemory(0); + module.setMemory(new Memory(memId, Limit.fixed(1), "Batch printing buffer")); + module.addExport(new Export(Export.Type.MEM, memId, "memory", "Linear memory for host I/O")); + } + for (Map.Entry entry : ((WebImageWasmGCCodeCache) codeCache).getExportedMethodMetadata().entrySet()) { HostedMethod m = entry.getKey(); String exportedName = entry.getValue(); @@ -157,10 +170,30 @@ protected void validateModule() { @Override protected void emitBootstrapDefinitions() { + if (WebImageOptions.isStandaloneWasm()) { + // In standalone mode, skip all JS bootstrap emissions. + // The WASM module runs without a JS host, so no JS imports or + // conversion code is needed. + return; + } super.emitBootstrapDefinitions(); emitJSBodyImports(); } + @Override + @SuppressWarnings("try") + protected void emitJSCode() { + if (WebImageOptions.isStandaloneWasm()) { + // In standalone mode, no JS host file is produced. + // Still create the metrics scope so post-processing counters are initialized. + try (ImageMetricsCollector collector = new ImageMetricsCollector.PreClosure(codeBuffer)) { + // No JS code to emit. + } + return; + } + super.emitJSCode(); + } + /** * Generates the import object for the {@link WasmJSCounterparts#JSBODY_MODULE_NAME} module * containing all JS code provided through {@link JSBody} nodes. diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCNodeLowerer.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCNodeLowerer.java index 225d9cb8ea94..df0947967af3 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCNodeLowerer.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCNodeLowerer.java @@ -46,6 +46,7 @@ import com.oracle.svm.hosted.webimage.js.JSBodyNode; import com.oracle.svm.hosted.webimage.js.JSBodyWithExceptionNode; import com.oracle.svm.hosted.webimage.options.WebImageOptions; +import com.oracle.svm.hosted.webimage.wasm.WasmImports; import com.oracle.svm.hosted.webimage.wasm.WasmJSCounterparts; import com.oracle.svm.hosted.webimage.wasm.WebImageWasmOptions; import com.oracle.svm.hosted.webimage.wasm.ast.Instruction; @@ -966,6 +967,13 @@ private Instruction callWrapExtern(Instruction externref) { @Override protected Instruction lowerWasmImportForeignCall(WasmImportForeignCallDescriptor descriptor, Instructions args) { + if (WebImageOptions.isStandaloneWasm() && descriptor == WasmImports.PROXY_CHAR_ARRAY) { + // In standalone mode, proxyCharArray (JS interop) is not available. + // Return a null WasmExtern ref (the caller just needs an opaque handle). + WasmRefType wasmExternType = (WasmRefType) masm().getWasmProviders().util().typeForJavaClass(WasmExtern.class); + return new Instruction.RefNull(wasmExternType); + } + Class[] argTypes = descriptor.getArgumentTypes(); for (int i = 0; i < argTypes.length; i++) { if (argTypes[i] == WasmExtern.class) { @@ -998,8 +1006,16 @@ protected Instruction lowerWasmForeignCall(WasmForeignCallDescriptor descriptor, } else if (descriptor == WasmGCCloneSupport.CLONE_TEMPLATE) { return new Instruction.Call(masm().getKnownIds().genericCloneTemplate.requestFunctionId(), args); } else if (descriptor == WasmGCJSConversion.EXTRACT_JS_NATIVE) { + if (WebImageOptions.isStandaloneWasm()) { + // JSValue native field access requires externref — return null ref in standalone + WasmRefType wasmExternType = (WasmRefType) masm().getWasmProviders().util().typeForJavaClass(WasmExtern.class); + return new Instruction.RefNull(wasmExternType); + } return new Instruction.Call(masm().getKnownIds().extractJSValueTemplate.requestGetterFunctionId(), args); } else if (descriptor == WasmGCJSConversion.SET_JS_NATIVE) { + if (WebImageOptions.isStandaloneWasm()) { + return new Instruction.Nop(); + } return new Instruction.Call(masm().getKnownIds().extractJSValueTemplate.requestSetterFunctionId(), args); } else { return super.lowerWasmForeignCall(descriptor, args); @@ -1056,6 +1072,10 @@ protected Instruction lowerSlotTypeCheck(Instructions args) { * @see WasmJSCounterparts */ private Instruction lowerJSCall(JSCallNode n) { + if (WebImageOptions.isStandaloneWasm()) { + return lowerJSCallStandalone(n); + } + JSSystemFunction func = n.getFunctionDefinition(); Instructions params = new Instructions(); @@ -1084,6 +1104,72 @@ private Instruction lowerJSCall(JSCallNode n) { return callResult; } + /** + * Lowers JSCallNode for standalone WASM mode (no JS interop). + *

+ * Known JS functions are routed to standalone host imports or stubbed. + * Printing is done via a per-character {@code io.print_char} import since WasmGC arrays + * cannot be passed as linear memory pointers. + */ + private Instruction lowerJSCallStandalone(JSCallNode n) { + JSSystemFunction func = n.getFunctionDefinition(); + String funcName = func.getFunctionName(); + + return switch (funcName) { + // Printing: call print_char_array template which iterates GC array + case "stdoutWriter.printChars" -> { + assert n.getArguments().size() == 1 : "Expected 1 arg for printChars"; + Instruction charArray = lowerExpression(n.getArguments().get(0)); + yield new Instruction.Call( + masm().getWasmProviders().knownIds().standalonePrintCharsTemplate.requestFunctionId(), + Const.forInt(1), charArray); + } + case "stderrWriter.printChars" -> { + assert n.getArguments().size() == 1 : "Expected 1 arg for printChars"; + Instruction charArray = lowerExpression(n.getArguments().get(0)); + yield new Instruction.Call( + masm().getWasmProviders().knownIds().standalonePrintCharsTemplate.requestFunctionId(), + Const.forInt(2), charArray); + } + + // Flush/close: no-op (print_char writes are unbuffered) + case "stdoutWriter.flush", "stderrWriter.flush", + "stdoutWriter.close", "stderrWriter.close" -> + new Instruction.Nop(); + + // Time: route to host_time_ms import + case "performance.now", "Date.now" -> + new Instruction.Call(masm().idFactory.forFunctionImport(WasmImports.hostTimeMs)); + + // Exit: route to WASI proc_exit + case "runtime.setExitCode" -> { + Instructions params = new Instructions(); + n.getArguments().forEach(param -> params.add(lowerExpression(param))); + yield new Instruction.Call(masm().idFactory.forFunctionImport(WasmImports.wasiProcExit), params); + } + + // Stack traces: stub with null refs (handles -H:+DisableStackTraces pattern) + case "genBacktrace", "gen_call_stack", "formatStackTrace" -> + getStub(n); + + // Console/debug: no-op + case "console.trace" -> new Instruction.Nop(); + + // Memory management: WasmGC manages its own heap, stub these + case "heap.malloc", "heap.calloc", "heap.realloc" -> Const.forLong(0); + case "heap.free" -> new Instruction.Nop(); + + // Array operations: stub (handled by WasmGC array copy templates) + case "arrayCopy", "arraysCopyOf", "arraysCopyOfWithHub" -> getStub(n); + + // CWD: return stub + case "getCurrentWorkingDirectory" -> getStub(n); + + // All other JS functions: stub + default -> getStub(n); + }; + } + /** * Generates a call to an imported JS function that contains the code of the given * {@link JSBody} node. @@ -1098,6 +1184,12 @@ private Instruction lowerJSCall(JSCallNode n) { * */ private Instruction lowerJSBody(T jsBody) { + if (WebImageOptions.isStandaloneWasm()) { + // In standalone mode, JSBody nodes cannot execute (no JS runtime). + // Return a stub value of the appropriate type. + return getStub(jsBody.asNode()); + } + WebImageWasmGCProviders wasmProviders = masm().getWasmProviders(); Instructions params = new Instructions(); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCProviders.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCProviders.java index 68b3f39b41fb..57ab3ad7cba0 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCProviders.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/WebImageWasmGCProviders.java @@ -32,6 +32,8 @@ import com.oracle.svm.core.graal.meta.RuntimeConfiguration; import com.oracle.svm.hosted.meta.HostedMethod; import com.oracle.svm.hosted.webimage.codegen.WebImageProviders; +import com.oracle.svm.hosted.webimage.options.WebImageOptions; +import com.oracle.svm.hosted.webimage.wasm.WasmImports; import com.oracle.svm.hosted.webimage.wasm.ast.id.KnownIds; import com.oracle.svm.hosted.webimage.wasm.ast.id.WasmIdFactory; import com.oracle.svm.hosted.webimage.wasm.codegen.WasmCodeGenTool; @@ -58,6 +60,42 @@ public class WebImageWasmGCProviders extends WebImageWasmProviders { public WebImageWasmGCProviders(RuntimeConfiguration runtimeConfig, CoreProviders underlyingProviders, PrintStream out, DebugContext debug) { super(runtimeConfig, underlyingProviders, out, debug); this.builder = new WasmGCBuilder(this); + + if (WebImageOptions.isStandaloneWasm()) { + registerComponentImportRemappings(); + } + } + + /** + * Registers import name remappings for WebAssembly Component Model compatibility. + *

+ * The component model requires fully-qualified module names + * (e.g. {@code graalvm:standalone/io@0.1.0}) and kebab-case function names + * (e.g. {@code print-char}). + */ + private void registerComponentImportRemappings() { + WasmIdFactory factory = idFactory(); + + // Compat math imports + factory.addImportRemapping(WasmImports.F32Rem, WasmImports.Component.F32Rem); + factory.addImportRemapping(WasmImports.F64Rem, WasmImports.Component.F64Rem); + factory.addImportRemapping(WasmImports.F64Log, WasmImports.Component.F64Log); + factory.addImportRemapping(WasmImports.F64Log10, WasmImports.Component.F64Log10); + factory.addImportRemapping(WasmImports.F64Sin, WasmImports.Component.F64Sin); + factory.addImportRemapping(WasmImports.F64Cos, WasmImports.Component.F64Cos); + factory.addImportRemapping(WasmImports.F64Tan, WasmImports.Component.F64Tan); + factory.addImportRemapping(WasmImports.F64Tanh, WasmImports.Component.F64Tanh); + factory.addImportRemapping(WasmImports.F64Exp, WasmImports.Component.F64Exp); + factory.addImportRemapping(WasmImports.F64Pow, WasmImports.Component.F64Pow); + factory.addImportRemapping(WasmImports.F64Cbrt, WasmImports.Component.F64Cbrt); + + // IO imports + factory.addImportRemapping(WasmImports.printChar, WasmImports.Component.printChar); + factory.addImportRemapping(WasmImports.printBuffer, WasmImports.Component.printBuffer); + factory.addImportRemapping(WasmImports.hostTimeMs, WasmImports.Component.hostTimeMs); + + // WASI imports + factory.addImportRemapping(WasmImports.wasiProcExit, WasmImports.Component.procExit); } public static WebImageWasmGCProviders singleton() {