WebAssembly / WebAssembly/component-model

Single-module components

Open
#435 3 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
WebAssembly
Stars
1.4k
Forks
130
Avg merge
2d 1h
Merged PRs (30d)
15

Description

Today, most components in practice contain 3 core modules (not even counting the preview1-to-preview2 adapter).

The reasons this happens it that we have an import cycle. Every executable Wasm module compiled from a linear-memory language exports a linear memory, and typically imports functions which access that memory, like this:

(core module
    ...
    (type (;4;) (func (param i32 i32)))
    ...
    (import "wasi:http/types@0.2.2" "[method]outgoing-body.write" (func $_ZN4wasi8bindings4wasi4http5types12OutgoingBody5write10wit_import17hc3612d9fffb4f5c7E (;10;) (type 4)))
    ...
    (export "memory" (memory 0))
    ...
)

One of the i32 parameters to write is a pointer into the exported memory. This forms an implicit import cycle; the Wasm module is importing a function, the functions needs to be able to access a memory that it imports from the Wasm module.

The component model disallows import cycles, however the component-model tooling knows how to automatically break cycles.

To do this, it first adds a module which defines a function table, but does not initialze it. This module has function exports to satisfy the original module's function imports, which are wrappers around call_indirect on an exported table:

(core module
    ...
    (table (;0;) 20 20 funcref)
    ...
    (export "$imports" (table 0))
    ...
    (export "2" (func $"indirect-wasi:http/types@0.2.2-[method]outgoing-body.write"))
    ...
    (func $"indirect-wasi:http/types@0.2.2-[method]outgoing-body.write" (;2;) (type 1) (param i32 i32)
      local.get 0
      local.get 1
      i32.const 2
      call_indirect (type 1)
    )
    ...
)

Because the function table isn't initialized here, this module doesn't import anything, so it can be instantiated first.

The other module imports that table, and imports the actual functions, and initializes the table with them:

(core module
    ...
    (type (;1;) (func (param i32 i32)))
    ...
    (import "" "2" (func (;2;) (type 1)))
    ...
    (import "" "$imports" (table (;0;) 20 20 funcref))
    (elem (;0;) (i32.const 0) func 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19)
    ...
)

This last module is instantiated last, and this table initialization is all it does. In all, this breaks the cycle.

It's cool that the tooling knows how to do this automatically, but it's unfortunate that this is necessary in this common situation. The call_indirects add overhead, the extra modules make instantiation more complex, and all this extra code makes it difficult for people looking at components to understand how they work.

One approach to fixing this that's been discussed in various places is to have special library modules that just export a linear memory and perhaps also malloc and similar functions. This may still be desirable to do for dynamic linking use cases, however that would still require two modules per component, so it's still desirable to way to avoid needing this for simple cases.

We can do this by adding support to the canonical ABI for modules that import their linear memories rather than export them. In wasm-ld, the --import-memory flag creates a module that imports its memory. If we use that, and add canonical-abi support for this mode, this should allow us to avoid the import cycles and the extra modules needed to break them.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the canonical ABI handling for module linear memories and the wasm-ld --import-memory behavior described in the issue. Trace how current tooling creates cycle-breaking modules. Done means supporting imported linear memories without the extra cycle-breaking modules while preserving component instantiation behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
wasm
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.