Skip to content

Latest commit

 

History

History
355 lines (299 loc) · 19.3 KB

c-api.md

File metadata and controls

355 lines (299 loc) · 19.3 KB

C boundary — pre-release ABI 1

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.

Build, install and link

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-install

The 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
./demo

For 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-static

The .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.

Ownership and lifecycle

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.

Records, pointers and UTF-8

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.

Presentation extension 1

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.

Events and errors

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.

Completion, history and external output

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.

Restoration, signals and panic containment

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.

Executable authority and current limits

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.py

The 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.