Skip to content

logos-co/logos-rust-sdk

Repository files navigation

logos-rust-sdk

A Rust SDK for calling other Logos modules from within a Logos module. It consumes the language-neutral lp_* C ABI from logos-protocol directly, and — together with logos-module-builder and its logos-lidl-gen code generator — gives you typed, generated clients for every module you depend on: typed sync/async calls and typed event subscriptions, with no hand-written IPC.

Overview

When you write a Logos module in Rust, everything that crosses the module boundary is generated from your .lidl contracts: the module-impl C ABI scaffold your code plugs into, and a typed client for each module you depend on. This SDK is the runtime those generated clients are built on — it binds the lp_* C ABI, serializes parameters, drives the callback trampolines, and delivers results over channels.

In day-to-day module code you rarely name a type from this crate directly. You call modules().<dep>.<method>(...) (or its _async twin), subscribe with on_<event>(), and read your module's context with context() — all generated. The SDK is what makes those calls work.

The SDK is a pure Rust rlib with no build-time C dependency. On the standard authoring path (a cdylib module built by logos-module-builder, interface = "cdylib") the lp_* symbols resolve against the logos-protocol archive already linked into the plugin — one protocol stack shared by the generated Qt glue and your Rust code.

The authoring model — no build.rs

You don't add this SDK to a Cargo.toml by hand or write a build.rs. A Rust module is:

  • a .lidl contract (or a Rust trait the builder derives one from),
  • an impl of the generated trait, plus a one-line logos_module_install() hook,
  • a metadata.json whose codegen.rust block tells the builder to run the Rust generator and compile your crate.

logos-module-builder then supplies this SDK as your crate's logos-rust-sdk dependency (matched to the generator it ran, so there is no version skew), runs logos-lidl-gen to emit the scaffold (generated/provider_gen.rs), compiles the crate to a static archive, and links it into the plugin. Your flake.nix and CMakeLists.txt end up as small as a C++ module's.

The complete, runnable walkthrough — a Rust provider, a C++ provider, and a Rust consumer wired together — is the executable doc-test doctests/cross-language-composition.test.yaml.

Calling other modules

Everything in this section is generated by the builder from the contracts and emitted into your crate. This is the entire cross-module surface you write against — every call is typed; there is no string-keyed dispatch in module code.

Concrete dependencies — modules()

A module you list in dependencies gets a typed client on the modules() aggregate, reached by its module name. The host auto-loads it with you.

impl MyModule for MyImpl {
    // Synchronous typed call.
    fn total(&mut self) -> i64 {
        modules().counter_module.increment(1).unwrap_or(-1)
    }

    // Asynchronous twin: returns immediately; the typed result is delivered to
    // the callback on the module's event loop, after this method returns.
    fn bump_async(&mut self) {
        modules().counter_module.increment_async(1, |res| {
            if let Ok(v) = res { /* stash v; read it back from a later method */ }
        });
    }
}

Interface dependencies — Client::bind(name)

Code against a contract shape (declared in interface_dependencies) and bind it to a concrete provider chosen at runtime — no build-time coupling to any particular module:

fn hello(&mut self, name: String) -> String {
    greeter::GreeterClient::bind("cpp_greeter_module")
        .greet(&name)
        .unwrap_or_else(|e| format!("greet failed: {}", e))
}

Typed events — on_<event>() / decode_<event>()

Each event in a dependency's contract generates a typed subscription. The returned EventSubscription owns its client share, so it keeps receiving after the proxy is dropped — move it into a listener thread and iterate it:

let mut calc = modules().rust_calc_module;
if let Ok(sub) = calc.on_computed() {
    std::thread::spawn(move || {
        for ev in sub {
            if let Some(e) = rust_calc_module::RustCalcModuleClient::decode_computed(&ev) {
                // e.total — the decoded, typed payload
            }
        }
    });
}

Module context — context() / RustModuleContext

