Basic Example — Zig
Source: apps/zig/heap/example/src/main.zig (also apps/zig/zeroheap/example/src/main.zig) | WASM demo not available — Zig 0.16 still lacks wasm32-emscripten support
The Zig example demonstrates the ove module with allocator-aware constructors, comptime-typed wrappers, exhaustive RTOS dispatch, narrow per-op error sets, and std.log integration.
Allocator-aware, one API across modes
Every primitive takes a std.mem.Allocator. In heap mode it's std.heap.page_allocator; in zero-heap mode it's a FixedBufferAllocator over a static BSS arena. Wrapper methods are identical in both modes — only the allocator differs.
const std = @import("std");
const ove = @import("ove");
// Route std.log.* through the oveRTOS console.
pub const std_options: std.Options = .{ .logFn = ove.log.logFn };
// Page allocator for heap-mode builds. Substrate's libc-malloc heap
// and allocator-managed storage stay separate for clean accounting.
const app_allocator = std.heap.page_allocator;
// Heap mode keeps wrappers as optionals — populated in appMain, then
// unwrapped with `.?` from thread entries (which can't capture closures).
var queue: ?ove.Queue(u32, 8) = null;
var ui_timer: ?ove.Timer = null;
var last_value: std.atomic.Value(u32) = std.atomic.Value(u32).init(0);
ove.Queue(u32, 8) is a generic queue type — element type and capacity are comptime parameters. std.atomic.Value(u32) provides lock-free shared state — same pattern as the Rust example's AtomicU32.
A debug-only address tracker inside each wrapper records &self at create() and panics if any subsequent method call sees a different address. The check is compiled out in ReleaseFast / ReleaseSmall.
RTOS detection — typed enum, exhaustive switch
ove.target.current_rtos is a Rtos enum resolved at comptime. The compiler enforces exhaustive switches; adding a new RTOS to the substrate fails every consuming switch until updated:
const app_title = switch (ove.target.current_rtos) {
.freertos => "oveRTOS(FreeRTOS) Zig Demo",
.nuttx => "oveRTOS(NuttX) Zig Demo",
.zephyr => "oveRTOS(Zephyr) Zig Demo",
.posix => "oveRTOS(POSIX) Zig Demo",
.wasm => "oveRTOS(wasm) Zig Demo",
};
This replaces brittle @hasDecl(ove.ffi, "CONFIG_OVE_RTOS_FREERTOS") string checks — string typos used to compile silently.
Producer — typed Duration, narrow error set
q.sendFor(item, duration) takes a Duration (constructed with .millis(n), .secs(n), .micros(n), etc.) and returns the narrow error set error{ QueueFull, Timeout }. Exhaustive switch arms catch every reachable case:
fn producerEntry() void {
std.log.info("Producer started", .{});
var count: u32 = 0;
while (true) {
count += 1;
queue.?.sendFor(&count, .millis(1000)) catch |e| switch (e) {
error.Timeout => std.log.warn("Producer: send timeout", .{}),
error.QueueFull => std.log.warn("Producer: queue full, dropped {d}", .{count}),
};
ove.thread.sleepMs(500);
}
}
Consumer — forever recv is infallible
The forever-blocking q.recv() returns T directly (no error union). Backend programming-bug errors (invalid handle, etc.) panic at the FFI boundary instead of leaking into typed return paths:
fn consumerEntry() void {
std.log.info("Consumer started", .{});
while (true) {
const val = queue.?.recv(); // infallible → u32
last_value.store(val, .release);
if (val % 5 == 0) {
std.log.info("Consumer: count = {d}", .{val});
}
}
}
Entry point — Thread.spawn with anytype + args
Thread(N).spawn(allocator, config, entry, args) mirrors std.Thread.spawn's shape. entry is comptime anytype (any function reference), args is a tuple of run-time arguments forwarded to the entry function:
fn appMain() void {
std.log.info("Zig example (heap mode): init", .{});
queue = ove.Queue(u32, 8).create(app_allocator) catch return;
_ = ove.Thread(4096).spawn(
app_allocator,
.{ .name = "producer", .priority = .normal },
producerEntry, .{},
) catch return;
_ = ove.Thread(4096).spawn(
app_allocator,
.{ .name = "consumer", .priority = .normal },
consumerEntry, .{},
) catch return;
ui_timer = ove.Timer.create(
app_allocator,
.{ .period_ms = 200, .mode = .periodic },
uiTimerCallback, .{},
) catch return;
ove.lvgl.init() catch return;
{
const guard = ove.lvgl.lock();
defer guard.deinit();
createUi();
}
ui_timer.?.start() catch return;
ove.run();
}
comptime { ove.exportMain(appMain); }
Priority is an enum(c_uint) whose variants pin against the substrate's OVE_PRIO_* C enum at comptime — use the enum-literal .normal / .high instead of the older ove.thread.prio.normal constants struct.
Zero-heap variant — same call sites, FBA allocator
The zero-heap version of this app lives at apps/zig/zeroheap/example/src/main.zig. Every create(allocator) call is identical to heap mode; only the allocator changes:
// Static-backed BSS arena routes every byte to caller-owned memory.
var arena_bytes: [4096]u8 = undefined;
var fba: std.heap.FixedBufferAllocator = undefined;
// Wrappers as `undefined` storage at file scope (movable in heap mode,
// pinned by the debug tracker in zero-heap).
var queue: ove.Queue(u32, 8) = undefined;
var producer_th: ove.Thread(4096) = undefined;
fn appMain() void {
fba = std.heap.FixedBufferAllocator.init(&arena_bytes);
const allocator = fba.allocator();
queue = ove.Queue(u32, 8).create(allocator) catch return;
producer_th = ove.Thread(4096).spawn(
allocator,
.{ .name = "producer", .priority = .normal },
producerEntry, .{},
) catch return;
ove.run();
}
ove.allocators.c_allocator, page_allocator, and GeneralPurposeAllocator(.{}) become @compileError under CONFIG_OVE_ZERO_HEAP=y so an accidental dynamic-allocator import fails at build time, not at runtime.
Logging — std.log via ove.log.logFn
Declare the std_options once at file scope and every std.log.* call (plus any library using std.log.scoped(.tag).info(...)) routes through the oveRTOS console:
pub const std_options: std.Options = .{ .logFn = ove.log.logFn };
const log = std.log.scoped(.producer);
log.info("queue at {*}", .{&queue});
std.io integration
Stream(N).reader() / Stream(N).writer() return std.io.GenericReader / std.io.GenericWriter. Same for fs.File. Compose with every std-shaped reader chain:
const stream = try ove.Stream(256).create(allocator, .{});
const w = stream.writer();
try std.fmt.format(w, "value = {d}\n", .{42});
Key APIs demonstrated
| Zig API | C Equivalent | Purpose |
|---|---|---|
ove.Queue(T, N).create(alloc) |
ove_queue_init |
Generic typed queue, allocator-aware |
ove.Thread(N).spawn(alloc, cfg, entry, args) |
ove_thread_init |
Templated thread, anytype entry |
ove.Timer.create(alloc, cfg, cb, args) |
ove_timer_init |
Periodic / one-shot timer |
Duration (.millis/.secs/...) |
uint64_t timeout_ns |
Typed timeouts |
Instant + .forever |
OVE_WAIT_FOREVER |
Typed deadlines |
.sendFor(item, .millis(N)) |
ove_queue_send(..., timeout_ns) |
Bounded wait |
.recv() (infallible) |
ove_queue_receive(..., OVE_WAIT_FOREVER) |
Forever wait |
error{ QueueFull, Timeout } |
int rc |
Narrow per-op error sets |
std.log.info(...) |
OVE_LOG_INF |
std.log routing via ove.log.logFn |
ove.target.current_rtos |
#ifdef CONFIG_OVE_RTOS_* |
Typed RTOS enum, exhaustive switch |
std.atomic.Value(u32) |
(lock-free) | Lock-free shared state |
ove.thread.sleepMs(n) |
ove_thread_sleep_ms(n) |
Sleep current thread |
ove.lvgl.init() |
ove_lvgl_init |
LVGL bring-up |
ove.run() |
ove_run |
Start scheduler |
ove.exportMain(fn) |
(entry-point shim) | Wire Zig fn as ove_main |
How to build
make host.posix.example_zig # heap mode
make host.posix.example_zig_zh # zero-heap mode
make configure && make download && make && make run