Skip to content

MVP+11 Experimental ABI Contract v1

Field Value
Status Private experimental schema; symbols exist with no compatibility promise
Observed 2026-09-03
Owner AR-0017 / MVP+11 Slices 1-3
Depends on Foreign contract inventory, SharedKvStore, ErrorReport, ADR-0001, AR-0017

Purpose

Define the behavior that a minimal independently compiled C harness should pressure before Tosumu chooses concrete exported layouts. This record does not reserve symbol names, numeric discriminants, struct layout, calling convention, or ABI version. Those become compatibility commitments only after the hostile corpus and independent caller justify stabilization.

Boundary Shape

The experiment has six subjects:

Subject Capability Explicit exclusion
Database handle create/open, single put/delete/get, snapshot creation, bounded connection observations, close No pager/WAL access, protector mutation, callback, or borrowed transaction
Snapshot handle generation, get, inclusive ordered range read, close No mutation or live/latest read
Immutable byte-result handle length, bounded copy/view, close No allocator mixing or mutation
Structured error handle code, status, message, typed details, close No global/thread-local last error and no Rust source object
Call result success/absence/not-applied/error plus exactly the payload allowed by that outcome No null-as-error convention
Mutation batch handle bounded copied put/delete commands, one consuming execute, explicit close/abort No staged reads, conditions, savepoints, generation result, or live core transaction

Every non-zero handle is opaque. Its bit pattern has no contractual address, kind, slot, or generation meaning and callers must not interpret it. Zero is always invalid and is never a live object.

Provisional Operation Set

abi_version() -> version

database_create(path_bytes, open_options) -> database | error
database_open(path_bytes, open_options) -> database | error
database_close(database) -> success | error
database_put(database, key, value) -> success | error
database_delete(database, key) -> success | error
database_get(database, key) -> bytes | absent | error
database_snapshot(database) -> snapshot | error
database_connection_info(database) -> observation | error

batch_create() -> batch | error
batch_append_put(batch, key, value) -> success | error
batch_append_delete(batch, key) -> success | error
database_execute_batch(database, batch) -> success | error
batch_close(batch) -> success | error

snapshot_generation(snapshot) -> u64 | error
snapshot_get(snapshot, key) -> bytes | absent | error
snapshot_scan(snapshot, inclusive_start, inclusive_end) -> encoded_pairs | error
snapshot_close(snapshot) -> success | error

bytes_length(bytes) -> length | error
bytes_copy(bytes, destination, capacity) -> written_or_required | error
bytes_close(bytes) -> success | error

error_code(error) -> borrowed_ascii_view | boundary_failure
error_status(error) -> status | boundary_failure
error_message(error) -> bytes | boundary_failure
error_detail_count(error) -> count | boundary_failure
error_detail(error, index) -> typed_detail | boundary_failure
error_close(error) -> success | boundary_failure

Names are descriptive pseudocode. There is no raw-pointer-returning get, no caller use of Rust layouts, and no API callback. Range results use one bounded, versioned encoding or iterator-like pull handle selected during implementation; the experiment must not return an unbounded graph of allocations.

Paths are length-delimited UTF-8 and reject malformed text, embedded NUL, and platform paths that Rust cannot represent without reinterpretation. This is a deliberately portable subset, not a promise to represent every native Windows path. Passphrases are also length-delimited UTF-8 because the current core contract is &str.

Create/open options initially admit only the protector modes already exposed by the selected provider-neutral owner. A future platform protector is not encoded as a passphrase variant and cannot trigger passphrase fallback.

Call Outcome Algebra

Each call produces exactly one top-level outcome:

success(payload?)
absent
not_applied(version?)       # reserved for later conditional operations
error(error_handle)
boundary_failure(code)      # only when no error object can safely be allocated

absent is valid only for lookup-like calls. not_applied is not an error and must not be collapsed into success. An error outcome owns exactly one error handle. Payload outputs remain zeroed/invalid on every non-success outcome.

Boundary failures are a tiny FFI-owned vocabulary for conditions such as unsupported ABI version, invalid output pointer, invalid/stale/wrong-kind handle, wrong thread, recoverable capacity exhaustion while constructing an error, or a contained panic. Allocator-level out-of-memory may abort under the selected Rust runtime and is not claimed as recoverable. These failures are not silently added to the core public error-code registry. Stabilizing them requires AR-0017 disposition and an Error Design Document update.

Buffer Contract

  • Input (pointer, length) is borrowed for the call and copied only where the operation explicitly requires retention.
  • Null with non-zero length is invalid. Null with zero length represents an empty slice, never absence.
  • Lengths above isize::MAX are rejected before pointer construction or arithmetic; smaller lengths are not evidence that the claimed region exists.
  • Non-null pointers must identify readable or writable ranges of the declared length for the duration of the call. Portable C cannot prove this; a dangling or forged non-null pointer is caller memory unsafety, not a typed Tosumu failure.
  • Immutable input regions may overlap each other. Copy-out destinations must not alias ABI-owned source storage; no ABI operation exposes an address into that private storage, so a conforming caller cannot derive such an alias from this contract.
  • Success outputs are immutable ABI-owned byte handles. Callers release them only through the matching ABI close operation.
  • The initial experiment prefers bounded copy-out: query/copy reports the exact required length when capacity is insufficient and writes nothing partial.
  • Any temporary direct view, if tested, remains valid only while its byte handle is live and no close races it; it is not the initial portability contract.
  • Secret inputs and error material are never returned through ordinary value buffers. Zeroization remains unclaimed unless separately implemented and evidenced.

