Skip to content

Single-module components #435

Open
Open
@sunfishcode

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/[email protected]" "[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/[email protected][method]outgoing-body.write"))
    ...
    (func $"indirect-wasi:http/[email protected][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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions