Skip to content

Single-module feature testing #1280

Open
@binji

Description

@binji

I was chatting with @sbc100, and we came up with an idea that seems like it might work, providing a way to do feature testing in a single module. The current suggestion is to call WebAssembly.validate() on a tiny feature-test module, then use this information to fetch a specific module. We could codify this in a new feature-test section, that must come before all other sections:

Section Name Code
FeatureTest X
Type 1
Function 2
... ...

This section would itself contain the modules that should be validated:

FeatureTest

Field Type Description
count varuint32 count of modules to follow
modules module* sequence of modules

a module is:

Field Type Description
size varuint32 size of data (in bytes)
data bytes sequence of size bytes

This creates an index space of valid and invalid modules.

As an alternative, the FeatureTest section could just contain a list of names. Each one names a feature. So for example, simd might mean that the v128 type is available, as well as all of the simd instructions.

FeatureTest (alternative)

Field Type Description
count varuint32 count of names to follow
names name* sequence of names

Then, we allow each section to be specified multiple times, as long as they are still in the correct order. The behavior is as-if the sequence in the section were concatenated. The Start section would behave the same (perhaps we would allow multiple start functions, though that's not required). The DataCount section would sum all of the counts.

We need some way to mark the section as conditional; one way is to use a new section code. Like other sections, it is required to come in a specified order. Unlike other sections, it inherits the order of the section it is emulating. The format could be:

Conditional

Field Type Description
id varuint32 Section id to emulate
condition condition Condition that must be met to include this section
contents ... The regular contents of a section with id id

I'm not quite sure how we'd want to encode a condition, but the idea is that each module that was previously validated in the FeatureTest section gets a bit, and we can perform a logical operation on the bits to determine whether this section is included. One cute way to do it would be by evaluating an expression, perhaps where each local is given the value 1 for a valid module (or feature name that engine supports) and 0 for invalid (or feature name that engine doesn't support), e.g.

;; include this section if either module 0 or module 1 are valid
(i32.or (local.get 0) (local.get 1))

;; include this section if module 0 is not valid
(i32.eqz (local.get 0))

When an engine is validating a module, it will skip any Conditional section where the condition is not met. This does mean that the index space may be different, depending on which features are enabled. It's up to the tool generating the module to ensure that this makes sense.

Example

For SIMD, we expect that there will be many functions that are shared, and only a few that need to have scalar fallback. At first, a tool can generate both sections and interleave them in the binary:

Section Index Section Condition Count
0 FeatureTest N/A
1 Type !SIMD 5
2 Type SIMD 6
3 Function !SIMD 20
4 Function SIMD 30

Note that it is not necessary for the number of entries in each section to be the same.

Later, the tool could optimize this and produce a common section where possible:

Section Index Section Condition Count
0 FeatureTest * N/A
1 Type * 5
2 Type SIMD 1
3 Function * 17
4 Function !SIMD 3
5 Function SIMD 13

In this example, types 0 through 4 can be shared, and only type 5 needs SIMD. Similarly, functions 0 through 16 are shared. Functions 17 through 19 have different implementations for SIMD and !SIMD. Logically, these should have the same behavior, but there's no way to enforce this, of course. Functions 20 through 29 are only valid when SIMD is enabled.

Globals

Using globals, we can even export the results of the feature test:

Section Index Section Condition Count
0 FeatureTest * N/A
1 Global SIMD 1
2 Global !SIMD 1

Where we set the value of global 0 to 1 if SIMD is true.

Imports and Exports

We could potentially have different imports and exports. For example, we could export an additional function when SIMD is enabled.

Similarly, if we allow the feature-tested modules to reference imports, we could provide a (admittedly clunky) weak-import mechanism. If an import is not provided, we can generate a stub function instead. This would be tricky to get right, since the imports have to come before all defined functions, but it may be possible.

You could use this for an optional memory import too. If the memory import is provided, then use it. Otherwise generate your own memory section. Not sure why you'd want to do this, however. :-)

Metadata

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