Skip to content

Interop overview

Geas connects languages through one artifact: the contract. A program declares contracts, the compiler builds them into a shared library, and any language that can load a C library signs and fulfills them. No protocol definitions, no serialization boilerplate, no hand written FFI bindings. This page is the model; the pages that follow drive it from C, from Python, and across processes over gRPC.

The runtime is a shared C library, libgeasrt. A compiled .geas.so module carries descriptor tables, static data that spells every contract, every pledge and its signature, and every vow and its default. When the runtime loads the module it registers all of that in the iname table, a sorted registry keyed by a mangled name that bakes in the contract, the pledge, a 64 bit hash of the type signature, and a version.

A host looks a contract up by that name. Because the name carries the shape hash, a version mismatch is a descriptive error at discovery time instead of silent corruption at call time: a host holding yesterday’s header misses today’s module cleanly, and a sign under the wrong expected hash is refused with GEAS_ERR_VERSION before an instance exists.

Every pledge a module compiles gets a mangled symbol in one grammar:

__geas_ash_{contract}_{pledge}_{sighash}_v{version}

The signature hash keeps two revisions of a pledge apart even when nothing about the surrounding contract moved; the version keeps two shipped generations apart on purpose. Hosts never have to spell these strings by hand: geas emit-header writes them as C defines, and geas emit-proto pins the shape hash into the wire artifacts.

Every host, in every language, drives a contract the same way:

  1. Initialize the runtime.
  2. Load a compiled module.
  3. Sign a contract, locking its vows.
  4. Fulfill its pledges and read the results.
  5. Break the contract, reclaiming everything.

In C those are geas_runtime_init, geas_module_load, geas_contract_sign, geas_pledge_fulfill_sync, and geas_contract_break. Every other surface, the Python wrapper included, is those calls with a thinner or thicker coat of paint. The lifecycle behind them is the language’s own; see Contracts and the lifecycle.

Everything crosses the boundary by value in a GeasValue, a small tagged union whose layout is fixed and documented in the ABI reference. Integers, floats, strings, lists, tuples, records, sums, options, and results all have a pinned representation:

typedef struct GeasValue {
uint32_t ty; /* GeasTypeTag, picks the union arm */
uint32_t tag; /* variant for the sum shaped types, 0 otherwise */
union {
int64_t i; /* Int */
uint64_t u; /* UInt */
double f; /* Float */
uint8_t b; /* Bool and Byte */
uint32_t ch; /* Char, a Unicode scalar value */
GeasString s; /* String */
GeasList list; /* List */
void* box; /* Option and Result payloads */
} as;
} GeasValue;

The numeric widths are fixed forever, and there is no implicit conversion at the boundary because there is none in the language.

The runtime owns every heap allocation that crosses the line. The rule has three consequences, and they are the whole memory story:

  • Arguments are deep copied onto the instance at fulfillment entry, on the caller’s thread. Once the fulfill call returns, the runtime holds nothing the host owns, and the host may free or reuse its argument memory immediately. A pledge body never sees host memory.
  • Results are owned by the instance that produced them and die when it breaks. A host that wants to keep a result copies it out before breaking. A host never has to free what Geas gave it.
  • A value passed by reference is copied in on entry and written back on return, never held by Geas past the call.

Breaking a contract frees everything the instance allocated in one walk: thunk allocations, boxed payloads, concatenated strings, copied vow values, argument frames, and ref slots.

Trailing parameters can pass by reference. The protocol has exactly two moments, both on a thread the host is blocked in a Geas call with, so the runtime never touches host memory behind the host’s back:

  1. Copy in, at fulfillment entry: the referenced value is deep copied onto the instance as a mutable slot. From here the pledge body mutates instance memory only.
  2. Write back, at delivery: each slot’s final value goes back to host memory, through a callback the host supplied or by the default otherwise.

Write back happens only when the fulfillment itself reported GEAS_OK; a thunk error leaves host memory untouched.

Fulfillment has one real shape, the future. geas_pledge_fulfill starts a fulfillment and hands back its receipt; geas_future_wait blocks until the outcome exists and collects it exactly once. Every later wait on the same future is GEAS_ERR_STATE.

The two calls bracket real concurrency: the fulfill validates and copies in on the caller’s thread, a pool worker runs the pledge body, and the wait delivers the value and performs the ref write back on the waiting thread. Every fulfillment error is delivered by the wait rather than the fulfill, so a host’s error handling has one place to live. The synchronous form, geas_pledge_fulfill_sync, is the two calls fused on the same internal path.

A pledge that returns Err still reports GEAS_OK from the runtime call: the status word says whether the fulfillment ran, the value says what the pledge decided. A non GEAS_OK status means the fulfillment itself did not run: wrong state, unknown name, argument mismatch, shape skew. The split holds in every host language and across the wire; see Errors as values.

geas_runtime_freeze latches the registration surface shut. After it, geas_module_load and geas_pledge_bind report GEAS_ERR_STATE, while signing, fulfilling, waits, vow reads, breaks, and every iname read continue unchanged. The call is idempotent and safe from any thread.

The point is the discovery contract: once a host has frozen the runtime, the iname table it enumerates is the table every later sign resolves against, byte for byte, and no module loaded behind its back can move a name.

The runtime owns a worker pool, four workers by default. Every instance carries one recursive mutex covering fulfillment validation and copy in, the whole thunk run, the outcome latch, and break. Fulfillments against one instance serialize in queue order, while distinct instances run truly in parallel.

Break against in flight work is safe by construction: a fulfillment racing a break resolves to exactly one of two outcomes, delivered before the break won or GEAS_ERR_STATE, never a crash and never freed memory.

  • C hosts: the ABI driven directly, the shape every other host builds on.
  • Python hosts: the same surface through ctypes, with no header parsed and no code generated.
  • The gRPC bridge: the same contracts across processes, the wire surface emitted by the compiler.
  • Park and resume: an instance’s durable state surviving its process.