Cross-Compiling HDF5 .so Libraries for NI cRIO Linux RT

Brian Holt8 min read
Data AcquisitionOther ManufacturerTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Problem Statement

A LabVIEW HDF5 interface such as h5labview ships with a Windows DLL and a set of VIs whose Call Library Function Nodes (CLFNs) bind to that DLL. Moving those VIs to a CompactRIO controller running NI Linux Real-Time does not work by copying the DLL: the controller runs Linux, and the binary must be rebuilt as an ELF shared object (.so) for the controller's CPU architecture.

The work splits into four distinct tasks, and each one can fail independently:

  1. Compile every C/C++ source file in the library to an object file (.o) for the target architecture.
  2. Link the full set of objects into a single shared object (.so) — the Linux equivalent of a Windows DLL. A single .o is not sufficient.
  3. Install the .so (and any dependency libraries such as zlib or szip that HDF5 was configured against) onto the cRIO.
  4. Review every VI in the toolkit so its CLFN library path, function name, calling convention, and parameter types match what the rebuilt .so actually exports.
Step 4 is not optional. Exported symbol names, struct packing, and the presence of wrapper functions can differ between the Windows build and the Linux build of the same source tree. A CLFN that resolves on Windows may silently fail to resolve, or resolve to a differently-typed function, on Linux RT.

Match the Binary to the Controller Architecture

cRIO controllers running NI Linux Real-Time fall into two incompatible binary families. Build for the wrong one and the shared object will not load at all.

Controller family CPU / build target Binary reuse
cRIO-906x ARM-based Linux build Not interchangeable with x86 controllers
cRIO-903x, 904x, 905x, 908x 64-bit Intel x86 Linux build Shared across these families

A cRIO-9056 belongs to the 905x group, so it takes the 64-bit Intel x86 build. If the deployed system later mixes a 906x chassis into the fleet, you must maintain and deploy two separately compiled .so files and select at deployment time.

Confirm the exact target triplet reported by your toolchain rather than assuming it. Descriptions of the Intel target in field notes vary between x86_64 and older i686-style wording; check the SDK environment script and verify the produced file with file libhdf5.so and readelf -h on the target before you spend time debugging LabVIEW.

Toolchain Option A — NI Linux RT Cross-Compile SDK

NI publishes cross-compilation documentation for NI Linux RT targets at https://nilrt-docs.ni.com/cross_compile/cross_compile_index.html. The general flow described there remains the standard starting point. Expect minor drift: Linux tooling, glibc versions, and SDK packaging move continuously, so the exact package names and script paths may differ slightly from the document.

Typical sequence on a Linux build host:

  1. Download and install the SDK matching your NI Linux RT version and the target architecture (ARM or x86-64). Do not mix an SDK from one NI Linux RT release with a controller imaged to a different release — glibc and libstdc++ symbol versioning is the usual failure point.
  2. Source the SDK environment script so CC, CXX, CFLAGS, LDFLAGS, and SYSROOT point at the cross toolchain.
  3. Configure the HDF5 source tree for cross-compilation, enabling shared libraries and disabling anything that requires running test binaries on the host.
  4. Build, then repeat for the LabVIEW-side wrapper sources that expose the HDF5 API to the CLFNs.
. /usr/local/oecore-x86_64/environment-setup-<target>

# HDF5 core library
./configure --host=$CROSS_TARGET --prefix=/usr/local \
            --enable-shared --disable-static \
            --disable-tests --disable-tools
make -j$(nproc)
make DESTDIR=$PWD/stage install

