wasm_table (wasm v0.1.0)
View SourceTables, 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
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
-type heaptype() :: func | extern | exn | any | eq | i31 | struct | array | nofunc | noextern | noexn | none | {type, typeidx()}.
-type reftype() :: {ref, null | nonull, heaptype()}.
-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.
-type typeidx() :: non_neg_integer().
Functions
-spec acquire(table(), wasm_keeper:token(), pid() | none) -> ok | {error, gone | keeper_unavailable}.
Add a holder, for an instance importing this table.
-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.
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).
-spec fill(table(), non_neg_integer(), non_neg_integer(), term()) -> ok.
-spec get(table(), non_neg_integer()) -> term().
-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.
-spec init(table(), non_neg_integer(), [term()]) -> ok.
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.
-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.
-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().
-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.
-spec release(table(), wasm_keeper:token()) -> ok.
Remove a holder. The table goes when the last one lets go.
-spec resource(table()) -> wasm_keeper:resource().
This table's registry identity.
-spec set(table(), non_neg_integer(), term()) -> ok.
-spec size(table()) -> non_neg_integer().