Releasing v0.2.2 — Observer Discovery, Reply Channel Fix, WebSocket Server

· 5 min read
v0.2.2

Three themes in this release: making the observer tooling work without configuration, fixing a subtle codegen bug that crashed actors with arguments, and adding WebSocket server support to the stdlib.

hew-observe auto-discovers running programs. Previously you had to know the port: hew-observe --addr localhost:6060. Now you can just run hew-observe with no arguments. When a Hew program starts with HEW_PPROF=auto, the profiler binds to a unix domain socket in a per-user directory ($XDG_RUNTIME_DIR/hew-profilers/ on Linux, $TMPDIR/hew-profilers-{uid}/ on macOS). hew-observe scans that directory and connects automatically.

# Terminal 1
HEW_PPROF=auto hew run my_program.hew

# Terminal 2
hew-observe           # auto-discovers and connects
hew-observe --list    # shows all running profilers
hew-observe --pid 42  # connects to a specific process

If the observed program crashes and restarts, hew-observe reconnects automatically — it re-scans every 3 seconds while disconnected. The socket and discovery files are mode 0600 in a mode 0700 directory, with ownership validation to prevent cross-user access.

Under the hood, the profiler HTTP server was rewritten from tiny_http to hyper 1.x. This was necessary because hyper can serve over any AsyncRead + AsyncWrite stream (TCP or unix socket), while tiny_http only supported TCP. The shutdown mechanism is cleaner too — a tokio::select! timeout replaces the old hack of sending a dummy HTTP request to unblock the accept loop.

Reply channel convention rewrite. This is the one that fixes #382, the crash that affected every actor with arguments when called from a supervisor.

The old convention: when you await actor.method(arg), the codegen packed the reply channel pointer at the end of the message data buffer. The receiver detected it by comparing data_size > expected_parameter_size. The problem: the MLIR type converter widens i32 to i64, so sizeof(packed_data) was 8 while sizeof(i32_parameter) was 4. The receiver saw 8 > 4, concluded “there’s a reply channel in there,” and read the argument value (e.g. 100) as a pointer. SIGSEGV.

The fix routes reply channels through HewMsgNode.reply_channel — a field that already existed in the message node struct but was unused by codegen. The scheduler sets a per-worker thread-local before dispatch; the codegen reads it via hew_get_reply_channel(). No more size comparisons, no more mixing user data with control flow pointers. This also fixes the ask pattern on WASM (same convention, different executor).

Duplicate trait impl generation. When a module imported another module that defined trait impls (e.g. import std::net::websocket), the codegen generated the impl methods twice — once with the defining module’s mangled name, once with the importing module’s. One of these duplicates could execute as a static constructor before main(), calling FFI functions with null arguments. The fix tracks which module defined each type and uses the defining module’s path for mangling, regardless of which module is currently being processed.

WebSocket server. The stdlib’s std::net::websocket module now supports server-side WebSockets:

let server = websocket.listen("0.0.0.0:8765");
let conn = server.accept();  // blocks until a client connects
let msg = conn.recv();        // same API as client connections
conn.send("echo: " + msg);
conn.close();
server.close();

The server returns the same Conn type as client connections, so send, recv, and close work identically. The attach() active-mode pattern (where incoming messages are dispatched to an actor) also works with server connections.

Connection status fix. hew-observe’s connection indicator was being overwritten by every HTTP fetch, so whichever endpoint ran last in the refresh cycle determined the displayed status. If a tab-specific endpoint failed (timeout, bad JSON), the indicator showed “Disconnected” even while metrics data flowed normally. Now only the primary metrics probe sets the status; secondary fetches are decoupled.

Clippy clean. The entire workspace now passes cargo clippy --workspace --tests -- -D warnings with zero errors. The pre-commit hook was silently broken on macOS because it used Bash 4.3+ namerefs (local -n), but macOS ships Bash 3.2. Fixed with eval-based indirection. Clippy now actually runs on every commit.

What didn’t change. No new syntax. No breaking changes. All 562 E2E tests pass. The wire protocol, the distributed node API, and the codegen’s public-facing behaviour are unchanged — this release is fixes and tooling.