# Wrapper layer: one .o per source file, then one .so
$CC $CFLAGS -fPIC -c src/*.c
$CC -shared -o libh5lv.so *.o -L stage/usr/local/lib -lhdf5 -lz

file libh5lv.so
readelf -d libh5lv.so | grep NEEDED

Cross-compiling is the reproducible option: it scripts cleanly into CI and produces the same binary every build. Its cost is the up-front effort of getting the SDK, sysroot, and autotools/CMake cross settings consistent.

Toolchain Option B — Native Build in a VM from the Recovery ISO

A practical alternative that has been used successfully: install the NI Linux RT compiler environment inside a VirtualBox virtual machine created from the cRIO recovery ISO, then compile natively inside that environment. Because the VM runs the same distribution and libc as the controller, the sysroot problem disappears — headers, library versions, and ABI match by construction.

Criterion Cross-compile SDK Native build in VM
Setup effort Higher (sysroot, toolchain flags) Lower for x86 targets
ABI/glibc mismatch risk Moderate — must match SDK to image Low — same image as controller
ARM (906x) support Supported via ARM SDK Not practical on an x86 host VM without emulation
Build speed Fast (host CPU, parallel make) Slower, VM-limited
CI automation Straightforward Requires VM image management

The VM route is the faster path to a first working binary on an x86-based controller such as the cRIO-9056. Keep the cross-compile SDK in mind for the ARM 906x variant and for automated builds later.

Deploy and Bind from LabVIEW

  1. Copy the .so and its dependencies to the controller over SSH/SFTP into a persistent directory on the target's writable filesystem.
  2. Ensure the dynamic loader can find it — either place it in a directory already on the loader search path and refresh the cache with ldconfig, or set an explicit path. Verify with ldd /path/libh5lv.so; every NEEDED entry must resolve, with no "not found" lines.
  3. Set the CLFN library name in the VIs to the Linux name (libh5lv.so) rather than the Windows DLL name. Use the platform-conditional library path or an environment-appropriate constant so the same VI hierarchy still runs on Windows.
  4. Confirm calling convention. On Linux RT there is no stdcall; the CLFN must be set to the C calling convention.
  5. Re-check every parameter: pointer-sized integers, struct alignment, string handling (C string vs LabVIEW handle), and array passing. A 32-bit-era wrapper written for a Windows DLL frequently mis-declares pointer parameters on a 64-bit Linux target.
Set every CLFN that touches HDF5 to Run in any thread only after you have verified thread safety of the built HDF5 configuration. If in doubt, keep the nodes in the UI/single-threaded execution setting during bring-up to eliminate reentrancy as a variable.

Verification Sequence

  1. Toolchain sanity: build and run a trivial "hello world" executable on the controller first. If that does not run, no amount of HDF5 debugging will help.
  2. Load test: run ldd on the target, then call one trivial exported function (for example a library version query) from a scratch VI.
  3. Function-by-function: validate exported entry points incrementally rather than deploying the whole toolkit at once. Bringing up the first handful of functions cleanly is a strong indicator the ABI and build configuration are correct.
  4. Round-trip data check: write a small dataset on the controller, transfer the file to a workstation, and read it with an independent HDF5 reader to confirm layout, datatype, and endianness.
  5. Soak test: run continuous acquisition and file writes for hours and monitor memory on the target. Handle leaks in HDF5 wrappers show up as slow RSS growth, not as immediate errors.

Risk-Reduction Fallback: Stream to the Host, Write HDF5 There

Porting a large C library to a real-time target is open-ended work with an uncertain schedule. A defensible interim architecture is to keep the file-format dependency off the controller entirely:

  • The cRIO acquires and buffers data, then streams it to the Windows GUI PC over a network transport (network streams, TCP, or your existing messaging layer).
  • The Windows application writes the HDF5 file using the already-working Windows DLL build.
  • The cRIO retains local buffering to ride through short network outages, sized from your acquisition rate and worst-case outage duration.

This removes the port from the project critical path, lets the acquisition system go live, and leaves the cross-compiled .so as an optimization to be dropped in later once it passes the verification sequence above. The main trade-off is that logging then depends on network availability and host uptime, so size the on-controller buffer deliberately rather than by default.

Common Failure Modes

Symptom Likely cause Action
CLFN error, library not loaded Wrong architecture (ARM binary on x86 controller or vice versa) Check file output against the controller family table
ldd shows "not found" Dependency (zlib, szip, libstdc++) not deployed Deploy dependencies or link statically where licensing allows
Symbol version errors on load SDK/glibc newer than the controller image Match SDK to the installed NI Linux RT version, or build in a VM from the recovery ISO
Function resolves but returns garbage Parameter type/size mismatch in the CLFN on 64-bit Re-declare pointers as pointer-sized integers; verify struct layout
Crash on second call Handle or memory ownership mismatch between wrapper and LabVIEW Confirm who allocates and frees each buffer

Which cRIO controllers need an ARM build versus an x86 build?

The cRIO-906x family runs Linux compiled for an ARM CPU. The cRIO-903x, 904x, 905x, and 908x families run 64-bit Intel x86 Linux. A shared object built for one family will not load on the other, so a cRIO-9056 requires the Intel x86 build.

Can I just compile one .o file and call it from LabVIEW?

No. You need an object file for every C/C++ source file in the library, then link all of them into a single .so. LabVIEW's Call Library Function Node loads a shared object, not a raw object file.

Is NI's cross-compile documentation still usable?

The NI Linux RT cross-compile guide at https://nilrt-docs.ni.com/cross_compile/cross_compile_index.html remains a reasonable starting point. Because Linux toolchains change continuously, expect small deviations in package names and script paths from what the document states, and validate against the SDK you actually install.

Is building inside a VM easier than cross-compiling?

For x86-based controllers, yes. Installing the RT compiler in a VirtualBox VM created from the cRIO recovery ISO gives you matching headers and libc, which eliminates most sysroot and symbol-version problems. Cross-compilation is still the better choice for ARM targets and for automated build pipelines.

What should I do if the port stalls mid-project?

Stream acquired data from the cRIO to the Windows host and write the HDF5 file there with the existing Windows build. That keeps the system deliverable while the shared-object port is finished and verified separately.

Back to blog