> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ilyama.golain.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a node

> Write the hello node in Rust or C, build it as a .dylib or .so, cross-compile for Linux, and verify it with Omega before shipping.

This page builds the reference **hello** node for macOS and Linux and checks it with the exact introspection Omega runs at install time. Windows nodes are COM servers with a different build; see [Windows](#windows) at the end.

## Prerequisites

* A checkout of [`golain-io/omega-rs`](https://github.com/golain-io/omega-rs). Your node depends on its `node-abi` crate (Rust) or header (C), and you verify with the `omega-agent` binary it builds.
* A Rust toolchain, or a C compiler for the C path.
* For Linux builds from a Mac: [`cross`](https://github.com/cross-rs/cross) and Docker.

## Rust

The whole node is one crate with one dependency. Start by copying `hello-node/` from the Omega repository, or write the two files below.

```toml Cargo.toml theme={null}
[package]
name = "hello-node"
version = "0.1.0"
edition = "2021"

# cdylib gives libhello_node.dylib on macOS and libhello_node.so on Linux.
[lib]
crate-type = ["cdylib"]

# The only dependency. Pin the Omega revision you build against.
[dependencies]
node-abi = { git = "https://github.com/golain-io/omega-rs", rev = "<omega commit>" }
```

```rust src/lib.rs theme={null}
use node_abi::{export_node, Capabilities, Health, HealthStatus, Node, Op, NODE_ABI_VERSION};

// Your codes: 0 is success, everything else positive. Negative codes belong to Omega.
const OK: i32 = 0;
const ERR_UNKNOWN_OP: i32 = 1;
const ERR_BAD_PARAMS: i32 = 2;

struct HelloNode;

impl Node for HelloNode {
    fn domain() -> &'static str {
        "hello"
    }

    fn capabilities() -> Capabilities {
        Capabilities {
            abi_version: NODE_ABI_VERSION,
            domain: Self::domain().into(),
            ops: vec![
                Op {
                    name: "greet".into(),
                    idempotent: true,
                    cadence_secs: None,
                    input_schema: Some(node_abi::serde_json::json!({
                        "type": "object",
                        "properties": { "name": { "type": "string" } }
                    })),
                },
                Op {
                    name: "ping".into(),
                    idempotent: true,
                    cadence_secs: Some(3600), // scheduled: Omega runs it hourly
                    input_schema: None,
                },
            ],
        }
    }

    fn health() -> Health {
        Health { status: HealthStatus::Healthy, detail: "hello node ready".into() }
    }

    fn invoke(op: &str, request_json: &str) -> (i32, String) {
        let params: node_abi::serde_json::Value = match node_abi::serde_json::from_str(request_json) {
            Ok(v) => v,
            Err(e) => return (ERR_BAD_PARAMS, format!("params are not valid JSON: {e}")),
        };
        match op {
            "greet" => {
                let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("omega");
                (OK, format!("Hello, {name}!"))
            }
            "ping" => (OK, "pong".into()),
            other => (ERR_UNKNOWN_OP, format!("unknown op '{other}' — this node serves greet and ping")),
        }
    }
}

// Generates the five C exports, each wrapped in catch_unwind so a panic never crosses the FFI boundary.
export_node!(HelloNode);
```

Everything Omega calls is generated by `export_node!`. Your code is the `Node` trait: a domain, a capabilities document, a health answer, and one `invoke` function.

Build it:

```bash theme={null}
cargo build --release
```

| Platform | Output |
| - | - |
| macOS | `target/release/libhello_node.dylib` |
| Linux | `target/release/libhello_node.so` |

Confirm the five exports are visible:

```bash theme={null}
nm -gU target/release/libhello_node.dylib | grep node_   # macOS
nm -D  target/release/libhello_node.so    | grep node_   # Linux
```

You should see `node_abi_version`, `node_capabilities`, `node_domain`, `node_health`, and `node_invoke`. Nothing else needs to be exported.

## C or C++

Include the header Omega owns and define the same five functions. The header documents the buffer protocol every string-returning export uses: always write the byte length needed to `*out_len`, copy only if the caller's buffer is large enough, and return `OMEGA_NODE_BUF_TOO_SMALL` otherwise. Omega retries with a buffer of that size.

```c hello_node.c theme={null}
#include <string.h>
#include "omega_node.h"

static int32_t write_buf(const char *s, uint8_t *buf, size_t len, size_t *out_len) {
    size_t n = strlen(s);
    if (!out_len) return OMEGA_NODE_ERR_NULL_PTR;
    *out_len = n;
    if (!buf || len < n) return OMEGA_NODE_BUF_TOO_SMALL;
    memcpy(buf, s, n);
    return OMEGA_NODE_OK;
}

uint32_t node_abi_version(void) { return OMEGA_NODE_ABI_VERSION; }

int32_t node_domain(uint8_t *buf, size_t len, size_t *out_len) {
    return write_buf("hello", buf, len, out_len);
}

int32_t node_capabilities(uint8_t *buf, size_t len, size_t *out_len) {
    return write_buf(
        "{\"abi_version\":1,\"domain\":\"hello\",\"ops\":["
        "{\"name\":\"greet\",\"idempotent\":true},"
        "{\"name\":\"ping\",\"idempotent\":true,\"cadence_secs\":3600}]}",
        buf, len, out_len);
}

int32_t node_health(uint8_t *buf, size_t len, size_t *out_len) {
    return write_buf("{\"status\":\"healthy\",\"detail\":\"hello node ready\"}", buf, len, out_len);
}

enum { HELLO_OK = 0, HELLO_ERR_UNKNOWN_OP = 1 };

int32_t node_invoke(const char *op, const char *request_json,
                    uint8_t *msg_buf, size_t msg_len, size_t *msg_out_len) {
    (void)request_json;  /* the op's params plus Omega's reserved "_omega" object */
    if (!op) return OMEGA_NODE_ERR_NULL_PTR;
    if (strcmp(op, "greet") == 0) {
        int32_t rc = write_buf("Hello, omega!", msg_buf, msg_len, msg_out_len);
        return rc ? rc : HELLO_OK;
    }
    if (strcmp(op, "ping") == 0) {
        int32_t rc = write_buf("pong", msg_buf, msg_len, msg_out_len);
        return rc ? rc : HELLO_OK;
    }
    int32_t rc = write_buf("unknown op - this node serves greet and ping", msg_buf, msg_len, msg_out_len);
    return rc ? rc : HELLO_ERR_UNKNOWN_OP;
}
```

```bash theme={null}
# macOS
cc -shared -fPIC -I<omega-rs>/node-abi/include -o libhello_node.dylib hello_node.c
# Linux
cc -shared -fPIC -I<omega-rs>/node-abi/include -o libhello_node.so hello_node.c
```

> [!NOTE]
>
> In C you write the JSON by hand and there is no panic containment, so `node_invoke` must never crash or throw. In C++ wrap the body of every export in `try` / `catch (...)` and return a positive code from the catch. A crash is contained to the child process, but it is reported as a dispatch failure with no message, which tells an operator nothing.

## Cross-compile a Linux `.so` from a Mac

Omega's own Raspberry Pi builds use [`cross`](https://github.com/cross-rs/cross), and a node builds the same way. It needs Docker running.

```bash theme={null}
cargo install cross --locked

cross build --release --target aarch64-unknown-linux-gnu    # 64-bit Pi, ARM servers
cross build --release --target x86_64-unknown-linux-gnu     # x86_64 Linux
cross build --release --target armv7-unknown-linux-gnueabihf # 32-bit Pi
```

The library lands at `target/<triple>/release/libhello_node.so`.

> [!WARNING]
>
> A `.so` links against the glibc of the image it was built in, and the device must have that version or newer. `cross`'s current images carry a newer glibc than Debian bookworm, so a node built with them fails to load on a bookworm Pi with a `GLIBC_2.xx not found` error. Pin `cross` 0.2.5, which uses older images, or build on the oldest distribution you must support. Check with `objdump -T libhello_node.so | grep GLIBC_ | sort -u` before shipping.

A pure-Rust node with no C dependencies can also be built with `rustup target add <triple>` and plain `cargo build --target`, but the moment a dependency compiles C you need a cross toolchain, which is what `cross` provides.

## Verify with Omega

Omega has a hidden subcommand that does exactly what a device does when it registers a node: load the library in a fresh process, check the ABI, and call `node_domain`, `node_capabilities`, and `node_health`.

```bash theme={null}
omega-agent __node-introspect --path target/release/libhello_node.dylib
```

```json theme={null}
{
  "abi_version": 1,
  "domain": "hello",
  "capabilities": {
    "abi_version": 1,
    "domain": "hello",
    "ops": [
      { "name": "greet", "idempotent": true, "cadence_secs": null,
        "input_schema": { "type": "object", "properties": { "name": { "type": "string" } } } },
      { "name": "ping",  "idempotent": true, "cadence_secs": 3600, "input_schema": null }
    ]
  },
  "health": { "status": "healthy", "detail": "hello node ready" }
}
```

If this prints, the device will register your node. If it errors, the message names the failing export. Build `omega-agent` from the same repository revision you built against:

```bash theme={null}
cargo build -p omega-agent --release --no-default-features
```

Run the verification on the target platform. A Linux `.so` cannot be introspected on a Mac.

## Windows

A Windows node is an out-of-process COM server, not a C-ABI DLL. Build it from the [`windows-com-node`](https://github.com/golain-io/omega-rs/tree/main/windows-com-node) reference with the MSVC toolchain, register it with `regsvr32`, and check it with the `verify.exe` that ships beside it.

Three things differ from the Unix path and are documented in that README:

* Your node needs its own **CLSID** and **LIBID**. The interface ID is Omega's and never changes.
* The class must be registered with a **COM surrogate** so `CLSCTX_LOCAL_SERVER` activation succeeds.
* Your type library must `importlib("omega_node.tlb")`, the library Omega itself embeds and registers, and declare only your coclass.

The capabilities, health, and invoke JSON, and the return-code rules in [Node contract](/edge/nodes/contract), are identical.

## Next

* [Node contract](/edge/nodes/contract): what to return, and what the platform does with each code.
* [Ship a node](/edge/nodes/ship): upload it as a release artifact and register it with a `place_file` step.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.