The host stamps each loaded instance with its identity. Read it through the generated context():

fn whereami(&mut self) -> String {
    match context() {
        Some(c) => format!(
            "module_path={} | instance_id={} | persistence={}",
            c.module_path, c.instance_id, c.instance_persistence_path
        ),
        None => "context not ready".to_string(),
    }
}

instance_persistence_path is a writable, per-instance directory — the place to keep a module's on-disk state.

Supporting types

The handful of SDK types that surface directly in module code:

EventSubscription and EventData

on_<event>() returns an EventSubscription: a Send handle bundling the event channel with ownership of the lp subscription and a share of the client (dropping a bare proxy would otherwise silently kill the subscription). It supports recv(), try_recv(), blocking iteration (for ev in sub), and unsubscribes on drop. Each item is an EventData, which the generated decode_<event>() turns into the typed payload struct:

pub struct EventData {
    pub event: String,            // event name
    pub data: serde_json::Value,  // raw payload as JSON — use decode_<event>() for the typed form
}

LogosError

Every typed call and subscription returns Result<_, LogosError>:

Variant Cause
PluginCallFailed Method call returned an error from the remote module
EventListenerFailed Event registration failed
InvalidString A string argument contained a null byte
JsonError Parameter serialization failed
ChannelClosed The callback channel was dropped unexpectedly
Other Miscellaneous error with a descriptive message

How symbols resolve

logos-rust-sdk declares extern "C" bindings to the lp_* protocol functions but links no C library at Rust compilation time. The crate compiles to a staticlib containing unresolved lp_* references, which resolve at the final plugin link:

librust_my_module.a       (your Rust staticlib, contains unresolved lp_* refs)
        ↓
CMake links plugin .dylib
  + logos-protocol archive          ← lp_* resolved here (one shared stack
        ↓                              with the generated Qt glue)
my_module_plugin.dylib    (complete, loadable Logos module)

The builder's cmake/LogosModule.cmake stages and links the archive from codegen.rust automatically — the author writes no link lines.

Building

The SDK itself has no standalone Nix build artifact — it is a library crate consumed by module builds. To work on it:

# Enter a dev shell with Rust toolchain
nix develop

# Run unit tests (params serialization, etc.)
cargo test

Testing

Two complementary checks exercise the SDK and the Rust-module pipeline it builds on (the cross-language composition showcases — typed calls and events crossing the C++/Rust boundary in both directions — live in logos-module-builder's doctests, since they exercise the builder across both SDKs):

  • IPC integration test (tests/) — builds a minimal provider + caller module pair on the cdylib authoring path: each fixture is a .lidl contract from which lidl-gen --provider generates the Rust module-impl C ABI scaffold (logos_module_* exports, typed trait, RustModuleContext) and logos-module-builder (interface = "cdylib") generates the uniform Qt glue. The author writes the trait impl plus a #[no_mangle] fn logos_module_install() hook; the plugin links one logos-protocol stack shared by the glue and the SDK, so the host token forwarded through logos_module_accept_token authenticates the caller's outbound add() call. Verified end-to-end through logoscore:

    nix build 'path:./tests#checks.x86_64-linux.ipc-test' \
      --override-input logos-rust-sdk path:. --print-build-logs
  • Executable doc-test (doctests/cross-language-composition.test.yaml) — a step-by-step, runnable tutorial that writes three modules from scratch on the builder-driven cdylib path (no build.rs): a Rust provider, a C++ provider, and a Rust consumer that ties them together. It packages each as an .lgx, installs them with lgpm, and drives the consumer through a logoscore daemon to exercise the whole consumer surface — module context (module_path / instance_id / instance_persistence_path), sync and async typed calls, typed event subscription, and both a concrete and an interface dependency. Run it with the shared doctest CLI:

    cd doctests && ./run.sh

    The rendered tutorial is committed at doctests/outputs/cross-language-composition/cross-language-composition.md.

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE-v2
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors