diff --git a/vm/src/org.graalvm.polybench/src/org/graalvm/polybench/PolyBenchLauncher.java b/vm/src/org.graalvm.polybench/src/org/graalvm/polybench/PolyBenchLauncher.java index e02d46a6dceb..0543a0719766 100644 --- a/vm/src/org.graalvm.polybench/src/org/graalvm/polybench/PolyBenchLauncher.java +++ b/vm/src/org.graalvm.polybench/src/org/graalvm/polybench/PolyBenchLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -591,7 +591,7 @@ public void run() { // language-specific lookup switch (languageId) { case "wasm": - result = evalSourceValue.getMember(memberName); + result = evalSourceValue.newInstance().getMember("exports").getMember(memberName); break; case "java": // Espresso doesn't provide methods as executable values. diff --git a/wasm/CHANGELOG.md b/wasm/CHANGELOG.md index 6b46c499693b..b70f0faeee32 100644 --- a/wasm/CHANGELOG.md +++ b/wasm/CHANGELOG.md @@ -4,6 +4,25 @@ This changelog summarizes major changes to the WebAssembly engine implemented in ## Version 25.0.0 +* BREAKING: Changed Context.eval of _wasm_ sources to return a compiled, but not yet instantiated, module object instead of the module instance. + To instantiate the module, you have to call `newInstance()` on the module object now, e.g.: + ```java + Context c = Context.create(); + Source wasmSource = Source.newBuilder...; + Value module = c.eval(wasmSource); + Value instance = module.newInstance(); // < 25.0: c.eval(wasmSource) + ``` + This change enables modules to be instantiated multiple times—and run independently—within the same context. Previously, each module could only be instantiated once per context. + `newInstance()` optionally also accepts an import object, similar to the JS WebAssembly API, as well as other modules to be linked together with the module. + The previous behavior can still be restored with the experimental option `--wasm.EvalReturnsInstance=true`. + Note: Modules instantiated using `module.newInstance()` are not accessible via `context.getBindings("wasm")`, unlike modules instantiated using `context.eval` when using `--wasm.EvalReturnsInstance=true`. +* BREAKING: Exports are no longer exposed as direct members of the module instance. + Use the `exports` member of the module instance to access its exports, e.g:. + ```java + Value mainFunction = instance.getMember("exports").getMember("main"); // < 25.0: instance.getMember("main") + ``` + This aligns with the JS WebAssembly API and allows other members to be introduced on the module instance without potential name clashes. + More information about these API changes and examples can be found in the [GraalWasm Polyglot API Migration Guide](docs/user/GraalWasmAPIMigration.md) and the [Readme](docs/user/README.md). * Implemented support for editing primitive values during debugging. Fixed several debugger-related issues. ## Version 24.2.0 diff --git a/wasm/docs/site/Gemfile b/wasm/docs/site/Gemfile index 2c11ff519e21..d84d6e21871e 100644 --- a/wasm/docs/site/Gemfile +++ b/wasm/docs/site/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem "jekyll", "~> 4.3.4" +gem "jekyll", "~> 4.4.0" gem "graal-languages-jekyll-theme" diff --git a/wasm/docs/site/Gemfile.lock b/wasm/docs/site/Gemfile.lock index 6a16bd6af85e..7bd272b3edc9 100644 --- a/wasm/docs/site/Gemfile.lock +++ b/wasm/docs/site/Gemfile.lock @@ -11,10 +11,11 @@ GEM io-event (~> 1.9) metrics (~> 0.12) traces (~> 0.15) + base64 (0.3.0) bigdecimal (3.1.8) coderay (1.1.3) colorator (1.1.0) - concurrent-ruby (1.3.4) + concurrent-ruby (1.3.5) console (1.29.3) fiber-annotation fiber-local (~> 1.1) @@ -26,17 +27,16 @@ GEM ethon (0.16.0) ffi (>= 1.15.0) eventmachine (1.2.7) - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86_64-linux-gnu) + ffi (1.17.0) fiber-annotation (0.2.0) fiber-local (1.1.0) fiber-storage fiber-storage (1.0.0) forwardable-extended (2.6.0) - google-protobuf (4.28.3-arm64-darwin) + google-protobuf (4.31.1-arm64-darwin) bigdecimal rake (>= 13) - google-protobuf (4.28.3-x86_64-linux) + google-protobuf (4.31.1-x86_64-linux-gnu) bigdecimal rake (>= 13) graal-languages-jekyll-theme (0.1.0) @@ -56,20 +56,23 @@ GEM csv mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.14.6) + i18n (1.14.7) concurrent-ruby (~> 1.0) io-event (1.9.0) - jekyll (4.3.4) + jekyll (4.4.1) addressable (~> 2.4) + base64 (~> 0.2) colorator (~> 1.0) + csv (~> 3.0) em-websocket (~> 0.5) i18n (~> 1.0) jekyll-sass-converter (>= 2.0, < 4.0) jekyll-watch (~> 2.0) + json (~> 2.6) kramdown (~> 2.3, >= 2.3.1) kramdown-parser-gfm (~> 1.0) liquid (~> 4.0) - mercenary (>= 0.3.6, < 0.5) + mercenary (~> 0.3, >= 0.3.6) pathutil (~> 0.9) rouge (>= 3.0, < 5.0) safe_yaml (~> 1.0) @@ -77,15 +80,15 @@ GEM webrick (~> 1.7) jekyll-relative-links (0.7.0) jekyll (>= 3.3, < 5.0) - jekyll-sass-converter (3.0.0) - sass-embedded (~> 1.54) + jekyll-sass-converter (3.1.0) + sass-embedded (~> 1.75) jekyll-seo-tag (2.8.0) jekyll (>= 3.8, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) json (2.10.1) - kramdown (2.4.0) - rexml + kramdown (2.5.1) + rexml (>= 3.3.9) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.4) @@ -117,18 +120,18 @@ GEM racc (1.8.1) rack (3.1.8) rainbow (3.1.1) - rake (13.2.1) + rake (13.3.0) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rexml (3.3.9) - rouge (4.5.1) + rexml (3.4.1) + rouge (4.5.2) ruby-rc4 (0.1.5) safe_yaml (1.0.5) - sass-embedded (1.81.0-arm64-darwin) - google-protobuf (~> 4.28) - sass-embedded (1.81.0-x86_64-linux-gnu) - google-protobuf (~> 4.28) + sass-embedded (1.89.1-arm64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.89.1-x86_64-linux-gnu) + google-protobuf (~> 4.31) siteleaf (2.3.0) httparty (>= 0.16.0) jekyll (>= 1.4.1) @@ -141,7 +144,7 @@ GEM typhoeus (1.4.1) ethon (>= 0.9.0) unicode-display_width (2.6.0) - webrick (1.9.0) + webrick (1.9.1) yell (2.2.2) zeitwerk (2.7.2) @@ -153,7 +156,7 @@ DEPENDENCIES graal-languages-jekyll-theme html-proofer http_parser.rb (~> 0.6.0) - jekyll (~> 4.3.4) + jekyll (~> 4.4.0) jekyll-relative-links jekyll-seo-tag pry diff --git a/wasm/docs/site/_plugins/wat_lexer.rb b/wasm/docs/site/_plugins/wat_lexer.rb new file mode 100644 index 000000000000..cb4f20fe4789 --- /dev/null +++ b/wasm/docs/site/_plugins/wat_lexer.rb @@ -0,0 +1,64 @@ +# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + +require 'rouge' + +# overly simplistic wat lexer +module Rouge + module Lexers + class Wat < RegexLexer + tag 'wat' + filenames '*.wat' + title "wat" + desc "WebAssembly Text Format" + + state :root do + rule %r/;;.*$/, Comment::Single + rule %r/\(;.*?;\)/m, Comment::Multiline + + # literals + rule %r/"([^\\"]|\\.)*?"/, Str + rule %r/[-+]?(?:\d+|0x[0-9a-fA-F]+)/, Num::Integer + rule %r/[-+]?\d+\.\d+(?:[eE][-+]?\d+)?/, Num::Float + + # keywords + rule %r/\b( + module|func|type|param|result|local|global|memory|data|table|elem|start|import|export|mut + )(?!\.)\b/x, Keyword + + # types + rule %r/\b( + i32|i64|f32|f64|v128|funcref|externref + )(?!\.)\b/x, Keyword::Type + + # instructions + rule %r/\b( + local\.(?:get|set|tee)| + global\.(?:get|set)| + (?:i32|i64|f32|f64|v128|i8x16|i16x8|i32x4|i64x2|f32x4|f64x2)\.[a-z0-9_]+| + nop|unreachable| + block|loop|if|else|end| + br|br_if|br_table| + return| + call|call_indirect| + drop|select| + table\.(?:get|set|size|grow|fill|copy|init)| + elem\.drop| + memory\.(?:size|grow|fill|copy|init) + data\.drop| + ref\.(?:null|is_null|func) + )(?!\.)\b/x, Name::Attribute + + # names + rule %r/\$[a-zA-Z0-9._-]*/, Name::Builtin + + # attributes + rule %r/(?:offset|align)=/, Name::Property + + rule %r/[()]/, Punctuation + + rule %r/\s+/, Text + end + end + end +end diff --git a/wasm/docs/site/index.md b/wasm/docs/site/index.md index 92f06a44d7bf..aae3a29dbb29 100644 --- a/wasm/docs/site/index.md +++ b/wasm/docs/site/index.md @@ -150,12 +150,15 @@ implementation("org.graalvm.polyglot:wasm:{{ site.language_version }}")

2. Create a WebAssembly module, for example with wat2wasm

- {%- highlight javascript -%} - (module - (func (export "addTwo") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.add)) + {%- highlight wat -%} +;; wat2wasm add-two.wat -o add-two.wasm +(module + (func (export "addTwo") (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.add + ) +) {%- endhighlight -%}
@@ -167,10 +170,34 @@ implementation("org.graalvm.polyglot:wasm:{{ site.language_version }}")
-
-

3. Embed the Wasm module in Java

-
- {%- highlight java -%} +
+

3. Embed the Wasm module in Java

+
+
+ +
+
+{% highlight java %} +import java.net.URL; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Value; + +try (Context context = Context.create()) { + URL wasmFile = Main.class.getResource("add-two.wasm"); + Value mainModule = context.eval(Source.newBuilder("wasm", wasmFile).build()); + Value mainInstance = mainModule.newInstance(); + Value addTwo = mainInstance.getMember("exports").getMember("addTwo"); + System.out.println("addTwo(40, 2) = " + addTwo.execute(40, 2)); +} +{% endhighlight %} +
+
+{% highlight java %} import java.net.URL; import org.graalvm.polyglot.Context; @@ -184,8 +211,11 @@ try (Context context = Context.create()) { Value addTwo = context.getBindings("wasm").getMember(moduleName).getMember("addTwo"); System.out.println("addTwo(40, 2) = " + addTwo.execute(40, 2)); } - {%- endhighlight -%} -
+{% endhighlight %} +
+
+
+
diff --git a/wasm/docs/user/GraalWasmAPIMigration.md b/wasm/docs/user/GraalWasmAPIMigration.md new file mode 100644 index 000000000000..60ac4f6260ae --- /dev/null +++ b/wasm/docs/user/GraalWasmAPIMigration.md @@ -0,0 +1,171 @@ +# GraalWasm Polyglot API Migration and Usage Guide + +In GraalWasm version 25.0, the GraalWasm Polyglot Embedding API has been changed to more closely align with the WebAssembly JS API and its usage patterns. +This document outlines the main differences and serves as a migration guide. + +## Overview of the Differences between the Old and New API + +| Difference | **New API (since 25.0)** | **Old API (before 25.0)** | +|------------------------------------|-----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| **Key Differences** | Explicit module instantiation. Exports are accessed via the `exports` member of the instance | Implicit module instantiation. Exports are direct members of the instance | +| **Result of `context.eval(src)`** | Returns a compiled _module_ object (not yet instantiated) [^3] | Returns an already instantiated module _instance_ [^3] | +| **Module Instantiation** | Explicitly instantiated by calling `newInstance()` on the _module_ returned by `context.eval` | Implicitly instantiated by `context.eval()` | +| **Repeated Instantiation** | Same module can be instantiated multiple times per context | Modules can be instantiated only once per context (cached by name) | +| **Import Linking** | Linking is explicit and eager, performed by `module.newInstance()` | Linking is implicit and lazy, occurs on first export access | +| **Start Function Invocation** | Runs during `module.newInstance()` | Runs on first export access if not yet linked | +| **Accessing Exports** | Via `instance.getMember("exports").getMember("exportName")` | Via `instance.getMember("exportName")` | +| **Invoking a Function[^1]** | `instance.getMember("exports").invokeMember("functionName", ...args);`, or: | `instance.invokeMember("functionName", ...args)`, or: | +| **Invoking a Function[^2]** | `Value fn = instance.getMember("exports").getMember("functionName"); fn.execute(...args)` | `Value fn = instance.getMember("functionName"); fn.execute(...args)` | +| **Module Instance Members** | The `"exports"` object member | The exports of the module _instance_ | +| **Importing Host Functions** | Supported via optional import object passed to `module.newInstance(importObject)` | Not supported | +| **Cyclic Import Dependencies** | Not allowed | Allowed | + +[^1] Recommended approach for invoking a function once or only a few times: Simply use `exports.invokeMember("functionName", ...args)` (combines lookup and function call). +[^2] Recommended approach for invoking a function repeatedly: First, use `Value function = exports.getMember("functionName");` to look up the function, then call it using `function.execute(...args)`. +[^3] Note: To ease migration, you can set the option `wasm.EvalReturnsInstance=true` to revert to the old `Context.eval` behavior for the time being. + +## Module Instantiation + +Old approach ≤24.2 (implicit instantiation), or with `wasm.EvalReturnsInstance=true`[^3]: +```java +Source source = Source.newBuilder("wasm", ByteSequence.create(bytes), "example").build(); + +// Evaluate the Source named "example", returns a module instance (cf. WebAssembly.Instance). +Value instance = context.eval(source); +``` + +New approach ≥25.0 (explicit instantiation): +```java +Source source = Source.newBuilder("wasm", ByteSequence.create(bytes), "example").build(); + +// Evaluate the Source named "example", returns a module object (cf. WebAssembly.Module). +Value module = context.eval(source); + +// Instantiate the example module (optional: provide an import object). +Value instance = module.newInstance(); +Value anotherInstance = module.newInstance(); +``` + +## Accessing Exports + +Old approach ≤24.2 (no _exports_ member indirection): +```java +// Compile and instantiate the module. +Value instance = context.eval(source); + +// Invoke an exported function. +assert instance.invokeMember("factorial", 6).asInt() == 720; + +// Or if you need to call a function multiple times: +Value factorial = instance.getMember("factorial"); +assert factorial.execute(7).asInt() == 5040; +assert factorial.execute(8).asInt() == 40320; +``` + +New approach ≥25.0 (via _exports_ member indirection): +```java +// Compile and instantiate the module +Value module = context.eval(source); +Value instance = module.newInstance(); + +// Get the exports member from the module instance. +Value exports = instance.getMember("exports"); + +// Invoke an exported function. +assert exports.invokeMember("factorial", 6).asInt() == 720; + +// Or if you need to call a function multiple times: +Value factorial = exports.getMember("factorial"); +assert factorial.execute(7).asInt() == 5040; +assert factorial.execute(8).asInt() == 40320; +``` + +## Importing Host Functions + +Old approach ≤24.2: N/A + +New approach ≥25.0 (using importObject argument to `newInstance()`): +```java +// Evaluate the example Source, returns a module object (cf. WebAssembly.Module). +Value module = context.eval(exampleSource); + +var imports = ProxyObject.fromMap(Map.of( + "system", ProxyObject.fromMap(Map.of( + "println", + (ProxyExecutable) (args) -> { + String text = (/* read string from memory */); + System.out.println(text); + return null; /* for void functions, simply return null */ + } + )) + )); + +// Instantiate the example module with imports obtained from the `imports` object. +Value instance = module.newInstance(imports); +``` + + +## Comparison with the WebAssembly JS API + +| Aspect | **WebAssembly JS API** | **GraalWasm Polyglot API (since 25.0)** | +|----------------------------|-------------------------------------------------------------------------|-------------------------------------------------------------| +| **Compilation** | `WebAssembly.compile(buffer)` or `new WebAssembly.Module(buffer)` | `context.eval(source)` → returns a compiled *Module* object | +| **Instantiation** | `WebAssembly.instantiate(module)` or `new WebAssembly.Instance(module)` | `module.newInstance()` | +| **Optional Import Object** | Passed as second argument to `WebAssembly.instantiate(module, imports)` | Passed as first argument to `module.newInstance(imports)` | +| **Exports Access** | `instance.exports.exportName` | `instance.getMember("exports").getMember("exportName")` | +| **Function Invocation** | `instance.exports.mul(3, 14)` | `instance.getMember("exports").invokeMember("mul", 3, 14)` | + +### Examples + +**Compile, instantiate, and call a function of a wasm module:** + +JS: +```js +const arrayBuffer = /*...*/; +const module = await WebAssembly.compile(arrayBuffer); // Compile +const instance = await WebAssembly.instantiate(module); // Instantiate + +const exports = instance.exports; +let theAnswer = exports.mul(3, 14); +``` + +Java (GraalWasm ≥25.0): +```java +Source source = Source.newBuilder("wasm", wasmBinary, "example").build(); +Value module = context.eval(source); // Compile +Value instance = module.newInstance(); // Instantiate + +Value exports = instance.getMember("exports"); +int theAnswer = exports.invokeMember("mul", 3, 14).asInt(); + +// or, alternatively: +Value mul = exports.getMember("mul"); +assert mul.execute(6, 7).asInt() == mul.execute(7, 6).asInt(); +``` + +**Instantiate a wasm module with an import object:** + +JS: +```js +const imports = { + console: { + log: (value) => console.log("The answer is: " + value), + }, +}; + +const instance = await WebAssembly.instantiate(module, imports); +instance.exports.callLog(42); +``` + +Java (GraalWasm ≥25.0): +```java +ProxyExecutable log = args -> { + System.out.println("The answer is: " + args[0].asInt()); + return null; +}; +var console = ProxyObject.fromMap(Map.of("log", log)); +var imports = ProxyObject.fromMap(Map.of("console", console)); + +Value instance = module.newInstance(imports); +instance.getMember("exports").invokeMember("callLog", 42); +``` diff --git a/wasm/docs/user/README.md b/wasm/docs/user/README.md index 2be896d05f53..4e3f53a7b061 100644 --- a/wasm/docs/user/README.md +++ b/wasm/docs/user/README.md @@ -15,37 +15,54 @@ A valid [Source](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Source Here is one way you can build a WebAssembly `Source`: ```java -Source source = Source.newBuilder("wasm", new File("example.wasm")).name("example").build(); +Source source = Source.newBuilder("wasm", new File("example.wasm")).build(); ``` -When you evaluate a WebAssembly `Source`, the module is parsed, and validated, and a module instance is returned as the resulting value. -The module instance can also later be retrieved from the top-level bindings. -The name of the binding in the top-level scope is the same as the name of the `Source` that was evaluated. +Or directly from a byte array: ```java -// Evaluate the Source named "example". +Source source = Source.newBuilder("wasm", ByteSequence.create(in.readAllBytes()), "example").build(); +``` + +When you evaluate a WebAssembly `Source`, the module is parsed and validated, and a module object is returned as the result value. +This module can then be instantiated using `module.`[`newInstance()`](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#newInstance(java.lang.Object...)). +To access exported functions and bindings, first get the _exports_ member of the instance using `instance.getMember("exports")`, which returns an object containing all the exported members. +You can then use `exports.getMember("bindingName")` to read an exported binding, and either `exports.getMember("functionName").execute(...args)` or `exports.invokeMember("functionName", ...args)` to call an exported function. + +```java +// Evaluate the Source. Value exampleModule = context.eval(source); -// It is now accessible under the binding name "example". -assert context.getBindings("wasm").getMember("example") == exampleModule; +// Instantiate the example module (optional: provide an import object). +Value exampleInstance = exampleModule.newInstance(); +// Get the exports of the module instance. +Value exampleExports = exampleInstance.getMember("exports"); +// Invoke an exported function. +assert exampleExports.invokeMember("factorial", 8).asInt() == 40320; +// Get the value of an exported global. +assert exampleExports.getMember("theAnswerToLife").asInt() == 42; ``` [Source names](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Source.html#getName()) are important in GraalWasm because they are also used to resolve module imports. If a module tries to import a symbol from module `foo`, then GraalWasm looks for that symbol in the module whose `Source` was named `foo`. -These imports are not resolved until when a WebAssembly module instance's members are accessed or executed for the first time in a given context. +These imports are resolved from the arguments passed to `module.newInstance()` (either the import object or a module argument). +When using the `wasm.EvalReturnsInstance=true` option, the imports are not resolved until the module instance's exports are accessed or executed for the first time. ### Module Instance Objects -By evaluating WebAssembly modules through the Polyglot API, you get access to module instance objects. -Module instance objects expose members for every symbol that was exported from the WebAssembly module. +By calling [`newInstance()`](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#newInstance(java.lang.Object...)) on the WebAssembly module value returned by [`Context.eval(Source)`](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Context.html#eval(org.graalvm.polyglot.Source)), you instantiate the module and create new module instance objects. +Module instance objects expose a read-only "exports" member, an object that contains a member for every symbol that was exported from the WebAssembly module. You can get a list of all exported symbols using [getMemberKeys](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#getMemberKeys()), access individual exports using [getMember](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#getMember(java.lang.String)) and, in the case of mutable globals, use [putMember](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#putMember(java.lang.String,java.lang.Object)) to set their values. +You can also call functions using [invokeMember](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#invokeMember(java.lang.String,java.lang.Object...)). Here is how the various kinds of WebAssembly exports map to polyglot values: * Functions Functions are exported as executable values, which you can call using [execute](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#execute(java.lang.Object...)). + Alternatively, you can use [invokeMember](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#invokeMember(java.lang.String,java.lang.Object...)) on the _exports_ object. Function arguments and return values are mapped between WebAssembly value types and polyglot values using the [type mapping](#type-mapping). If a function returns multiple values, these are wrapped in an interop array. + Use [getArrayElement](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#getArrayElement(long)) to extract the individual return values. * Globals @@ -61,7 +78,7 @@ Here is how the various kinds of WebAssembly exports map to polyglot values: * Tables - Exported tables are opaque and cannot be queried or modified. + Exported tables are opaque and cannot be queried or modified currently. ## Type Mapping diff --git a/wasm/src/org.graalvm.wasm.benchmark/src/org/graalvm/wasm/benchmark/MemoryFootprintBenchmarkRunner.java b/wasm/src/org.graalvm.wasm.benchmark/src/org/graalvm/wasm/benchmark/MemoryFootprintBenchmarkRunner.java index 79a521c95cc1..ce9827055694 100644 --- a/wasm/src/org.graalvm.wasm.benchmark/src/org/graalvm/wasm/benchmark/MemoryFootprintBenchmarkRunner.java +++ b/wasm/src/org.graalvm.wasm.benchmark/src/org/graalvm/wasm/benchmark/MemoryFootprintBenchmarkRunner.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -124,6 +124,7 @@ public static void main(String[] args) throws IOException, InterruptedException contextBuilder.allowExperimentalOptions(true); contextBuilder.option("wasm.Builtins", "go"); contextBuilder.option("wasm.MemoryOverheadMode", "true"); + contextBuilder.option("wasm.EvalReturnsInstance", "true"); final List results = new ArrayList<>(); diff --git a/wasm/src/org.graalvm.wasm.benchmark/src/org/graalvm/wasm/benchmark/WasmBenchmarkSuiteBase.java b/wasm/src/org.graalvm.wasm.benchmark/src/org/graalvm/wasm/benchmark/WasmBenchmarkSuiteBase.java index 6caf376a8b9a..aa336728f984 100644 --- a/wasm/src/org.graalvm.wasm.benchmark/src/org/graalvm/wasm/benchmark/WasmBenchmarkSuiteBase.java +++ b/wasm/src/org.graalvm.wasm.benchmark/src/org/graalvm/wasm/benchmark/WasmBenchmarkSuiteBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -87,6 +87,7 @@ public void setup() throws IOException, InterruptedException { final Context.Builder contextBuilder = Context.newBuilder(WasmLanguage.ID); contextBuilder.option("wasm.Builtins", "testutil,env:emscripten,wasi_snapshot_preview1"); + contextBuilder.option("wasm.EvalReturnsInstance", "true"); contextBuilder.allowExperimentalOptions(true); if (!Objects.isNull(DISABLE_COMPILATION_FLAG)) { contextBuilder.option("engine.Compilation", "false"); @@ -106,7 +107,7 @@ public void setup() throws IOException, InterruptedException { sources.forEach(context::eval); String mainModuleName = benchmarkCase.name(); - Value benchmarkModule = context.getBindings(WasmLanguage.ID).getMember(mainModuleName); + Value benchmarkModule = context.getBindings(WasmLanguage.ID).getMember(mainModuleName).getMember("exports"); Value benchmarkSetupOnce = benchmarkModule.getMember("benchmarkSetupOnce"); benchmarkSetupEach = benchmarkModule.getMember("benchmarkSetupEach"); benchmarkTeardownEach = benchmarkModule.getMember("benchmarkTeardownEach"); diff --git a/wasm/src/org.graalvm.wasm.launcher/src/org/graalvm/wasm/launcher/WasmLauncher.java b/wasm/src/org.graalvm.wasm.launcher/src/org/graalvm/wasm/launcher/WasmLauncher.java index 0024e7f171da..f80d57ded266 100644 --- a/wasm/src/org.graalvm.wasm.launcher/src/org/graalvm/wasm/launcher/WasmLauncher.java +++ b/wasm/src/org.graalvm.wasm.launcher/src/org/graalvm/wasm/launcher/WasmLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -40,13 +40,6 @@ */ package org.graalvm.wasm.launcher; -import org.graalvm.launcher.AbstractLanguageLauncher; -import org.graalvm.options.OptionCategory; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.PolyglotException; -import org.graalvm.polyglot.Source; -import org.graalvm.polyglot.Value; - import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -55,6 +48,13 @@ import java.util.Map; import java.util.Set; +import org.graalvm.launcher.AbstractLanguageLauncher; +import org.graalvm.options.OptionCategory; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Value; + public class WasmLauncher extends AbstractLanguageLauncher { private File file = null; private VersionAction versionAction = VersionAction.None; @@ -146,7 +146,7 @@ private int execute(Context.Builder contextBuilder) { try (Context context = contextBuilder.build()) { runVersionAction(versionAction, context.getEngine()); - Value mainModule = context.eval(Source.newBuilder(getLanguageId(), file).build()); + Value mainModule = context.eval(Source.newBuilder(getLanguageId(), file).build()).newInstance(); Value entryPoint = detectEntryPoint(mainModule); if (entryPoint == null) { @@ -172,9 +172,10 @@ private Value detectEntryPoint(Value mainModule) { if (customEntryPoint != null) { return mainModule.getMember(customEntryPoint); } - Value candidate = mainModule.getMember("_start"); + Value exports = mainModule.getMember("exports"); + Value candidate = exports.getMember("_start"); if (candidate == null) { - candidate = mainModule.getMember("_main"); + candidate = exports.getMember("_main"); } return candidate; } diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/AbstractBinarySuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/AbstractBinarySuite.java index 9472fea99cc8..b52ae37f9fab 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/AbstractBinarySuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/AbstractBinarySuite.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -66,8 +66,7 @@ protected static void runRuntimeTest(byte[] binary, Consumer op try (Context context = contextBuilder.build()) { Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(binary), "main"); Source source = sourceBuilder.build(); - context.eval(source); - testCase.accept(context.getBindings(WasmLanguage.ID).getMember("main")); + testCase.accept(context.eval(source).newInstance().getMember("exports")); } } diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmFileSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmFileSuite.java index 9a615f39af82..a608178c1401 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmFileSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmFileSuite.java @@ -165,11 +165,25 @@ private static boolean isPoorShell() { return inCI() || inWindows(); } - private static Value findMain(WasmStore wasmStore) { - for (final WasmInstance instance : wasmStore.moduleInstances().values()) { - final WasmFunctionInstance function = instance.inferEntryPoint(); + private static WasmInstance toWasmInstance(Value instance) { + final WasmContext wasmContext = WasmContext.get(null); + return (WasmInstance) wasmContext.environment().asGuestValue(instance); + } + + private static Value inferEntryPoint(Value moduleInstance) { + final WasmInstance wasmInstance = toWasmInstance(moduleInstance); + final WasmFunctionInstance function = wasmInstance.inferEntryPoint(); + if (function != null) { + return Value.asValue(function); + } + return null; + } + + private static Value findMain(List moduleInstances) { + for (final Value instance : moduleInstances) { + final Value function = inferEntryPoint(instance); if (function != null) { - return Value.asValue(function); + return function; } } throw new AssertionFailedError("No start function exported."); @@ -185,16 +199,18 @@ private void runInContext(WasmCase testCase, Context context, List sourc // This is needed so that we can call WasmContext.getCurrent(). context.enter(); + final List moduleInstances; try { - sources.forEach(context::eval); + moduleInstances = sources.stream().map(context::eval).toList(); } catch (PolyglotException e) { validateThrown(testCase.data(), WasmCaseData.ErrorType.Validation, e); return; } final WasmContext wasmContext = WasmContext.get(null); - final var contextStore = wasmContext.contextStore(); - final Value mainFunction = findMain(contextStore); + final Value mainFunction = findMain(moduleInstances); + final List instanceList = moduleInstances.stream().map(i -> toWasmInstance(i)).toList(); + final var contextStore = instanceList.get(0).store(); resetStatus(System.out, phaseIcon, phaseLabel); @@ -211,7 +227,7 @@ private void runInContext(WasmCase testCase, Context context, List sourc // If no exception is expected and the program returns with success exit status, // then we check stdout. if (e.isExit() && testCase.data().expectedErrorMessage() == null) { - Assert.assertEquals("Program exited with non-zero return code.", e.getExitStatus(), 0); + Assert.assertEquals("Program exited with non-zero return code.", 0, e.getExitStatus()); WasmCase.validateResult(testCase.data().resultValidator(), null, testOut); } else if (testCase.data().expectedErrorTime() == WasmCaseData.ErrorType.Validation) { validateThrown(testCase.data(), WasmCaseData.ErrorType.Validation, e); @@ -246,11 +262,10 @@ private void runInContext(WasmCase testCase, Context context, List sourc contextStore.tables().table(j).reset(); } } - List instanceList = new ArrayList<>(contextStore.moduleInstances().values()); - instanceList.sort(Comparator.comparingInt(RuntimeState::startFunctionIndex)); - for (WasmInstance instance : instanceList) { - if (!instance.isBuiltin()) { - contextStore.reinitInstance(instance, reinitMemory); + + for (WasmInstance instance : instanceList.stream().sorted(Comparator.comparingInt(RuntimeState::startFunctionIndex)).toList()) { + if (!instance.module().isBuiltin()) { + instance.store().reinitInstance(instance, reinitMemory); } } @@ -391,7 +406,7 @@ private void runInContexts(WasmCase testCase, Context.Builder contextBuilder, Ar interpreterIterations = Math.min(interpreterIterations, 1); } - context = contextBuilder.options(getInterpretedNoInline()).build(); + context = contextBuilder.options(getInterpretedNoInline()).option("wasm.EvalReturnsInstance", "true").build(); runInContext(testCase, context, sources, interpreterIterations, PHASE_INTERPRETER_ICON, "interpreter", testOut); // Run in synchronous compiled mode, with inlining turned off. @@ -401,7 +416,7 @@ private void runInContexts(WasmCase testCase, Context.Builder contextBuilder, Ar if (WasmTestOptions.COVERAGE_MODE) { syncNoinlineIterations = Math.min(syncNoinlineIterations, 1); } - context = contextBuilder.options(getSyncCompiledNoInline()).build(); + context = contextBuilder.options(getSyncCompiledNoInline()).option("wasm.EvalReturnsInstance", "true").build(); runInContext(testCase, context, sources, syncNoinlineIterations, PHASE_SYNC_NO_INLINE_ICON, "sync,no-inl", testOut); // Run in synchronous compiled mode, with inlining turned on. @@ -411,7 +426,7 @@ private void runInContexts(WasmCase testCase, Context.Builder contextBuilder, Ar if (WasmTestOptions.COVERAGE_MODE) { syncInlineIterations = Math.min(syncInlineIterations, 1); } - context = contextBuilder.options(getSyncCompiledWithInline()).build(); + context = contextBuilder.options(getSyncCompiledWithInline()).option("wasm.EvalReturnsInstance", "true").build(); runInContext(testCase, context, sources, syncInlineIterations, PHASE_SYNC_INLINE_ICON, "sync,inl", testOut); // Run with normal, asynchronous compilation. @@ -419,7 +434,7 @@ private void runInContexts(WasmCase testCase, Context.Builder contextBuilder, Ar if (WasmTestOptions.COVERAGE_MODE) { asyncIterations = Math.min(asyncIterations, 1); } - context = contextBuilder.options(getAsyncCompiled()).build(); + context = contextBuilder.options(getAsyncCompiled()).option("wasm.EvalReturnsInstance", "true").build(); runInContext(testCase, context, sources, asyncIterations, PHASE_ASYNC_ICON, "async,multi", testOut); } else { int asyncSharedIterations = testCase.options().containsKey("async-iterations") && !testCase.options().containsKey("async-shared-iterations") @@ -428,7 +443,7 @@ private void runInContexts(WasmCase testCase, Context.Builder contextBuilder, Ar if (WasmTestOptions.COVERAGE_MODE) { asyncSharedIterations = Math.min(asyncSharedIterations, 1); } - context = contextBuilder.build(); + context = contextBuilder.option("wasm.EvalReturnsInstance", "true").build(); runInContext(testCase, context, sources, asyncSharedIterations, PHASE_SHARED_ENGINE_ICON, "async,shared", testOut); } } @@ -546,7 +561,7 @@ public void test() throws IOException { } System.err.printf("\uD83D\uDCA5\u001B[31m %d/%d Wasm tests passed.\u001B[0m%n", qualifyingTestCases.size() - errors.size(), qualifyingTestCases.size()); System.out.println(); - fail(); + fail("Some tests failed. See errors above."); } else { System.out.printf("\uD83C\uDF40\u001B[32m %d/%d Wasm tests passed.\u001B[0m%n", qualifyingTestCases.size() - errors.size(), qualifyingTestCases.size()); System.out.println(); diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmJsApiSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmJsApiSuite.java index b2e7ca50378b..9f2ab7b23458 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmJsApiSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmJsApiSuite.java @@ -47,6 +47,7 @@ import java.nio.ByteOrder; import java.util.Arrays; import java.util.HashMap; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.function.Consumer; @@ -82,7 +83,6 @@ import org.graalvm.wasm.globals.WasmGlobal; import org.graalvm.wasm.memory.WasmMemory; import org.graalvm.wasm.memory.WasmMemoryLibrary; -import org.graalvm.wasm.predefined.testutil.TestutilModule; import org.junit.Assert; import org.junit.Test; @@ -95,7 +95,6 @@ import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.InvalidArrayIndexException; import com.oracle.truffle.api.interop.InvalidBufferOffsetException; -import com.oracle.truffle.api.interop.TruffleObject; import com.oracle.truffle.api.interop.UnknownIdentifierException; import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.interop.UnsupportedTypeException; @@ -2381,7 +2380,7 @@ public void testReimportWasmFunctionViaImportObject() throws IOException, Interr }), }); WasmInstance instance1 = moduleInstantiate(wasm, sourceBytesMod1, importObj1); - var mod1Sum = instance1.readMember("sum"); + var mod1Sum = WebAssembly.instanceExport(instance1, "sum"); Dictionary importObj2 = Dictionary.create(new Object[]{ "mod1", Dictionary.create(new Object[]{ "sum", mod1Sum, @@ -2527,24 +2526,10 @@ public static void runTest(Consumer options, Consumer, TruffleObject { - private final Consumer testCase; - - private GuestCode(Consumer testCase) { - this.testCase = testCase; - } - - @Override - public void accept(WasmContext context) { - testCase.accept(context); + WasmTestUtils.runInWasmContext(context, testCase); } } @@ -2916,6 +2901,6 @@ public void accept(WasmContext context) { public static WasmInstance moduleInstantiate(WebAssembly wasm, byte[] source, Object importObject) { final WasmModule module = wasm.moduleDecode(source); - return wasm.moduleInstantiate(module, importObject); + return wasm.moduleInstantiate(module, Objects.requireNonNullElse(importObject, WasmConstant.NULL)); } } diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmLateLinkingSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmLateLinkingSuite.java index b4ee0b8c34c4..c10658768203 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmLateLinkingSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmLateLinkingSuite.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -62,9 +62,9 @@ public class WasmLateLinkingSuite { @Test public void testLateMemoryLink() throws IOException { runTest(binaryWithMemoryExport, context -> { - Value main = context.getBindings(WasmLanguage.ID).getMember("main").getMember("main"); + Value main = context.getBindings(WasmLanguage.ID).getMember("main").getMember("exports").getMember("main"); main.execute(); - Value memory = context.getBindings(WasmLanguage.ID).getMember("main").getMember("memory"); + Value memory = context.getBindings(WasmLanguage.ID).getMember("main").getMember("exports").getMember("memory"); memory.setArrayElement(0, 11); Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(binaryWithMemoryImport), "aux"); try { @@ -72,7 +72,7 @@ public void testLateMemoryLink() throws IOException { } catch (IOException e) { throw new RuntimeException(e); } - Value loadZero = context.getBindings(WasmLanguage.ID).getMember("aux").getMember("loadZero"); + Value loadZero = context.getBindings(WasmLanguage.ID).getMember("aux").getMember("exports").getMember("loadZero"); Value result = loadZero.execute(); Assert.assertEquals(11, result.asInt()); }); @@ -84,10 +84,10 @@ public void linkingTooLate() throws IOException, InterruptedException { final ByteSequence binaryMain = ByteSequence.create(compileWat("file1", textWithImportFunExportFun)); final Source sourceAux = Source.newBuilder(WasmLanguage.ID, binaryAux, "m1").build(); final Source sourceMain = Source.newBuilder(WasmLanguage.ID, binaryMain, "main").build(); - try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + try (Context context = Context.newBuilder(WasmLanguage.ID).option("wasm.EvalReturnsInstance", "true").build()) { context.eval(sourceMain); // main context.eval(sourceAux); // m1 - final Value g = context.getBindings(WasmLanguage.ID).getMember("main").getMember("g"); + final Value g = context.getBindings(WasmLanguage.ID).getMember("main").getMember("exports").getMember("g"); Assert.assertEquals(42, g.execute().asInt()); } } @@ -100,12 +100,12 @@ public void linkingTooLateShared() throws IOException, InterruptedException { final Source sourceMain = Source.newBuilder(WasmLanguage.ID, binaryMain, "main").build(); try (Engine engine = Engine.create()) { for (int i = 0; i < N_CONTEXTS; i++) { - try (Context context = Context.newBuilder(WasmLanguage.ID).engine(engine).build()) { + try (Context context = Context.newBuilder(WasmLanguage.ID).option("wasm.EvalReturnsInstance", "true").engine(engine).allowExperimentalOptions(true).build()) { Value main = context.eval(sourceMain); // main Value main2 = context.eval(sourceMain); // main Assert.assertEquals(main, main2); context.eval(sourceAux); // m1 - final Value g = context.getBindings(WasmLanguage.ID).getMember("main").getMember("g"); + final Value g = context.getBindings(WasmLanguage.ID).getMember("main").getMember("exports").getMember("g"); Assert.assertEquals(42, g.execute().asInt()); } } @@ -122,7 +122,7 @@ public void linkingTooLateDifferentImportedFunctions() throws IOException, Inter final Source sourceAux2 = Source.newBuilder(WasmLanguage.ID, binaryAux2, "m1").build(); try (Engine engine = Engine.create()) { for (int i = 0; i < N_CONTEXTS; i++) { - try (Context context = Context.newBuilder(WasmLanguage.ID).engine(engine).build()) { + try (Context context = Context.newBuilder(WasmLanguage.ID).option("wasm.EvalReturnsInstance", "true").engine(engine).allowExperimentalOptions(true).build()) { Value mainModule = context.eval(sourceMain); // main int expected; if (i < N_CONTEXTS - 1) { @@ -133,10 +133,10 @@ public void linkingTooLateDifferentImportedFunctions() throws IOException, Inter expected = 43; } // call g(), which is the reexported f() from m1. - final Value g = mainModule.getMember("g"); + final Value g = mainModule.getMember("exports").getMember("g"); Assert.assertEquals(expected, g.execute().asInt()); // call f() via call_f() function in main module. - final Value f = mainModule.getMember("call_f"); + final Value f = mainModule.getMember("exports").getMember("call_f"); Assert.assertEquals(expected, f.execute().asInt()); } } @@ -145,7 +145,7 @@ public void linkingTooLateDifferentImportedFunctions() throws IOException, Inter @Test public void linkingFailureInvertedOnlyAux() throws IOException, InterruptedException { - try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + try (Context context = Context.newBuilder(WasmLanguage.ID).option("wasm.EvalReturnsInstance", "true").build()) { final ByteSequence binaryAux = ByteSequence.create(compileWat("file1", "(import \"non_existing\" \"f\" (func))")); final ByteSequence binaryMain = ByteSequence.create(compileWat("file2", "(func (export \"g\") (result i32) (i32.const 42))")); final Source sourceAux = Source.newBuilder(WasmLanguage.ID, binaryAux, "module1").build(); @@ -157,7 +157,7 @@ public void linkingFailureInvertedOnlyAux() throws IOException, InterruptedExcep // referenced by the import 'f' in the module 'module1', // does not exist." try { - final Value g = module2Instance.getMember("g"); + final Value g = module2Instance.getMember("exports").getMember("g"); g.execute(); Assert.assertFalse("Should not reach here.", true); } catch (Throwable e) { @@ -169,7 +169,7 @@ public void linkingFailureInvertedOnlyAux() throws IOException, InterruptedExcep } try { - final Value g2 = module2Instance.getMember("g"); + final Value g2 = module2Instance.getMember("exports").getMember("g"); final Value result2 = g2.execute(); Assert.assertEquals(42, result2.asInt()); } catch (Throwable e) { @@ -180,7 +180,7 @@ public void linkingFailureInvertedOnlyAux() throws IOException, InterruptedExcep @Test public void linkingFailureOnlyAux() throws IOException, InterruptedException { - try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + try (Context context = Context.newBuilder(WasmLanguage.ID).option("wasm.EvalReturnsInstance", "true").build()) { final ByteSequence binaryAux = ByteSequence.create(compileWat("file1", "(import \"non_existing\" \"f\" (func))")); final ByteSequence binaryMain = ByteSequence.create(compileWat("file2", "(func (export \"g\") (result i32) (i32.const 42))")); final Source sourceAux = Source.newBuilder(WasmLanguage.ID, binaryAux, "module1").build(); @@ -192,7 +192,7 @@ public void linkingFailureOnlyAux() throws IOException, InterruptedException { // referenced by the import 'f' in the module 'module1', // does not exist." try { - final Value g = module2Instance.getMember("g"); + final Value g = module2Instance.getMember("exports").getMember("g"); g.execute(); Assert.assertFalse("Should not reach here.", true); } catch (Throwable e) { @@ -204,7 +204,7 @@ public void linkingFailureOnlyAux() throws IOException, InterruptedException { } try { - final Value g2 = module2Instance.getMember("g"); + final Value g2 = module2Instance.getMember("exports").getMember("g"); final Value result2 = g2.execute(); Assert.assertEquals(42, result2.asInt()); } catch (Throwable e) { @@ -215,7 +215,7 @@ public void linkingFailureOnlyAux() throws IOException, InterruptedException { @Test public void linkingFailureDueToDependency() throws IOException, InterruptedException { - try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + try (Context context = Context.newBuilder(WasmLanguage.ID).option("wasm.EvalReturnsInstance", "true").build()) { final ByteSequence binaryAux = ByteSequence.create(compileWat("file1", "(import \"non_existing\" \"f\" (func)) (func (export \"h\") (result i32) (i32.const 42))")); final ByteSequence binaryMain = ByteSequence.create(compileWat("file2", "(import \"main\" \"h\" (func)) (func (export \"g\") (result i32) (i32.const 42))")); final Source sourceAux = Source.newBuilder(WasmLanguage.ID, binaryAux, "module1").build(); @@ -224,7 +224,7 @@ public void linkingFailureDueToDependency() throws IOException, InterruptedExcep final Value module2Instance = context.eval(sourceMain); try { - final Value g = module2Instance.getMember("g"); + final Value g = module2Instance.getMember("exports").getMember("g"); g.execute(); Assert.fail("Should not reach here."); } catch (Throwable e) { @@ -233,7 +233,7 @@ public void linkingFailureDueToDependency() throws IOException, InterruptedExcep } try { - final Value g2 = module2Instance.getMember("g"); + final Value g2 = module2Instance.getMember("exports").getMember("g"); g2.execute(); Assert.fail("Should not reach here."); } catch (Throwable e) { @@ -266,15 +266,15 @@ public void lazyLinkEquivalenceClasses() throws IOException, InterruptedExceptio ) """); - try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + try (Context context = Context.newBuilder(WasmLanguage.ID).option("wasm.EvalReturnsInstance", "true").build()) { final ByteSequence exportByteSeq = ByteSequence.create(exportBytes); final ByteSequence importByteSeq = ByteSequence.create(importBytes); final Source exportSource = Source.newBuilder(WasmLanguage.ID, exportByteSeq, "exportModule").build(); final Source importSource = Source.newBuilder(WasmLanguage.ID, importByteSeq, "importModule").build(); - final Value exportModuleInstance = context.eval(exportSource); + final Value exportModuleInstance = context.eval(exportSource).getMember("exports"); exportModuleInstance.getMember("table"); // Linking of the first module was triggered by this point. - final Value importModuleInstance = context.eval(importSource); + final Value importModuleInstance = context.eval(importSource).getMember("exports"); importModuleInstance.getMember("testFunc").execute(0); // Linking of the second module was triggered by this point. } @@ -283,6 +283,7 @@ public void lazyLinkEquivalenceClasses() throws IOException, InterruptedExceptio private static void runTest(byte[] firstBinary, Consumer testCase) throws IOException { final Context.Builder contextBuilder = Context.newBuilder(WasmLanguage.ID); contextBuilder.option("wasm.Builtins", "testutil:testutil"); + contextBuilder.option("wasm.EvalReturnsInstance", "true"); try (Context context = contextBuilder.build()) { Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(firstBinary), "main"); Source source = sourceBuilder.build(); diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmMemoryLeakSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmMemoryLeakSuite.java index c2d45694e79a..4b8ccb78c6e1 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmMemoryLeakSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmMemoryLeakSuite.java @@ -85,9 +85,10 @@ public void testMemoryLeak() throws IOException, InterruptedException { var strongInstances = new ArrayList<>(); for (int iter = 0; iter < 16; iter++) { var instance = wasm.moduleInstantiate(module, importObject); - Object mem = lib.readMember(instance, "mem"); - Object fun = lib.readMember(instance, "fun"); - Object tab = lib.readMember(instance, "tab"); + Object exports = lib.readMember(instance, "exports"); + Object mem = lib.readMember(exports, "mem"); + Object fun = lib.readMember(exports, "fun"); + Object tab = lib.readMember(exports, "tab"); weakStores.add(new WeakReference<>(instance.store())); weakInstances.add(new WeakReference<>(instance)); @@ -113,7 +114,8 @@ public void testMemoryLeak() throws IOException, InterruptedException { switch (i % 4) { case 0 -> { // WasmInstance - Assert.assertTrue(lib.isMemberReadable(instance, "fun")); + Object exports = lib.readMember(instance, "exports"); + Assert.assertTrue(lib.isMemberReadable(exports, "fun")); } case 1 -> { // WasmFunction diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmPolyglotTestSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmPolyglotTestSuite.java index 3ea3d3ff1e49..e6538d66e1b3 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmPolyglotTestSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmPolyglotTestSuite.java @@ -45,13 +45,19 @@ import java.io.IOException; import java.nio.ByteOrder; +import java.util.Collections; +import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.PolyglotException; import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Value; import org.graalvm.polyglot.io.ByteSequence; +import org.graalvm.polyglot.proxy.Proxy; +import org.graalvm.polyglot.proxy.ProxyExecutable; +import org.graalvm.polyglot.proxy.ProxyObject; import org.graalvm.wasm.WasmContext; import org.graalvm.wasm.WasmLanguage; import org.graalvm.wasm.exception.WasmException; @@ -59,6 +65,7 @@ import org.graalvm.wasm.memory.UnsafeWasmMemory; import org.graalvm.wasm.memory.WasmMemoryLibrary; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import com.oracle.truffle.api.TruffleLanguage; @@ -77,13 +84,10 @@ public void testEmpty() throws IOException { @Test public void test42() throws IOException { Context.Builder contextBuilder = Context.newBuilder(WasmLanguage.ID); - Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, - ByteSequence.create(binaryReturnConst), - "main"); + Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(binaryReturnConst), "main"); Source source = sourceBuilder.build(); try (Context context = contextBuilder.build()) { - context.eval(source); - Value mainFunction = context.getBindings(WasmLanguage.ID).getMember("main").getMember("main"); + Value mainFunction = context.eval(source).newInstance().getMember("exports").getMember("main"); Value result = mainFunction.execute(); Assert.assertEquals("Should be equal: ", 42, result.asInt()); } @@ -92,9 +96,7 @@ public void test42() throws IOException { @Test public void unsafeMemoryFreed() throws IOException { Context.Builder contextBuilder = Context.newBuilder(WasmLanguage.ID); - Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, - ByteSequence.create(binaryReturnConst), - "main"); + Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(binaryReturnConst), "main"); Source source = sourceBuilder.build(); contextBuilder.allowExperimentalOptions(true); contextBuilder.option("wasm.UseUnsafeMemory", "true"); @@ -102,11 +104,11 @@ public void unsafeMemoryFreed() throws IOException { contextBuilder.option("wasm.DirectByteBufferMemoryAccess", "true"); Context context = contextBuilder.build(); context.enter(); - context.eval(source); - final Value mainModule = context.getBindings(WasmLanguage.ID).getMember("main"); - mainModule.getMember("main").execute(); + + final Value mainExports = context.eval(source).newInstance().getMember("exports"); + mainExports.getMember("main").execute(); final TruffleLanguage.Env env = WasmContext.get(null).environment(); - final Value memoryValue = mainModule.getMember("memory"); + final Value memoryValue = mainExports.getMember("memory"); final UnsafeWasmMemory memory = (UnsafeWasmMemory) env.asGuestValue(memoryValue); Assert.assertFalse("Memory should have been allocated.", WasmMemoryLibrary.getUncached().freed(memory)); context.leave(); @@ -124,20 +126,17 @@ public void unsafeMemoryFreed() throws IOException { @Test public void nativeMemoryFreed() throws IOException { Context.Builder contextBuilder = Context.newBuilder(WasmLanguage.ID); - Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, - ByteSequence.create(binaryReturnConst), - "main"); + Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(binaryReturnConst), "main"); Source source = sourceBuilder.build(); contextBuilder.allowExperimentalOptions(true); contextBuilder.option("wasm.UseUnsafeMemory", "true"); contextBuilder.option("wasm.DirectByteBufferMemoryAccess", "false"); Context context = contextBuilder.build(); context.enter(); - context.eval(source); - final Value mainModule = context.getBindings(WasmLanguage.ID).getMember("main"); - mainModule.getMember("main").execute(); + final Value mainExports = context.eval(source).newInstance().getMember("exports"); + mainExports.getMember("main").execute(); final TruffleLanguage.Env env = WasmContext.get(null).environment(); - final Value memoryValue = mainModule.getMember("memory"); + final Value memoryValue = mainExports.getMember("memory"); final NativeWasmMemory memory = (NativeWasmMemory) env.asGuestValue(memoryValue); Assert.assertFalse("Memory should have been allocated.", WasmMemoryLibrary.getUncached().freed(memory)); context.leave(); @@ -159,8 +158,7 @@ public void overwriteElement() throws IOException, InterruptedException { Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, test, "main"); Source source = sourceBuilder.build(); try (Context context = contextBuilder.build()) { - context.eval(source); - Value mainFunction = context.getBindings(WasmLanguage.ID).getMember("main").getMember("main"); + Value mainFunction = context.eval(source).newInstance().getMember("exports").getMember("main"); Value result = mainFunction.execute(); Assert.assertEquals("Should be equal: ", 11, result.asInt()); } @@ -171,9 +169,8 @@ public void divisionByZeroStressTest() throws IOException, InterruptedException String divisionByZeroWAT = "(module (func (export \"main\") (result i32) i32.const 1 i32.const 0 i32.div_s))"; ByteSequence test = ByteSequence.create(compileWat("test", divisionByZeroWAT)); Source source = Source.newBuilder(WasmLanguage.ID, test, "main").build(); - try (Context context = Context.create(WasmLanguage.ID)) { - context.eval(source); - Value mainFunction = context.getBindings(WasmLanguage.ID).getMember("main").getMember("main"); + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + Value mainFunction = context.eval(source).newInstance().getMember("exports").getMember("main"); for (int iteration = 0; iteration < 20000; iteration++) { try { @@ -190,10 +187,9 @@ public void divisionByZeroStressTest() throws IOException, InterruptedException public void extractKeys() throws IOException { ByteSequence test = ByteSequence.create(binaryReturnConst); Source source = Source.newBuilder(WasmLanguage.ID, test, "main").build(); - try (Context context = Context.create(WasmLanguage.ID)) { - context.eval(source); - Value instance = context.getBindings(WasmLanguage.ID).getMember("main"); - Set keys = instance.getMemberKeys(); + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + Value instance = context.eval(source).newInstance(); + Set keys = instance.getMember("exports").getMemberKeys(); Assert.assertTrue("Should contain function 'main'", keys.contains("main")); Assert.assertTrue("Should contain memory 'memory'", keys.contains("memory")); Assert.assertTrue("Should contain global '__heap_base'", keys.contains("__heap_base")); @@ -213,9 +209,8 @@ public void deeplyNestedBrIf() throws IOException, InterruptedException { ByteSequence bytes = ByteSequence.create(compileWat("test", wat)); Source source = Source.newBuilder(WasmLanguage.ID, bytes, "main").build(); - try (Context context = Context.create(WasmLanguage.ID)) { - context.eval(source); - Value mainFunction = context.getBindings(WasmLanguage.ID).getMember("main").getMember("main"); + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + Value mainFunction = context.eval(source).newInstance().getMember("exports").getMember("main"); Value result = mainFunction.execute(); Assert.assertEquals("Should be equal: ", 42, result.asInt()); } @@ -265,4 +260,337 @@ public void deeplyNestedBrIf() throws IOException, InterruptedException { (elem (i32.const 3) $g) ) """; + + private static final String simpleTestModule = """ + (module + (func (export "main") (result i32) + i32.const 13 + ) + ) + """; + + private static final String simpleImportModule = """ + (module + (import "main" "main" (func $m (result i32))) + (func (export "test") (result i32) + call $m + ) + ) + """; + + private static Source simpleTestModuleSource; + private static Source simpleImportModuleSource; + + @BeforeClass + public static void setup() throws IOException, InterruptedException { + final ByteSequence simpleTestModuleData = ByteSequence.create(compileWat("simpleTestModule", simpleTestModule)); + simpleTestModuleSource = Source.newBuilder(WasmLanguage.ID, simpleTestModuleData, "main").build(); + + final ByteSequence simpleImportModuleData = ByteSequence.create(compileWat("simpleImportModule", simpleImportModule)); + simpleImportModuleSource = Source.newBuilder(WasmLanguage.ID, simpleImportModuleData, "test").build(); + } + + @Test + public void instantiateModuleWithImportObject() { + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + final Proxy executable = (ProxyExecutable) args -> 42; + final Proxy function = ProxyObject.fromMap(Map.of("main", executable)); + final Proxy importObject = ProxyObject.fromMap(Map.of("main", function)); + + final Value importModule = context.eval(simpleImportModuleSource); + + final Value instance = importModule.newInstance(importObject); + + final Value result = instance.getMember("exports").invokeMember("test"); + + Assert.assertEquals(42, result.asInt()); + } + } + + @Test + public void instantiateModuleWithMissingImportObject() { + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + final Value importModule = context.eval(simpleImportModuleSource); + + try { + importModule.newInstance(); + Assert.fail("Should have failed because of missing import"); + } catch (PolyglotException e) { + Assert.assertFalse(e.isExit()); + } + } + } + + @Test + public void instantiateModuleWithMissingFunction() { + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + final Proxy importObject = ProxyObject.fromMap(Map.of("main", ProxyObject.fromMap(Collections.emptyMap()))); + + final Value importModule = context.eval(simpleImportModuleSource); + + try { + importModule.newInstance(importObject); + Assert.fail("Should have failed because of incorrect import"); + } catch (PolyglotException e) { + Assert.assertFalse(e.isExit()); + Assert.assertTrue(e.getMessage().contains("does not contain \"main\"")); + } + } + } + + @Test + public void instantiateModuleWithNonExecutableFunction() { + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + final Proxy importObject = ProxyObject.fromMap(Map.of("main", ProxyObject.fromMap(Map.of("main", 5)))); + + final Value importModule = context.eval(simpleImportModuleSource); + + try { + importModule.newInstance(importObject); + Assert.fail("Should have failed because of incorrect import"); + } catch (PolyglotException e) { + Assert.assertFalse(e.isExit()); + Assert.assertTrue(e.getMessage().contains("is not callable")); + } + } + } + + @Test + public void instantiateModuleWithTwoImportObjects() { + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + final Proxy importObject = ProxyObject.fromMap(Map.of("main", ProxyObject.fromMap(Map.of("main", 5)))); + final Proxy importObject2 = ProxyObject.fromMap(Map.of("main2", ProxyObject.fromMap(Map.of("main2", 6)))); + + final Value importModule = context.eval(simpleImportModuleSource); + + try { + importModule.newInstance(importObject, importObject2); + Assert.fail("Should have failed because of incorrect number of import objects"); + } catch (PolyglotException e) { + Assert.assertFalse(e.isExit()); + Assert.assertTrue(e.getMessage().contains("single import object")); + } + } + } + + @Test + public void instantiateModuleWithExportsFromAnotherModule() { + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + final Value testModule = context.eval(simpleTestModuleSource); + final Value importModule = context.eval(simpleImportModuleSource); + + final Value testInstance = testModule.newInstance(); + final Value importInstance = importModule.newInstance(ProxyObject.fromMap(Map.of( + "main", testInstance.getMember("exports")))); + + final Value result = importInstance.getMember("exports").getMember("test").execute(); + + Assert.assertEquals(13, result.asInt()); + } + } + + @Test + public void instantiateModuleWithGlobalImport() throws IOException, InterruptedException { + final ByteSequence supplierBytes = ByteSequence.create(compileWat("supplier", """ + (module + (global (export "g") i32 (i32.const 42)) + ) + """)); + final ByteSequence consumerBytes = ByteSequence.create(compileWat("consumer", """ + (module + (import "supplier" "g" (global $g i32)) + (export "gg" (global $g)) + (func (export "main") (result i32) + (global.get $g) + ) + ) + """)); + + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + final Value supplierModule = context.eval(Source.newBuilder(WasmLanguage.ID, supplierBytes, "supplier").build()); + final Value consumerModule = context.eval(Source.newBuilder(WasmLanguage.ID, consumerBytes, "consumer").build()); + + final Value supplierInstance = supplierModule.newInstance(); + final Value consumerInstance = consumerModule.newInstance(ProxyObject.fromMap(Map.of( + "supplier", supplierInstance.getMember("exports")))); + + final Value consumerExports = consumerInstance.getMember("exports"); + final Value result = consumerExports.invokeMember("main"); + final Value global = consumerExports.getMember("gg"); + + Assert.assertEquals(42, result.asInt()); + Assert.assertEquals(42, global.getMember("value").asInt()); + + Assert.assertThrows(UnsupportedOperationException.class, () -> global.putMember("value", 43)); + } + } + + @Test + public void mutableGlobals() throws IOException, InterruptedException { + final ByteSequence globalsBytes = ByteSequence.create(compileWat("mutable-globals", """ + (module + (global (export "global-i32") (mut i32) (i32.const 42)) + (global (export "global-i64") (mut i64) (i64.const 0x1_ffff_ffff)) + (global (export "global-f32") (mut f32) (f32.const 3.14)) + (global (export "global-f64") (mut f64) (f64.const 3.14)) + ) + """)); + + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + final Source source = Source.newBuilder(WasmLanguage.ID, globalsBytes, "mutable-globals").build(); + final Value globals = context.eval(source).newInstance().getMember("exports"); + + final Value globalI32 = globals.getMember("global-i32"); + Assert.assertEquals(42, globalI32.getMember("value").asInt()); + globalI32.putMember("value", 43); + Assert.assertEquals(43, globalI32.getMember("value").asInt()); + + final Value globalI64 = globals.getMember("global-i64"); + Assert.assertEquals(0x1_ffff_ffffL, globalI64.getMember("value").asLong()); + globalI64.putMember("value", -1L); + Assert.assertEquals(-1L, globalI64.getMember("value").asLong()); + + final Value globalF32 = globals.getMember("global-f32"); + Assert.assertEquals(3.14f, globalF32.getMember("value").asFloat(), 0.0f); + globalF32.putMember("value", 13.37f); + Assert.assertEquals(13.37f, globalF32.getMember("value").asFloat(), 0.0f); + + final Value globalF64 = globals.getMember("global-f64"); + Assert.assertEquals(3.14, globalF64.getMember("value").asDouble(), 0.0); + globalF64.putMember("value", 13.37); + Assert.assertEquals(13.37, globalF64.getMember("value").asDouble(), 0.0); + } + } + + @Test + public void newInstanceWASI() throws IOException, InterruptedException { + final ByteSequence mainModuleBytes = ByteSequence.create(compileWat("main", """ + (module + (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) + (memory 1) + (export "memory" (memory 0)) + (func (export "main") (result i32) + (i32.const 13) + ) + ) + """)); + final ByteSequence importModuleBytes = ByteSequence.create(compileWat("test", """ + (module + (func (export "f") (result i32) + (i32.const 42) + ) + ) + """)); + + final Source mainModuleSource = Source.newBuilder(WasmLanguage.ID, mainModuleBytes, "main-mod").build(); + final Source importModuleSource = Source.newBuilder(WasmLanguage.ID, importModuleBytes, "import-mod").build(); + + try (Context context = Context.newBuilder(WasmLanguage.ID).option("wasm.Builtins", "wasi_snapshot_preview1").build()) { + final Value mainModule = context.eval(mainModuleSource); + final Value otherModule = context.eval(importModuleSource); + + final Value mainInstance = mainModule.newInstance(); + final Value mainExports = mainInstance.getMember("exports"); + + Assert.assertEquals(13, mainExports.invokeMember("main").asInt()); + + final Value otherInstance = otherModule.newInstance(); + final Value otherExports = otherInstance.getMember("exports"); + Assert.assertEquals(42, otherExports.invokeMember("f").asInt()); + } + } + + @Test + public void newInstanceWASIWithSupportModule() throws IOException, InterruptedException { + try (Context context = Context.newBuilder(WasmLanguage.ID).option("wasm.Builtins", "wasi_snapshot_preview1").build()) { + Value mainModule = context.eval(Source.newBuilder("wasm", ByteSequence.create(compileWat("main", """ + (module + ;; Import WASI function: args_sizes_get(argc_ptr, argv_buf_size_ptr) + (import "wasi_snapshot_preview1" "args_sizes_get" + (func $args_sizes_get (param i32 i32) (result i32))) + + ;; Import printf function from support module + (import "env" "printf" + (func $printf (param i32))) + + ;; Export memory 0 (used by WASI) + (memory $mem 1) + (export "memory" (memory $mem)) + + ;; Dummy start function that calls both imported functions + (func (export "_start") + ;; Write to 0 and 4 memory offsets (pretend we use these as argc/argv) + (call $args_sizes_get + (i32.const 0) ;; argc pointer + (i32.const 4)) ;; argv_buf_size pointer + drop + + ;; Call printf with a dummy i32 pointer, say address 16 (assuming a string is there) + (call $printf (i32.const 16)) + ) + ) + """)), "main").build()); + Value envModule = context.eval(Source.newBuilder("wasm", ByteSequence.create(compileWat("env", """ + (module + (func $printf (export "printf") (import "hostEnv" "printf") (param i32)) + ) + """)), "env").build()); + + AtomicBoolean printfCalled = new AtomicBoolean(); + ProxyExecutable printf = (args) -> { + Assert.assertEquals(16, args[0].asInt()); + Assert.assertFalse(printfCalled.getAndSet(true)); + return null; + }; + var hostEnvObject = ProxyObject.fromMap(Map.of("printf", printf)); + Value envInstance = envModule.newInstance(ProxyObject.fromMap(Map.of( + "hostEnv", hostEnvObject))); + Value envExports = envInstance.getMember("exports"); + Value mainInstance = mainModule.newInstance(ProxyObject.fromMap(Map.of( + "hostEnv", hostEnvObject, + "env", envExports))); + Value mainExports = mainInstance.getMember("exports"); + mainExports.getMember("_start").execute(); + Assert.assertTrue("printf called", printfCalled.get()); + } + } + + @Test + public void indirectImportOfHostFunction() throws IOException, InterruptedException { + try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { + Value mainModule = context.eval(Source.newBuilder("wasm", ByteSequence.create(compileWat("main", """ + (module + ;; Import printf function from support module + (import "env" "printf" + (func $printf (param i32))) + + (func (export "_start") + ;; Call printf with a dummy i32 pointer, say address 16 + (call $printf (i32.const 16)) + ) + ) + """)), "main").build()); + Value envModule = context.eval(Source.newBuilder("wasm", ByteSequence.create(compileWat("env", """ + (module + (func $printf (export "printf") (import "hostEnv" "printf") (param i32)) + ) + """)), "env").build()); + + AtomicBoolean printfCalled = new AtomicBoolean(); + ProxyExecutable printf = (args) -> { + Assert.assertEquals(16, args[0].asInt()); + Assert.assertFalse(printfCalled.getAndSet(true)); + return null; + }; + var hostEnvObject = ProxyObject.fromMap(Map.of("printf", printf)); + Value envModuleInstance = envModule.newInstance(ProxyObject.fromMap(Map.of( + "hostEnv", hostEnvObject))); + Value envExports = envModuleInstance.getMember("exports"); + Value mainModuleInstance = mainModule.newInstance(ProxyObject.fromMap(Map.of( + "hostEnv", hostEnvObject, + "env", envExports))); + mainModuleInstance.getMember("exports").getMember("_start").execute(); + Assert.assertTrue("printf called", printfCalled.get()); + } + } } diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmStackTraceTest.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmStackTraceTest.java index e19854cc46cc..09c38baf12eb 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmStackTraceTest.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmStackTraceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -83,8 +83,7 @@ public void testStackTrace() throws IOException, InterruptedException { final Source sourceMain = Source.newBuilder(WasmLanguage.ID, binaryMain, "main").build(); try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { - context.eval(sourceMain); // main - final Value g = context.getBindings(WasmLanguage.ID).getMember("main").getMember("main"); + final Value g = context.eval(sourceMain).newInstance().getMember("exports").getMember("main"); try { g.execute(); } catch (PolyglotException e) { diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmTestUtils.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmTestUtils.java index bff910af0c5f..5b8f74620340 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmTestUtils.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/WasmTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -42,6 +42,11 @@ package org.graalvm.wasm.test; import java.io.ByteArrayOutputStream; +import java.util.function.Consumer; + +import org.graalvm.polyglot.Context; +import org.graalvm.wasm.WasmContext; +import org.graalvm.wasm.WasmLanguage; public final class WasmTestUtils { private WasmTestUtils() { @@ -59,4 +64,11 @@ public static byte[] hexStringToByteArray(String... strings) { } return bytes.toByteArray(); } + + public static void runInWasmContext(Context context, Consumer testCase) { + context.initialize(WasmLanguage.ID); + context.enter(); + testCase.accept(WasmContext.get(null)); + context.leave(); + } } diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/regress/GR54505Test.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/regress/GR54505Test.java index 67d771ed0c8b..a1c6e1ffac06 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/regress/GR54505Test.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/regress/GR54505Test.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -70,9 +70,9 @@ public void testWrongArity() throws IOException, InterruptedException { """)); final Source sourceMain = Source.newBuilder(WasmLanguage.ID, binaryMain, "main").build(); - try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { - Value mainModule = context.eval(sourceMain); // main - final Value main = mainModule.getMember("_main"); + try (Context context = Context.create(WasmLanguage.ID)) { + Value mainExports = context.eval(sourceMain).newInstance().getMember("exports"); // main + final Value main = mainExports.getMember("_main"); // Too few arguments try { main.execute(); @@ -109,12 +109,12 @@ public void testDifferentArgumentType() throws IOException, InterruptedException """)); final Source sourceMain = Source.newBuilder(WasmLanguage.ID, binaryMain, "main").build(); - try (Context context = Context.newBuilder(WasmLanguage.ID).build()) { - Value mainModule = context.eval(sourceMain); // main - final Value main = mainModule.getMember("_main"); + try (Context context = Context.create(WasmLanguage.ID)) { + Value mainExports = context.eval(sourceMain).newInstance().getMember("exports"); // main + final Value main = mainExports.getMember("_main"); // Expected argument types - assertEquals(42, main.execute(0.0, 0, 0).asInt()); + assertEquals(42, mainExports.execute(0.0, 0, 0).asInt()); // Invalid argument type(s) try { diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/WasmImplementationLimitationsSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/WasmImplementationLimitationsSuite.java index 70385adee8d9..f8356204c04d 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/WasmImplementationLimitationsSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/WasmImplementationLimitationsSuite.java @@ -41,6 +41,12 @@ package org.graalvm.wasm.test.suites; +import static org.graalvm.wasm.utils.WasmBinaryTools.compileWat; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + import org.graalvm.polyglot.Context; import org.graalvm.polyglot.PolyglotException; import org.graalvm.polyglot.Source; @@ -53,12 +59,6 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; - -import static org.graalvm.wasm.utils.WasmBinaryTools.compileWat; - @RunWith(Parameterized.class) public class WasmImplementationLimitationsSuite { @Parameterized.Parameters(name = "{0}") @@ -85,7 +85,7 @@ public void test() throws IOException { final Context context = Context.newBuilder(WasmLanguage.ID).build(); final Source source = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(bytecode), "dummy_main").build(); try { - context.eval(source).getMember("_main").execute(); + context.eval(source).newInstance().getMember("exports").getMember("_main").execute(); } catch (final PolyglotException e) { final Value actualFailureObject = e.getGuestObject(); diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/bytecode/MultiInstantiationSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/bytecode/MultiInstantiationSuite.java index 3eae2a0f0b6d..0b0293b26995 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/bytecode/MultiInstantiationSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/bytecode/MultiInstantiationSuite.java @@ -43,8 +43,8 @@ import java.io.IOException; import java.util.List; +import java.util.Objects; import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Function; import org.graalvm.polyglot.Context; @@ -52,7 +52,6 @@ import org.graalvm.polyglot.Value; import org.graalvm.polyglot.io.ByteSequence; import org.graalvm.wasm.WasmConstant; -import org.graalvm.wasm.WasmContext; import org.graalvm.wasm.WasmInstance; import org.graalvm.wasm.WasmLanguage; import org.graalvm.wasm.WasmModule; @@ -67,7 +66,7 @@ import org.graalvm.wasm.globals.WasmGlobal; import org.graalvm.wasm.memory.WasmMemory; import org.graalvm.wasm.memory.WasmMemoryLibrary; -import org.graalvm.wasm.predefined.testutil.TestutilModule; +import org.graalvm.wasm.test.WasmTestUtils; import org.graalvm.wasm.utils.WasmBinaryTools; import org.junit.Assert; import org.junit.Test; @@ -75,7 +74,6 @@ import com.oracle.truffle.api.interop.ArityException; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.InvalidArrayIndexException; -import com.oracle.truffle.api.interop.TruffleObject; import com.oracle.truffle.api.interop.UnknownIdentifierException; import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.interop.UnsupportedTypeException; @@ -84,42 +82,32 @@ * Tests that modules can be instantiated several times. */ public class MultiInstantiationSuite { - private static void test(Function sourceFun, Function importFun, BiConsumer check) throws IOException { + private static void test(byte[] testSource, Function importFun, BiConsumer check) throws IOException { final Context.Builder contextBuilder = Context.newBuilder(WasmLanguage.ID); contextBuilder.option("wasm.Builtins", "testutil:testutil"); try (Context context = contextBuilder.build()) { Source.Builder sourceBuilder = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(binaryWithExports), "main"); Source source = sourceBuilder.build(); - context.eval(source); - Value main = context.getBindings(WasmLanguage.ID).getMember("main").getMember("main"); + + Value mainModule = context.eval(source); + Value mainInstance = mainModule.newInstance(); + + Value main = mainInstance.getMember("exports").getMember("main"); main.execute(); - Value run = context.getBindings(WasmLanguage.ID).getMember("testutil").getMember(TestutilModule.Names.RUN_CUSTOM_INITIALIZATION); - run.execute(new GuestCode(c -> { + + WasmTestUtils.runInWasmContext(context, c -> { WebAssembly wasm = new WebAssembly(c); - WasmModule module = wasm.moduleDecode(sourceFun.apply(wasm)); - WasmInstance instance1 = wasm.moduleInstantiate(module, importFun.apply(wasm)); + WasmModule module = wasm.moduleDecode(testSource); + WasmInstance instance1 = wasm.moduleInstantiate(module, Objects.requireNonNullElse(importFun.apply(wasm), WasmConstant.NULL)); Value v1 = Value.asValue(instance1); // link module v1.getMember("main"); - WasmInstance instance2 = wasm.moduleInstantiate(module, importFun.apply(wasm)); + WasmInstance instance2 = wasm.moduleInstantiate(module, Objects.requireNonNullElse(importFun.apply(wasm), WasmConstant.NULL)); Value v2 = Value.asValue(instance2); // link module v2.getMember("main"); check.accept(wasm, instance2); - })); - } - } - - private static final class GuestCode implements Consumer, TruffleObject { - private final Consumer testCase; - - private GuestCode(Consumer testCase) { - this.testCase = testCase; - } - - @Override - public void accept(WasmContext context) { - testCase.accept(context); + }); } } @@ -183,7 +171,7 @@ public void testImportsAndExports() throws IOException, InterruptedException { ) """); final Executable tableFun = new Executable(args -> 13); - test(wasm -> source, wasm -> { + test(source, wasm -> { final Dictionary imports = new Dictionary(); final Dictionary a = new Dictionary(); @@ -246,7 +234,7 @@ public void testGlobalInitialization() throws IOException, InterruptedException (global (export "g6") (mut funcref) (ref.func 0)) ) """); - test(wasm -> source, wasm -> { + test(source, wasm -> { WasmGlobal g = wasm.globalAlloc(ValueType.i32, false, 16); return Dictionary.create(new Object[]{"a", Dictionary.create(new Object[]{"g", g})}); }, (wasm, i) -> { @@ -318,7 +306,7 @@ public void testTableInitialization() throws IOException, InterruptedException { (elem (i32.const 0) func 0) ) """); - test(wasm -> source, wasm -> null, (wasm, i) -> { + test(source, wasm -> null, (wasm, i) -> { InteropLibrary lib = InteropLibrary.getUncached(); try { final Object tableRead = wasm.readMember("table_read"); @@ -350,7 +338,7 @@ public void testMemoryInitialization() throws IOException, InterruptedException (data (i32.const 0) "\\01\\02\\03\\04") ) """); - test(wasm -> source, wasm -> null, (wasm, i) -> { + test(source, wasm -> null, (wasm, i) -> { final Value m = Value.asValue(WebAssembly.instanceExport(i, "m")); final int b0 = m.getArrayElement(0).asByte(); @@ -387,7 +375,7 @@ public void testDataSections() throws IOException, InterruptedException { (data "\\08\\09\\0A\\0B") ) """); - test(wasm -> source, wasm -> null, (wasm, i) -> { + test(source, wasm -> null, (wasm, i) -> { final Value m = Value.asValue(WebAssembly.instanceExport(i, "m")); final int b0 = m.getArrayElement(0).asByte(); @@ -457,7 +445,7 @@ public void testElemSections() throws IOException, InterruptedException { (elem func 0 1 2 3) ) """); - test(wasm -> source, wasm -> null, (wasm, i) -> { + test(source, wasm -> null, (wasm, i) -> { InteropLibrary lib = InteropLibrary.getUncached(); try { final Object tableRead = wasm.readMember("table_read"); @@ -522,8 +510,7 @@ public void testMultiValueReturn() throws IOException, InterruptedException { final Context.Builder contextBuilder = Context.newBuilder(WasmLanguage.ID); contextBuilder.option("wasm.Builtins", "testutil:testutil"); try (Context context = contextBuilder.build()) { - Value run = context.getBindings(WasmLanguage.ID).getMember("testutil").getMember(TestutilModule.Names.RUN_CUSTOM_INITIALIZATION); - run.execute(new GuestCode(c -> { + WasmTestUtils.runInWasmContext(context, c -> { WebAssembly wasm = new WebAssembly(c); WasmModule module = wasm.moduleDecode(sourceCode); @@ -542,17 +529,17 @@ public void testMultiValueReturn() throws IOException, InterruptedException { WasmInstance instance2 = wasm.moduleInstantiate(module, imports2); - Value v1 = context.asValue(instance1); + Value v1 = context.asValue(instance1).getMember("exports"); v1.getMember("main"); Value main1 = v1.getMember("main"); Value result1 = main1.execute(); Assert.assertEquals("Return value of main", List.of(42L, 1, 2, 3.14), List.copyOf(result1.as(List.class))); - Value v2 = context.asValue(instance2); + Value v2 = context.asValue(instance2).getMember("exports"); Value main2 = v2.getMember("main"); Value result2 = main2.execute(); Assert.assertEquals("Return value of main", List.of(42L, 6, 8, 2.72), List.copyOf(result2.as(List.class))); - })); + }); } } } diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/bytecode/WasmOSRSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/bytecode/WasmOSRSuite.java index c0ca8a33f34c..4c45f419fa8b 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/bytecode/WasmOSRSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/bytecode/WasmOSRSuite.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -93,8 +93,8 @@ public void testOSR() throws IOException, InterruptedException { try (Engine engine = eb.build()) { for (int i = 0; i < N_CONTEXTS; i++) { try (Context context = Context.newBuilder(WasmLanguage.ID).engine(engine).build()) { - Value mainMod = context.eval(sourceMain); - Value mainFun = mainMod.getMember("_main"); + Value mainExports = context.eval(sourceMain).newInstance().getMember("exports"); + Value mainFun = mainExports.getMember("_main"); Assert.assertEquals(0, mainFun.execute().asInt()); } } diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/debugging/DebugValidationSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/debugging/DebugValidationSuite.java index e517ef37c953..3063bf52f758 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/debugging/DebugValidationSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/debugging/DebugValidationSuite.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -789,7 +789,7 @@ private static void runTest(byte[] data) throws IOException { DebuggerSession session = debugger.startSession(event -> { }); try { - Value val = context.eval(source); + Value val = context.eval(source).newInstance().getMember("exports"); val.getMember("_main").execute(); session.suspendNextExecution(); } finally { diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/validation/ValidationSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/validation/ValidationSuite.java index 89f1b6d2f237..a661439f0199 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/validation/ValidationSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/validation/ValidationSuite.java @@ -1022,7 +1022,7 @@ public void test() throws IOException { final Source source = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(bytecode), "dummy_main").build(); final Context context = contextBuilder.build(); try { - context.eval(source).getMember("_main").execute(); + context.eval(source).newInstance().getMember("exports").getMember("_main").execute(); } catch (final PolyglotException e) { final Value actualFailureObject = e.getGuestObject(); diff --git a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/wasi/WasiOptionsSuite.java b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/wasi/WasiOptionsSuite.java index 273135f170b9..f480882a5e8b 100644 --- a/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/wasi/WasiOptionsSuite.java +++ b/wasm/src/org.graalvm.wasm.test/src/org/graalvm/wasm/test/suites/wasi/WasiOptionsSuite.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -189,8 +189,7 @@ public void test() throws IOException, InterruptedException { contextBuilder.option("wasm.WasiMapDirs", dir); try (Context context = contextBuilder.build()) { final Source s = Source.newBuilder(WasmLanguage.ID, ByteSequence.create(source), "main").build(); - context.eval(s); - final Value main = context.getBindings(WasmLanguage.ID).getMember("main"); + final Value main = context.eval(s).newInstance().getMember("exports"); final Value relative = main.getMember("relative"); final Value direct = main.getMember("direct"); final Value absolute = main.getMember("absolute"); diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/GlobalRegistry.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/GlobalRegistry.java index 74d50c8e24dd..75b88a7189c7 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/GlobalRegistry.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/GlobalRegistry.java @@ -40,13 +40,13 @@ */ package org.graalvm.wasm; -import com.oracle.truffle.api.CompilerDirectives; import org.graalvm.wasm.api.Vector128; import org.graalvm.wasm.exception.Failure; import org.graalvm.wasm.exception.WasmException; import org.graalvm.wasm.globals.WasmGlobal; import com.oracle.truffle.api.CompilerAsserts; +import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; /** @@ -59,12 +59,13 @@ public class GlobalRegistry { private static final int INITIAL_GLOBALS_SIZE = 8; - // If we support late linking, we need to ensure that methods accessing the global array - // are compiled with assumptions on what this field points to. - // Such an assumption can be invalidated if the late-linking causes this array - // to be replaced with a larger array. - @CompilationFinal(dimensions = 0) private long[] globals; - @CompilationFinal(dimensions = 0) private Object[] objectGlobals; + /** + * If we support late linking, and the global arrays are @CompilationFinal, we need to ensure + * that methods accessing the global array are compiled with assumptions that will be + * invalidated if the late linking causes this array to be replaced with a larger array. + */ + private long[] globals; + private Object[] objectGlobals; @CompilationFinal(dimensions = 1) private WasmGlobal[] externalGlobals; private int globalCount; private int externalGlobalCount; diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/ImportValueSupplier.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/ImportValueSupplier.java index 2d12f44da024..8ebdfb5e6302 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/ImportValueSupplier.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/ImportValueSupplier.java @@ -54,4 +54,26 @@ public interface ImportValueSupplier { * @param intoInstance importing module instance of the import to be resolved. */ Object get(ImportDescriptor importDesc, WasmInstance intoInstance); + + /** + * Creates a union of this and another supplier. + */ + default ImportValueSupplier andThen(ImportValueSupplier next) { + if (this == none()) { + return next; + } else if (next == none()) { + return this; + } + return (importDesc, intoInstance) -> { + Object found = this.get(importDesc, intoInstance); + if (found != null) { + return found; + } + return next.get(importDesc, intoInstance); + }; + } + + static ImportValueSupplier none() { + return (importDesc, intoInstance) -> null; + } } diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/Linker.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/Linker.java index e552a03f9826..f2b2e12bc729 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/Linker.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/Linker.java @@ -160,7 +160,7 @@ private static WasmException linkFailedError(WasmInstance instance) { @CompilerDirectives.TruffleBoundary private void tryLinkOutsidePartialEvaluation(WasmInstance entryPointInstance) { - tryLinkOutsidePartialEvaluation(entryPointInstance, null); + tryLinkOutsidePartialEvaluation(entryPointInstance, ImportValueSupplier.none()); } /** @@ -188,9 +188,10 @@ private void tryLinkOutsidePartialEvaluation(WasmInstance entryPointInstance, Im if (resolutionDag == null) { resolutionDag = new ResolutionDag(); } + var importValues = imports.andThen(store.instantiateBuiltinInstances()); Map instances = store.moduleInstances(); ArrayList failures = new ArrayList<>(); - final int maxStartFunctionIndex = runLinkActions(store, instances, imports, failures); + final int maxStartFunctionIndex = runLinkActions(store, instances, importValues, failures); linkTopologically(store, failures, maxStartFunctionIndex); assignTypeEquivalenceClasses(store); resolutionDag = null; @@ -476,8 +477,6 @@ void resolveMemoryImport(WasmStore store, WasmInstance instance, ImportDescripto boolean shared, ImportValueSupplier imports) { final String importedModuleName = importDescriptor.moduleName(); final String importedMemoryName = importDescriptor.memberName(); - // Special import of main module memory into WASI built-in module. - final boolean importsMainMemory = instance.module().isBuiltin() && importedModuleName.equals("main") && importedMemoryName.equals("memory"); final Runnable resolveAction = () -> { final WasmMemory importedMemory; final WasmMemory externalMemory = lookupImportObject(instance, importDescriptor, imports, WasmMemory.class); @@ -486,7 +485,9 @@ void resolveMemoryImport(WasmStore store, WasmInstance instance, ImportDescripto importedMemory = store.memories().memory(contextMemoryIndex); assert memoryIndex == importDescriptor.targetIndex(); } else { - final WasmInstance importedInstance = importsMainMemory ? store.lookupMainModule() : store.lookupModuleInstance(importedModuleName); + // WASIp1 memory import should have been resolved via ImportValueSupplier above. + assert !instance.module().isBuiltin() : importDescriptor; + final WasmInstance importedInstance = store.lookupModuleInstance(importedModuleName); if (importedInstance == null) { throw WasmException.create(Failure.UNKNOWN_IMPORT, String.format("The module '%s', referenced in the import of memory '%s' in module '%s', does not exist", importedModuleName, importedMemoryName, instance.name())); diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmContextOptions.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmContextOptions.java index 16b2abf4f8e3..3547f5dc99f4 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmContextOptions.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmContextOptions.java @@ -65,6 +65,7 @@ public final class WasmContextOptions { @CompilationFinal private boolean constantRandomGet; @CompilationFinal private boolean directByteBufferMemoryAccess; @CompilationFinal private boolean debugTestMode; + @CompilationFinal private boolean evalReturnsInstance; private final OptionValues optionValues; @@ -94,6 +95,7 @@ private void setOptionValues() { this.constantRandomGet = readBooleanOption(WasmOptions.WasiConstantRandomGet); this.directByteBufferMemoryAccess = readBooleanOption(WasmOptions.DirectByteBufferMemoryAccess); this.debugTestMode = readBooleanOption(WasmOptions.DebugTestMode); + this.evalReturnsInstance = readBooleanOption(WasmOptions.EvalReturnsInstance); } private void checkOptionDependencies() { @@ -173,6 +175,10 @@ public boolean debugTestMode() { return debugTestMode; } + public boolean evalReturnsInstance() { + return evalReturnsInstance; + } + @Override public int hashCode() { int hash = 5; @@ -190,6 +196,7 @@ public int hashCode() { hash = 53 * hash + (this.constantRandomGet ? 1 : 0); hash = 53 * hash + (this.directByteBufferMemoryAccess ? 1 : 0); hash = 53 * hash + (this.debugTestMode ? 1 : 0); + hash = 53 * hash + (this.evalReturnsInstance ? 1 : 0); return hash; } @@ -246,6 +253,9 @@ public boolean equals(Object obj) { if (this.debugTestMode != other.debugTestMode) { return false; } + if (this.evalReturnsInstance != other.evalReturnsInstance) { + return false; + } return true; } } diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstance.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstance.java index 6b91395e7c0d..f201403ac0fb 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstance.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstance.java @@ -40,18 +40,12 @@ */ package org.graalvm.wasm; -import java.util.ArrayList; import java.util.List; -import org.graalvm.wasm.api.Sequence; -import org.graalvm.wasm.constants.GlobalModifier; - -import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.TruffleObject; import com.oracle.truffle.api.interop.UnknownIdentifierException; -import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.library.ExportLibrary; import com.oracle.truffle.api.library.ExportMessage; @@ -123,6 +117,8 @@ protected WasmInstance instance() { return this; } + private static final String EXPORTS_MEMBER = "exports"; + @ExportMessage boolean hasMembers() { return true; @@ -130,132 +126,25 @@ boolean hasMembers() { @ExportMessage @TruffleBoundary - public Object readMember(String member) throws UnknownIdentifierException { - ensureLinked(); - final SymbolTable symbolTable = symbolTable(); - final WasmFunction function = symbolTable.exportedFunctions().get(member); - if (function != null) { - return functionInstance(function); - } - final Integer tableIndex = symbolTable.exportedTables().get(member); - if (tableIndex != null) { - return store().tables().table(tableAddress(tableIndex)); - } - final Integer memoryIndex = symbolTable.exportedMemories().get(member); - if (memoryIndex != null) { - return memory(memoryIndex); - } - final Integer globalIndex = symbolTable.exportedGlobals().get(member); - if (globalIndex != null) { - return readGlobal(symbolTable, globalIndex); - } - throw UnknownIdentifierException.create(member); + boolean isMemberReadable(String member) { + return EXPORTS_MEMBER.equals(member); } @ExportMessage @TruffleBoundary - public void writeMember(String member, Object value) throws UnknownIdentifierException, UnsupportedMessageException { - ensureLinked(); - // This method works only for mutable globals. - final SymbolTable symbolTable = symbolTable(); - final Integer index = symbolTable.exportedGlobals().get(member); - if (index == null) { + Object readMember(String member) throws UnknownIdentifierException { + if (!isMemberReadable(member)) { throw UnknownIdentifierException.create(member); } - final int address = globalAddress(index); - if (!(value instanceof Number)) { - throw UnsupportedMessageException.create(); - } - final boolean mutable = symbolTable.globalMutability(index) == GlobalModifier.MUTABLE; - if (module().isParsed() && !mutable) { - // Constant variables cannot be modified after linking. - throw UnsupportedMessageException.create(); - } - long longValue = ((Number) value).longValue(); - store().globals().storeLong(address, longValue); - } - - @ExportMessage - @TruffleBoundary - boolean isMemberReadable(String member) { ensureLinked(); - final SymbolTable symbolTable = symbolTable(); - try { - return symbolTable.exportedFunctions().containsKey(member) || - symbolTable.exportedMemories().containsKey(member) || - symbolTable.exportedTables().containsKey(member) || - symbolTable.exportedGlobals().containsKey(member); - } catch (NumberFormatException exc) { - return false; - } - } - - @ExportMessage - @TruffleBoundary - boolean isMemberModifiable(String member) { - ensureLinked(); - final SymbolTable symbolTable = symbolTable(); - final Integer index = symbolTable.exportedGlobals().get(member); - if (index == null) { - return false; - } - return symbolTable.globalMutability(index) == GlobalModifier.MUTABLE; - } - - @ExportMessage - @TruffleBoundary - boolean isMemberInsertable(@SuppressWarnings("unused") String member) { - return false; - } - - private Object readGlobal(SymbolTable symbolTable, int globalIndex) { - final int address = globalAddress(globalIndex); - final GlobalRegistry globals = store().globals(); - final byte type = symbolTable.globalValueType(globalIndex); - switch (type) { - case WasmType.I32_TYPE: - return globals.loadAsInt(address); - case WasmType.I64_TYPE: - return globals.loadAsLong(address); - case WasmType.F32_TYPE: - return Float.intBitsToFloat(globals.loadAsInt(address)); - case WasmType.F64_TYPE: - return Double.longBitsToDouble(globals.loadAsLong(address)); - case WasmType.V128_TYPE: - return globals.loadAsVector128(address); - case WasmType.FUNCREF_TYPE: - case WasmType.EXTERNREF_TYPE: - return globals.loadAsReference(address); - default: - CompilerDirectives.transferToInterpreter(); - throw new RuntimeException("Unknown type: " + type); - } + assert EXPORTS_MEMBER.equals(member) : member; + return new WasmInstanceExports(this); } @ExportMessage @TruffleBoundary Object getMembers(@SuppressWarnings("unused") boolean includeInternal) { - ensureLinked(); - // TODO: Handle includeInternal. - final SymbolTable symbolTable = symbolTable(); - final List exportNames = new ArrayList<>(); - for (String functionName : symbolTable.exportedFunctions().getKeys()) { - exportNames.add(functionName); - } - for (String tableName : symbolTable.exportedTables().getKeys()) { - exportNames.add(tableName); - } - for (String memoryName : symbolTable.exportedMemories().getKeys()) { - exportNames.add(memoryName); - } - for (String globalName : symbolTable.exportedGlobals().getKeys()) { - exportNames.add(globalName); - } - return new Sequence<>(exportNames); - } - - public boolean isBuiltin() { - return module().isBuiltin(); + return new WasmNamesObject(new String[]{EXPORTS_MEMBER}); } @Override diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstanceExports.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstanceExports.java new file mode 100644 index 000000000000..226408cd7f99 --- /dev/null +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstanceExports.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.graalvm.wasm; + +import java.util.ArrayList; +import java.util.List; + +import org.graalvm.wasm.api.Sequence; +import org.graalvm.wasm.api.ValueType; +import org.graalvm.wasm.globals.ExportedWasmGlobal; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.interop.ArityException; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.interop.UnsupportedTypeException; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; + +@ExportLibrary(InteropLibrary.class) +@SuppressWarnings("static-method") +public final class WasmInstanceExports implements TruffleObject { + private final WasmInstance instance; + + public WasmInstanceExports(WasmInstance instance) { + this.instance = instance; + } + + @ExportMessage + boolean hasMembers() { + return true; + } + + @ExportMessage + @TruffleBoundary + public Object readMember(String member) throws UnknownIdentifierException { + final SymbolTable symbolTable = instance.symbolTable(); + final WasmFunction function = symbolTable.exportedFunctions().get(member); + if (function != null) { + return instance.functionInstance(function); + } + final Integer tableIndex = symbolTable.exportedTables().get(member); + if (tableIndex != null) { + return instance.store().tables().table(instance.tableAddress(tableIndex)); + } + final Integer memoryIndex = symbolTable.exportedMemories().get(member); + if (memoryIndex != null) { + return instance.memory(memoryIndex); + } + final Integer globalIndex = symbolTable.exportedGlobals().get(member); + if (globalIndex != null) { + return readGlobal(symbolTable, globalIndex); + } + throw UnknownIdentifierException.create(member); + } + + @ExportMessage + @TruffleBoundary + boolean isMemberReadable(String member) { + final SymbolTable symbolTable = instance.symbolTable(); + return symbolTable.exportedFunctions().containsKey(member) || + symbolTable.exportedMemories().containsKey(member) || + symbolTable.exportedTables().containsKey(member) || + symbolTable.exportedGlobals().containsKey(member); + } + + private Object readGlobal(SymbolTable symbolTable, int globalIndex) { + final int address = instance.globalAddress(globalIndex); + if (address < 0) { + return instance.store().globals().externalGlobal(address); + } else { + final ValueType valueType = ValueType.fromByteValue(symbolTable.globalValueType(globalIndex)); + final boolean mutable = symbolTable.isGlobalMutable(globalIndex); + return new ExportedWasmGlobal(valueType, mutable, instance.store().globals(), address); + } + } + + @ExportMessage + @TruffleBoundary + boolean isMemberInvocable(String member) { + final SymbolTable symbolTable = instance.symbolTable(); + return symbolTable.exportedFunctions().containsKey(member); + } + + @ExportMessage + @TruffleBoundary + Object invokeMember(String member, Object... arguments) throws UnknownIdentifierException, UnsupportedMessageException, UnsupportedTypeException, ArityException { + if (!isMemberInvocable(member)) { + throw UnknownIdentifierException.create(member); + } + final SymbolTable symbolTable = instance.symbolTable(); + final WasmFunction function = symbolTable.exportedFunctions().get(member); + if (function != null) { + final WasmFunctionInstance functionInstance = instance.functionInstance(function); + final InteropLibrary lib = InteropLibrary.getUncached(functionInstance); + assert lib.isExecutable(functionInstance); + return lib.execute(functionInstance, arguments); + } + return WasmConstant.VOID; + } + + @ExportMessage + @TruffleBoundary + Object getMembers(@SuppressWarnings("unused") boolean includeInternal) { + // TODO: Handle includeInternal. + final SymbolTable symbolTable = instance.symbolTable(); + final List exportNames = new ArrayList<>(); + for (String functionName : symbolTable.exportedFunctions().getKeys()) { + exportNames.add(functionName); + } + for (String tableName : symbolTable.exportedTables().getKeys()) { + exportNames.add(tableName); + } + for (String memoryName : symbolTable.exportedMemories().getKeys()) { + exportNames.add(memoryName); + } + for (String globalName : symbolTable.exportedGlobals().getKeys()) { + exportNames.add(globalName); + } + return new Sequence<>(exportNames); + } + + @Override + public String toString() { + return "wasm-instance-exports(" + instance.name() + ")"; + } +} diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstantiator.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstantiator.java index a192873ead2c..92c814252ff8 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstantiator.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmInstantiator.java @@ -267,7 +267,8 @@ static List recreateLinkActions(WasmModule module) { } final byte[] dataOffsetBytecode; final long dataOffsetAddress; - if ((encoding & BytecodeBitEncoding.DATA_SEG_BYTECODE_OR_OFFSET_MASK) == BytecodeBitEncoding.DATA_SEG_BYTECODE) { + if ((encoding & BytecodeBitEncoding.DATA_SEG_BYTECODE_OR_OFFSET_MASK) == BytecodeBitEncoding.DATA_SEG_BYTECODE && + ((encoding & BytecodeBitEncoding.DATA_SEG_VALUE_MASK) != BytecodeBitEncoding.DATA_SEG_VALUE_UNDEFINED)) { int dataOffsetBytecodeLength = (int) value; dataOffsetBytecode = Arrays.copyOfRange(bytecode, effectiveOffset, effectiveOffset + dataOffsetBytecodeLength); effectiveOffset += dataOffsetBytecodeLength; @@ -285,7 +286,7 @@ static List recreateLinkActions(WasmModule module) { effectiveOffset++; switch (memoryIndexEncoding & BytecodeBitEncoding.DATA_SEG_MEMORY_INDEX_MASK) { case BytecodeBitEncoding.DATA_SEG_MEMORY_INDEX_U6: - memoryIndex = encoding & BytecodeBitEncoding.DATA_SEG_MEMORY_INDEX_VALUE; + memoryIndex = memoryIndexEncoding & BytecodeBitEncoding.DATA_SEG_MEMORY_INDEX_VALUE; break; case BytecodeBitEncoding.DATA_SEG_MEMORY_INDEX_U8: memoryIndex = BinaryStreamParser.rawPeekU8(bytecode, effectiveOffset); diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmLanguage.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmLanguage.java index 8db365a5020c..0131277a35ad 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmLanguage.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmLanguage.java @@ -163,21 +163,27 @@ private ParsedWasmModuleRootNode(WasmLanguage language, WasmModule module, Sourc * The CallTarget returned by {@code parse} supports two calling conventions: * *
    - *
  1. (default) zero arguments provided: on the first call, instantiates the decoded module - * and puts it in the context's module instance map; then returns the {@link WasmInstance}. - *
  2. first argument is {@code "module_decode"}: returns the decoded {@link WasmModule} - * (i.e. behaves like {@link WebAssembly#moduleDecode module_decode}). Used by the JS API. + *
  3. (default) returns the decoded {@link WasmModule} (i.e. behaves like + * {@link WebAssembly#moduleDecode module_decode} in the JS API). + *
  4. (enabled with {@link WasmOptions#EvalReturnsInstance}), instantiates the decoded + * module and puts it in the context's module instance map; then returns the + * {@link WasmInstance}. *
*/ @Override public Object execute(VirtualFrame frame) { if (frame.getArguments().length == 0) { - final WasmStore contextStore = WasmContext.get(this).contextStore(); - WasmInstance instance = contextStore.lookupModuleInstance(module); - if (instance == null) { - instance = contextStore.readInstance(module); + final WasmContext context = WasmContext.get(this); + if (context.getContextOptions().evalReturnsInstance()) { + final WasmStore contextStore = context.contextStore(); + WasmInstance instance = contextStore.lookupModuleInstance(module); + if (instance == null) { + instance = contextStore.readInstance(module); + } + return instance; + } else { + return module; } - return instance; } else { if (frame.getArguments()[0] instanceof String mode) { if (mode.equals(MODULE_DECODE)) { @@ -203,7 +209,7 @@ WasmModule getModule() { /** * Parses simple expressions required to modify values during debugging. - * + * * @param request request for parsing */ @Override @@ -249,7 +255,7 @@ protected void finalizeContext(WasmContext context) { super.finalizeContext(context); context.memoryContext().close(); try { - context.contextStore().fdManager().close(); + context.fdManager().close(); } catch (IOException e) { throw new RuntimeException("Error while closing WasmFilesManager."); } diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmModule.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmModule.java index 05229ad6f01f..ff61dd18f33a 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmModule.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmModule.java @@ -44,22 +44,36 @@ import java.lang.invoke.VarHandle; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; import org.graalvm.collections.EconomicMap; +import org.graalvm.wasm.constants.ImportIdentifier; import org.graalvm.wasm.debugging.data.DebugFunction; import org.graalvm.wasm.debugging.parser.DebugTranslator; +import org.graalvm.wasm.exception.ExceptionProvider; +import org.graalvm.wasm.exception.Failure; +import org.graalvm.wasm.exception.WasmException; +import org.graalvm.wasm.globals.WasmGlobal; +import org.graalvm.wasm.memory.WasmMemory; import org.graalvm.wasm.parser.ir.CodeEntry; +import com.oracle.truffle.api.CompilerAsserts; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; import com.oracle.truffle.api.source.Source; /** * Represents a parsed and validated WebAssembly module, which has not yet been instantiated. */ +@ExportLibrary(InteropLibrary.class) @SuppressWarnings("static-method") public final class WasmModule extends SymbolTable implements TruffleObject { private final String name; @@ -270,4 +284,134 @@ public EconomicMap debugFunctions() { } return debugFunctions; } + + @ExportMessage + boolean isInstantiable() { + return true; + } + + @ExportMessage + @TruffleBoundary + Object instantiate(Object... arguments) { + final WasmContext context = WasmContext.get(null); + final Object importObject; + if (arguments.length == 0) { + importObject = WasmConstant.NULL; + } else if (arguments.length == 1) { + importObject = arguments[0]; + } else { + throw WasmException.provider().createTypeError(Failure.TYPE_MISMATCH, "Can only provide a single import object."); + } + final WasmStore store = new WasmStore(context, context.language()); + return createInstance(store, importObject, WasmException.provider(), false); + } + + public WasmInstance createInstance(WasmStore store, Object importObject, ExceptionProvider exceptionProvider, boolean importsOnlyInImportObject) { + final WasmInstance instance = store.readInstance(this); + var imports = resolveModuleImports(importObject, exceptionProvider, importsOnlyInImportObject); + store.linker().tryLink(instance, imports); + return instance; + } + + private ImportValueSupplier resolveModuleImports(Object importObject, ExceptionProvider exceptionProvider, boolean importsOnlyInImportObject) { + CompilerAsserts.neverPartOfCompilation(); + Objects.requireNonNull(importObject); + List resolvedImports = new ArrayList<>(numImportedSymbols()); + + if (!importedSymbols().isEmpty()) { + if (!importObjectExists(importObject)) { + if (importsOnlyInImportObject) { + throw exceptionProvider.createTypeError(Failure.TYPE_MISMATCH, "Module requires imports, but import object is undefined."); + } else { + // imports could be provided by another source, such as a module. + return ImportValueSupplier.none(); + } + } + } + + for (ImportDescriptor descriptor : importedSymbols()) { + final int listIndex = resolvedImports.size(); + assert listIndex == descriptor.importedSymbolIndex(); + + final Object member = getImportObjectMember(importObject, descriptor, exceptionProvider, importsOnlyInImportObject); + if (member == null) { + // import could be provided by another source, such as a module. + assert !importsOnlyInImportObject; + resolvedImports.add(null); + continue; + } + + resolvedImports.add(switch (descriptor.identifier()) { + case ImportIdentifier.FUNCTION -> requireCallable(member, descriptor, exceptionProvider); + case ImportIdentifier.TABLE -> requireWasmTable(member, descriptor, exceptionProvider); + case ImportIdentifier.MEMORY -> requireWasmMemory(member, descriptor, exceptionProvider); + case ImportIdentifier.GLOBAL -> requireWasmGlobal(member, descriptor, exceptionProvider); + default -> throw WasmException.create(Failure.UNSPECIFIED_INTERNAL, "Unknown import descriptor type: " + descriptor.identifier()); + }); + } + + assert resolvedImports.size() == numImportedSymbols(); + return (importDesc, instance) -> { + // Import values are only valid in the module where they were resolved. + if (instance.module() == WasmModule.this) { + return resolvedImports.get(importDesc.importedSymbolIndex()); + } else { + return null; + } + }; + } + + private static boolean importObjectExists(Object importObject) { + final InteropLibrary interop = InteropLibrary.getUncached(importObject); + return !interop.isNull(importObject) && interop.hasMembers(importObject); + } + + private static Object getImportObjectMember(Object importObject, ImportDescriptor descriptor, ExceptionProvider exceptionProvider, boolean importsOnlyInImportObject) { + try { + final InteropLibrary importObjectInterop = InteropLibrary.getUncached(importObject); + if (!importObjectInterop.isMemberReadable(importObject, descriptor.moduleName())) { + // import could be provided by another source, such as a module. + if (!importsOnlyInImportObject) { + return null; + } + throw exceptionProvider.formatTypeError(Failure.TYPE_MISMATCH, "Import object does not contain module \"%s\".", descriptor.moduleName()); + } + final Object importedModuleObject = importObjectInterop.readMember(importObject, descriptor.moduleName()); + final InteropLibrary moduleObjectInterop = InteropLibrary.getUncached(importedModuleObject); + if (!moduleObjectInterop.isMemberReadable(importedModuleObject, descriptor.memberName())) { + throw exceptionProvider.formatLinkError(Failure.UNKNOWN_IMPORT, "Import module object \"%s\" does not contain \"%s\".", descriptor.moduleName(), descriptor.memberName()); + } + return moduleObjectInterop.readMember(importedModuleObject, descriptor.memberName()); + } catch (UnknownIdentifierException | UnsupportedMessageException e) { + throw WasmException.create(Failure.UNSPECIFIED_INTERNAL, "Unexpected state."); + } + } + + private static Object requireCallable(Object member, ImportDescriptor importDescriptor, ExceptionProvider exceptionProvider) { + if (!(member instanceof WasmFunctionInstance || InteropLibrary.getUncached().isExecutable(member))) { + throw exceptionProvider.createLinkError(Failure.INCOMPATIBLE_IMPORT_TYPE, "Member " + member + " " + importDescriptor + " is not callable."); + } + return member; + } + + private static WasmMemory requireWasmMemory(Object member, ImportDescriptor importDescriptor, ExceptionProvider exceptionProvider) { + if (!(member instanceof WasmMemory memory)) { + throw exceptionProvider.createLinkError(Failure.INCOMPATIBLE_IMPORT_TYPE, "Member " + member + " " + importDescriptor + " is not a valid memory."); + } + return memory; + } + + private static WasmTable requireWasmTable(Object member, ImportDescriptor importDescriptor, ExceptionProvider exceptionProvider) { + if (!(member instanceof WasmTable table)) { + throw exceptionProvider.createLinkError(Failure.INCOMPATIBLE_IMPORT_TYPE, "Member " + member + " " + importDescriptor + " is not a valid table."); + } + return table; + } + + private static WasmGlobal requireWasmGlobal(Object member, ImportDescriptor importDescriptor, ExceptionProvider exceptionProvider) { + if (!(member instanceof WasmGlobal global)) { + throw exceptionProvider.createLinkError(Failure.INCOMPATIBLE_IMPORT_TYPE, "Member " + member + " " + importDescriptor + " is not a valid global."); + } + return global; + } } diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmNamesObject.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmNamesObject.java new file mode 100644 index 000000000000..a6a28166f2df --- /dev/null +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmNamesObject.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.graalvm.wasm; + +import com.oracle.truffle.api.dsl.Bind; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.InvalidArrayIndexException; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.profiles.InlinedBranchProfile; + +@ExportLibrary(InteropLibrary.class) +public final class WasmNamesObject implements TruffleObject { + private final String[] names; + + public WasmNamesObject(String[] names) { + this.names = names; + } + + @SuppressWarnings("static-method") + @ExportMessage + boolean hasArrayElements() { + return true; + } + + @ExportMessage + boolean isArrayElementReadable(long index) { + return index >= 0 && index < names.length; + } + + @ExportMessage + long getArraySize() { + return names.length; + } + + @ExportMessage + Object readArrayElement(long index, + @Bind Node node, + @Cached InlinedBranchProfile errorBranch) throws InvalidArrayIndexException { + if (!isArrayElementReadable(index)) { + errorBranch.enter(node); + throw InvalidArrayIndexException.create(index); + } + return names[(int) index]; + } +} diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmOptions.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmOptions.java index e82d7cb64d5f..0477b343700c 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmOptions.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmOptions.java @@ -40,17 +40,39 @@ */ package org.graalvm.wasm; +import java.util.HashMap; +import java.util.Map; + import org.graalvm.options.OptionCategory; import org.graalvm.options.OptionKey; import org.graalvm.options.OptionStability; import org.graalvm.options.OptionType; +import org.graalvm.wasm.exception.Failure; +import org.graalvm.wasm.exception.WasmException; +import org.graalvm.wasm.predefined.BuiltinModule; import com.oracle.truffle.api.Option; @Option.Group(WasmLanguage.ID) public class WasmOptions { @Option(help = "A comma-separated list of builtin modules to use.", category = OptionCategory.USER, stability = OptionStability.STABLE, usageSyntax = "[:],[:],...")// - public static final OptionKey Builtins = new OptionKey<>(""); + public static final OptionKey> Builtins = new OptionKey<>(Map.of(), new OptionType<>("Builtins", optionValue -> { + if (optionValue.isEmpty()) { + return Map.of(); + } + final String[] moduleSpecs = optionValue.split(","); + final Map builtinModules = new HashMap<>(moduleSpecs.length); + for (String moduleSpec : moduleSpecs) { + final String[] parts = moduleSpec.split(":"); + if (parts.length > 2) { + throw WasmException.create(Failure.UNSPECIFIED_INVALID, "Module specification '" + moduleSpec + "' is not valid."); + } + final String linkingName = parts[0]; + final String predefinedModuleName = parts.length == 2 ? parts[1] : parts[0]; + builtinModules.put(linkingName, BuiltinModule.requireBuiltinModule(predefinedModuleName)); + } + return Map.copyOf(builtinModules); + })); @Option(help = "The minimal binary size for which to use async parsing. If threads are not supported, async parsing will not be used.", category = OptionCategory.USER, stability = OptionStability.STABLE, usageSyntax = "[0, inf)", // deprecated = true, deprecationMessage = "Option no longer has any effect and can be safely omitted.")// @@ -125,4 +147,7 @@ public enum ConstantsStorePolicy { @Option(help = "Support instrumentation for functions that do not have their source available. For testing purpose only.", category = OptionCategory.INTERNAL, stability = OptionStability.EXPERIMENTAL, usageSyntax = "false|true") // public static final OptionKey DebugTestMode = new OptionKey<>(false); + + @Option(help = "Makes Context#eval return a wasm instance (runtime representation) instead of a wasm module (symbolic representation).", category = OptionCategory.EXPERT, stability = OptionStability.EXPERIMENTAL, usageSyntax = "false|true") // + public static final OptionKey EvalReturnsInstance = new OptionKey<>(false); } diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmScope.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmScope.java index e0a9e4b28524..1d9ae6429c78 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmScope.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmScope.java @@ -42,18 +42,13 @@ import java.util.Map; -import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.TruffleLanguage; -import com.oracle.truffle.api.dsl.Bind; -import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.interop.InteropLibrary; -import com.oracle.truffle.api.interop.InvalidArrayIndexException; import com.oracle.truffle.api.interop.TruffleObject; import com.oracle.truffle.api.interop.UnknownIdentifierException; import com.oracle.truffle.api.library.ExportLibrary; import com.oracle.truffle.api.library.ExportMessage; -import com.oracle.truffle.api.nodes.Node; -import com.oracle.truffle.api.profiles.InlinedBranchProfile; @ExportLibrary(InteropLibrary.class) @SuppressWarnings({"static-method"}) @@ -84,7 +79,7 @@ private Map instances() { } @ExportMessage - @CompilerDirectives.TruffleBoundary + @TruffleBoundary Object readMember(String member) throws UnknownIdentifierException { var instances = instances(); Object value = instances.get(member); @@ -95,18 +90,18 @@ Object readMember(String member) throws UnknownIdentifierException { } @ExportMessage - @CompilerDirectives.TruffleBoundary + @TruffleBoundary boolean isMemberReadable(String member) { var instances = instances(); return instances.containsKey(member); } @ExportMessage - @CompilerDirectives.TruffleBoundary + @TruffleBoundary Object getMembers(@SuppressWarnings("unused") boolean includeInternal) { var instances = instances(); - String[] keys = instances.keySet().toArray(new String[instances.size()]); - return new InstanceNamesObject(keys); + final String[] keys = instances.keySet().toArray(new String[instances.size()]); + return new WasmNamesObject(keys); } @ExportMessage @@ -115,44 +110,8 @@ boolean isScope() { } @ExportMessage - @CompilerDirectives.TruffleBoundary + @TruffleBoundary Object toDisplayString(@SuppressWarnings("unused") boolean allowSideEffects) { return "wasm-global-scope" + instances().keySet(); } - - @ExportLibrary(InteropLibrary.class) - static final class InstanceNamesObject implements TruffleObject { - - private final String[] names; - - InstanceNamesObject(String[] names) { - this.names = names; - } - - @ExportMessage - boolean hasArrayElements() { - return true; - } - - @ExportMessage - boolean isArrayElementReadable(long index) { - return index >= 0 && index < names.length; - } - - @ExportMessage - long getArraySize() { - return names.length; - } - - @ExportMessage - Object readArrayElement(long index, - @Bind Node node, - @Cached InlinedBranchProfile error) throws InvalidArrayIndexException { - if (!isArrayElementReadable(index)) { - error.enter(node); - throw InvalidArrayIndexException.create(index); - } - return names[(int) index]; - } - } } diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmStore.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmStore.java index e348a5c63114..4b330cdaa099 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmStore.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/WasmStore.java @@ -43,14 +43,17 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.graalvm.wasm.api.WebAssembly; import org.graalvm.wasm.exception.Failure; import org.graalvm.wasm.exception.WasmException; +import org.graalvm.wasm.exception.WasmJsApiException; import org.graalvm.wasm.parser.bytecode.BytecodeParser; import org.graalvm.wasm.predefined.BuiltinModule; import org.graalvm.wasm.predefined.wasi.fd.FdManager; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.TruffleLanguage.Env; +import com.oracle.truffle.api.interop.TruffleObject; /** * Holds shared (a.k.a. global) state that belongs to a module instantiation & linking context. @@ -62,7 +65,7 @@ * {@link WasmContext}'s primary {@link WasmStore}, while modules instantiated through the * {@code module_instantiate} JS API each get their own private {@link WasmStore}. */ -public final class WasmStore { +public final class WasmStore implements TruffleObject { private final WasmContext context; private final WasmLanguage language; private final MemoryRegistry memoryRegistry; @@ -70,7 +73,6 @@ public final class WasmStore { private final TableRegistry tableRegistry; private final Linker linker; private final Map moduleInstances; - private WasmInstance mainModuleInstance; private final FdManager filesManager; private final WasmContextOptions contextOptions; @@ -84,7 +86,6 @@ public WasmStore(WasmContext context, WasmLanguage language) { this.moduleInstances = new LinkedHashMap<>(); this.linker = new Linker(); this.filesManager = context.fdManager(); - instantiateBuiltinInstances(); } public WasmContext context() { @@ -138,50 +139,56 @@ public WasmInstance lookupModuleInstance(String name) { return moduleInstances.get(name); } - /** - * Returns the first module evaluated in this context (not including built-in modules). - */ - public WasmInstance lookupMainModule() { - return mainModuleInstance; - } - public void register(WasmInstance instance) { if (moduleInstances.containsKey(instance.name())) { throw WasmException.create(Failure.UNSPECIFIED_INTERNAL, "Context already contains an instance named '" + instance.name() + "'."); } moduleInstances.put(instance.name(), instance); - if (mainModuleInstance == null && !instance.isBuiltin()) { - mainModuleInstance = instance; - } } - private void instantiateBuiltinInstances() { - final String extraModuleValue = WasmOptions.Builtins.getValue(environment().getOptions()); - if (extraModuleValue.isEmpty()) { - return; + public ImportValueSupplier instantiateBuiltinInstances() { + final Map builtinModules = WasmOptions.Builtins.getValue(environment().getOptions()); + var importValues = ImportValueSupplier.none(); + if (builtinModules.isEmpty()) { + return importValues; } - final String[] moduleSpecs = extraModuleValue.split(","); - for (String moduleSpec : moduleSpecs) { - final String[] parts = moduleSpec.split(":"); - if (parts.length > 2) { - throw WasmException.create(Failure.UNSPECIFIED_INVALID, "Module specification '" + moduleSpec + "' is not valid."); + for (Map.Entry entry : builtinModules.entrySet()) { + final String name = entry.getKey(); + final BuiltinModule builtinModule = entry.getValue(); + + final WasmInstance importingModuleInstance = lookupImportingModuleInstance(name); + // only instantiate built-in modules that are actually imported + if (importingModuleInstance == null) { + continue; + } + final WasmInstance builtinInstance = builtinModule.createInstance(language, this, name); + moduleInstances.put(name, builtinInstance); + if (builtinInstance.module().numImportedSymbols() != 0) { + // Link imports of the built-in module (WASIp1 memory) to the importing module. + importValues = importValues.andThen((importDesc, intoInstance) -> { + if (intoInstance == builtinInstance) { + try { + return WebAssembly.instanceExport(importingModuleInstance, importDesc.memberName()); + } catch (WasmJsApiException e) { + // fallthrough + } + } + return null; + }); } - final String name = parts[0]; - final String key = parts.length == 2 ? parts[1] : parts[0]; - final WasmInstance module = BuiltinModule.createBuiltinInstance(language, this, name, key); - moduleInstances.put(name, module); } + return importValues; } - public WasmModule readModule(byte[] data, ModuleLimits moduleLimits) { - return readModule("Unnamed", data, moduleLimits); - } - - public WasmModule readModule(String moduleName, byte[] data, ModuleLimits moduleLimits) { - final WasmModule module = WasmModule.create(moduleName, moduleLimits); - final BinaryParser reader = new BinaryParser(module, context, data); - reader.readModule(); - return module; + private WasmInstance lookupImportingModuleInstance(String moduleName) { + for (WasmInstance instance : moduleInstances().values()) { + for (ImportDescriptor importDesc : instance.module().importedSymbols()) { + if (moduleName.equals(importDesc.moduleName())) { + return instance; + } + } + } + return null; } @TruffleBoundary diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/api/WebAssembly.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/api/WebAssembly.java index 4055a077cf39..b50b9dc084df 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/api/WebAssembly.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/api/WebAssembly.java @@ -53,7 +53,6 @@ import org.graalvm.polyglot.io.ByteSequence; import org.graalvm.wasm.EmbedderDataHolder; import org.graalvm.wasm.ImportDescriptor; -import org.graalvm.wasm.ImportValueSupplier; import org.graalvm.wasm.WasmConstant; import org.graalvm.wasm.WasmContext; import org.graalvm.wasm.WasmCustomSection; @@ -80,7 +79,6 @@ import com.oracle.truffle.api.CompilerAsserts; import com.oracle.truffle.api.interop.InteropException; import com.oracle.truffle.api.interop.InteropLibrary; -import com.oracle.truffle.api.interop.UnknownIdentifierException; import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.Source; @@ -139,102 +137,8 @@ public WasmInstance moduleInstantiate(Object[] args) { public WasmInstance moduleInstantiate(WasmModule module, Object importObject) { CompilerAsserts.neverPartOfCompilation(); - WasmStore instanceStore = new WasmStore(currentContext, currentContext.language()); - WasmInstance instance = instantiateModule(module, instanceStore); - var imports = resolveModuleImports(module, importObject); - instance.store().linker().tryLink(instance, imports); - return instance; - } - - private static WasmInstance instantiateModule(WasmModule module, WasmStore store) { - return store.readInstance(module); - } - - private static ImportValueSupplier resolveModuleImports(WasmModule module, Object importObject) { - CompilerAsserts.neverPartOfCompilation(); - List resolvedImports = new ArrayList<>(module.numImportedSymbols()); - - if (!module.importedSymbols().isEmpty()) { - requireImportObject(importObject); - } - - for (ImportDescriptor descriptor : module.importedSymbols()) { - final int listIndex = resolvedImports.size(); - assert listIndex == descriptor.importedSymbolIndex(); - - final Object member = getImportObjectMember(importObject, descriptor); - - resolvedImports.add(switch (descriptor.identifier()) { - case ImportIdentifier.FUNCTION -> requireCallable(member, descriptor); - case ImportIdentifier.TABLE -> requireWasmTable(member, descriptor); - case ImportIdentifier.MEMORY -> requireWasmMemory(member, descriptor); - case ImportIdentifier.GLOBAL -> requireWasmGlobal(member, descriptor); - default -> throw WasmException.create(Failure.UNSPECIFIED_INTERNAL, "Unknown import descriptor type: " + descriptor.identifier()); - }); - } - - assert resolvedImports.size() == module.numImportedSymbols(); - return (importDesc, instance) -> { - // Import values are only valid in the module where they were resolved. - if (instance.module() == module) { - return resolvedImports.get(importDesc.importedSymbolIndex()); - } else { - return null; - } - }; - } - - private static Object requireImportObject(Object importObject) { - InteropLibrary interop = InteropLibrary.getUncached(importObject); - if (interop.isNull(importObject) || !interop.hasMembers(importObject)) { - throw new WasmJsApiException(WasmJsApiException.Kind.TypeError, "Module requires imports, but import object is undefined."); - } - return importObject; - } - - private static Object getImportObjectMember(Object importObject, ImportDescriptor descriptor) { - try { - final InteropLibrary importObjectInterop = InteropLibrary.getUncached(importObject); - if (!importObjectInterop.isMemberReadable(importObject, descriptor.moduleName())) { - throw WasmJsApiException.format(WasmJsApiException.Kind.TypeError, "Import object does not contain module \"%s\".", descriptor.moduleName()); - } - final Object importedModuleObject = importObjectInterop.readMember(importObject, descriptor.moduleName()); - final InteropLibrary moduleObjectInterop = InteropLibrary.getUncached(importedModuleObject); - if (!moduleObjectInterop.isMemberReadable(importedModuleObject, descriptor.memberName())) { - throw WasmJsApiException.format(WasmJsApiException.Kind.LinkError, "Import module object \"%s\" does not contain \"%s\".", descriptor.moduleName(), descriptor.memberName()); - } - return moduleObjectInterop.readMember(importedModuleObject, descriptor.memberName()); - } catch (UnknownIdentifierException | UnsupportedMessageException e) { - throw WasmException.create(Failure.UNSPECIFIED_INTERNAL, "Unexpected state."); - } - } - - private static Object requireCallable(Object member, ImportDescriptor importDescriptor) { - if (!(member instanceof WasmFunctionInstance || InteropLibrary.getUncached().isExecutable(member))) { - throw new WasmJsApiException(WasmJsApiException.Kind.LinkError, "Member " + member + " " + importDescriptor + " is not callable."); - } - return member; - } - - private static WasmMemory requireWasmMemory(Object member, ImportDescriptor importDescriptor) { - if (!(member instanceof WasmMemory memory)) { - throw new WasmJsApiException(WasmJsApiException.Kind.LinkError, "Member " + member + " " + importDescriptor + " is not a valid memory."); - } - return memory; - } - - private static WasmTable requireWasmTable(Object member, ImportDescriptor importDescriptor) { - if (!(member instanceof WasmTable table)) { - throw new WasmJsApiException(WasmJsApiException.Kind.LinkError, "Member " + member + " " + importDescriptor + " is not a valid table."); - } - return table; - } - - private static WasmGlobal requireWasmGlobal(Object member, ImportDescriptor importDescriptor) { - if (!(member instanceof WasmGlobal global)) { - throw new WasmJsApiException(WasmJsApiException.Kind.LinkError, "Member " + member + " " + importDescriptor + " is not a valid global."); - } - return global; + final WasmStore store = new WasmStore(currentContext, currentContext.language()); + return module.createInstance(store, importObject, WasmJsApiException.provider(), true); } private static String makeModuleName(byte[] data) { diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/ExceptionProvider.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/ExceptionProvider.java new file mode 100644 index 000000000000..22a23c6f34a2 --- /dev/null +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/ExceptionProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.graalvm.wasm.exception; + +import com.oracle.truffle.api.exception.AbstractTruffleException; + +public interface ExceptionProvider { + AbstractTruffleException createTypeError(Failure failure, String message); + + AbstractTruffleException formatTypeError(Failure failure, String format, Object... args); + + AbstractTruffleException createLinkError(Failure failure, String message); + + AbstractTruffleException formatLinkError(Failure failure, String format, Object... args); +} diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/ExceptionProviders.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/ExceptionProviders.java new file mode 100644 index 000000000000..3b6f3614f0a8 --- /dev/null +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/ExceptionProviders.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.graalvm.wasm.exception; + +import com.oracle.truffle.api.exception.AbstractTruffleException; + +final class ExceptionProviders { + + static final ExceptionProvider WASM_JS_API_EXCEPTION_PROVIDER = new ExceptionProvider() { + @Override + public AbstractTruffleException createTypeError(Failure failure, String message) { + return new WasmJsApiException(WasmJsApiException.Kind.TypeError, message); + } + + @Override + public AbstractTruffleException formatTypeError(Failure failure, String format, Object... args) { + return WasmJsApiException.format(WasmJsApiException.Kind.TypeError, format, args); + } + + @Override + public AbstractTruffleException createLinkError(Failure failure, String message) { + return new WasmJsApiException(WasmJsApiException.Kind.LinkError, message); + } + + @Override + public AbstractTruffleException formatLinkError(Failure failure, String format, Object... args) { + return WasmJsApiException.format(WasmJsApiException.Kind.LinkError, format, args); + } + }; + + static final ExceptionProvider POLYGLOT_EXCEPTION_PROVIDER = new ExceptionProvider() { + @Override + public AbstractTruffleException createTypeError(Failure failure, String message) { + return WasmException.create(failure, message); + } + + @Override + public AbstractTruffleException formatTypeError(Failure failure, String format, Object... args) { + return WasmException.format(failure, format, args); + } + + @Override + public AbstractTruffleException createLinkError(Failure failure, String message) { + return WasmException.create(failure, message); + } + + @Override + public AbstractTruffleException formatLinkError(Failure failure, String format, Object... args) { + return WasmException.format(failure, format, args); + } + }; + + private ExceptionProviders() { + } +} diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/WasmException.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/WasmException.java index 6c9b7c80ad70..8302e788eb95 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/WasmException.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/WasmException.java @@ -100,6 +100,10 @@ public static WasmException format(Failure failure, Node location, String format return create(failure, location, String.format(Locale.ROOT, format, arg)); } + public static ExceptionProvider provider() { + return ExceptionProviders.POLYGLOT_EXCEPTION_PROVIDER; + } + @ExportMessage public boolean hasMembers() { return true; diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/WasmJsApiException.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/WasmJsApiException.java index 41bf7dbc16bd..20998bbde54b 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/WasmJsApiException.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/exception/WasmJsApiException.java @@ -94,4 +94,7 @@ public static WasmJsApiException format(WasmJsApiException.Kind kind, String s, return new WasmJsApiException(kind, String.format(Locale.ROOT, s, args)); } + public static ExceptionProvider provider() { + return ExceptionProviders.WASM_JS_API_EXCEPTION_PROVIDER; + } } diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/globals/WasmGlobal.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/globals/WasmGlobal.java index 4d3e16a973ce..478867f9fe2f 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/globals/WasmGlobal.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/globals/WasmGlobal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -41,11 +41,22 @@ package org.graalvm.wasm.globals; -import com.oracle.truffle.api.interop.TruffleObject; import org.graalvm.wasm.EmbedderDataHolder; +import org.graalvm.wasm.WasmNamesObject; import org.graalvm.wasm.api.ValueType; import org.graalvm.wasm.constants.GlobalModifier; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; + +@SuppressWarnings("static-method") +@ExportLibrary(InteropLibrary.class) public abstract class WasmGlobal extends EmbedderDataHolder implements TruffleObject { private final ValueType valueType; @@ -56,15 +67,15 @@ protected WasmGlobal(ValueType valueType, boolean mutable) { this.mutable = mutable; } - public ValueType getValueType() { + public final ValueType getValueType() { return valueType; } - public boolean isMutable() { + public final boolean isMutable() { return mutable; } - public byte getMutability() { + public final byte getMutability() { return mutable ? GlobalModifier.MUTABLE : GlobalModifier.CONSTANT; } @@ -79,4 +90,71 @@ public byte getMutability() { public abstract void storeLong(long value); public abstract void storeObject(Object value); + + private static final String VALUE_MEMBER = "value"; + + @ExportMessage + final boolean hasMembers() { + return true; + } + + @ExportMessage + @TruffleBoundary + final boolean isMemberReadable(String member) { + return VALUE_MEMBER.equals(member); + } + + @ExportMessage + @TruffleBoundary + final Object readMember(String member) throws UnknownIdentifierException { + if (!isMemberReadable(member)) { + throw UnknownIdentifierException.create(member); + } + assert VALUE_MEMBER.equals(member) : member; + return switch (getValueType()) { + case i32 -> loadAsInt(); + case i64 -> loadAsLong(); + case f32 -> Float.intBitsToFloat(loadAsInt()); + case f64 -> Double.longBitsToDouble(loadAsLong()); + case v128, anyfunc, externref -> loadAsObject(); + }; + } + + @ExportMessage + @TruffleBoundary + final boolean isMemberModifiable(String member) { + return VALUE_MEMBER.equals(member) && isMutable(); + } + + @ExportMessage + @TruffleBoundary + final boolean isMemberInsertable(@SuppressWarnings("unused") String member) { + return false; + } + + @ExportMessage + @TruffleBoundary + final void writeMember(String member, Object value, + @CachedLibrary(limit = "4") InteropLibrary valueLibrary) throws UnknownIdentifierException, UnsupportedMessageException { + if (!isMemberReadable(member)) { + throw UnknownIdentifierException.create(member); + } + if (!mutable) { + // Constant variables cannot be modified after linking. + throw UnsupportedMessageException.create(); + } + switch (getValueType()) { + case i32 -> storeInt(valueLibrary.asInt(value)); + case i64 -> storeLong(valueLibrary.asLong(value)); + case f32 -> storeInt(Float.floatToRawIntBits(valueLibrary.asFloat(value))); + case f64 -> storeLong(Double.doubleToRawLongBits(valueLibrary.asDouble(value))); + default -> throw UnsupportedMessageException.create(); + } + } + + @ExportMessage + @TruffleBoundary + final Object getMembers(@SuppressWarnings("unused") boolean includeInternal) { + return new WasmNamesObject(new String[]{VALUE_MEMBER}); + } } diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/parser/bytecode/BytecodeParser.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/parser/bytecode/BytecodeParser.java index 4af84e8fc14a..ac47f54b97d2 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/parser/bytecode/BytecodeParser.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/parser/bytecode/BytecodeParser.java @@ -144,7 +144,8 @@ public static void resetMemoryState(WasmStore store, WasmModule module, WasmInst } final byte[] offsetBytecode; long offsetAddress; - if ((flags & BytecodeBitEncoding.DATA_SEG_BYTECODE_OR_OFFSET_MASK) == BytecodeBitEncoding.DATA_SEG_BYTECODE) { + if ((flags & BytecodeBitEncoding.DATA_SEG_BYTECODE_OR_OFFSET_MASK) == BytecodeBitEncoding.DATA_SEG_BYTECODE && + ((flags & BytecodeBitEncoding.DATA_SEG_VALUE_MASK) != BytecodeBitEncoding.DATA_SEG_VALUE_UNDEFINED)) { int offsetBytecodeLength = (int) value; offsetBytecode = Arrays.copyOfRange(bytecode, effectiveOffset, effectiveOffset + offsetBytecodeLength); effectiveOffset += offsetBytecodeLength; diff --git a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/predefined/BuiltinModule.java b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/predefined/BuiltinModule.java index 4b27bb3d5569..60b43caa5a5f 100644 --- a/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/predefined/BuiltinModule.java +++ b/wasm/src/org.graalvm.wasm/src/org/graalvm/wasm/predefined/BuiltinModule.java @@ -58,8 +58,6 @@ import org.graalvm.wasm.predefined.testutil.TestutilModule; import org.graalvm.wasm.predefined.wasi.WasiModule; -import com.oracle.truffle.api.CompilerAsserts; - public abstract class BuiltinModule { private static final Map predefinedModules = Map.of( "emscripten", new EmscriptenModule(), @@ -68,13 +66,12 @@ public abstract class BuiltinModule { "spectest", new SpectestModule(), "go", new GoModule()); - public static WasmInstance createBuiltinInstance(WasmLanguage language, WasmStore store, String name, String predefinedModuleName) { - CompilerAsserts.neverPartOfCompilation(); + public static BuiltinModule requireBuiltinModule(String predefinedModuleName) { final BuiltinModule builtinModule = predefinedModules.get(predefinedModuleName); if (builtinModule == null) { throw WasmException.create(Failure.UNSPECIFIED_INVALID, "Unknown predefined module: " + predefinedModuleName); } - return builtinModule.createInstance(language, store, name); + return builtinModule; } protected BuiltinModule() { @@ -82,7 +79,7 @@ protected BuiltinModule() { protected abstract WasmModule createModule(WasmLanguage language, WasmContext context, String name); - protected WasmInstance createInstance(WasmLanguage language, WasmStore store, String name) { + public WasmInstance createInstance(WasmLanguage language, WasmStore store, String name) { final WasmModule module = language.getOrCreateBuiltinModule(this, bm -> createModule(language, store.context(), name)); final WasmInstance instance = new WasmInstance(store, module);