diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 6a8c8a1..a8483d3 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -2,8 +2,8 @@
"version": 1,
"isRoot": true,
"tools": {
- "fallout.cli": {
- "version": "11.0.18",
+ "fallout.globaltool": {
+ "version": "10.4.0",
"commands": [
"fallout"
]
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..a9feabf
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,80 @@
+# AngleSharp.Wasm
+
+## Purpose
+
+AngleSharp.Wasm extends AngleSharp with WebAssembly execution and a DOM/JavaScript-facing WebAssembly surface. The repository produces the `AngleSharp.Wasm` NuGet package.
+
+## Repository Layout
+
+- `src/AngleSharp.Wasm/`: library implementation.
+- `src/AngleSharp.Wasm/Dom/`: DOM-annotated WebAssembly bridge types exposed to scripting integrations.
+- `src/AngleSharp.Wasm.Tests/`: NUnit tests for configuration, runtime behavior, and the DOM bridge.
+- `src/AngleSharp.Wasm.Docs/`: TypeScript documentation site configuration and entry point.
+- `docs/`: Markdown documentation, including the API, examples, and WebAssembly specification coverage.
+- `nuke/`: NUKE build project and build configuration.
+- `build.sh`, `build.ps1`, `build.cmd`: platform-specific build entry points.
+
+## Development Environment
+
+- The solution is `src/AngleSharp.Wasm.sln`.
+- The library and test projects target `net8.0` and `net10.0`.
+- The library references AngleSharp 1.x and Wasmtime 44.0.0.
+- The default runtime is `WasmtimeWasmRuntime`, registered through `Configuration.WithWasm()`.
+- `global.json`, when present, controls the SDK version used by the build wrapper.
+
+## Common Commands
+
+Run from the repository root:
+
+```sh
+./build.sh
+```
+
+The build wrapper restores local .NET tools and delegates to NUKE. On Windows, use `build.ps1` or `build.cmd`.
+
+For focused local checks, use the .NET CLI against the solution or test project:
+
+```sh
+dotnet build src/AngleSharp.Wasm.sln
+dotnet test src/AngleSharp.Wasm.Tests/AngleSharp.Wasm.Tests.csproj
+```
+
+When changing the DOM bridge or runtime behavior, run the relevant NUnit tests and then the full test project for both target frameworks when practical.
+
+## Architecture and Conventions
+
+- Keep runtime abstractions in `src/AngleSharp.Wasm/` independent of the concrete Wasmtime implementation where possible.
+- `IWasmRuntime`, `IWasmCompiledModule`, `IWasmInstance`, and `IWasmRuntimeFactory` define the runtime boundary.
+- Host imports are supplied through `IWasmImportProvider`, `WasmImportFunction`, and the `WithWasmImports(...)` configuration extensions.
+- The default runtime is Wasmtime-specific; do not make Wasmtime types part of the public abstraction unless the feature requires it.
+- DOM-facing members use AngleSharp DOM annotations such as `[DomName]`, `[DomExposed]`, `[DomAccessor]`, and `[DomNoInterfaceObject]`.
+- Preserve the existing synchronous bridge style unless deliberately adding a Promise/streaming API.
+- Use existing AngleSharp and Wasmtime APIs before introducing new abstractions.
+- Keep changes focused and preserve public APIs unless the task explicitly requires a breaking change.
+- Use ASCII for new source and documentation unless the surrounding file already requires another character set.
+
+## Current DOM/WebAssembly Surface
+
+The package exposes a minimal `window.WebAssembly` bridge through the types in `src/AngleSharp.Wasm/Dom/`:
+
+- `WebAssembly.compile(byte[])` creates a `WasmJsModule`.
+- `WebAssembly.instantiate(WasmJsModule)` creates a `WasmJsInstance`.
+- Module metadata includes imports, exports, and custom sections.
+- Instance exports support name lookup and function invocation through `WasmJsExportedFunction`.
+- Non-function exports are currently descriptor objects rather than full `Memory`, `Table`, `Global`, `Tag`, or `Exception` projections.
+
+The documented gaps include `WebAssembly.validate(...)`, streaming APIs, compile options, full non-function export objects, and dedicated WebAssembly error constructors. Consult `docs/general/02-Spec-Coverage.md` before extending the DOM surface.
+
+## Testing Guidance
+
+Add or update focused NUnit tests in `src/AngleSharp.Wasm.Tests/` for every behavior change. DOM bridge tests should configure a browsing context with `Configuration.Default.WithWasm()`. Runtime tests should cover both successful behavior and invalid input or disposal paths where applicable.
+
+For new WebAssembly binary fixtures, keep the fixture small and document its module shape near the byte array. Prefer testing the public bridge and runtime contracts rather than implementation details of Wasmtime.
+
+## Documentation
+
+Update the relevant Markdown documentation and the specification coverage matrix when adding or changing public WebAssembly functionality. Keep API names aligned with the WebAssembly JavaScript API where the package intentionally provides a compatible projection, and document any synchronous or .NET-specific adaptation.
+
+## Change Hygiene
+
+Do not commit generated output from `bin/`, `obj/`, or NUKE build folders unless the repository explicitly requires it. Do not alter unrelated user changes. Do not commit changes unless explicitly requested.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79e0d4b..ffd4982 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+# 1.1.0
+
+Released on Friday, August 21 2026.
+
+- Updated with compile options support (#13)
+- Added `WebAssembly.CompileError`, `WebAssembly.LinkError`, and `WebAssembly.RuntimeError` projections (#14)
+- Added `WebAssembly.compileStreaming` and `WebAssembly.instantiateStreaming` methods (#12)
+- Added `WebAssembly.validate` method (#11)
+- Added `WebAssembly.Exception` object projection (#10)
+- Added `WebAssembly.Tag` object projection (#9)
+- Added `WebAssembly.Global` object projection (#8)
+- Added `WebAssembly.Table` object projection (#7)
+- Added `WebAssembly.Memory` object projection (#6)
+
# 1.0.0
Released on Friday, July 31 2026.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..9f02784
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,8 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+The guidance is shared with every AI agent working here, so it lives in AGENTS.md and is
+imported below. Record new guidance there rather than in this file.
+
+@AGENTS.md
diff --git a/README.md b/README.md
index 11982fd..9008090 100644
--- a/README.md
+++ b/README.md
@@ -88,12 +88,22 @@ The current package targets `net8.0` and `net10.0`.
- `exports()`
- `imports()`
- `customSections(name)`
+- Module validation via `validate(bytes)`
+- Response-based compilation and instantiation via `compileStreaming(response)` and `instantiateStreaming(response)`
+- Compile option routing for `builtins` and `importedStringConstants`
- Instance export access
- `exports` lookup by export name
- export key enumeration
- Export invocation helpers
- `instance.Invoke(...)`
- `WasmJsExportedFunction.Invoke(...)`
+- WebAssembly object projections
+ - memory `buffer`, `read`, `write`, and `grow`
+ - table `length`, `get`, `set`, and `grow`
+ - global `value` and `valueOf`
+ - tag `type().parameters`
+ - exception `is(tag)` and `getArg(tag, index)`
+ - `CompileError`, `LinkError`, and `RuntimeError`
- Multi-target support for `net8.0` and `net10.0`
## Current Scope and Limitations
@@ -102,9 +112,10 @@ AngleSharp.Wasm currently provides a practical subset of the WebAssembly JS API.
- Bridge methods are synchronous from the caller perspective.
- Promise-based namespace operations are not currently exposed.
-- `validate(...)` and streaming APIs are not yet implemented.
-- Compile options such as builtins / imported string constants are not yet implemented.
-- Non-function exports are currently represented as descriptors (`name`, `kind`) rather than full `Memory` / `Table` / `Global` / `Tag` objects.
+- Streaming APIs synchronously buffer AngleSharp response streams because module metadata requires the complete binary.
+- Compile options are validated and forwarded to capable runtimes. Wasmtime 44 does not expose JavaScript string builtins or imported string constants, so requesting either option with the default backend throws `NotSupportedException`.
+- Exported memories, tables, globals, and tags have object projections; other non-function exports remain descriptors (`name`, `kind`).
+- Host-created tags and exceptions are supported. Wasmtime 44 does not expose native Tag or Exception handles or enable exception modules through its .NET API, so runtime-thrown Wasm exceptions are not yet projected.
See [Spec Coverage Matrix](docs/general/02-Spec-Coverage.md) for a section-by-section status overview.
diff --git a/build/_build.csproj b/build/_build.csproj
index 647ac26..11c89b0 100644
--- a/build/_build.csproj
+++ b/build/_build.csproj
@@ -11,9 +11,7 @@
-
-
-
+
diff --git a/docs/general/01-Basics.md b/docs/general/01-Basics.md
index ced8a1e..ee7b144 100644
--- a/docs/general/01-Basics.md
+++ b/docs/general/01-Basics.md
@@ -79,9 +79,11 @@ var config = Configuration.Default
AngleSharp.Wasm currently implements a pragmatic subset of the WebAssembly JS API.
- Synchronous bridge calls are used (`compile` / `instantiate` are not Promise-based APIs).
-- `WebAssembly.validate(...)`, streaming APIs, and compile options are not implemented.
+- Streaming APIs synchronously buffer AngleSharp response streams. Compile options are forwarded to capable runtimes, but Wasmtime 44 does not support JavaScript string builtins or imported string constants.
- `Instance.exports` function members are wrapper objects requiring `.invoke(...)`.
-- Non-function exports are currently represented as descriptors (`name`, `kind`), not full `Memory` / `Table` / `Global` / `Tag` objects.
+- Compile, link, and execution failures are projected as `CompileError`, `LinkError`, and `RuntimeError` objects.
+- Exported memories, tables, globals, and tags have object projections; other non-function exports remain descriptors (`name`, `kind`).
+- Host-created exceptions support tag identity and typed payload inspection. The default Wasmtime 44 backend does not expose native Tag or Exception handles, so runtime-thrown Wasm exceptions are not yet projected.
- `customSections(...)` returns payload bytes (`byte[]`) mapped from custom sections.
- Runtime invocation/import marshaling is currently focused on numeric value kinds (`i32`, `i64`, `f32`, `f64`).
diff --git a/docs/general/02-Spec-Coverage.md b/docs/general/02-Spec-Coverage.md
index a327c20..77e2af3 100644
--- a/docs/general/02-Spec-Coverage.md
+++ b/docs/general/02-Spec-Coverage.md
@@ -18,22 +18,22 @@ Status values:
| --- | --- | --- |
| WebAssembly namespace: `compile(bytes)` | Partial | Exposed as synchronous bridge method `WebAssembly.compile(byte[])`. |
| WebAssembly namespace: `instantiate(...)` | Partial | Exposed as synchronous bridge method `WebAssembly.instantiate(WasmJsModule)`. |
-| WebAssembly namespace: `validate(bytes)` | Not yet | Not currently exposed. |
-| WebAssembly namespace: streaming APIs | Not yet | No `instantiateStreaming` / `compileStreaming`. |
-| WebAssembly namespace: compile options | Not yet | No `builtins` / `importedStringConstants` options support yet. |
+| WebAssembly namespace: `validate(bytes)` | Implemented | Exposed as synchronous `WebAssembly.validate(byte[])`, using native Wasmtime validation or compile-and-dispose fallback for custom runtimes. |
+| WebAssembly namespace: streaming APIs | Partial | `compileStreaming(IResponse)` and `instantiateStreaming(IResponse)` synchronously consume successful `application/wasm` AngleSharp responses. Response bodies are buffered for metadata extraction. |
+| WebAssembly namespace: compile options | Partial | `WasmCompileOptions` validates and routes `builtins` and `importedStringConstants` through compile, byte instantiate, and streaming entry points. Wasmtime 44 cannot execute these options. |
| `Module.exports(module)` | Partial | Available as instance method `module.exports()` returning descriptor objects. |
| `Module.imports(module)` | Partial | Available as instance method `module.imports()` returning descriptor objects. |
| `Module.customSections(module, name)` | Partial | Available as instance method `module.customSections(name)` returning `byte[][]`. |
-| `Instance.exports` | Partial | Available as `instance.exports`, with function wrappers and descriptor values for non-function exports. |
+| `Instance.exports` | Partial | Available as `instance.exports`, with function, memory, table, global, and tag wrappers plus descriptor values for other export kinds. |
| Exported function invocation | Implemented | Supported via `instance.invoke(...)` and `WasmJsExportedFunction.invoke(...)`. |
| Host import functions | Implemented | Supported via `WithWasmImports(...)` and `WasmImportFunction`. |
-| Memory object API (`Memory`) | Not yet | No full JS API `Memory` object projection yet. |
-| Table object API (`Table`) | Not yet | No full JS API `Table` object projection yet. |
-| Global object API (`Global`) | Not yet | No full JS API `Global` object projection yet. |
-| Tag object API (`Tag`) | Not yet | No full JS API `Tag` object projection yet. |
-| Exception object API (`Exception`) | Not yet | No full JS API exception projection yet. |
-| Error constructors (`CompileError`, `LinkError`, `RuntimeError`) | Not yet | No dedicated namespace error constructor projection yet. |
-| JS String builtins set | Not yet | No compile-option builtin-set wiring yet. |
+| Memory object API (`Memory`) | Partial | Exported memories expose `buffer`, `read(offset, count)`, `write(offset, bytes)`, and `grow(delta)`. `buffer` is a current `byte[]` snapshot rather than a live JavaScript `ArrayBuffer`; memory construction and imports are not yet exposed. |
+| Table object API (`Table`) | Partial | Exported `funcref` and `externref` tables expose `length`, `get(index)`, `set(index, value)`, and `grow(delta, value)`. Table construction and imports are not yet exposed. |
+| Global object API (`Global`) | Partial | Exported numeric and reference globals expose a mutable or immutable `value` accessor and `valueOf()`. Global construction and imports are not yet exposed. |
+| Tag object API (`Tag`) | Partial | Host-created and exported tags expose `type().parameters`, including re-exported imported tags. Wasmtime 44 does not expose native Tag handles or enable exception modules through its .NET API. |
+| Exception object API (`Exception`) | Partial | Host-created exceptions validate typed payloads and expose `is(tag)` and `getArg(tag, index)`. Runtime-thrown Wasm exceptions cannot be projected through Wasmtime 44. |
+| Error constructors (`CompileError`, `LinkError`, `RuntimeError`) | Implemented | Public DOM-named constructors are available, and compile, instantiate/link, and execution failures are translated at bridge boundaries. |
+| JS String builtins set | Partial | The `js-string` builtin set is recognized and forwarded to capable custom runtimes; Wasmtime 44 has no corresponding API. |
## Runtime and Type Support
@@ -41,14 +41,23 @@ Status values:
| --- | --- | --- |
| Runtime backend | Implemented | Uses Wasmtime through the default `WithWasm()` registration. |
| Invocation numeric value types | Implemented | `i32`, `i64`, `f32`, `f64` are supported for import/export invocation paths. |
-| Extended/reference value kinds | Not yet | Rich reference-type projections are not yet exposed through JS API object wrappers. |
+| Extended/reference value kinds | Partial | Table projections support `funcref` and `externref`; broader reference-type invocation marshaling is not yet exposed. |
## Test-Backed Behavior
The current implementation is validated by runtime and bridge tests in the repository, including:
- compile and instantiate flows
+- module validation
+- response-based compilation and instantiation
+- compile option validation and runtime forwarding
- export invocation
+- exported memory read, write, and growth
+- exported table access, mutation, and growth
+- exported global access and mutation
+- exported and re-exported tag type metadata
+- host-created exception payload validation and inspection
+- compile, link, and runtime error translation
- import descriptor extraction
- export descriptor extraction
- custom section lookup behavior
diff --git a/docs/tutorials/01-API.md b/docs/tutorials/01-API.md
index afaeb2d..84ff770 100644
--- a/docs/tutorials/01-API.md
+++ b/docs/tutorials/01-API.md
@@ -38,10 +38,37 @@ When used with a scripting integration that discovers DOM attributes, the follow
Compiles bytes and returns a `WasmJsModule`.
+An overload accepts `WasmCompileOptions`.
+
+### `WebAssembly.validate(byte[] moduleBytes)`
+
+Returns whether the bytes form a valid WebAssembly module without instantiating it.
+
+### `WebAssembly.compileStreaming(IResponse source)`
+
+Synchronously consumes a successful `application/wasm` response and returns a compiled `WasmJsModule`.
+
+An overload accepts `WasmCompileOptions`.
+
+### `WebAssembly.instantiateStreaming(IResponse source)`
+
+Synchronously consumes a successful `application/wasm` response and returns a `WasmJsInstantiationResult` containing `module` and `instance`.
+
+An overload accepts `WasmCompileOptions`.
+
### `WebAssembly.instantiate(WasmJsModule module)`
Instantiates a compiled module and returns a `WasmJsInstance`.
+Byte-array overloads compile and instantiate in one operation and return `WasmJsInstantiationResult`; compile options can be supplied.
+
+## `WasmCompileOptions`
+
+- `builtins`: requested builtin sets; currently the standard `js-string` value is recognized.
+- `importedStringConstants`: module namespace for imported string constants.
+
+Options are forwarded to runtimes implementing `IWasmCompileOptionsCompiler`. Wasmtime 44 does not expose either feature and rejects non-empty options.
+
## `WasmJsModule`
### `exports()`
@@ -80,7 +107,11 @@ Invokes an exported function by name.
Returns an export entry by name:
- `WasmJsExportedFunction` for function exports
-- `WasmJsExportValue` for non-function exports
+- `WasmJsMemory` for memory exports
+- `WasmJsTable` for table exports
+- `WasmJsGlobal` for global exports
+- `WasmJsTag` for tag exports
+- `WasmJsExportValue` for other non-function exports
- `null` if no export is found
### `keys()`
@@ -93,6 +124,30 @@ Returns all export names.
Invokes the wrapped exported function.
+## `WasmJsMemory`
+
+Provides `buffer`, `read(offset, count)`, `write(offset, bytes)`, and `grow(delta)`.
+
+## `WasmJsTable`
+
+Provides `length`, `get(index)`, `set(index, value)`, and `grow(delta, value)`. Function references returned by `get(...)` expose `invoke(...)`.
+
+## `WasmJsGlobal`
+
+Provides a `value` accessor and `valueOf()`. Assigning to an immutable global throws `InvalidOperationException`.
+
+## `WasmJsTag`
+
+Can be constructed with an ordered array of WebAssembly value type names. Provides `type()`, whose descriptor exposes the tag's `parameters`.
+
+## `WasmJsException`
+
+Construct with a `WasmJsTag` and matching payload array. Provides `is(tag)` and `getArg(tag, index)` and can be thrown and caught as a .NET exception. Runtime-thrown Wasm exceptions are not available through the default Wasmtime 44 backend.
+
+## Error Constructors
+
+`WasmJsCompileError`, `WasmJsLinkError`, and `WasmJsRuntimeError` expose the standard `CompileError`, `LinkError`, and `RuntimeError` DOM names. Each can be constructed with an optional message. Bridge compilation, linking, and execution failures are translated automatically.
+
## `WasmJsExportValue`
Descriptor type for non-function exports.
diff --git a/docs/tutorials/03-Questions.md b/docs/tutorials/03-Questions.md
index 58b785a..7e63161 100644
--- a/docs/tutorials/03-Questions.md
+++ b/docs/tutorials/03-Questions.md
@@ -27,9 +27,11 @@ Yes, when used with a scripting integration that discovers DOM-annotated members
The current bridge methods are synchronous from the caller perspective.
-## Are `Memory`, `Table`, `Global`, and `Tag` exposed as full JS API objects?
+## Are `Memory`, `Table`, `Global`, and `Tag` exposed as JS API objects?
-Not yet. Non-function exports currently appear as `WasmJsExportValue` descriptors (`name`, `kind`).
+Exported memories, tables, and globals have usable object projections. Host-created and exported tags expose `type().parameters`, including metadata for re-exported imported tags. Host-created exceptions support `is(tag)` and `getArg(tag, index)`. Memory, table, and global construction and imports are not yet exposed.
+
+Wasmtime 44 does not expose native Tag or Exception handles or enable exception modules through its .NET API, so runtime-thrown Wasm exceptions are not yet projected through the default backend.
## How do I invoke exported functions?
@@ -64,9 +66,8 @@ Current numeric value kinds are supported:
## What are known limitations today?
- No Promise-based `compile` / `instantiate` bridge methods.
-- No `validate(...)` method yet.
-- No streaming APIs.
-- No compile options support (`builtins`, `importedStringConstants`).
-- Non-function exports are descriptors instead of full JS API objects.
+- Streaming APIs buffer response bodies and return synchronously rather than returning promises.
+- Wasmtime 44 cannot execute the `builtins` or `importedStringConstants` compile options; capable custom runtimes can implement them through `IWasmCompileOptionsCompiler`.
+- Runtime-thrown Wasm exception interoperability is not yet available.
These limits are expected at this stage and can be expanded in future versions.
diff --git a/src/AngleSharp.Wasm.Docs/package.json b/src/AngleSharp.Wasm.Docs/package.json
index 088253a..3defbe1 100644
--- a/src/AngleSharp.Wasm.Docs/package.json
+++ b/src/AngleSharp.Wasm.Docs/package.json
@@ -1,6 +1,6 @@
{
"name": "@anglesharp/wasm",
- "version": "1.0.0",
+ "version": "1.1.0",
"preview": true,
"description": "The doclet for the AngleSharp.Wasm documentation.",
"keywords": [
diff --git a/src/AngleSharp.Wasm.Tests/WasmJsBridgeTests.cs b/src/AngleSharp.Wasm.Tests/WasmJsBridgeTests.cs
index 9fd0d33..e82b93e 100644
--- a/src/AngleSharp.Wasm.Tests/WasmJsBridgeTests.cs
+++ b/src/AngleSharp.Wasm.Tests/WasmJsBridgeTests.cs
@@ -1,9 +1,14 @@
namespace AngleSharp.Wasm.Tests;
using AngleSharp.Wasm.Dom;
+using AngleSharp.Io;
using NUnit.Framework;
using System;
+using System.Collections.Generic;
+using System.IO;
using System.Linq;
+using System.Net;
+using System.Reflection;
using System.Threading.Tasks;
[TestFixture]
@@ -54,6 +59,97 @@ public sealed class WasmJsBridgeTests
0x07, 0x07, 0x01, 0x03, 0x6D, 0x65, 0x6D, 0x02, 0x00,
};
+ // (module
+ // (func $answer (export "answer") (result i32) i32.const 42)
+ // (table (export "functions") 1 3 funcref)
+ // (elem (i32.const 0) $answer))
+ private static readonly byte[] TableExportModule =
+ {
+ 0x00, 0x61, 0x73, 0x6D,
+ 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7F,
+ 0x03, 0x02, 0x01, 0x00,
+ 0x04, 0x05, 0x01, 0x70, 0x01, 0x01, 0x03,
+ 0x07, 0x16, 0x02,
+ 0x09, 0x66, 0x75, 0x6E, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x01, 0x00,
+ 0x06, 0x61, 0x6E, 0x73, 0x77, 0x65, 0x72, 0x00, 0x00,
+ 0x09, 0x07, 0x01, 0x00, 0x41, 0x00, 0x0B, 0x01, 0x00,
+ 0x0A, 0x06, 0x01, 0x04, 0x00, 0x41, 0x2A, 0x0B,
+ };
+
+ // (module (table (export "objects") 1 3 externref))
+ private static readonly byte[] ExternalReferenceTableExportModule =
+ {
+ 0x00, 0x61, 0x73, 0x6D,
+ 0x01, 0x00, 0x00, 0x00,
+ 0x04, 0x05, 0x01, 0x6F, 0x01, 0x01, 0x03,
+ 0x07, 0x0B, 0x01, 0x07, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x73, 0x01, 0x00,
+ };
+
+ // (module
+ // (global $counter (export "counter") (mut i32) (i32.const 42))
+ // (global (export "constant") f64 (f64.const 1.5))
+ // (func (export "read_counter") (result i32) global.get $counter))
+ private static readonly byte[] GlobalExportModule =
+ {
+ 0x00, 0x61, 0x73, 0x6D,
+ 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7F,
+ 0x03, 0x02, 0x01, 0x00,
+ 0x06, 0x12, 0x02,
+ 0x7F, 0x01, 0x41, 0x2A, 0x0B,
+ 0x7C, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x0B,
+ 0x07, 0x25, 0x03,
+ 0x07, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x65, 0x72, 0x03, 0x00,
+ 0x08, 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x74, 0x03, 0x01,
+ 0x0C, 0x72, 0x65, 0x61, 0x64, 0x5F, 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x65, 0x72, 0x00, 0x00,
+ 0x0A, 0x06, 0x01, 0x04, 0x00, 0x23, 0x00, 0x0B,
+ };
+
+ // (module (global (export "object") (mut externref) (ref.null extern)))
+ private static readonly byte[] ExternalReferenceGlobalExportModule =
+ {
+ 0x00, 0x61, 0x73, 0x6D,
+ 0x01, 0x00, 0x00, 0x00,
+ 0x06, 0x06, 0x01, 0x6F, 0x01, 0xD0, 0x6F, 0x0B,
+ 0x07, 0x0A, 0x01, 0x06, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x03, 0x00,
+ };
+
+ // (module (tag (export "error") (export "alias") (param i32 f64)))
+ private static readonly byte[] TagExportModule =
+ {
+ 0x00, 0x61, 0x73, 0x6D,
+ 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x06, 0x01, 0x60, 0x02, 0x7F, 0x7C, 0x00,
+ 0x0D, 0x03, 0x01, 0x00, 0x00,
+ 0x07, 0x11, 0x02,
+ 0x05, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x04, 0x00,
+ 0x05, 0x61, 0x6C, 0x69, 0x61, 0x73, 0x04, 0x00,
+ };
+
+ // (module
+ // (import "host" "tag" (tag (param i64)))
+ // (export "tag" (tag 0)))
+ private static readonly byte[] ImportedTagExportModule =
+ {
+ 0x00, 0x61, 0x73, 0x6D,
+ 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x05, 0x01, 0x60, 0x01, 0x7E, 0x00,
+ 0x02, 0x0D, 0x01, 0x04, 0x68, 0x6F, 0x73, 0x74, 0x03, 0x74, 0x61, 0x67, 0x04, 0x00, 0x00,
+ 0x07, 0x07, 0x01, 0x03, 0x74, 0x61, 0x67, 0x04, 0x00,
+ };
+
+ // (module (func (export "fail") unreachable))
+ private static readonly byte[] TrapModule =
+ {
+ 0x00, 0x61, 0x73, 0x6D,
+ 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
+ 0x03, 0x02, 0x01, 0x00,
+ 0x07, 0x08, 0x01, 0x04, 0x66, 0x61, 0x69, 0x6C, 0x00, 0x00,
+ 0x0A, 0x05, 0x01, 0x03, 0x00, 0x00, 0x0B,
+ };
+
// AnswerModule + two custom sections named "meta"
private static readonly byte[] AnswerModuleWithCustomSections =
{
@@ -93,6 +189,163 @@ public async Task BridgeCanCompileInstantiateAndInvokeViaInstanceExportsFunction
Assert.That(exportKeys, Is.EquivalentTo(new[] { "answer" }));
}
+ [Test]
+ public async Task BridgeTranslatesCompileLinkAndRuntimeFailures()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+
+ var compileError = Assert.Throws(() =>
+ WebAssembly.Compile(context.Current!, new byte[] { 0, 1, 2, 3 }));
+
+ Assert.That(compileError!.Name, Is.EqualTo("CompileError"));
+ Assert.That(compileError.InnerException, Is.Not.Null);
+
+ var importModule = WebAssembly.Compile(context.Current!, HostImportModule);
+ var linkError = Assert.Throws(() =>
+ WebAssembly.Instantiate(context.Current!, importModule));
+
+ Assert.That(linkError!.Name, Is.EqualTo("LinkError"));
+ Assert.That(linkError.InnerException, Is.Not.Null);
+
+ var trapModule = WebAssembly.Compile(context.Current!, TrapModule);
+ var trapInstance = WebAssembly.Instantiate(context.Current!, trapModule);
+ var runtimeError = Assert.Throws(() => trapInstance.Invoke("fail"));
+ var fail = (WasmJsExportedFunction)trapInstance.Exports["fail"]!;
+
+ Assert.That(runtimeError!.Name, Is.EqualTo("RuntimeError"));
+ Assert.That(runtimeError.InnerException, Is.Not.Null);
+ Assert.That(() => fail.Invoke(), Throws.TypeOf());
+ }
+
+ [Test]
+ public void ErrorConstructorsPreserveMessages()
+ {
+ Assert.That(new WasmJsCompileError().Message, Is.Empty);
+ Assert.That(new WasmJsLinkError().Message, Is.Empty);
+ Assert.That(new WasmJsRuntimeError().Message, Is.Empty);
+ Assert.That(new WasmJsCompileError("compile").Message, Is.EqualTo("compile"));
+ Assert.That(new WasmJsLinkError("link").Message, Is.EqualTo("link"));
+ Assert.That(new WasmJsRuntimeError("runtime").Message, Is.EqualTo("runtime"));
+ }
+
+ [Test]
+ public async Task BridgeValidateReturnsModuleValidity()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+ var invalidModule = (byte[])AnswerModule.Clone();
+ invalidModule[^1] = 0xFF;
+
+ Assert.That(WebAssembly.Validate(context.Current!, AnswerModule), Is.True);
+ Assert.That(WebAssembly.Validate(context.Current!, invalidModule), Is.False);
+ Assert.That(WebAssembly.Validate(context.Current!, Array.Empty()), Is.False);
+ Assert.That(() => WebAssembly.Validate(context.Current!, null!), Throws.ArgumentNullException);
+
+ var module = WebAssembly.Compile(context.Current!, AnswerModule);
+
+ Assert.That(module, Is.Not.Null);
+ }
+
+ [Test]
+ public async Task BridgeValidateFallsBackToCompileForCustomRuntime()
+ {
+ var runtime = new ValidationFallbackRuntime();
+ var config = Configuration.Default.WithWasm(_ => new ValidationFallbackRuntimeFactory(runtime));
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+
+ Assert.That(WebAssembly.Validate(context.Current!, new byte[] { 1 }), Is.True);
+ Assert.That(runtime.LastModuleDisposed, Is.True);
+ Assert.That(WebAssembly.Validate(context.Current!, Array.Empty()), Is.False);
+ }
+
+ [Test]
+ public async Task BridgeCanCompileAndInstantiateStreamingResponse()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+ using var compileResponse = CreateWasmResponse(AnswerModule, contentType: "Application/Wasm; charset=binary");
+
+ var module = WebAssembly.CompileStreaming(context.Current!, compileResponse);
+
+ Assert.That(module.Exports().Single().Name, Is.EqualTo("answer"));
+
+ using var instantiateResponse = CreateWasmResponse(AnswerModule);
+ var result = WebAssembly.InstantiateStreaming(context.Current!, instantiateResponse);
+ var function = result.Instance.Exports["answer"] as WasmJsExportedFunction;
+
+ Assert.That(result.Module.Exports().Single().Name, Is.EqualTo("answer"));
+ Assert.That(function, Is.Not.Null);
+ Assert.That(function!.Invoke(), Is.EqualTo(42));
+ }
+
+ [Test]
+ public async Task StreamingMethodsValidateResponseStatusAndContentType()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+ using var badStatus = CreateWasmResponse(AnswerModule, HttpStatusCode.NotFound);
+ using var badContentType = CreateWasmResponse(AnswerModule, contentType: "application/octet-stream");
+
+ Assert.That(() => WebAssembly.CompileStreaming(context.Current!, null!), Throws.ArgumentNullException);
+ Assert.That(() => WebAssembly.CompileStreaming(context.Current!, badStatus), Throws.InvalidOperationException);
+ Assert.That(() => WebAssembly.CompileStreaming(context.Current!, badContentType), Throws.InvalidOperationException);
+ }
+
+ [Test]
+ public async Task CompileOptionsAreValidatedAcrossCompileEntryPoints()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+ var emptyOptions = new WasmCompileOptions();
+ var builtinOptions = new WasmCompileOptions { Builtins = new[] { "js-string" } };
+ var stringOptions = new WasmCompileOptions { ImportedStringConstants = "strings" };
+ var invalidOptions = new WasmCompileOptions { Builtins = new[] { "invalid" } };
+
+ Assert.That(WebAssembly.Compile(context.Current!, AnswerModule, emptyOptions), Is.Not.Null);
+ Assert.That(() => WebAssembly.Compile(context.Current!, AnswerModule, builtinOptions), Throws.TypeOf());
+ Assert.That(() => WebAssembly.Compile(context.Current!, AnswerModule, stringOptions), Throws.TypeOf());
+ Assert.That(() => WebAssembly.Compile(context.Current!, AnswerModule, invalidOptions), Throws.ArgumentException);
+
+ using var response = CreateWasmResponse(AnswerModule);
+ Assert.That(() => WebAssembly.CompileStreaming(context.Current!, response, builtinOptions), Throws.TypeOf());
+ }
+
+ [Test]
+ public async Task CompileOptionsAreForwardedToCapableRuntime()
+ {
+ using var runtime = new CompileOptionsRuntime();
+ var config = Configuration.Default.WithWasm(_ => new ValidationFallbackRuntimeFactory(runtime));
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+ var builtins = new[] { "js-string" };
+ var options = new WasmCompileOptions
+ {
+ Builtins = builtins,
+ ImportedStringConstants = "strings",
+ };
+
+ var result = WebAssembly.Instantiate(context.Current!, AnswerModule, options);
+ builtins[0] = "changed";
+
+ Assert.That(runtime.LastOptions, Is.Not.Null);
+ Assert.That(runtime.LastOptions!.Builtins, Is.EqualTo(new[] { "js-string" }));
+ Assert.That(runtime.LastOptions.ImportedStringConstants, Is.EqualTo("strings"));
+ Assert.That(result.Instance.Invoke("answer"), Is.EqualTo(42));
+
+ builtins[0] = "js-string";
+ using var response = CreateWasmResponse(AnswerModule);
+ var streamingResult = WebAssembly.InstantiateStreaming(context.Current!, response, options);
+
+ Assert.That(streamingResult.Instance.Invoke("answer"), Is.EqualTo(42));
+ }
+
[Test]
public async Task ModuleImportsExposeSpecDescriptorFields()
{
@@ -127,7 +380,7 @@ public async Task ModuleCustomSectionsFilterByNameAndReturnAllMatches()
}
[Test]
- public async Task InstanceExportsExposeNonFunctionKindsAsValueDescriptors()
+ public async Task InstanceExportsExposeMemoryAsLiveMemoryObject()
{
var config = Configuration.Default.WithWasm();
using var context = BrowsingContext.New(config);
@@ -137,17 +390,212 @@ public async Task InstanceExportsExposeNonFunctionKindsAsValueDescriptors()
var moduleExports = module.Exports();
var instance = WebAssembly.Instantiate(context.Current!, module);
- var memExport = instance.Exports["mem"] as WasmJsExportValue;
+ var memExport = instance.Exports["mem"] as WasmJsMemory;
Assert.That(moduleExports, Has.Length.EqualTo(1));
Assert.That(moduleExports[0].Name, Is.EqualTo("mem"));
Assert.That(moduleExports[0].Kind, Is.EqualTo("memory"));
Assert.That(memExport, Is.Not.Null);
- Assert.That(memExport!.Name, Is.EqualTo("mem"));
- Assert.That(memExport.Kind, Is.EqualTo("memory"));
+ Assert.That(memExport!.Buffer, Has.Length.EqualTo(65536));
+
+ memExport.Write(42, new byte[] { 1, 2, 3 });
+
+ Assert.That(memExport.Read(42, 3), Is.EqualTo(new byte[] { 1, 2, 3 }));
+ Assert.That(memExport.Buffer[43], Is.EqualTo(2));
+ Assert.That(memExport.Grow(1), Is.EqualTo(1));
+ Assert.That(memExport.Buffer, Has.Length.EqualTo(2 * 65536));
Assert.That(instance.Exports.Keys().Single(), Is.EqualTo("mem"));
}
+ [Test]
+ public async Task MemoryProjectionValidatesArgumentsAndBounds()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+
+ var module = WebAssembly.Compile(context.Current!, MemoryExportModule);
+ var instance = WebAssembly.Instantiate(context.Current!, module);
+ var memory = instance.Exports["mem"] as WasmJsMemory;
+
+ Assert.That(memory, Is.Not.Null);
+ Assert.That(() => memory!.Read(-1, 1), Throws.TypeOf());
+ Assert.That(() => memory!.Read(0, -1), Throws.TypeOf());
+ Assert.That(() => memory!.Read(65536, 1), Throws.TypeOf());
+ Assert.That(() => memory!.Write(-1, Array.Empty()), Throws.TypeOf());
+ Assert.That(() => memory!.Write(0, null!), Throws.ArgumentNullException);
+ Assert.That(() => memory!.Write(65536, new byte[] { 1 }), Throws.TypeOf());
+ Assert.That(() => memory!.Grow(-1), Throws.TypeOf());
+ }
+
+ [Test]
+ public async Task InstanceExportsExposeTableAsLiveTableObject()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+
+ var module = WebAssembly.Compile(context.Current!, TableExportModule);
+ var instance = WebAssembly.Instantiate(context.Current!, module);
+ var table = instance.Exports["functions"] as WasmJsTable;
+ var exportedFunction = instance.Exports["answer"] as WasmJsExportedFunction;
+
+ Assert.That(table, Is.Not.Null);
+ Assert.That(table!.Length, Is.EqualTo(1));
+
+ var function = table.Get(0) as WasmJsExportedFunction;
+
+ Assert.That(function, Is.Not.Null);
+ Assert.That(function!.Invoke(), Is.EqualTo(42));
+
+ table.Set(0, null);
+
+ Assert.That(table.Get(0), Is.Null);
+
+ table.Set(0, exportedFunction);
+
+ Assert.That(((WasmJsExportedFunction)table.Get(0)!).Invoke(), Is.EqualTo(42));
+ Assert.That(table.Grow(1, function), Is.EqualTo(1));
+ Assert.That(table.Length, Is.EqualTo(2));
+ Assert.That(((WasmJsExportedFunction)table.Get(1)!).Invoke(), Is.EqualTo(42));
+ Assert.That(() => table.Get(2), Throws.TypeOf());
+ Assert.That(() => table.Set(2, null), Throws.TypeOf());
+ }
+
+ [Test]
+ public async Task TableProjectionSupportsExternalReferences()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+
+ var module = WebAssembly.Compile(context.Current!, ExternalReferenceTableExportModule);
+ var instance = WebAssembly.Instantiate(context.Current!, module);
+ var table = instance.Exports["objects"] as WasmJsTable;
+ var value = new object();
+
+ Assert.That(table, Is.Not.Null);
+ Assert.That(table!.Get(0), Is.Null);
+
+ table.Set(0, value);
+
+ Assert.That(table.Get(0), Is.SameAs(value));
+ Assert.That(table.Grow(1, value), Is.EqualTo(1));
+ Assert.That(table.Get(1), Is.SameAs(value));
+ }
+
+ [Test]
+ public async Task InstanceExportsExposeGlobalAsLiveGlobalObject()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+
+ var module = WebAssembly.Compile(context.Current!, GlobalExportModule);
+ var instance = WebAssembly.Instantiate(context.Current!, module);
+ var counter = instance.Exports["counter"] as WasmJsGlobal;
+ var constant = instance.Exports["constant"] as WasmJsGlobal;
+
+ Assert.That(counter, Is.Not.Null);
+ Assert.That(counter!.Value, Is.EqualTo(42));
+ Assert.That(counter.ValueOf(), Is.EqualTo(42));
+ Assert.That(constant, Is.Not.Null);
+ Assert.That(constant!.Value, Is.EqualTo(1.5));
+
+ counter.Value = 7.0;
+
+ Assert.That(counter.Value, Is.EqualTo(7));
+ Assert.That(instance.Invoke("read_counter"), Is.EqualTo(7));
+ Assert.That(() => constant.Value = 2.0, Throws.InvalidOperationException);
+ Assert.That(constant.Value, Is.EqualTo(1.5));
+ }
+
+ [Test]
+ public async Task GlobalProjectionSupportsExternalReferences()
+ {
+ var config = Configuration.Default.WithWasm();
+ using var context = BrowsingContext.New(config);
+ using var document = await context.OpenNewAsync();
+
+ var module = WebAssembly.Compile(context.Current!, ExternalReferenceGlobalExportModule);
+ var instance = WebAssembly.Instantiate(context.Current!, module);
+ var global = instance.Exports["object"] as WasmJsGlobal;
+ var value = new object();
+
+ Assert.That(global, Is.Not.Null);
+ Assert.That(global!.Value, Is.Null);
+
+ global.Value = value;
+
+ Assert.That(global.Value, Is.SameAs(value));
+
+ global.Value = null;
+
+ Assert.That(global.Value, Is.Null);
+ }
+
+ [Test]
+ public void InstanceExportsExposeTagTypeMetadata()
+ {
+ using var instance = CreateMetadataOnlyInstance(TagExportModule);
+ var tag = instance.Exports["error"] as WasmJsTag;
+ var alias = instance.Exports["alias"] as WasmJsTag;
+
+ Assert.That(tag, Is.Not.Null);
+ Assert.That(tag!.Type().Parameters, Is.EqualTo(new[] { "i32", "f64" }));
+ Assert.That(alias, Is.SameAs(tag));
+
+ var parameters = tag.Type().Parameters;
+ parameters[0] = "changed";
+
+ Assert.That(tag.Type().Parameters, Is.EqualTo(new[] { "i32", "f64" }));
+ }
+
+ [Test]
+ public void ReExportedImportedTagRetainsTypeMetadata()
+ {
+ using var instance = CreateMetadataOnlyInstance(ImportedTagExportModule);
+ var tag = instance.Exports["tag"] as WasmJsTag;
+
+ Assert.That(tag, Is.Not.Null);
+ Assert.That(tag!.Type().Parameters, Is.EqualTo(new[] { "i64" }));
+ }
+
+ [Test]
+ public void ExceptionProjectionValidatesAndExposesTypedPayload()
+ {
+ var tag = new WasmJsTag(new[] { "i32", "f64" });
+ var otherTag = new WasmJsTag(new[] { "i32", "f64" });
+ var payload = new object?[] { 42.0, 1.5 };
+ var exception = new WasmJsException(tag, payload);
+
+ payload[0] = 0;
+
+ Assert.That(exception.Message, Is.EqualTo("wasm exception"));
+ Assert.That(exception.Is(tag), Is.True);
+ Assert.That(exception.Is(otherTag), Is.False);
+ Assert.That(exception.GetArg(tag, 0), Is.EqualTo(42));
+ Assert.That(exception.GetArg(tag, 1), Is.EqualTo(1.5));
+ Assert.That(() => exception.GetArg(otherTag, 0), Throws.ArgumentException);
+ Assert.That(() => exception.GetArg(tag, -1), Throws.TypeOf());
+ Assert.That(() => exception.GetArg(tag, 2), Throws.TypeOf());
+ Assert.That(() => new WasmJsException(tag, new object?[] { 42 }), Throws.ArgumentException);
+ Assert.That(() => new WasmJsException(tag, new object?[] { "invalid", 1.5 }), Throws.ArgumentException);
+ Assert.That(() => new WasmJsTag(new[] { "invalid" }), Throws.ArgumentException);
+ }
+
+ [Test]
+ public void ExceptionProjectionCanBeThrownAndCaught()
+ {
+ var tag = new WasmJsTag(new[] { "i64" });
+ var exception = new WasmJsException(tag, new object?[] { 42L });
+
+ var caught = Assert.Throws(() => throw exception);
+
+ Assert.That(caught, Is.SameAs(exception));
+ Assert.That(caught!.GetArg(tag, 0), Is.EqualTo(42L));
+ }
+
[Test]
public async Task ExportedFunctionWrapperSupportsArguments()
{
@@ -180,7 +628,7 @@ public async Task BridgeInstantiateThrowsForNullModule()
using var context = BrowsingContext.New(config);
using var document = await context.OpenNewAsync();
- Assert.That(() => WebAssembly.Instantiate(context.Current!, null!), Throws.ArgumentNullException);
+ Assert.That(() => WebAssembly.Instantiate(context.Current!, (WasmJsModule)null!), Throws.ArgumentNullException);
}
[Test]
@@ -246,4 +694,139 @@ public async Task BridgeCompileWithoutWasmRegistrationThrows()
Assert.That(() => WebAssembly.Compile(context.Current!, AnswerModule), Throws.InvalidOperationException);
}
+
+ private static MetadataOnlyInstanceScope CreateMetadataOnlyInstance(byte[] moduleBytes)
+ {
+ var metadataType = typeof(WasmJsModule).Assembly.GetType("AngleSharp.Wasm.Dom.WasmModuleMetadata")!;
+ var parse = metadataType.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static)!;
+ var metadata = parse.Invoke(null, new object[] { moduleBytes })!;
+ var exports = (IReadOnlyList)metadataType
+ .GetProperty("Exports", BindingFlags.Public | BindingFlags.Instance)!
+ .GetValue(metadata)!;
+ var runtimeInstance = new MetadataOnlyInstance();
+ var instance = (WasmJsInstance)Activator.CreateInstance(
+ typeof(WasmJsInstance),
+ BindingFlags.NonPublic | BindingFlags.Instance,
+ null,
+ new object[] { runtimeInstance, exports },
+ null)!;
+ return new MetadataOnlyInstanceScope(runtimeInstance, instance);
+ }
+
+ private static IResponse CreateWasmResponse(
+ byte[] moduleBytes,
+ HttpStatusCode statusCode = HttpStatusCode.OK,
+ string contentType = "application/wasm") =>
+ VirtualResponse.Create(response => response
+ .Status(statusCode)
+ .Header("Content-Type", contentType)
+ .Content(new MemoryStream(moduleBytes, writable: false), shouldDispose: true));
+
+ private sealed class MetadataOnlyInstanceScope : IDisposable
+ {
+ private readonly MetadataOnlyInstance _runtimeInstance;
+
+ public MetadataOnlyInstanceScope(MetadataOnlyInstance runtimeInstance, WasmJsInstance instance)
+ {
+ _runtimeInstance = runtimeInstance;
+ Exports = instance.Exports;
+ }
+
+ public WasmJsExports Exports { get; }
+
+ public void Dispose() => _runtimeInstance.Dispose();
+ }
+
+ private sealed class MetadataOnlyInstance : IWasmInstance
+ {
+ public ValueTask