Castalong
← Engineering notes

One plugin, two C runtimes: the Rust core and the Qt dock

InternalsJune 2026

The Castalong plugin you install is actually two DLLs: castalong.dll (the Rust core) and castalong_dock.dll (the control dock). That split isn't an aesthetic choice — it's forced by a C-runtime conflict, and working within it shaped how the two halves talk.

The /MT vs /MD problem

The core links LiveKit, whose prebuilt libwebrtc is compiled against the static C runtime (/MT). OBS and its Qt6 are compiled against the dynamic runtime (/MD). Loading both into one module and letting them share a CRT is undefined behaviour — you get mismatched allocators and heap corruption, the classic LNK2038 "mismatch detected for RuntimeLibrary" if you're lucky enough to catch it at link time.

So the code is partitioned by runtime. The Rust core is built /MT with crt-static to match libwebrtc. The Qt dock is a separate /MD DLL to match OBS and Qt. They never share CRT objects.

A deliberately narrow C ABI

The two halves communicate only over a small C interface: primitives and NUL-terminated UTF-8 strings, never CRT-allocated objects crossing the boundary. Each side owns what it allocates; strings are copied on receipt. The directions are clean:

Pushing the whole model as JSON rather than fine-grained widget mutations keeps the ABI tiny and the threading rules simple: one "render this state" entry point, no partial updates to reason about.

Talking to OBS without its SDK

The Rust core uses no generated bindings and no obs.lib. libobs is called via #[link(kind = "raw-dylib")], so the import is resolved at load time against the already-loaded OBS — nothing to ship or version-match at build time. The struct layouts (obs_source_info, the full obs_source_frame, and friends) are hand-transcribed from the OBS headers for the targeted version, including the prefix-by-size trick OBS uses to stay ABI-stable across releases. The frontend API (scenes, docks) is reached with runtime GetProcAddress, since it isn't always present.

It sounds fragile written down, but it buys a self-contained plugin with no build-time SDK dependency, and it's exactly what lets the same Rust source target Windows and Linux OBS from one codebase.

Two DLLs, one C ABI, no SDK link: an unusual shape, but the one the runtime constraints actually allow — and it keeps the heavy WebRTC/render core and the native Qt UI cleanly apart.
← Engineering notes