# TinyBus Module ABI Explained: How OpenHuman Loads Native Extensions

> Understand the TinyBus module ABI, a strict contract for cdylib libraries. Learn how OpenHuman validates, matches platforms, and verifies SHA-256 digests for native extension loading.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: internals
- Published: 2026-08-31

---

**The TinyBus module ABI is a strict contract that compiled `cdylib` libraries must satisfy before OpenHuman's core loads them, covering descriptor validation, platform matching, and SHA‑256 digest verification.**

The **TinyBus module ABI** defines exactly how native, dynamically-linked libraries integrate with the OpenHuman runtime. In `tinyhumansai/openhuman`, modules are self-contained `cdylib` artifacts that extend core capabilities—such as ML inference, document processing, or memory management—while remaining sandboxed in their own in-process TinyBus broker. This article breaks down the ABI's validation gates, shows how to author compliant modules, and walks through the loading pipeline implemented in the source code.

---

## What Defines a TinyBus Module

A **module** in OpenHuman is not merely a shared library. It is a `cdylib` downloaded from a pinned release, verified against a known SHA‑256 digest, and attached to a dedicated TinyBus broker that mediates all RPC traffic.

According to [`src/openhuman/modules/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/mod.rs), modules expose callable objects through the `#[tinybus::interface]` macro. This macro registers an **interface name** (the bus name) and an **object path** that callers use to locate methods. The host installs its side of the contract via `connection.serve_at` before requesting the module's bus name.

Modules live for the entire process lifetime. TinyBus never unloads a library, and a failed load is cached for the remainder of the run—this behavior is enforced in [`src/openhuman/modules/ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/ops.rs).

---

## TinyBus Module ABI Validation Checks

Before `dlopen` executes, TinyBus inspects the **module descriptor** against seven strict criteria. These checks form the ABI gate that protects the host process from incompatible or tampered code.

| Check | Purpose |
|-------|---------|
| **Magic number** | Confirms the file is a TinyBus module, not an arbitrary shared library |
| **ABI revision** | Ensures the binary layout matches the core's expected version |
| **Descriptor layout** | Validates object names, paths, and structural metadata |
| **Target triple** | Matches the compiled target (e.g., `x86_64-unknown-linux-gnu`) against the host |
| **Pointer width / endianness** | Rejects 32-bit modules on 64-bit hosts or mismatched endianness |
| **Feature bits** | Flags such as `panic = abort` cause rejection—modules must use unwind-safe panics |
| **Digest verification** | Compares the SHA‑256 hash from [`src/openhuman/modules/registry.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/registry.rs) against the downloaded artifact |

If any check fails, TinyBus returns a **sanitized error**—no file paths or URLs are exposed to the caller. This hardening prevents information leakage about the module resolution pipeline.

---

## Module Loading Pipeline in ops.rs

The `ops::ensure_loaded` function in [`src/openhuman/modules/ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/ops.rs) implements the complete loading workflow. This is the entry point for all module interactions.

```rust
use crate::openhuman::config::Config;
use crate::openhuman::modules::ops;

async fn use_documents_module(config: &Config) -> Result<(), String> {
    ops::ensure_loaded(config, "tinydocs").await?;
    
    let runtime = modules::host::runtime().await?;
    let proxy = runtime
        .connection()
        .proxy("ai.tinyhumans.tinydocs", "/ai.tinyhumans.tinydocs.DocumentWriter")?;
    
    // proxy.call(...).await to invoke RPC methods
    Ok(())
}

```

`ensure_loaded` executes this resolution sequence:

1. **Cache check** — Skip if the module is already loaded or marked failed for this process
2. **Local resolution** — Search for a developer override, installed artifact, or TinyBus search path match
3. **Remote fetch** — Download the pinned release if enabled and local sources exhaust
4. **Verification gate** — Validate the SHA‑256 digest against [`registry.rs`](https://github.com/tinyhumansai/openhuman/blob/main/registry.rs), then run the full ABI descriptor checks
5. **Broker attachment** — `dlopen` the module and connect it to its dedicated TinyBus broker in [`host.rs`](https://github.com/tinyhumansai/openhuman/blob/main/host.rs)

---

## Declaring a TinyBus Interface in a Module

Modules expose functionality through the `#[tinybus::interface]` macro. The contract-generated names ensure synchronization between host and module.

```rust
use tinybus::ObjectPath;
use tinyjuice_bus::names::{ML_HOST_NAME as NAME, ML_HOST_PATH as PATH};

#[derive(Clone)]
struct MlHost;

#[tinybus::interface(name = "ai.tinyhumans.tinyjuice.MlHost")]
impl MlHost {
    async fn compress(
        &self,
        text: String,
        options: serde_json::Value,
    ) -> tinybus::Result<Option<String>> {
        let options = serde_json::from_value(options).map_err(method_error)?;
        crate::openhuman::inference::tokenjuice::ml::compress(&text, &options)
            .await
            .map_err(method_error)
    }
}

fn method_error(error: impl std::fmt::Display) -> tinybus::Error {
    tinybus::Error::MethodFailed {
        name: "ai.tinyhumans.tinyjuice.Error.Host".to_string(),
        message: error.to_string(),
    }
}

pub async fn install(connection: &tinybus::Connection) -> tinybus::Result<()> {
    connection
        .serve_at(ObjectPath::new(PATH)?, MlHost)
        .await?;
    connection.request_name(NAME).await
}

```

Key implementation details from [`src/openhuman/modules/tokenjuice_host.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/tokenjuice_host.rs):

- The **interface name** `ai.tinyhumans.tinyjuice.MlHost` is a globally unique bus identifier
- The **object path** comes from the generated `tinyjuice_bus` crate, preventing drift between contract and implementation
- Host-side objects use `connection.serve_at` to install callbacks that modules can invoke

---

## Host-Side Broker Architecture

[`src/openhuman/modules/host.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/host.rs) creates a **separate TinyBus broker** for every loaded module. This isolation prevents modules from directly accessing the host's main bus or other modules' interfaces.

The `ModuleHost::new` constructor attaches the module loader to this dedicated broker. As noted in comments at lines 169–172, the following descriptor fields are verified automatically:

- ABI revision and descriptor layout
- Target triple, pointer width, and endianness
- Feature bits, with explicit rejection of `panic = abort` modules

Once admitted, the module communicates exclusively through its broker. The host obtains proxies to module objects via `runtime.connection().proxy(bus_name, object_path)`.

---

## Registry and Security Model

[`src/openhuman/modules/registry.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/registry.rs) serves as the **source of truth** for the ABI gate. It contains:

- The compile-time list of available modules
- Pinned release versions for each module
- SHA‑256 digests used during [`ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/ops.rs) verification

This registry-enforced trust model means:

- Only modules with pre-registered digests can load
- Version pinning prevents supply-chain drift
- Digest mismatches abort loading before any code executes

---

## Summary

- The **TinyBus module ABI** is a multi-layer contract spanning descriptor validation, platform matching, and cryptographic verification
- **Seven checks** run before `dlopen`: magic number, ABI revision, descriptor layout, target triple, pointer width/endianness, feature bits, and SHA‑256 digest
- Modules are **process-immortal**—once loaded or failed, they remain cached for the process lifetime
- **`ops::ensure_loaded`** in [`src/openhuman/modules/ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/ops.rs) implements the full resolution, download, and verification pipeline
- **[`host.rs`](https://github.com/tinyhumansai/openhuman/blob/main/host.rs)** provides **broker isolation** so each module operates in its own TinyBus instance
- The **`#[tinybus::interface]`** macro binds interface names and object paths to concrete Rust implementations

---

## Frequently Asked Questions

### What happens if a module's SHA‑256 digest doesn't match the registry?

Loading **aborts immediately** with a sanitized error. The mismatch is detected in [`src/openhuman/modules/ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/ops.rs) before any descriptor checks or `dlopen` calls occur. No file paths or URLs are leaked to the caller.

### Can TinyBus unload a module to free memory?

**No.** As implemented in [`src/openhuman/modules/ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/ops.rs), modules are **never unloaded** once admitted. A failed load is also cached for the process lifetime to prevent repeated expensive resolution attempts.

### Why does the ABI reject modules compiled with `panic = abort`?

The `panic = abort` strategy terminates the entire process on panic, violating TinyBus's isolation guarantees. The feature bit check in [`src/openhuman/modules/host.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/modules/host.rs) explicitly rejects such modules to preserve host stability.

### How do host and module stay synchronized on method signatures?

Both sides import names from a **generated `-bus` crate** (e.g., `tinyjuice_bus`). This crate contains the interface name and object path constants, ensuring the host's `serve_at` call and the module's bus registration refer to identical identifiers.