lace-ffi is the shared binding layer for Rust-owned Lace
sans-IO machines and values. Thin C/WASM exports and the PyO3 binding
share safe typed Rust APIs; raw trusted-record byte commits are an
explicitly unsafe subset described below. Hosts own stores, raw byte IO,
waits, scheduling, cancellation, and lifecycle.
ABI version 35 exposes records, record stream decoding,
canonical policy execution, optional policy authoring, private runtime
ILTP framing, interlace polling, the retention sweep, and move-only
StoreWork. Hosts require this exact lace_ffi_version()
before other ABI calls; there is no alternate-version surface.
Connected-carrier construction receives no transport endpoint or generic
host authority.
Rust generators emit checked-in JavaScript numeric values, and
cbindgen emits the C header. Declaration hashes, generated
values, C policy calls, and the exact raw-WASM export allowlist are
checked independently.
The ABI exposes opaque Rust-owned parts, owned values, borrowed views, explicit destructors, caller-owned diagnostic output, typed polls, and typed errors. There is no context, runtime, handle table, store registry, executor, scheduler, reactor, worker, callback table, route map, socket, or public event queue.
Rust owns:
Hosts own Lace values and handles, stores, mutation publication, raw duplex or split byte IO, timers, waits, scheduling, cancellation, operations, public outcomes, and lifecycle. Each operation maps an interlace side to its retained Lace handle; no store identity crosses the ABI.
Opaque parts are single-thread-affine unless documented otherwise.
LaceStoreWork is move-only and may cross one foreign thread
or await, but is never accessed concurrently. Destructors free Rust
memory only and do not close host IO or schedule cleanup.
Every public Rust extern "C" function that dereferences
caller-supplied raw pointers is unsafe and links to this
contract. Each unsafe call requires:
Violating pointer provenance is undefined behavior. No global
last-error buffer exists. Constructors and integration failures may fill
caller-owned LaceErrorOut; terminal machine failures remain
typed fields in the retained poll. Interlace and ILTP entry points
always write a category: core and machine failures keep their typed
category (an oversized operand is LimitExceeded,
replacement of a closed interlace is Policy), decoder
failures are PeerProtocol except local stream-decoder
failures, and argument misuse and local encoding failures are
Integration. A diagnostic output is optional and reusable;
read its message/category only after a failed call, never as a last-call
success flag. Success need not clear an earlier diagnostic. Borrowed
machine/value slices remain valid until the next mutating owner call.
WASM callers reacquire memory.buffer after reservations
that may grow memory.
lace_store_memory_builder_add_record and the StoreWork
lace_store_work_result_reserve_entry_bytes/lace_store_work_result_commit_entry_bytes
pair are trusted-record transport, not record-import APIs. Their bytes
must be one complete canonical, fully validated record, unchanged since
validation or read from trusted committed storage. This includes valid
UTF-8, structure, outer and embedded hashes, and Seal Mark evidence. The
caller fills the complete reservation before commit, does not mutate it
concurrently, and does not use its pointer after commit, submission, or
work free. Cheap glyph and request/hash checks do not establish these
preconditions; violating them can invalidate unchecked record views, not
merely produce a typed error. Untrusted bytes enter through
lace_record_parse before use on these paths.
The Rust StoreWork::commit_entry_bytes method is
likewise unsafe: its callers establish the same
validated-byte and reservation-lifetime preconditions.
StoreWork::add_entry_record accepts a validated
Record owner and remains safe; PyO3 uses it and
set_result instead of raw-byte commits.
lace_ffi.h is generated and
the current ABI version’s declaration hash is frozen by
abi-v<version>.sha256. The repository validates the
header and link shape; it contains no current C application consumer.
lace_store_fs_open requires
LACE_FFI_FEATURE_NATIVE_FS when linking a library built
with Cargo feature native-fs on a 64-bit Unix or Windows
target; otherwise it is absent, not an unavailable stub.
lace_store_lmdb_open requires
LACE_FFI_FEATURE_LMDB when linking a library built with
Cargo feature lmdb; it is otherwise absent. Building that
feature for a target not listed in native-lmdb-store.md
fails.wasm.exports inventories the
import-free artifact used by the JavaScript runtime; rows prefixed
compiler require that Cargo feature. The native filesystem
and LMDB constructors are absent; the LMDB dependency is excluded from
the WASM target graph. Two deterministic record-builder inputs are
WASM-only because the module has no ambient entropy import.A safe Rust use by PyO3 does not retain a C symbol. The C surface omits duplicate slice-return helpers, copy conveniences, and diagnostic-only values.
The rlib owns:
It owns no runtime policy, host store, worker, future, socket, or callback.
Safe Rust enums define numeric values for protocol side, participant
position, carrier role, interlace poll/effect, observation, close
reason, error category, timeout phase, StoreOp kind, query constraint
kind, store error, and peer-proof assurance. Foreign construction
accepts Disabled (0), ConfidentialChannel (1),
and ChannelBound (2), using ILTP 0.13 and
lace-iltp-peer-proof-v3 proofs. The host establishes its
channel before adoption and supplies any opaque 32-byte binding. Lace
supplies no encryption or transport handshake.
C and generated JavaScript values come from these enums. The C header
explicitly includes LacePeerProofAssurance and
LaceStoreErrorKind constants even where functions accept
integer tags; unknown integer inputs remain rejectable. Result
construction uses typed functions, not a separate result-kind tag enum.
PyO3 maps the shared enums directly to Python values and carries no
numeric copy. Unknown machine enum inputs are integration errors. Hosts
never infer categories from diagnostic text.
Cargo feature compiler enables the optional
lace-compiler dependency and one source entry point:
LacePolicy *lace_policy_compile(
const uint8_t *source, size_t length, size_t max_source_bytes,
LaceErrorOut *error);The feature is off by default. C consumers define
LACE_FFI_FEATURE_COMPILER only when linking a
compiler-enabled library. There are no disabled stubs. Standard
JavaScript WASM builds explicitly enable it; compiler-free builds use
the same canonical execution and composition ABI. Python uses its direct
safe compiler dependency, not the C entry point.
Compilation is one synchronous call over complete UTF-8 source
borrowed for the call. It accepts anonymous Interlang with local helpers
and datalog: blocks, not module declarations, imports,
finite targets, or unresolved references. Empty and helper-only source
cannot produce an exchange policy. Success returns one independently
owned LacePolicy, freed by lace_policy_free.
Source bytes are not retained; there is no compiler owner, slot,
descriptor, or replay owner.
max_source_bytes is an explicit complete-source byte
bound; pass 1048576 for the standard default. Zero is a
real bound. Fixed exchange rule/arity/TopK bounds are enforced by shared
Rust preparation for compiled and canonical inputs, not ABI options.
Rust enforces source accounting before lowering; JavaScript does not
implement a second compiler or parser.
Failure returns null and overwrites both message and category in
LaceErrorOut: preparation limits are
LimitExceeded; malformed source and argument errors are
Policy. Read the category only after failure, not as a
last-call success flag.
All text is pointer/byte-length UTF-8, not NUL-terminated. A null
source pointer is allowed only for zero length. Composition pointer
arrays require aligned storage for their complete lengths.
lace_alloc guarantees only byte alignment; JavaScript
stages one padded allocation with an aligned interior pointer, then
frees the original pointer and length in finally. It
reacquires memory.buffer after allocating calls and never
retains a WASM view across a call that can grow memory.
Canonical execution uses
lace_policy_from_canonical_text, which validates the
ordinary 030/040 bytes and rejects noncanonical text, unresolved source
syntax, and helper-only operands before operation-owned constraints are
added.
lace_policy_compose(authoring, authoring_count, guests, guest_count, exclusions, exclusion_count, error)
borrows one aligned array of borrowed const LacePolicy *
values per Lace-130
class and one aligned array of LaceFieldExclusion values,
and invokes the core n-ary facet-constraint merger once with all three.
Each exclusion component is a borrowed UTF-8 pointer and length; a null
pointer is the anonymous _. It returns one new owned
policy, without calling the authoring compiler. Inputs remain live and
unchanged. Null arrays are permitted only for zero count; empty
authoring input, null entries and non-UTF-8 or invalid exclusion text
are policy errors. Empty guest and exclusion arrays leave the output
identical to an authoring-only composition. Every input has at least one
facet; helper-only programs are rejected before operation-owned
constraints can make them executable. Veto-only inputs are valid. Input
order and exact canonical duplicates within a class do not affect output
bytes. Nested calls need not have the same bytes or limit behavior as a
single n-ary call.
lace_policy_all_records(),
lace_policy_tx_disabled(), and
lace_policy_rx_disabled() each return a new owned copy of
the core’s constant all-records, transmission-veto, and reception-veto
policy. Porcelain hosts compose their private operands from these with
lace_policy_compose rather than parsing policy text.
Failed canonical construction and composition overwrite both
diagnostic message and category from the core’s typed
PreparationError: exceeding fixed rule, arity or static
TopK bounds is LimitExceeded; malformed, noncanonical,
helper-only or otherwise invalid policy input is Policy.
Other unclassified diagnostic writes reset the category to
None. Read it only after failure.
Safe Rust clients use lace_compiler::compile and the
core’s prepare_facet_constraints directly.
InterlaceModule::from_text and from_rules
preserve PreparationError; rules construction derives
canonical text and identity. The shared safe binding layer exposes
LacePolicy::from_module(InterlaceModule) -> Result<Self, PreparationError>,
module(&self), and strict
from_canonical_text(&str) -> Result<Self, PreparationError>.
PyO3 wraps these Rust APIs directly and preserves their categories;
JavaScript projects the FFI category rather than parsing diagnostic
text. This local preparation distinction does not reclassify invalid
incoming operands: peer validation remains
PeerProtocol.
Plan compilation receives exactly two ordered modules and no third
policy input. The C direct constructor borrows two canonical
LacePolicy values and limits and uses empty final session
authorities. Safe Rust direct composition can supply explicit final
authority through its DirectConfig. Connected construction
receives one local module or port selection, carrier role, optional
By-secret, resolved proof assurance, optional stream encodings, optional
expected peer By, optional port, fresh 32-byte nonce seed,
optional operand cache, and limits. It receives no transport, endpoint,
Lace, or store argument. Bound assurance includes the established
channel’s opaque binding.
lace_interlace_machine_new_connected_carrier takes these
arguments in order:
const LacePolicy *policy,
uint32_t role,
uint32_t has_by_secret, const uint8_t *by_secret_ptr, size_t by_secret_len,
uint32_t peer_proof_mode, const uint8_t *binding_ptr, size_t binding_len,
uint32_t has_stream_encodings, const uint8_t *stream_encodings_ptr,
size_t stream_encodings_len,
uint32_t has_expected_peer_by, const uint8_t *expected_peer_by_ptr,
size_t expected_peer_by_len,
uint32_t has_port, const uint8_t *port_ptr, size_t port_len,
const uint8_t *nonce_seed_ptr, size_t nonce_seed_len,
const LaceOperandCache *operand_cache,
const LaceInterlaceResourceLimits *limits, LaceErrorOut *errorA null policy constructs a port-selecting carrier: it
owns no operand until the host answers each logical session’s
SelectOperand poll, and takes no expected peer
By at construction, so has_expected_peer_by
must be 0.
A host Lace owns one LaceOperandCache
(lace_operand_cache_new(entries), entries
0 holding none; lace_operand_cache_free) and
passes it to every connected carrier it runs, which share it and outlive
a freed handle. A null cache gives the carrier its own 32-entry cache,
so its re-prefaces still hit.
ChannelBound requires exactly 32 binding bytes and a
local By-secret. The constructor copies the binding; caller storage need
not outlive the call. Other modes require a null binding pointer and
zero length. Invalid assurance configuration fails before protocol work.
The host owns transport establishment and its timeout; encryption
failures are transport failures.
has_stream_encodings is exactly 0 (absent;
every compiled-in encoding) or 1 (present). Present text is
UTF-8 tokens separated by , in preference order; empty text
offers identity only. Construction applies Lace-100’s
stream-encoding rules: an unknown or uncompiled token,
identity, or a repeat fails as Integration.
lace_stream_encodings_validate(text_ptr, text_len, error)
applies the same parse alone, so a host can reject a list when it
constructs its connected peer. Cargo features deflate and
zstd, on by default, compile the deflate12c
and zstd20c codecs; they change no declaration.
has_expected_peer_by is exactly 0 (absent;
text ignored) or 1 (present). Present text must parse as
canonical V.<b64a>.H3; empty text is invalid rather
than absent. Rust construction applies Lace-100’s
expected-peer requirements. Failure returns null with the
configuration diagnostic before protocol work; the host remains
responsible for its accepted IO.
has_port is exactly 0 (absent; no
Port line) or 1 (present). Present text must
be a valid Lace-060 port;
invalid text fails as Integration before protocol work. The
carrier sends it in every preface.
lace_port_validate(text_ptr, text_len, error) applies the
same parse alone, so a host can reject a port when it constructs its
connected peer.
The carrier retains the expectation across replacement. Rust establishment owns Lace-050’s proof checks; foreign hosts consume typed failures rather than inspect proofs or repeat checks.
lace_by_parse validates canonical public
V.<b64a>.H3 text and returns that same canonical
text. The record builder accepts data, ordered application fields, and
optional By-secret. lace_record_builder_build returns an
independently owned LaceRecord *, or null with diagnostic
output on failure. Free the result with lace_record_free.
Builder reuse, failure, and destruction do not invalidate prior results;
the builder retains inputs only, not output bytes. Construction moves
the validated Rust Record into the existing FFI record owner without
serializing or reparsing it. Native Seal construction uses Rust entropy.
Raw WASM uses explicit auxiliary randomness.
lace_record_parse remains the validating external-byte
boundary. Both paths expose hash, kind, optional
By/Mark, every application-field occurrence,
every valid record link, and the record’s bytes:
size_t lace_record_canonical_len(const LaceRecord *record);
size_t lace_record_data_offset(const LaceRecord *record);
size_t lace_record_data_len(const LaceRecord *record);
const uint8_t *lace_record_contiguous_ptr(const LaceRecord *record);
int32_t lace_record_read_at(const LaceRecord *record, size_t offset, uint8_t *buffer,
size_t capacity, size_t *read, LaceErrorOut *error);The data occupies [data_offset, canonical_len).
contiguous_ptr designates all canonical bytes where the
record holds them contiguously (constructed, parsed, in-memory and
mapped records) and is null otherwise; no accessor promises a complete
slice. read_at copies at most capacity bytes
starting at offset and writes the count, fewer only at the
record’s end; it returns LACE_FFI_STEP_IDLE, or
LACE_FFI_STEP_ERROR with Integration for
invalid arguments or an offset past the record and Store
when the source cannot be read. Record slices remain valid until that
record is freed.
A host reading a Lace-010
record stream pushes its bytes through one
LaceRecordStreamDecoder, which validates each record as it
passes and receives it into memory, as the host’s ILTP responses
are:
LaceRecordStreamDecoder *lace_record_stream_decoder_new(uint64_t max_bytes);
void lace_record_stream_decoder_free(LaceRecordStreamDecoder *decoder);
int32_t lace_record_stream_decoder_push(LaceRecordStreamDecoder *decoder, const uint8_t *bytes,
size_t len, LaceErrorOut *error);
LaceRecord *lace_record_stream_decoder_take(LaceRecordStreamDecoder *decoder);
int32_t lace_record_stream_decoder_finish(LaceRecordStreamDecoder *decoder, LaceErrorOut *error);max_bytes bounds the canonical bytes of the whole stream
(UINT64_MAX for none). push consumes input
until one record completes and returns the consumed count, at most
INT32_MAX; while a completed record waits it consumes
nothing. take returns that record as a new owned
LaceRecord *, or null. The host pushes the unconsumed
suffix again after taking. finish at EOF returns
LACE_FFI_STEP_IDLE. Failures return
LACE_FFI_STEP_ERROR with a typed category and end the
stream, discarding the partial record: a record longer than the stream’s
remaining bound is LimitExceeded at its start, before its
bytes are held; invalid bytes, or a stream ending inside a record, are
PeerProtocol. PyO3 wraps the same safe Rust decoder.
One LaceInterlaceMachine owns either a direct pair or
connected carrier. A poll retains exactly one typed value:
Effects { side0, side1 }
Observation
EnteredFixedPoint
NeedPeerInput
NeedStoreAdvance
Closed
Failed
SelectOperand
Each side independently reports no effect, StoreCall, or PeerOutput. One side’s store call never hides the other. Connected peer output remains machine-owned until the host accepts it into its codec.
Poll values remain readable until machine mutation. A retained
observation exposes its kind, side, and record hash; for
RecordSaved,
lace_interlace_machine_observation_record also returns a
new owned LaceRecord * for the admitted record (null for
any other poll), freed with lace_record_free. Each call
returns an independent owner sharing the Arc-backed record; porcelain
hosts read it at poll time instead of reloading the record from their
private store. Fixed-point wake boundaries carry side, StorePosition,
machine id, and fixed-point id. Per-side phase generations tell the host
when to create one absolute deadline; partial IO and unrelated progress
do not reset it. EOF is submitted only after decoder finalization.
A port-selecting carrier reports SelectOperand once it
holds a logical session’s peer preface, before announcing its operand,
and repeats it until answered:
int32_t lace_interlace_machine_select_operand(LaceInterlaceMachine *machine, const LacePolicy *policy,
uint32_t has_expected_peer_by,
const uint8_t *expected_peer_by_ptr,
size_t expected_peer_by_len, LaceErrorOut *error);
const uint8_t *lace_interlace_machine_peer_port_ptr(const LaceInterlaceMachine *machine);
size_t lace_interlace_machine_peer_port_len(const LaceInterlaceMachine *machine);
const uint8_t *lace_interlace_machine_peer_by_ptr(const LaceInterlaceMachine *machine);
size_t lace_interlace_machine_peer_by_len(const LaceInterlaceMachine *machine);
const uint8_t *lace_interlace_machine_outgoing_encoding_ptr(const LaceInterlaceMachine *machine);
size_t lace_interlace_machine_outgoing_encoding_len(const LaceInterlaceMachine *machine);
const uint8_t *lace_interlace_machine_incoming_encoding_ptr(const LaceInterlaceMachine *machine);
size_t lace_interlace_machine_incoming_encoding_len(const LaceInterlaceMachine *machine);select_operand borrows policy as that
session’s operand, with an optional expected peer By under
the constructor’s flag rules, or refuses the port with a null
policy and no expectation. It returns
LACE_FFI_STEP_IDLE, or LACE_FFI_STEP_ERROR: a
refusal fails the carrier as PeerProtocol
(port refused), and a selection construction would reject
(an expectation without proof assurance, an oversized operand) fails it
with that category; both are also the retained Failed poll.
Argument misuse and answering when no selection is pending are
Integration and leave the machine unchanged. Replacing a
port-selecting carrier’s policy is Policy. The peer-port
accessor borrows the port of the latest peer preface until the next
mutating machine call: null with length zero when that preface named
none, before any peer preface, and for a direct pair. During
SelectOperand it is the port being selected for. The
peer-By accessor likewise borrows the canonical By text the
live peer proved in the latest logical session under confidential or
channel-bound assurance: null with length zero before that proof
validates, under disabled assurance, after an empty proof, from a later
preface cut until the next proof validates, and for a direct pair. It is
a diagnostic, never a policy fact or authority. The encoding accessors
return the static 065 token of the local-to-peer and peer-to-local
encoding the latest preface pair selected: null with length zero before
the first peer preface and for a direct pair. Each peer preface selects
them again. They are diagnostics.
One LaceRetentionSweep owns a sans-IO retention sweep
(store-compaction.md).
It evaluates policies only; the host executes its store operations
through ordinary StoreWork against the root’s exclusively owned store
and hands the resulting kept set to that store’s physical
compaction.
LaceRetentionSweep *lace_retention_sweep_new(const LacePolicy *const *policies, size_t count,
LaceErrorOut *error);
void lace_retention_sweep_free(LaceRetentionSweep *sweep);
int32_t lace_retention_sweep_poll(LaceRetentionSweep *sweep);
LaceStoreWork *lace_retention_sweep_issue_store_work(LaceRetentionSweep *sweep, LaceErrorOut *error);
uint32_t lace_retention_sweep_store_work_is_current(const LaceRetentionSweep *sweep,
const LaceStoreWork *work);
int32_t lace_retention_sweep_submit_store_work(LaceRetentionSweep *sweep, LaceStoreWork *work,
LaceErrorOut *error); /* consumes work */
uint64_t lace_retention_sweep_boundary(const LaceRetentionSweep *sweep);
uint64_t lace_retention_sweep_retained_count(const LaceRetentionSweep *sweep);
const uint8_t *lace_retention_sweep_bitmap_ptr(const LaceRetentionSweep *sweep);
size_t lace_retention_sweep_bitmap_len(const LaceRetentionSweep *sweep);
LaceErrorCategory lace_retention_sweep_error_category(const LaceRetentionSweep *sweep);
const uint8_t *lace_retention_sweep_error_message_ptr(const LaceRetentionSweep *sweep);
size_t lace_retention_sweep_error_message_len(const LaceRetentionSweep *sweep);new borrows a nonempty aligned array of policies like
lace_policy_compose. Null, empty, authority-dependent, and
SelectStoredRecord-free inputs fail with
Policy; preparation bounds fail with
LimitExceeded.poll returns LACE_FFI_STEP_OUTPUT while
one store operation is outstanding, LACE_FFI_STEP_COMPLETE
once the kept set is readable, and LACE_FFI_STEP_ERROR
after a terminal failure. It reports the retained step again until the
outstanding work is submitted.Integration. Retention work reports side 0 and its
operation through the ordinary request accessors; any store primitive or
host store fills it.LACE_FFI_STEP_IDLE means the
sweep took the result; a whole-request store error it carries fails the
sweep, and the next poll reports Store. Work from another
sweep or an interlace machine, stale work, and incomplete results are
Integration and leave the sweep unchanged. Interlace
machines likewise reject sweep work.boundary is the swept B
and the bitmap holds ceil(B / 8) bytes: id n
is bit (n - 1) % 8 (least significant first) of byte
(n - 1) / 8. The bitmap is borrowed until the sweep is
freed; it is null with length zero before completion and for
B = 0.Policy,
LimitExceeded, Store, or
Integration with its message; both read
None/empty otherwise.Binding hosts never receive LaceStoreCall *. For each
reported StoreCall side, they issue one opaque
LaceStoreWork containing an owned StoreOp, hidden ticket,
and empty or complete result construction state.
Before submitting late work, a host checks whether it is still current. Invalid, partial, wrong-family, or stale work cannot partially mutate the machine. Complete results validate when installed on StoreWork; incremental results validate when assembled for submission. Submission consumes work, verifies the exact ticket, fills the machine-owned complete response, and submits once. Late work is freed only after the owning Lace performs required durable mutation classification and position publication.
The detailed operation and result contract is defined by ../../../docs/architecture/store-request-response-ffi-boundary.md.
Accessors are positional and operation-neutral. They take
const LaceStoreWork * and return zero, null, or
None for a value the operation does not carry. Text and
bytes are borrowed until the work is consumed or freed.
void lace_store_work_free(LaceStoreWork *work);
LaceInterlaceSide lace_store_work_side(const LaceStoreWork *work);
uint64_t lace_store_work_id(const LaceStoreWork *work);
LaceStoreOpKind lace_store_work_op_kind(const LaceStoreWork *work);
const uint8_t *lace_store_work_error_ptr(const LaceStoreWork *work);
size_t lace_store_work_error_len(const LaceStoreWork *work);
uint32_t lace_store_work_entry_count(const LaceStoreWork *work);
const uint8_t *lace_store_work_entry_hash_ptr(const LaceStoreWork *work, uint32_t slot);
size_t lace_store_work_entry_hash_len(const LaceStoreWork *work, uint32_t slot);
uint64_t lace_store_work_entry_id(const LaceStoreWork *work, uint32_t slot);
const uint8_t *lace_store_work_entry_bytes_ptr(const LaceStoreWork *work, uint32_t slot);
size_t lace_store_work_entry_bytes_len(const LaceStoreWork *work, uint32_t slot);
uint64_t lace_store_work_read_through(const LaceStoreWork *work);
uint64_t lace_store_work_lower_exclusive(const LaceStoreWork *work);
uint64_t lace_store_work_max_rows(const LaceStoreWork *work);
uint64_t lace_store_work_resume_after(const LaceStoreWork *work);
uint32_t lace_store_work_constraint_count(const LaceStoreWork *work);
LaceStoreConstraintKind lace_store_work_constraint_kind(const LaceStoreWork *work, uint32_t slot);
const uint8_t *lace_store_work_constraint_text_ptr(const LaceStoreWork *work, uint32_t slot, uint32_t part);
size_t lace_store_work_constraint_text_len(const LaceStoreWork *work, uint32_t slot, uint32_t part);| Operation | Entries | Entry values | Scalars |
|---|---|---|---|
| CurrentPosition | none | — | — |
| ResolveIds | requested hashes | hash | read_through |
| Find | none | — | read_through (upper inclusive),
lower_exclusive, max_rows,
resume_after, constraints |
| LoadHash | requested hashes | hash | — |
| LoadId | requested ids | id | — |
| Store | records to store | hash, canonical bytes | — |
read_through raw u64::MAX selects the
observed high-water. max_rows is u64::MAX when
unbounded; resume_after is zero without a cursor (store ids
are nonzero). Find constraints are one flattened slot sequence: the
exact hash when present, then field constraints, then record-link
constraints, each family in request order with duplicates kept. Kinds
are ExactHash, FieldPresent,
FieldExact, FieldPrefix, and
Link; a slot past the end is None. Text part 0
is the hash, field name, or link name; part 1 the field value, prefix
start, or link data; part 2 the link target. An absent part is a null
pointer with zero length; present empty text is a non-null pointer with
zero length.
Builders take LaceStoreWork *, return
LACE_FFI_STEP_IDLE or LACE_FFI_STEP_ERROR (a
null reservation), and record the rejection in the work’s error text
without changing filled state. The operation the work holds selects the
result family.
int32_t lace_store_work_result_set_position(LaceStoreWork *work, uint64_t position);
int32_t lace_store_work_result_set_complete(LaceStoreWork *work, uint32_t complete);
int32_t lace_store_work_result_add_entry(LaceStoreWork *work, uint32_t slot, uint64_t id, uint32_t duplicate);
uint8_t *lace_store_work_result_reserve_entry_bytes(LaceStoreWork *work, uint32_t slot, uint64_t id, size_t len);
int32_t lace_store_work_result_commit_entry_bytes(LaceStoreWork *work);
int32_t lace_store_work_result_set_error(LaceStoreWork *work, uint32_t kind,
const uint8_t *message, size_t message_len);set_position: CurrentPosition, or the post-store
position of Store.set_complete: Find only, once.add_entry: a ResolveIds id (zero is missing), a missing
LoadHash/LoadId entry (id zero), or a Store outcome (nonzero id,
duplicate 0 or 1).reserve_entry_bytes + commit_entry_bytes:
a Find row (nonzero id) or a loaded LoadHash/LoadId record (id zero).
Commit is the next call on the work; LoadHash bytes must match the
requested hash.set_error: one whole-request store error with a
LaceStoreErrorKind tag.Entries fill in slot order starting at zero; request-positional slots
stay within entry_count. Find rows use their row index.
Nothing echoes request hashes or ids.
One opaque LaceStore holds a Rust store behind the
shared StoreBackend implementation. Its constructors choose
the backend; every other call is backend-neutral:
LaceStore *lace_store_memory_new(void);
LaceStoreMemoryBuilder *lace_store_memory_builder_new(void);
int32_t lace_store_memory_builder_add_record(LaceStoreMemoryBuilder *builder,
const uint8_t *bytes, size_t len, LaceErrorOut *error);
LaceStore *lace_store_memory_builder_build(LaceStoreMemoryBuilder *builder, LaceErrorOut *error);
void lace_store_memory_builder_free(LaceStoreMemoryBuilder *builder);
LaceStore *lace_store_fs_open(const uint8_t *path, size_t path_len, LaceErrorOut *error); /* native-fs */
LaceStore *lace_store_lmdb_open(const uint8_t *path, size_t path_len, LaceErrorOut *error); /* lmdb */
void lace_store_free(LaceStore *store);
int32_t lace_store_current_position(LaceStore *store, uint64_t *position, LaceErrorOut *error);
int32_t lace_store_execute_work(LaceStore *store, LaceStoreWork *work, LaceErrorOut *error);Native and WASM builds expose the indexed-memory store. Native builds
may also expose the filesystem-shaped store or, with default-off feature
lmdb, the internal lmdb-lace store. Native
open calls accept only the root path; a root’s native
capacity is fixed when it is created. Mobile builds do not qualify
physical-device address space or app-sandbox access. Finite providers
seed indexed memory once through the builder over validated records;
build consumes its records. Stores execute StoreWork and
fill its result; they do not submit it, own Lace, publish watches, or
schedule work.
lace_store_current_position returns
LACE_FFI_STEP_IDLE (0) on success and writes
the complete position, including valid before-first zero. Failure
returns LACE_FFI_STEP_ERROR (-1) and leaves
the output unchanged. position names one aligned,
exclusively writable uint64_t; it need not be initialized
before the call and is read only after success. A null store/output is
Integration; a backend failure, including a wrong result
family, is Store. Status remains unambiguous when the
optional diagnostic pointer is null.
Native store open uses Integration for malformed foreign
arguments and Store for backend-open failure. Work
execution uses Integration for missing owners or rejected
result installation. A backend StoreError successfully
installed on StoreWork is a completed execute call, not an
execute-status error; the machine consumes that typed result on
submission. Free releases directly owned resources, not a runtime
participant; there is no separate close operation. Native Rust
work-layer consumers continue using lace-sans-io
directly.
Runtime drivers allocate one LaceIltpCodec per connected
carrier with lace_iltp_codec_new. They push bytes until one
item is ready (lace_iltp_codec_push_until_item), take that
opaque item (lace_iltp_codec_next), and finalize EOF
(lace_iltp_codec_finish). Taking an item returns a step
status: an item, no ready item, or a decode failure, never conflating
the last two. Before decoding toward each item the machine takes, hosts
call
lace_iltp_codec_set_record_ingress(codec, machine, error):
the carrier’s state decides at each record’s start, before allocation,
whether it is legal and within which bound, as the transition
tables define; an illegal record fails with
PeerProtocol and one over a local limit with
LimitExceeded. The codec receives accepted records into
memory; a record discarded during reset prefacing becomes an item that
carries only its whole item length, including its label line and framing
LF, to the machine.
Output is one accepted batch at a time, produced in chunks of at most the codec’s chunk size (64 KiB):
int32_t lace_interlace_machine_accept_peer_output(LaceInterlaceMachine *machine,
LaceIltpCodec *codec, LaceErrorOut *error);
int32_t lace_iltp_codec_accept_end(LaceIltpCodec *codec, LaceErrorOut *error);
int32_t lace_iltp_codec_output(LaceIltpCodec *codec, LaceErrorOut *error);
const uint8_t *lace_iltp_codec_output_ptr(const LaceIltpCodec *codec);
size_t lace_iltp_codec_output_len(const LaceIltpCodec *codec);
int32_t lace_iltp_codec_advance_output(LaceIltpCodec *codec, size_t written, LaceErrorOut *error);accept_peer_output accepts the connected carrier’s
retained machine output and moves it into an idle codec as one batch;
accept_end accepts the outgoing stream’s end mark instead,
which hosts write in bounded completion and offer on cancellation at
fixed point. Either fails with Integration while the codec
still holds output. lace_iltp_codec_output then returns
LACE_FFI_STEP_OUTPUT with a nonempty current chunk,
borrowed through output_ptr/_len until the
next mutating codec call, or LACE_FFI_STEP_IDLE once the
accepted output is complete. The host writes some prefix of the chunk
and advances by the written count; the rest stays current, so partial
writes keep the same chunk and offset. After
LACE_FFI_STEP_IDLE the host flushes: each batch’s bytes end
with its encoding’s flush, and an end mark may produce no bytes. A batch
carrying a preface switches the logical session’s encodings as its bytes
are produced, so hosts drain such a batch rather than discard it.
Hosts cannot construct or inspect semantic ILTP items or invoke
unrestricted codec operations. This preserves one-item handoff and keeps
phase interpretation inside the interlace machine. The codec selects
each direction’s Lace-065
encoding from the two prefaces of every logical session it carries and
applies it: hosts push and write wire bytes and never select, inspect,
or flush an encoding themselves. Decoded bytes following a ready item
stay in the codec and are framed by lace_iltp_codec_next
before more input is needed.
The ABI has no callbacks, store vtables, JSON operation protocol, detached response builders, runtime objects, integer handle registry, FFI futures, workers, clocks, routes, watches, host-visible StoreCall retirement hooks, transport implementation, or duplicate symbol family.
make check verifies the current ABI declaration
baseline, generated header, JavaScript values,
target/feature-preprocessed declarations, native C policy calls, link
smoke tests, exact WASM exports, and empty-import WASM instantiation.
Focused tests cover PyO3 use of the same safe poll/work layer, every
StoreOp/result family, direct side effects, stale/wrong work, atomic
invalid-result rejection, reserve cleanup, and typed error parity.
make check-c-lmdb adds a focused native C link/work test:
memory-to-LMDB transfer, explicit host submission, and durable reopen
followed by LMDB-to-memory transfer. Each suite stays below 60
seconds.