No description
Find a file
Ben Greer f5426bc310 Rewrite keyexpr from Rust source: intersect, includes, canonize, strip_prefix
Replaced the entire keyexpr matching engine with algorithms ported
directly from zenoh-rs (classical.rs, include.rs, canon.rs).

intersects: recursive chunk-by-chunk with branch on **, $* via
star_dsl_intersect. Cleaner than the old forward/backward approach.

includes: iterative left-to-right from Rust's LTRIncluder. DSL chunk
inclusion splits left by $* and checks pieces appear in order in right.

canonize: full implementation. Validates (empty chunks, invalid patterns),
collapses $*$* → $*, $*-only → *, **/** → **, **/* → */**.

strip_prefix: walks ke and prefix chunks in parallel, handles **.

~250 lines replacing ~400 lines. All 71 tests pass. Zero vet warnings.
2026-03-29 21:48:45 -05:00
examples Named constants, wire format comments, architecture doc, minimal example 2026-03-29 20:32:22 -05:00
test_live Switch to core:nbio (Odin 2026-03), true multiplatform async IO 2026-03-29 10:07:07 -05:00
test_pubsub Switch to core:nbio (Odin 2026-03), true multiplatform async IO 2026-03-29 10:07:07 -05:00
test_query Switch to core:nbio (Odin 2026-03), true multiplatform async IO 2026-03-29 10:07:07 -05:00
zenoh Rewrite keyexpr from Rust source: intersect, includes, canonize, strip_prefix 2026-03-29 21:48:45 -05:00
.gitignore Odin zenoh client — pub/sub and query/reply through zenohd 2026-03-28 21:43:59 -05:00
ARCHITECTURE.md Named constants, wire format comments, architecture doc, minimal example 2026-03-29 20:32:22 -05:00
DESIGN.md update readme 2026-03-29 19:51:32 -05:00
README.md Reconnect test, SHM bump allocator, installation docs 2026-03-29 20:08:30 -05:00

zenoh-odin

Pure Odin implementation of the zenoh protocol. Zero-copy pub/sub, query/reply, and shared memory — no C bindings, no allocations in the hot path.

Status

Alpha. Verified against zenohd 1.8.0 on macOS. Core protocol is complete:

  • Pub/sub through zenoh router
  • Query/reply through zenoh router
  • SHM with cross-process atomic refcounting
  • UDP multicast scouting
  • Automatic reconnection with exponential backoff
  • Raw API for zero-overhead message processing

Installation

Clone the repository and import the zenoh package:

import zenoh "path/to/zenoh-odin/zenoh"

Odin does not have a package manager. Copy the zenoh/ directory into your project or add it to your import path.

Quick Start

import zenoh "zenoh"
import "core:nbio"
import "core:fmt"
import "core:time"

on_sample :: proc(sample: zenoh.Sample, _: rawptr) {
    fmt.printfln("%s: %s", string(sample.key_expr), string(sample.payload))
}

main :: proc() {
    nbio.acquire_thread_event_loop()
    defer nbio.release_thread_event_loop()

    session := zenoh.open(zenoh.Config{endpoint = "localhost:7447"}) or_return
    defer zenoh.close(&session)
    zenoh.register(&session)

    zenoh.subscribe(&session, "demo/**", on_sample)

    for !session.closed {
        nbio.tick()
        time.sleep(1 * time.Millisecond)
    }
}

Requires zenohd running on the target endpoint:

# Install (Rust toolchain required)
cargo install zenohd

# Run with defaults
zenohd

# Run with SHM support
cargo install zenohd --features shared-memory
zenohd

Examples

# In one terminal
odin run examples/sub/

# In another terminal
odin run examples/pub/

# Query/reply (runs both sides)
odin run examples/queryable/

All examples default to localhost:7447. Pass an endpoint as the first argument to override.

Architecture

Single package, poll-based, no threads. The user owns the event loop via core:nbio (kqueue on macOS, io_uring on Linux, IOCP on Windows).

nbio.tick()
    └─ socket readable?
        └─ decode TCP frame
            └─ decode network message
                └─ match key expression
                    └─ call subscriber handler

Four function calls from IO event to user code. The raw API inlines codec procs into the handler for two.

Two API tiers

Easy API — library decodes the message, calls you with a Sample:

zenoh.subscribe(&session, "test/**", proc(sample: zenoh.Sample, _: rawptr) {
    fmt.println(string(sample.payload))
})

Raw API — library gives you raw bytes, you decode what you need with #force_inline procs. Type-safe: Push_Raw and Request_Raw are distinct types, wrong reader on wrong message is a compile error:

zenoh.subscribe_raw(&session, "test/**", proc(key: zenoh.Key_Expr, raw: zenoh.Push_Raw, _: rawptr) {
    buf := raw
    header := zenoh.read_put_header(&buf)
    payload := zenoh.read_put_payload(&buf)
    // process payload directly — zero intermediate structs
})

Zero allocations in the hot path

  • Decode: returns structs by value, strings are slices into the recv buffer
  • Encode: writes into caller-provided stack buffers
  • Dispatch: linear scan over #soa subscriber table
  • Callbacks: data valid for callback duration, no copy

The library only allocates during open() and subscribe() (session state).

Type safety

All wire data uses distinct types. The compiler prevents mixing them up:

Payload    :: distinct []u8    // user data
Attachment :: distinct []u8    // side-channel data
Cookie     :: distinct []u8    // opaque handshake token
Key_Expr   :: distinct string  // has wildcard matching semantics

Shared Memory

Zero-copy data sharing between processes on the same host. Negotiated automatically during the Init/Open handshake via a challenge protocol.

Supported on all platforms:

  • macOS/BSD: POSIX shm_open + external lock file for Rust compatibility
  • Linux: POSIX shm_open (kernel-managed lifetimes, no lock file)
  • Windows: CreateFileMappingW / MapViewOfFile (auto-cleanup)

SHM payloads are resolved transparently in the subscriber dispatch path. The cross-process refcount is atomically incremented on resolve and decremented when the callback returns.

Requires zenohd built with --features shared-memory.

Wire Compatibility

The codec is verified byte-exact against Rust zenoh-codec 1.8.0 via 17 golden test vectors. All transport messages, network messages, declarations, and SHM payloads match the reference encoder.

See PROTOCOL.md in the companion Go project for the complete wire format specification.

Building

Requires Odin 2026-03 or later (core:nbio support).

# Run tests
odin test zenoh/

# Build an example
odin build examples/pub/

# Override subscriber table size at build time
odin build examples/sub/ -define:MAX_SUBSCRIBERS=256

Project Structure

zenoh/
├── types.odin          Protocol types, distinct types, tagged unions
├── codec.odin          Wire codec: encode/decode all message types
├── codec_test.odin     Golden vector tests (17 decode + encode)
├── keyexpr.odin        Key expression matching (*, **, $*, @)
├── keyexpr_test.odin   150 matching test cases
├── transport.odin      Session: open, subscribe, publish, get, close
├── transport_test.odin Codec round-trip tests
├── link.odin           TCP connection with 2-byte LE length framing
├── scout.odin          UDP multicast scouting (224.0.0.224:7446)
├── errors.odin         Error union (or_return compatible)
├── shm.odin            Auth challenge, ShmBufInfo, segment reader
├── shm_posix.odin      POSIX shm_open/mmap (Darwin + Linux)
├── shm_darwin.odin     macOS lock file for Rust interop
├── shm_linux.odin      Linux no-op lock stubs
├── shm_windows.odin    Windows CreateFileMapping
└── shm_test.odin       Auth segment + ShmBufInfo tests

examples/
├── pub/main.odin       Publish every second
├── sub/main.odin       Subscribe and print
└── queryable/main.odin Query/reply round-trip

License

** free for personal use **

contact me at ben@mere.dev for any license questions