Skip to content

The C boundary

The shell and the core meet at a single C header, textchum.h. It is generated from the Rust source by cbindgen during every core build, and committed so shell-side tooling works without a Rust toolchain. CI fails if a build leaves the header out of date, so it can never drift from the code.

Conventions

Every function in the interface follows the same small set of rules.

Opaque handles. The core's types (TcApp, TcBuffer) are opaque structs; callers hold pointers, pass them back, and release them with the matching tc_*_free function. Nothing is ever allocated by the caller on the core's behalf.

UTF-8 in, explicit lengths. Strings passed into the core are (pointer, length) pairs of UTF-8 bytes — no nul terminators required, no encodings other than UTF-8. Strings returned by the core are nul-terminated UTF-8 owned by the core; release them with tc_string_free.

Two position units. Functions address text either in UTF-8 byte offsets (the core's native unit) or in UTF-16 code units (suffixed _utf16), because that is what NSRange and the Language Server Protocol count in. The core does the conversion; callers use whichever unit they naturally have.

Transactional failure. Fallible calls return bool. false means the input was validated, rejected, and nothing changed — an out-of-bounds offset, a mid-character byte position, invalid UTF-8. Callers can always treat failure as "the operation did not happen".

Panics do not cross. Every entry point catches Rust panics and converts them into the function's failure value. A bug in the core cannot unwind into Swift stack frames.

One thread in, one thread out. Calls into the core must come from a single thread. Events flow the other way through the callback registered with tc_app_new, invoked on a single core-owned dispatch thread. The callback's job is to marshal to the shell's UI thread; TextchumKit does exactly that and nothing else.

The event channel

Some information originates inside the core (today: pong replies used to verify the channel; next: diagnostics from language servers, highlight invalidations). It reaches the shell as a TcEvent — a kind discriminant plus event payload — delivered to the registered callback.

Shells must tolerate unknown kind values: a newer core emitting an event an older shell does not understand is forward compatibility, not an error.

tc_app_free blocks until queued events have been delivered and guarantees the callback is never invoked afterwards, which is what makes teardown safe to write on the shell side.

Current surface

Function Purpose
tc_version Core version as a static string.
tc_app_new / tc_app_free Create/destroy a core instance and its event channel.
tc_app_ping Request an async pong; exercises the event path.
tc_buffer_new / tc_buffer_free Create/destroy a text buffer.
tc_buffer_insert Insert UTF-8 at a byte offset.
tc_buffer_delete Delete a byte range.
tc_buffer_replace_utf16 Replace a UTF-16 code unit range — the shape of an AppKit edit.
tc_buffer_text Copy out the full contents.
tc_buffer_len_bytes / tc_buffer_len_utf16 Lengths in both units.
tc_document_new / tc_document_open / tc_document_free Create a document (empty, or from a file) and destroy it.
tc_document_replace_utf16 Edit a document, recording the undo history.
tc_document_undo / tc_document_redo Walk the history; an out-parameter reports the edit for the shell to replay.
tc_document_break_undo_group End the current undo coalescing run.
tc_document_save / tc_document_save_as Atomic saves; failures fill an optional message out-parameter.
tc_document_text / tc_document_len_bytes / tc_document_len_utf16 Contents and lengths.
tc_document_is_dirty / tc_document_can_undo / tc_document_can_redo State queries.
tc_document_path / tc_document_encoding_name File identity and encoding.
tc_string_free Release a core-returned string.

Fallible file operations follow one more convention: they return their failure value and, when the caller passed a non-null out-parameter, store a human-readable UTF-8 message there (released with tc_string_free) — the shell shows it in the alert verbatim.

The surface is deliberately small and grows only when a shell feature needs it. Bulk data (future: highlight spans, diagnostics) will cross as compact structs or serialized payloads rather than one call per item.

How Swift consumes it

The CTextchum target wraps the header as a Clang module, so Swift imports it like any library. TextchumKit then translates raw calls into idiomatic Swift — classes with deinit-based ownership, NSRange parameters, thrown errors for rejected operations, and typed events delivered on the main actor. Application code never touches a pointer.