REPLAI exposes one POSIX C ABI for Linux and macOS through the separate replai-c
package. ABI 1 identifies an exact header/binary contract; it is not a promise
of long-term ABI or SemVer stability. No release has been published.
Producer requirements: current stable Rust, Python 3 and Linux or macOS. Consumer requirements: C11 compiler and standard headers; pkg-config is convenient. The C consumer does not need Cargo, Rust sources or private headers.
cargo build --locked --release -p replai-c
python3 tools/stage_c.py --prefix /tmp/replai-installThe prefix must be absent or empty. Installation writes only that prefix:
include/replai.h
lib/libreplai_c.a
lib/libreplai_c.so # Linux; libreplai_c.dylib on macOS
lib/pkgconfig/replai.pc
share/licenses/replai/LICENSE
Copy examples/c/demo.c into an unrelated build directory, then:
export PKG_CONFIG_PATH=/tmp/replai-install/lib/pkgconfig
cc -std=c11 -Wall -Wextra -Wpedantic -Werror demo.c \
$(pkg-config --cflags --libs replai) \
-Wl,-rpath,/tmp/replai-install/lib -o demo
./demoFor static REPLAI linkage use the archive explicitly (system dependencies may remain shared):
cc -std=c11 -Wall -Wextra -Wpedantic -Werror demo.c \
$(pkg-config --cflags replai) /tmp/replai-install/lib/libreplai_c.a \
$(pkg-config --libs-only-other --libs-only-l --static replai | sed 's/-lreplai_c//g') \
-o demo-staticThe .pc file derives paths from its installed location. It contains no author
workstation paths. Shared consumers must arrange loader search explicitly;
qualification verifies ldd resolves the staged .so. Mixing another REPLAI
copy into the same process is unsupported: the active-terminal lease is per
linked library image, not an inter-library or inter-process lock.
Initialize replai_config to zero, set struct_size = sizeof(config),
abi_version = REPLAI_C_ABI_VERSION, the byte capacity and history entry bound.
Compare replai_abi_version(&version) with the header macro before creation.
Pass an initialized NULL handle slot to replai_create. The library owns the
resulting allocation. replai_destroy(&handle) is its only release operation;
it closes any active terminal and sets the slot to NULL, including when close
reports an I/O failure. A second destroy of NULL returns INVALID_ARGUMENT.
The normal sequence is create → optional prompt/draft/history configuration →
open → poll/complete/output → outcome/close → configure/reopen or destroy.
Prompt and direct draft/history mutation require a closed interaction. close
is idempotent for a valid handle. Submit, interrupt and EOF restore the terminal
and release its duplicated descriptors. They retain the editor and history;
clearing and history admission remain host decisions. Successful reopen clears
the previous submitted-text snapshot, but failed open preserves it.
Input/output FDs are borrowed only during open. The library duplicates what
it needs; callers retain ownership and may close their originals after a
successful open. During the call they must keep the descriptors valid. The
library closes only its duplicates. Both FDs must designate the same suitable
TTY. Opening while already active returns INVALID_STATE. A second interaction
in the same library instance returns BUSY, even on another TTY.
Calls using the same handle must be serialized by the caller, including reads and destruction. No concurrent close/destroy is permitted. Coordinate all other terminal writers, mode changes and environment changes while opening. This is not an internally synchronized or broadly thread-safe API.
All externally governed tags are fixed-width integers, not C enums. Config and
event records have uint32_t struct_size, uint32_t abi_version, explicit
fields and two reserved uint64_ts. Size must equal the ABI 1 size; version must
be 1; reserved fields must be zero. Before each poll/interrupt initialize a
fresh zero event and set its size/version. Output fields are not request flags.
Unsupported size/version is ABI_MISMATCH, not a request to guess a layout.
Every text argument is pointer plus byte length. NULL is permitted only with
zero length. Storage must be live, readable for that extent and valid UTF-8.
No implicit strlen occurs. Length/address overflow, detectable alignment
errors and NULL misuse return INVALID_ARGUMENT. Arbitrary non-NULL invalid,
dangling, forged or inaccessible pointers cannot be reliably detected; those
remain caller contract violations. Record pointers must provide at least their
aligned size prefix and the full extent advertised by that prefix. Output
storage must be writable, properly aligned and disjoint from other arguments.
No reference, slice, Rust enum, bool, String or allocator-owned text crosses C.
The optional replai_presentation_version identity is separate from C ABI 1.
Existing ABI 1 records, functions and statuses retain their exact meanings.
| Extension tag | Value | Meaning |
|---|---|---|
| PRESENTATION_VERSION | 1 | Optional presentation identity, not the base ABI |
| BLOCK_PARAGRAPH | 0 | Semantic text |
| BLOCK_HEADING | 1 | level 1–3 |
| BLOCK_FIELD | 2 | label / text; adjacent fields form a record |
| BLOCK_LIST | 3 | One list item; depth 0–8 |
| BLOCK_LITERAL | 4 | Whitespace-preserving literal |
| BLOCK_STATUS | 5 | level info/success/warning/error = 0/1/2/3 |
| BLOCK_SPACER | 6 | One empty line |
| BLOCK_TABLE_HEADER | 7 | Column headings in cells |
| BLOCK_TABLE_ROW | 8 | Matching row after its header |
| APPLIED | 0 | Current revision presentation installed |
| STALE | 1 | No editor, presentation or output mutation |
| WAKE_INPUT | 0 | Advisory input-ready notification |
| WAKE_RESIZE | 1 | Host observed terminal resize |
| WAKE_DEADLINE | 2 | Opaque deadline ticket |
| WAIT_READY | 0 | Already-read bytes; advance without waiting |
| WAIT_INPUT | 1 | Wait for FD or deadline; timeout_ms = -1 means none |
Extension records carry an exact struct_size, extension_version = 1 and
reserved-zero fields. Hosts requiring the extension must link its qualified
producer; the identity query does not make old binaries export new symbols.
Semantic spans, flat document blocks, composed prompts and completion candidates
are copied during the call. No caller allocation is retained. Documents support
paragraphs, headings, key/value records, lists, literal blocks, notices, spacers
and responsive tables. Layout, safe roles, indentation and Unicode cell geometry
belong to REPLAI. Domain meaning and discovery remain host-owned. Standalone
rendering does not acquire an editor or terminal; it uses explicit width/style
and the existing exact-size caller-buffer contract. Controls cannot inject ANSI.
The Rust document limits also bound the C extension; fields are at most 16 KiB,
documents 1 MiB, 4096 blocks, rendering 8 MiB and 65,536 rows. Width is 2–4096
cells; an outer gutter is at most 16 cells and leaves at least two layout cells.
replai_role_sequence projects only the producer-authored foreground/weight
palette for existing host streaming serializers; it does not admit caller ANSI.
The host resolves style availability from the output capability and NO_COLOR.
Consecutive FIELD blocks compose one responsive record; each uses label and
text. TABLE_HEADER supplies 1–32 heading cells and consecutive TABLE_ROW blocks
supply exactly that many value cells. HEADER level is a column-alignment bitset:
bit n selects right-aligned values in column n; zero selects start alignment.
Bits beyond the supplied columns are refused. Wrong row geometry
returns INVALID_RANGE, an orphan row INVALID_ARGUMENT. Other blocks have no cells
or label. Unused embedded text records remain valid versioned empty records.
level is 1–3 for a heading, 0–8 for an unordered list item, 0–3 for
info/success/warning/error status, the alignment bitset for TABLE_HEADER, and zero
otherwise. Render width is 2–4096
cells, with an outer indent of 0–16 and at least two remaining content cells.
No fact is truncated merely to fit a narrow terminal.
Completion snapshots return a handle-local opaque ticket and a coherent draft with its byte cursor. One snapshot is outstanding per handle: a new capture supersedes the preceding ticket. Deliver candidates against the latest captured ticket, not against a fresh draft. When that captured revision is stale, delivery returns a stale disposition before inspecting candidate payload and performs no terminal I/O. Unknown or superseded tickets fail closed. Candidates distinguish insertion, label and optional annotation; the existing 4096-candidate/4-MiB aggregate bounds apply. All current candidates validate before the menu changes. Enter accepts a menu selection, not a submission.
Driven interest exposes only a borrowed readiness FD, ready/input disposition, relative deadline delay and opaque deadline ticket. Unregister before close; never read, close or alter the borrowed FD. The host owns resize notification and its reactor. Advance never waits. Early/stale deadlines are no-ops; no signal handler or thread is installed. ABI 1 polling remains supported.
The optional quiet-output scope is disjoint from editing and uses the same exclusive terminal lease. It suppresses echo, not ISIG or canonical input. Temporary semantic feedback is replaced/cleared without becoming transcript content. Callers establish a line boundary and clear before other writers run; this is not a multi-producer scheduler. Closing attempts both feedback cleanup and mode restoration. Queued-input discard is an explicit host-selected flag, never an implicit domain policy. Destroy also retires the scope. No unrestricted writer, raw ANSI injection, alternate screen or background painting is exposed.
An isolated ESC immediately hides a completion menu provisionally without discarding candidates or shortening the fragmented-sequence deadline. A later sequence continuation restores the menu and performs its original action.
Draft text accepts LF and TAB; other controls, including NUL and ESC, fail. Prompt fields accept no controls and are bounded to 1024 bytes each. They are literal label, suffix and continuation text, without ANSI. External text accepts LF/TAB and normalizes CRLF; rejected controls cannot inject terminal escapes. Core capacity is measured in UTF-8 bytes. C cursor/range values are byte offsets on extended grapheme boundaries, not scalar indices or display cells.
draft_copy and submitted_copy use caller-owned buffers. NULL/zero capacity
queries the exact required byte count. Exact-size and larger buffers copy that
many bytes, without a NUL terminator. A too-small buffer returns
BUFFER_TOO_SMALL, writes the required count (and draft cursor), and leaves all
buffer bytes untouched. It never produces a partial successful string.
submitted_copy requires an earlier submission and returns that entire input,
including newlines. No separate text-free function exists or is needed.
All functions return replai_status (int32_t). Event outputs are meaningful
only on OK. On a failed poll/interrupt the caller's event record stays unchanged.
| Event kind | Value | Host action / terminal state |
|---|---|---|
| NONE | 0 | No event this poll; continue; active |
| SUBMITTED | 1 | Copy complete input, decide admission/execution; closed |
| INTERRUPTED | 2 | Decide cancellation/clear/retry; closed |
| END_OF_INPUT | 3 | Decide exit; closed |
| COMPLETION_REQUESTED | 4 | Read draft/cursor and choose replacement; active |
| EDIT_REJECTED | 5 | event.status gives edit rejection; draft unchanged; active |
text_bytes and cursor_bytes describe the current draft at the event. On
submission text_bytes also matches the retained submitted snapshot. A call
returning OK can still report EDIT_REJECTED; it is not a terminal I/O error.
Each poll waits at most 100 ms, even with a larger requested timeout, and
consumes bounded already-ready input until the first event or a driver yield.
It never merges independent host events. Read-ahead stays with the opaque handle
across close/reopen; destroying that handle discards bytes it already consumed.
The shared input contract defines the
budgets and presentation boundaries. Host scheduling controls latency and output
timing. This internal scheduling change does not alter ABI 1 records, functions,
status values, validation, output-buffer or descriptor ownership rules.
| Status | Value | Contract |
|---|---|---|
| OK | 0 | Operation completed |
| INVALID_ARGUMENT | 1 | Detectable pointer/length/tag/reserved/FD misuse |
| INVALID_UTF8 | 2 | Invalid UTF-8 span |
| INVALID_RANGE | 3 | Unordered/out-of-bounds/non-grapheme replacement range |
| CAPACITY | 4 | Configured text or prompt bound exceeded |
| INVALID_STATE | 5 | Operation unavailable in current lifecycle |
| UNSUITABLE_TERMINAL | 6 | Non-TTY, mismatched TTY or unsupported dimensions |
| IO | 7 | Terminal syscall/read/write/cleanup failure |
| BUFFER_TOO_SMALL | 8 | Query reported required bytes; destination unchanged |
| ABI_MISMATCH | 9 | Unsupported record size/version |
| BUSY | 10 | Another active interaction owns the library lease |
| INTERNAL | 11 | A Rust panic was contained |
| INVALID_TEXT | 12 | Unsupported control text |
| HISTORY_DISABLED | 13 | Host configured no history entries |
| INVALID_SEQUENCE | 14 | Rejected terminal input sequence |
replai_status_text copies a generic diagnostic using the same buffer contract;
unknown codes return INVALID_ARGUMENT. Human messages are not machine tags. ABI 1
does not retain per-handle errno or OS-specific diagnostic strings.
Rejected validation operations preserve draft, cursor, history and terminal
state. Invalid event/config records do not consume input. Capacity and completion
failures are atomic. NULL/zero set_draft is valid and intentionally clears the
draft. Buffer-too-small may update required/cursor output fields as specified.
I/O failure is different: visible output may be partial; restoration is attempted
and the host should close/destroy before deciding whether reopening is safe.
Tab yields COMPLETION_REQUESTED. Read draft and byte cursor, discover candidates
outside the library, then call replai_complete(start, end, bytes, length) for a
host-selected replacement. No callback or registry exists. No candidates, multiple
candidates without a choice, or host lookup failure require no mutation.
Invalid UTF-8, capacity and grapheme-splitting ranges fail before redraw.
replai_history_add admits one entry while closed; the host owns persistence,
privacy, deduplication and admission. Up/Down use the same Rust history mechanism,
including restoring the pre-history draft and cursor. History zero is allowed.
replai_external_output(role, bytes, length) delegates to the single Rust
renderer. Style tags are:
| Role | Value |
|---|---|
| DEFAULT | 0 |
| STRONG | 1 |
| ACCENT | 2 |
| DIM | 3 |
| SUCCESS | 4 |
| WARNING | 5 |
| ERROR | 6 |
The transaction validates text, suspends the visible editing surface, writes a complete host text line, then redraws exact draft/cursor. It is a synchronous line transaction, not an arbitrary ANSI writer or no-newline stream. The terminal background remains default; no alternate screen is introduced.
The native backend restores the exact captured termios on normal outcomes, close, read/write failure, partial acquisition and ordinary unwinding. Cleanup also disables bracketed paste and releases owned FDs. A disconnected terminal can refuse restoration; IO reports that limit. Drop is best effort and does not panic or call process exit. No library can clean up after SIGKILL or process abort.
No signal handler, mask, background thread or cancellation policy is installed.
Raw-mode Ctrl-C is a decoded byte; explicit replai_interrupt supports a host
signal policy. Resize uses dimension polling. Signal handlers must not call
REPLAI: these functions are not async-signal-safe. Notify the serialized host loop.
Every exported function uses the same catch_unwind guard. The binding refuses
panic=abort builds. A contained operational panic returns INTERNAL, poisons the
handle and attempts close. Only close/destroy are then supported. Panic hooks
remain the host's Rust process policy. A panic payload whose own destructor
panics is separately caught; its secondary payload is deliberately retained to
prevent another unwind across C. This exceptional case is not a zero-allocation
recovery promise. Normal and tested error paths free every allocation. Allocation
failure or foreign undefined behavior cannot be converted into safe Rust unwind.
api/c-abi.json owns the narrow ABI declarations. tools/generate_abi.py --check
checks generated C/Rust records, numeric constants, probes and Rust function
pointer signature assertions. The real C and Rust probes compare size, alignment
and every field offset/tag. The C binding imports only public replai items.
Only its src/lib.rs dereferences C pointers, constructs borrowed FDs or manages
the opaque allocation. The implementation crate still forbids unsafe code.
Run the complete qualification with current stable Rust, cc/c++, pkg-config, Python 3, plus Valgrind/Landlock on Linux or native leaks/sandbox isolation on macOS. Install the documentation checker prerequisites from development as well:
python3 tools/qualify.pyThe command builds/tests Rust, stages release artifacts, compiles the independent
C consumer and layout/header probes, executes static/shared PTYs and adversarial
contracts, checks FD ownership and the platform memory tool, audits symbols/loader/metadata, runs
release Rust regressions, then requires a clean repository. --allow-dirty is a
development aid and explicitly does not qualify the clean closure gate.
--work /tmp/new-directory retains commands, layout, misuse, PTY JSON and memory
reports. The native CI separates prepare/static/shared/memory/audit gates.
Consumer compilation and execution deny repository reads with Landlock on Linux
or a generated sandbox policy on macOS;
only staged artifacts, copied C files and system toolchain/runtime are available.
The shared POSIX backend supports Linux and macOS. C11/C++17 headers, static and dynamic consumers, real PTYs and native resource gates run on both. This does not establish every compiler/architecture ABI or a Windows backend. Unicode width, terminal reflow, TERM=dumb, unframed paste and external-output limitations remain those in interaction and presentation. No consumer integration or release is performed by this qualification.
ABI 1 descriptor acquisition remains a POSIX-style Linux/macOS compatibility contract. The underlying Rust interaction engine is platform-neutral; this does not make integer file descriptors a Windows terminal endpoint contract. No new C acquisition API or ABI identity is introduced by the architecture refoundation.
The staged prefix also provides relocatable CMake package files. Consumers use
find_package(replai 0.1 CONFIG REQUIRED) and link exactly one of
replai::static or replai::shared; CMake consumes staged artifacts and does
not rebuild Rust. The versioned source-SDK build and moved-prefix contract is
documented in C SDK source bundle.