WebAssembly / WebAssembly/component-model

WIT: Import/Export inconsistencies

Open
#629 13 comments 0 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

This is from the point of view of using component composition for system design where dynamic linking / composing components is key.

Issue: Asymmetrical import/export resolution

Transitive import/export dependencies are resolved inconsistently for imports and exports:

  • Imports dependencies are resolved as additional imports.
  • Exports dependencies are resolved as additional imports.

This causes worlds with symmetrical definitions to be resolved asymmetrical.
While both worlds look like they would compose perfectly together, once resolved they don't and instead leave a gap behind.

Example:

/// Shared types
interface types {
    type start = string;
    type end = string;

    record path {
        start: start,
        end: end
    }

    resource path-graph {
        constructor();
        push: func(path: path);
        get-ends: func(start: start) -> list<end>;
        get-starts: func(end: end) -> list<start>;
    }
}

/// Validate provided graphs for correctness
interface validation {
    use types.{start, end, path-graph};

    validate: func(graph: path-graph) -> bool;
    validate-path: func(start: start, end: end) -> bool;
}

/// Component that want to use validation
world service {
    import validation;
}

/// Component that provides validation
world validator {
    export validation;
}

This would resolve into these worlds on the components:

/// Component that want to use validation
world service {
    import types;
    import validation;
}

/// Component that provides validation
world validator {
    import types;
    export validation;
}

While the validation interface can be composed, the types interface cannot be composed because it is imported by both components.
This would require an explicit export or a third component or the host to provide these types.

The obvious solution would be to update the resolution to be symmetrical:

  • Imports dependencies are resolved as additional imports.
  • Exports dependencies are resolved as additional exports.

But this isn't a valuable solution either as it forces the exporter to implement the types even tho it has no use to do so.

Solution: Fix inconsistent differentiation between values and signatures (interfaces)

For the sake of less confusion I will call the classic interfaces/traits "signatures"

Wit types can be split into 2 categories.
These categories have different uses cases for wit itself.

Please correct me if I'm wrong

Category type Compilation Runtime
Values string, u32, record, variant, ...
Signatures resource, function

For the runtime, Values don't really matter as the actual implementation is added automatically and doesn't need to be imported or exported.
For the runtime, Signatures matter as the actual implementation is not added automatically and needs to be either imported or implemented to be exported.

So, looking back at the example:

/// Shared types
interface types {
    type start = string;      // <--- import/export doesn't matter during runtime
    type end = string;        // <--- import/export doesn't matter during runtime

    record path {             // <--- import/export doesn't matter during runtime
        start: start,
        end: end
    }

    resource path-graph {     // <--- ❗ import/export does matter during runtime
        constructor();
        push: func(path: path);
        get-ends: func(start: start) -> list<end>;
        get-starts: func(end: end) -> list<start>;
    }
}

This insight alone doesn't change anything as we also need to rethink the import/export behaviour.

  • Values don't need import / export schemantic at all.
    The same definitions can be used for both imports and exports.
    It's same for generated code via bindgen (kind of relates to #308).
  • Signatures require import / export schemantic.
    Whenever a signature is taken into scope it needs to be defined if it's an export or import.

This requires changes to the way entries are referenced via the use directive.

  • use to reference a value
  • use import to import a signature reference
  • use export to export a signature reference
    That's the relation to #308

Looking at the example again:

/// Validate provided graphs for correctness
interface validation {
    use types.{start, end};             
    
    // use types.{path-graph};
    use import types.{path-graph};      // <--- The validator only wants the signature to validate the graph and doesn't provide the implementation.

    validate: func(graph: path-graph) -> bool;
    validate-path: func(start: start, end: end) -> bool;
}

/// Component that want to use validation
world service {                         // <--- Has to export the path-graph
    import validation;
}

/// Component that provides validation
world validator {                       // <--- Will import the path-graph
    export validation;
}

This would resolve into these worlds on the components:

/// Component that want to use validation
world service {
    use types.{start, end, path};
    use export types.{path-graph};
    import validation;
}

/// Component that provides validation
world validator {
    use types.{start, end, path};
    use import types.{path-graph};
    export validation;
}

Summary

With these changes interfaces are more expressive in what they require / provide and are resolved symmetrical.

I just want to share my point of view from using wit and it's interfaces as contracts in general where a symmetrical definition is important.
I'm open for any discussion :)

Benefits

  • Symmetrical worlds
  • Symmetrical import/export resolution
  • More expressive interfaces
  • Easier component composition
  • No export/import type mismatches in bindgen for value types
  • #308: Same behaviour for use in worlds and interfaces

Changes to WIT

  • Add export keyword to use directive (world and interface)
  • Add import keyword to use directive (world and interface)
  • Require export / import on use for resources and functions

Changes to bindgen (optional)

  • Remove different export / import handling for wit types of the Values category (string, u32, record, variant...)

Handling breaking changes

Make use syntactical suguar for use import on resources and functions.

use types;

translates to

use types.{start, end, path};
use import types.{path-graph};       // <--- Same behaviour
World resolution

Export Side

// Before
import types;

// After
use types.{start, end, path};
use import types.{path-graph};       // <--- Same behaviour

Import Side

// Before
import types;

// After
use types.{start, end, path};
use export types.{path-graph};       // <--- Breaking change

Could be feature gated via adding it by default as import aswell and defing the export to reference the import

use import types.{path-graph};
use export types.{path-graph} with { types = import types };

Resulting in

// Before
import types;

// After
use types.{start, end, path};
use import types.{path-graph};       // <--- Same behaviour
use export types.{path-graph} with { types = import types };

Example uses in WASI

wasi:http@0.3
interface types {
    resource request {}
    resource response {}
    variant error-code {}
}

interface handler {
  use types.{error-code};
  use import types.{request, response};    // <--- As a request handler i only need the signature of request and response

  handle: async func(request: request) -> result<response, error-code>;
}
wasi:keyvalue@0.2
interface types {
    resource bucket {}
    variant error {}
}

interface store {
    use types.{error};
    use export types.{bucket};             // <--- As a store i need to provide the implementation of the bucket signature

    open: func(identifier: string) -> result<bucket, error>;
}

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

No file, test, or entry point is named in the issue. Start by locating the WIT world and interface resolution rules and existing tests for transitive import/export handling, then compare them with the supplied service and validator examples. Done should mean an agreed resolution model, specified syntax and compatibility behavior, and corresponding test coverage.

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
Quiet
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.