Structured Error Projection

Core failures are first converted to ErrorReport. The ABI copies its stable code, coarse status, message, and ordered typed details into an independently owned error object. Detail values preserve bool, UTF-8 string, u16, and u64 distinctions. Unknown future detail types fail as unsupported in the experimental ABI rather than being formatted into strings.

The error object contains no Rust backtrace, dynamic source error, OS object, secret, provider handle, or borrowed pointer into a database. It remains readable after the originating database is closed.

Provisional Handle State Machines

Database

           create/open success
Invalid --------------------------> Active
                                      |  \
                  integrity/panic     |   \ close wins registry removal
                                      v    v
                                  Poisoned  Closed/Stale
                                      |
                                      +------ close ------> Closed/Stale
  • Failed create/open produces no handle.
  • Active permits the bounded operation set.
  • A contained panic or core poison outcome marks the handle Poisoned; only error observation and close remain valid.
  • Close atomically removes the live generation before destruction. Later use and double close receive invalid/stale-handle boundary failure.
  • Ordinary operations begin thread-affine. Cross-thread use returns wrong_thread until independent mobile caller evidence justifies a wider rule. Close remains callable from another thread so foreign-runtime cleanup cannot strand the object solely because finalization moved threads.

Snapshot

Invalid -- snapshot success --> Active -- close --> Closed/Stale
                                  |
                                  +-- parent database close: remains Active

A snapshot owns its generation pin and may outlive the database handle that created it, matching the underlying owned Rust snapshot behavior. Its reads are thread-affine in the initial experiment, its close is thread-independent, and it never silently changes to a latest read. Snapshot-limit failure produces no handle.

Bytes and error objects

Invalid -- successful allocation --> Active -- close --> Closed/Stale

They are immutable and independent of the originating database lifetime. Close is thread-independent. Read access may become thread-independent only when the registry proves concurrent close/read cannot free storage while borrowed; otherwise reads apply the same conservative thread-affinity rule.

Registry And Concurrency Hypothesis

The experiment should use kind- and generation-checked integer handles backed by an adapter-owned registry. Lookup obtains a temporary strong owner before releasing the registry lock, so concurrent close cannot free an object during a call. Closing removes the entry first; a generation prevents slot reuse from reviving stale handles.

A close/use race linearizes at registry lookup. An operation that acquires its temporary owner first may complete after close returns; an operation that looks up the handle after removal receives stale_handle. This rule must be tested and documented rather than inferred from scheduling.

This is a hypothesis to test, not an accepted public representation. The registry must be bounded, must return explicit exhaustion, and must not hold its global lock while performing storage I/O or Argon2 work. Fork/process cloning, dynamic unload, and calls from signal handlers are unsupported.

Panic And Reentrancy Policy

Every exported entry point is wrapped at the adapter boundary. A panic becomes a boundary failure or owned structured error, publishes no fabricated success payload, and marks an affected database handle poisoned when its state may be uncertain. Panic text is not a stable error and is not exposed by default.

The ABI invokes no application function pointer. Progress, cancellation, and asynchronous platform authorization are absent from v1. If later evidence requires them, AR-0017 must define thread, reentrancy, lifetime, shutdown, and late-completion behavior first.

Multi-Mutation Batch

No transaction_begin operation exists in this schema. The current Rust write transaction remains a closure-scoped borrow and never survives a call. AR-0019 admits a private adapter-owned batch that copies at most 1,024 commands and 16 MiB of logical key/value payload, applies unconditional put/delete commands in append order, and is consumed before core execution. Close is an explicit abort before execution. Empty execution is rejected and consumed.

The batch is deliberately not called a transaction: it has no staged reads, conditions, savepoints, returned commit generation, or resubmission. Exact generation and conditional results require a new core contract; a racy later connection observation cannot fabricate them.

Required Falsification Before Implementation Graduation

  • stale generation never resolves after registry slot reuse;
  • wrong-kind handles cannot dispatch to another object's operation;
  • double close and close/use races never double free;
  • null/length and capacity failures write no partial output;
  • panic injection never unwinds into C or returns success;
  • database close does not invalidate a live snapshot;
  • error and byte objects remain valid after database close;
  • thread-affinity rejection is deterministic; and
  • allocation/registry exhaustion is explicit and leak-free.
  • copied batch input is independent of caller mutation after append;
  • batch limit rejection preserves the builder and never reaches the database;
  • returned error and panic after staged commands publish none of them; and
  • execute/close races produce exactly one consuming winner.

Disposition

Suitable as the contract for a private C experiment. It does not yet justify exported stable symbols, headers distributed to consumers, a public ABI version, unsafe-code exceptions beyond a narrowly reviewed adapter, or mobile support.