A helper shim to wrap around NDI's SDK/Runtime for Rust program, specifically grafton-ndi, to use so applications actually can start without NDI present on the system.
  • C 50.1%
  • Python 27.2%
  • Rust 22.7%
Find a file
Slaygon d392a085a4 Add ndi_shim_runtime_path() to report the loaded runtime path
A shim-specific export (not part of NDIlib) so a host can show which NDI runtime was actually loaded - useful when several versions are installed. It triggers the lazy load, then resolves the path via dladdr(NDIlib_v5_load) on POSIX and GetModuleFileNameA on Windows, returning NULL when no runtime is available. Exposed to Rust as ndi_shim::runtime_path() -> Option<String>.

The symbol resolves at link only when the consumer also references grafton-ndi (its -lndi keeps the shim linked under --as-needed), which is the sole intended use.
2026-06-23 15:00:15 +02:00
src Add ndi_shim_runtime_path() to report the loaded runtime path 2026-06-23 15:00:15 +02:00
.gitignore Add ndi-shim: NDI runtime forwarding shim for graceful degradation 2026-06-23 12:24:38 +02:00
AGENTS.md Add ndi-shim: NDI runtime forwarding shim for graceful degradation 2026-06-23 12:24:38 +02:00
build.rs Add ndi-shim: NDI runtime forwarding shim for graceful degradation 2026-06-23 12:24:38 +02:00
Cargo.toml Add ndi-shim: NDI runtime forwarding shim for graceful degradation 2026-06-23 12:24:38 +02:00
CLAUDE.md Add ndi-shim: NDI runtime forwarding shim for graceful degradation 2026-06-23 12:24:38 +02:00
gen-shim.py Add ndi_shim_runtime_path() to report the loaded runtime path 2026-06-23 15:00:15 +02:00
README.md Add ndi-shim: NDI runtime forwarding shim for graceful degradation 2026-06-23 12:24:38 +02:00

ndi-shim

A forwarding shim that lets a binary linked against the NDI SDK start even when the NDI runtime is not installed, and degrade to "NDI unavailable" instead of failing to launch.

The problem

grafton-ndi (and the C SDK it binds) link-bind NDI: the build emits -lndi / -lProcessing.NDI.Lib.x64, and the bindings are direct externs. If libndi is missing at runtime, the dynamic linker fails before main() - the app cannot even show an error. NDI::new()'s Err path is never reached.

How the shim fixes it

The shim is linked in place of the real NDI import library. It exports every NDIlib_* symbol the binary references; on first use it lazily dlopens the real runtime and resolves the single entry point NDIlib_v5_load(), forwarding every call through the function table that returns. If the runtime is missing, NDIlib_initialize() returns false and the host degrades gracefully.

  • NDI 5 and 6 are both supported. NDIlib_v5_load is exported by both runtime generations and returns an append-only table whose v5-prefix layout is ABI-stable across both. The shim probes libndi.so.6 then libndi.so.5 (Linux), or NDI_RUNTIME_DIR_V6 then NDI_RUNTIME_DIR_V5 (the SDK's own redist env vars), so a machine with only NDI 5 works unchanged.
  • RTLD_DEEPBIND (Linux). The shim exports the NDIlib_* symbols globally (it must, to satisfy the link). The real runtime defines the same symbols, so it is loaded with RTLD_DEEPBIND; without it the runtime's own internal NDIlib_* calls would resolve back to the shim's exports and re-enter it (a pthread_once deadlock). macOS two-level namespaces and Windows explicit DLL imports bind each library's own symbols first, so the issue does not arise there.

Distinct identity (no recursive self-load)

-lndi must resolve to the shim, but the shim's own dependency must be the real library, or it would load itself. So the link-name file and the runtime identity differ:

Platform link name (-lndi resolves to) runtime identity (SONAME / install-name) shim dlopens
Linux libndi.so libndi_shim.so.1 libndi.so.6 / .5
macOS libndi.dylib libndi_shim.1.dylib libndi.dylib
Windows Processing.NDI.Lib.x64.lib (import lib) ndi_shim.dll Processing.NDI.Lib.x64.dll

The final binary's runtime dependency is therefore the shim (libndi_shim.so.1), which must be shipped next to the executable (rpath $ORIGIN). The link-name file (libndi.so) is only needed at link time and must not be shipped, or a stray libndi.so on the search path could be self-loaded.

Using it (consumer build)

  1. Add the dependency (git, not crates.io):
    ndi-shim = { git = "https://git.c64.org/slayradio-public/ndi-shim" }
    
  2. Put the shim's OUT_DIR on the linker search path ahead of grafton-ndi's SDK path so -lndi resolves to the shim. Build-script -L ordering between sibling crates is not controllable; the reliable lever is a rustflags -L in the consumer's .cargo/config.toml (rustflags -L precede build-script -L). The shim dir is available to the consumer's build.rs as DEP_NDI_SHIM_DIR, and to Rust as ndi_shim::ARTIFACT_DIR.
  3. Ship libndi_shim.so.1 next to the binary with rpath $ORIGIN.
  4. NDI_SDK_DIR must be set at build time (for the SDK headers), the same as grafton-ndi requires.

Symbol set

The shim exports exactly the flat NDIlib_* functions grafton-ndi references. That set is captured in gen-shim.py and was taken from the link surface of a debug (non-LTO) build:

nm <debug libgrafton_ndi-*.rlib> | grep ' U NDIlib_'

Release/LTO builds reference a subset of these, never a superset, so the set is the safe target. Regenerate when grafton-ndi or the SDK changes:

python3 gen-shim.py "$NDI_SDK_DIR/include/Processing.NDI.DynamicLoad.h" > src/shim.c

Status

  • Linux: verified. Forwarding against real NDI 6.2.0.3, 6.3.1.0, and 5.6.1 runtimes, plus the graceful-NULL path with the runtime masked.
  • Windows: verified (MSVC, x86_64-pc-windows-msvc). cargo build produces ndi_shim.dll + Processing.NDI.Lib.x64.lib; the DLL exports the 55 symbols with undecorated C names, the import lib points at ndi_shim.dll, a consumer links and forwards against the real NDI 6.3.2.0 runtime, and degrades to NULL when the runtime is hidden. The shim DLL is built /MT (static CRT) so it has no vcruntime/ucrt dependency of its own.
  • macOS: written but unverified (no Apple host to test on).