Cross-mod: bus, services and lanes¶
Section notes in abi.h
Cross-mod event bus
See PierBusCb above for why the loader owns the table instead of mods exchanging pointers. All four are thread-safe; callbacks run on the publishing thread.
A mod does not receive its own publishes. Two reasons: a mod that wants to notify itself has a direct function call available, and self-delivery is the one loop shape that no depth limit can distinguish from legitimate work. Cross-mod loops (A publishes β B's handler publishes β A's handler publishes ββ¦) are caught by a depth cap instead; hitting it drops the innermost publish and logs once.
Cross-mod service registry (query-style calls)
The bus is one-way broadcast; this is request/response. The shapes differ on every axis, which is why they are separate tables rather than one:
| Text Only | |
|---|---|
Registration is EXCLUSIVE. Two mods answering plot:can is not "both run" β it is an ambiguous answer with no way for the caller to pick, so the second registrar is refused loudly. Silent last-wins would make the answer depend on mod load order, which nobody controls and which changes when an unrelated mod is installed.
Ownership follows the same weak_ptr + ticket discipline as the bus and the forms: the loader keeps the table, and the call path revalidates the provider immediately before crossing into its dylib.
Synchronous, on the caller's thread, no timeout. A provider that blocks blocks the server thread exactly like any other callback; returning "timed out" while the callback kept running would hand the caller a wrong answer AND leave the provider running.
Same-toolchain fast lane, appended and struct_size-gated.
Five slots appended without touching PIER_ABI_VERSION: a pure append is not a version change, and struct_size is the precise gate.
Both directions hold. A new loader running an old mod: the old table is a byte-identical prefix of the new one, the mod cannot reach these five slots, and it works unchanged. A new mod on an old loader: SDK runtime init compares struct_size, finds the loader's table shorter than the one it was compiled against, and refuses to load. That is the right outcome, since a mod that reads the lane_publish cell on a loader without it would read out of bounds.
In short: the version number tracks "semantics changed", struct_size tracks "the table grew". This change is only the latter.
See the long comment at PierLaneDesc above. In one line: service is the cross-language (name, JSON) -> JSON channel, while this is a direct function-table call that holds only when both sides were built by the same toolchain; a fingerprint mismatch yields no pointer and the consumer falls back to service.
Server thread only.
Appended: bulk block reads and writes
Slots¶
bus_subscribe¶
| C | |
|---|---|
Subscribe mod to topic. Returns a subscription id (>0), or 0 if the topic is empty/oversized, the callback is null, or the mod is unknown. Subscriptions are dropped automatically when the mod unloads.
- Call:
api->bus_subscribe(mod, topic, cb, user) - Parameters:
- mod :
PierModHandle - topic :
PierStr - cb :
PierBusCb - user :
void*
- mod :
- Return type:
uint64_t - Section of abi.h: Cross-mod event bus
- Position in the table: slot 157, counting from 0
- Callers in each binding:
- Rust:
bus::subscribe - Go:
BusSubscribe - Zig:
busSubscribe
- Rust:
bus_unsubscribe¶
| C | |
|---|---|
Drop one of this mod's subscriptions. Scoped to the caller β a mod cannot unsubscribe another mod. Returns true if one was removed. Safe to call from inside a callback (including one's own).
- Call:
api->bus_unsubscribe(mod, sub_id) - Parameters:
- mod :
PierModHandle - sub_id :
uint64_t
- mod :
- Return type:
bool - Section of abi.h: Cross-mod event bus
- Position in the table: slot 158, counting from 0
- Callers in each binding:
bus_publish¶
| C | |
|---|---|
Deliver payload to every *other* mod subscribed to topic. Returns how many subscribers actually ran (0 is normal β nobody is listening). Return values from subscribers are ignored.
- Call:
api->bus_publish(mod, topic, payload) - Parameters:
- mod :
PierModHandle - topic :
PierStr - payload :
PierStr
- mod :
- Return type:
uint32_t - Section of abi.h: Cross-mod event bus
- Position in the table: slot 159, counting from 0
- Callers in each binding:
- Rust:
bus::publish - Go:
BusPublish,Raw.BusPublish - Zig:
busPublish
- Rust:
bus_publish_vetoable¶
| C | |
|---|---|
As above, but collects the veto bit: returns true when any subscriber returned true. Every subscriber still runs β no short-circuit β so observers see a consistent stream whether or not an earlier one refused. out_delivered may be NULL.
- Call:
api->bus_publish_vetoable(mod, topic, payload, out_delivered) - Parameters:
- mod :
PierModHandle - topic :
PierStr - payload :
PierStr - out_delivered :
uint32_t*
- mod :
- Return type:
bool - Section of abi.h: Cross-mod event bus
- Position in the table: slot 160, counting from 0
- Callers in each binding:
bus_subscriber_count¶
| C | |
|---|---|
How many subscribers a topic has right now, across all mods. Intended for skipping the cost of building a payload nobody will read.
- Call:
api->bus_subscriber_count(topic) - Parameters:
- topic :
PierStr
- topic :
- Return type:
uint32_t - Section of abi.h: Cross-mod event bus
- Position in the table: slot 161, counting from 0
- Callers in each binding:
service_register¶
Register mod as the provider of name. Returns a registration id (>0), or 0 if the name is empty/oversized/already taken, the callback is null, or the mod is unknown. Dropped automatically on unload.
- Call:
api->service_register(mod, name, cb, user) - Parameters:
- mod :
PierModHandle - name :
PierStr - cb :
PierServiceCb - user :
void*
- mod :
- Return type:
uint64_t - Section of abi.h: Cross-mod service registry (query-style calls)
- Position in the table: slot 163, counting from 0
- Callers in each binding:
- Rust:
service::register,service::register_json - Go:
RegisterService - Zig:
registerService
- Rust:
service_unregister¶
| C | |
|---|---|
Drop one of this mod's registrations. Scoped to the caller β a mod cannot unregister another mod's service.
- Call:
api->service_unregister(mod, reg_id) - Parameters:
- mod :
PierModHandle - reg_id :
uint64_t
- mod :
- Return type:
bool - Section of abi.h: Cross-mod service registry (query-style calls)
- Position in the table: slot 164, counting from 0
- Callers in each binding:
service_call¶
| C | |
|---|---|
Call name with request; the provider's answer arrives through reply. Returns one of PIER_SERVICE_*. A mod cannot call its own service (it has a direct function call, and self-calls are the least legible loop shape).
- Call:
api->service_call(mod, name, request, ctx, reply) - Parameters:
- mod :
PierModHandle - name :
PierStr - request :
PierStr - ctx :
void* - reply :
PierStrSink
- mod :
- Return type:
int32_t - Section of abi.h: Cross-mod service registry (query-style calls)
- Position in the table: slot 165, counting from 0
- Callers in each binding:
service_list¶
| C | |
|---|---|
Every registered service as a JSON array of {"name":β¦,"mod":β¦}. For diagnostics and for a caller deciding whether to build a request nobody can answer.
- Call:
api->service_list(ctx, sink) - Parameters:
- ctx :
void* - sink :
PierStrSink
- ctx :
- Section of abi.h: Cross-mod service registry (query-style calls)
- Position in the table: slot 166, counting from 0
- Callers in each binding:
lane_publish¶
| C | |
|---|---|
Publish a lane. Exclusive, same discipline as service_register: if the name is taken, return 0 and name the incumbent in the log. Returns a publish id (> 0), withdrawn automatically at unload.
- Call:
api->lane_publish(mod, name, desc) - Parameters:
- mod :
PierModHandle - name :
PierStr - desc :
PierLaneDesc const*
- mod :
- Return type:
uint64_t - Section of abi.h: Same-toolchain fast lane, appended and struct_size-gated.
- Position in the table: slot 172, counting from 0
- Callers in each binding:
- Rust:
lane::publish
- Rust:
lane_unpublish¶
| C | |
|---|---|
Withdraw a lane owned by this mod. Calls release for every outstanding lease and clears the liveness flag, so a consumer's next check sees the lane gone instead of jumping through a dead pointer.
- Call:
api->lane_unpublish(mod, pub_id) - Parameters:
- mod :
PierModHandle - pub_id :
uint64_t
- mod :
- Return type:
bool - Section of abi.h: Same-toolchain fast lane, appended and struct_size-gated.
- Position in the table: slot 173, counting from 0
- Callers in each binding:
lane_acquire¶
| C | |
|---|---|
Acquire a lane. want_fingerprint must be the value the consumer computed itself.
0 is not a valid fingerprint and always yields PIER_LANE_FINGERPRINT. It must not mean "skip the check": this call hands over raw vtable and data pointers, which the consumer then calls through its own table offsets, so skipping the check is type confusion. To inspect which lanes exist and what their fingerprints are, use lane_list, which hands over no pointers at all.
Returns a PIER_LANE_* value. After a successful acquire, lane_release is mandatory, or the provider's state stays retained.
- Call:
api->lane_acquire(mod, name, want_fingerprint, out) - Parameters:
- mod :
PierModHandle - name :
PierStr - want_fingerprint :
uint64_t - out :
PierLaneRef*
- mod :
- Return type:
int32_t - Section of abi.h: Same-toolchain fast lane, appended and struct_size-gated.
- Position in the table: slot 174, counting from 0
- Callers in each binding:
- Rust:
lane::acquire,lane::acquire
- Rust:
lane_release¶
| C | |
|---|---|
Return a lease. Only a lease held by this mod. Returns false when the provider is already gone, since the loader has called release for it by then and calling again would be a double free.
- Call:
api->lane_release(mod, lease) - Parameters:
- mod :
PierModHandle - lease :
uint64_t
- mod :
- Return type:
bool - Section of abi.h: Same-toolchain fast lane, appended and struct_size-gated.
- Position in the table: slot 175, counting from 0
- Callers in each binding:
- Go:
Raw.LaneRelease
- Go:
lane_list¶
| C | |
|---|---|
Every lane, as a JSON array: [{"name":β¦,"mod":β¦,"fingerprint":"0xβ¦","protocol":1,"leases":N,"alive":true}]
- Call:
api->lane_list(ctx, sink) - Parameters:
- ctx :
void* - sink :
PierStrSink
- ctx :
- Section of abi.h: Same-toolchain fast lane, appended and struct_size-gated.
- Position in the table: slot 176, counting from 0
- Callers in each binding:
- Rust:
lane::list,lane::list,lane::list_json,lane::list_json - Go:
Raw.LaneList
- Rust:
service_caller¶
| C | |
|---|---|
Who is calling the service callback that is running right now.
A service callback receives a request and nothing about its sender, so a provider that keys anything on a name inside the request (an owner, an acting player) is trusting the request to tell the truth. This slot lets the provider ask the host instead: inside a PierServiceCb, the sink receives the manifest name of the mod whose service_call is on the stack. Calls nest, and the innermost one is reported.
Outside a callback, or when the call came without a mod handle, the sink is not called at all; a provider then knows it cannot attribute the request rather than attributing it to an empty name. Reads a thread-local, so any thread; a callback that hops threads loses the attribution, which is the correct answer there.
- Call:
api->service_caller(ctx, sink) - Parameters:
- ctx :
void* - sink :
PierStrSink
- ctx :
- Section of abi.h: Appended: bulk block reads and writes
- Position in the table: slot 197, counting from 0
- Callers in each binding:
- Rust:
service::caller - Go:
ServiceCaller,Raw.ServiceCaller - Zig:
serviceCaller
- Rust:
Macros¶
PIER_LANE_PROTOCOL¶
| C | |
|---|---|
Lane protocol version. Kept separate from PIER_ABI_VERSION: the lane shape evolves independently, and a mismatch is handled differently (reject this one lane, not the whole mod).
Types¶
PierBusCb¶
| C | |
|---|---|
Subscriber callback. topic and payload are borrowed for the duration of the call. Anything kept past it must be copied.
The return value is a veto, and only for bus_publish_vetoable:
It is ignored entirely by bus_publish. There is deliberately no way to turn a refusal back into an approval: a subscriber can only tighten, never loosen. Letting one mod override another's refusal means the *last* subscriber to run decides, and subscriber order is not something either mod controls.
Called on the thread that published. Never called after the owning mod is unloaded or while it is disabled.
PierServiceCb¶
| C | |
|---|---|
Provider callback for the cross-mod service registry (query-style calls, as opposed to the bus's one-way broadcast).
Write the answer through reply(ctx, ...) β exactly once β and return true. Return false to report failure; anything written first is handed to the caller as the error text, which is what makes "no such plot" and "the database is down" distinguishable at the call site.
request and reply are opaque UTF-8 the two mods agree on out of band. The loader never looks inside either.
Runs synchronously on the CALLING thread, inside service_call. Never called after the providing mod is unloaded. It IS still called while the provider is merely disabled: service_call deliberately does not consult isEnabled() (see Services.cpp) because LeviLamina enables mods only after every on_load has run, and a service that is unreachable during that window breaks every consumer that resolves it in its own on_load.
PierLaneRefFn¶
| C | |
|---|---|
Reference-count hooks, executed inside the provider's own dylib.
The loader calls them from lane_acquire and lane_release, and calls release for every outstanding lease when the provider unloads or calls lane_unpublish. Publishing itself does not hold a count: the loader never calls release for the data handed to lane_publish, and the provider reclaims that itself after unpublishing.
These must not call back into the loader (any lane_* slot); that self- deadlocks. A typical implementation is one atomic increment or decrement on the provider's own refcount, touching no lock.
PierLaneDesc¶
How a provider describes a lane when publishing it. Every field is filled in by the provider; the loader only carries it.
PierLaneRef¶
What lane_acquire produces.
PIER_SERVICE_*¶
service_call return codes.
| Name | Value | Description |
|---|---|---|
PIER_SERVICE_OK |
0 |
provider ran and wrote a reply |
PIER_SERVICE_NOT_FOUND |
1 |
nobody provides this name (or is disabled/unloaded) |
PIER_SERVICE_ERROR |
2 |
provider returned false; reply holds its message |
PIER_SERVICE_REFUSED |
3 |
bad name, self-call, or call-depth limit |
PIER_LANE_*¶
lane_acquire return values.
| Name | Value | Description |
|---|---|---|
PIER_LANE_OK |
0 |
acquired; out is filled in |
PIER_LANE_NOT_FOUND |
1 |
nobody published this name (that mod is not installed) |
PIER_LANE_FINGERPRINT |
2 |
published, but the fingerprint differs; degrade, hand over nothing |
PIER_LANE_REFUSED |
3 |
bad name, self-acquire, provider disabled, or protocol mismatch |