TinyBus Module ABI Explained: How OpenHuman Loads Native Extensions

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


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 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 implements the complete loading workflow. This is the entry point for all module interactions.

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, then run the full ABI descriptor checks
  5. Broker attachment — dlopen the module and connect it to its dedicated TinyBus broker in 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.

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:

  • 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 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 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 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 implements the full resolution, download, and verification pipeline
  • 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 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, 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →