wasm_table (wasm v0.1.0)

View Source

Tables, with reference semantics.

Read this when you are linking modules that share a table. A table is mutable state two instances can share: module A exports a table, module B imports it and writes an element segment into it, and a call_indirect in A has to see B's entry. That is how dynamic linking works, and an array held inside each instance cannot do it, because array is immutable and every instance ends up initialising its own copy.

Memories already share correctly, because atomics is a reference. Tables now do too, by the same route: the handle is a reference, the contents live in one place, and every holder sees the same thing.

How reads stay fast

The contents are an array in a shared ETS row, so a naive read would copy the whole table on every call_indirect. Instead each process caches the array it last saw together with a version counter held in atomics. A hit is an atomics:get plus a process dictionary lookup, neither of which copies; any writer bumps the counter and every other process reloads on its next read.

That is the same mechanism wasm_instance:mut/1 uses, and it was measured there at 398 ns down to 19 ns.

Lifetime

A table is held by whoever can reach it, the same way a memory is: the instance that created it, every instance that imported it, or the process that made it standalone. It goes when the last of them lets go. You do not have to release anything by hand.

It was not so. The row was tied to the creating process alone, so an exported table vanished out from under an importer the moment the exporter's process exited, and call_indirect through it found nothing.

Summary

Types

An opaque, shareable table handle.

Functions

Add a holder, for an instance importing this table.

Copy a range, possibly between two different tables.

The element type this table was declared with.

Grow by Delta, returning the previous size.

Whether a term is a table handle.

The limits this table was declared with.

Create a table, saying who holds it.

Remove a holder. The table goes when the last one lets go.

This table's registry identity.

Types

heaptype()

-type heaptype() ::
          func | extern | exn | any | eq | i31 | struct | array | nofunc | noextern | noexn | none |
          {type, typeidx()}.

instr()

-type instr() :: atom() | tuple().

reftype()

-type reftype() :: {ref, null | nonull, heaptype()}.

table()

-nominal table() ::
             {wasm_table,
              reference(),
              atomics:atomics_ref(),
              #tabletype{limits ::
                             #limits{min :: non_neg_integer(),
                                     max :: undefined | non_neg_integer(),
                                     shared :: boolean(),
                                     index_type :: i32 | i64},
                         elemtype :: reftype(),
                         init :: undefined | [instr()]}}.

An opaque, shareable table handle.

The declared limits travel in the handle rather than being supplied at each call, because a table imported from another module is bounded by the defining module's declaration, not by the importer's view of it. That is also what lets an importer check that the index type it declared is the one it was handed.

typeidx()

-type typeidx() :: non_neg_integer().

Functions

acquire/3

-spec acquire(table(), wasm_keeper:token(), pid() | none) -> ok | {error, gone | keeper_unavailable}.

Add a holder, for an instance importing this table.

copy(Dst, DstIdx, Src, SrcIdx, Len)

-spec copy(table(), non_neg_integer(), table(), non_neg_integer(), non_neg_integer()) -> ok.

Copy a range, possibly between two different tables.

The source slice is read out in full before anything is written, so an overlap within one table behaves like memmove rather than smearing the first element.

elemtype/1

-spec elemtype(table()) -> reftype().

The element type this table was declared with.

Carried for the same reason a global's type is: an importer has to check that the table it was handed holds what it expects, and the contents alone cannot say whether they are funcref or (ref $t).

fill(T, Dst, Len, Value)

-spec fill(table(), non_neg_integer(), non_neg_integer(), term()) -> ok.

get(T, Idx)

-spec get(table(), non_neg_integer()) -> term().

grow/3

-spec grow(table(), non_neg_integer(), term()) -> {ok, non_neg_integer()} | {error, exceeds_max}.

Grow by Delta, returning the previous size.

Refusal is a value rather than a trap, because table.grow is specified to push -1 rather than fault.

init(T, Dst, Vals)

-spec init(table(), non_neg_integer(), [term()]) -> ok.

is_table/1

-spec is_table(term()) -> boolean().

Whether a term is a table handle.

An embedder hands over bare terms, so a module importing a table may be given a memory, a function or a number. That has to come back as a link error rather than as a function_clause from inside the runtime.

limits/1

-spec limits(table()) ->
                #limits{min :: non_neg_integer(),
                        max :: undefined | non_neg_integer(),
                        shared :: boolean(),
                        index_type :: i32 | i64}.

The limits this table was declared with.

new(Size, Default)

-spec new(non_neg_integer() |
          #limits{min :: non_neg_integer(),
                  max :: undefined | non_neg_integer(),
                  shared :: boolean(),
                  index_type :: i32 | i64} |
          #tabletype{limits ::
                         #limits{min :: non_neg_integer(),
                                 max :: undefined | non_neg_integer(),
                                 shared :: boolean(),
                                 index_type :: i32 | i64},
                     elemtype :: reftype(),
                     init :: undefined | [instr()]},
          term()) ->
             table().

new/3

-spec new(non_neg_integer() |
          #limits{min :: non_neg_integer(),
                  max :: undefined | non_neg_integer(),
                  shared :: boolean(),
                  index_type :: i32 | i64} |
          #tabletype{limits ::
                         #limits{min :: non_neg_integer(),
                                 max :: undefined | non_neg_integer(),
                                 shared :: boolean(),
                                 index_type :: i32 | i64},
                     elemtype :: reftype(),
                     init :: undefined | [instr()]},
          term(),
          map()) ->
             table().

Create a table, saying who holds it.

holder is a {Token, Owner} pair naming the registry entry to create and the process whose death removes it. Leave it out and the table belongs to the calling process; an instance passes its own token, so the table survives for as long as anything that imported it is alive.

release/2

-spec release(table(), wasm_keeper:token()) -> ok.

Remove a holder. The table goes when the last one lets go.

resource/1

-spec resource(table()) -> wasm_keeper:resource().

This table's registry identity.

set(T, Idx, Value)

-spec set(table(), non_neg_integer(), term()) -> ok.

size(T)

-spec size(table()) -> non_neg_integer().

to_list(T)

-spec to_list(table()) -> [term()].