- C 50.1%
- Python 27.2%
- Rust 22.7%
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. |
||
|---|---|---|
| src | ||
| .gitignore | ||
| AGENTS.md | ||
| build.rs | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| gen-shim.py | ||
| README.md | ||
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_loadis exported by both runtime generations and returns an append-only table whose v5-prefix layout is ABI-stable across both. The shim probeslibndi.so.6thenlibndi.so.5(Linux), orNDI_RUNTIME_DIR_V6thenNDI_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 theNDIlib_*symbols globally (it must, to satisfy the link). The real runtime defines the same symbols, so it is loaded withRTLD_DEEPBIND; without it the runtime's own internalNDIlib_*calls would resolve back to the shim's exports and re-enter it (apthread_oncedeadlock). 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)
- Add the dependency (git, not crates.io):
ndi-shim = { git = "https://git.c64.org/slayradio-public/ndi-shim" } - Put the shim's
OUT_DIRon the linker search path ahead of grafton-ndi's SDK path so-lndiresolves to the shim. Build-script-Lordering between sibling crates is not controllable; the reliable lever is a rustflags-Lin the consumer's.cargo/config.toml(rustflags-Lprecede build-script-L). The shim dir is available to the consumer'sbuild.rsasDEP_NDI_SHIM_DIR, and to Rust asndi_shim::ARTIFACT_DIR. - Ship
libndi_shim.so.1next to the binary with rpath$ORIGIN. NDI_SDK_DIRmust 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 buildproducesndi_shim.dll+Processing.NDI.Lib.x64.lib; the DLL exports the 55 symbols with undecorated C names, the import lib points atndi_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).