openDAQ Rust bindings
Safe Rust bindings for interacting with openDAQ devices. Prebuilt native binaries for x64 Linux, x64 Windows, and ARM macOS are downloaded automatically on first use (only for your platform, ~15 MB), so the crate builds and runs without an openDAQ SDK, headers, or C toolchain.
Quickstart
cargo add opendaq
use ;
This prints 100 samples from the simulator device set to run at 100 Hz, a sine wave at 0.5 Hz.
If something fails, run a healthcheck to verify that the crate can find and load the native openDAQ libraries:
healthcheck;
See the examples/ folder for the most common use cases: stream/tail/block/multi readers, building signals and sending packets by hand, property batching, events, function properties, search filters, device locking, log-file download, and stateful callbacks driven by data-ready notifications.
Native binaries
The first use downloads the prebuilt openDAQ libraries for your platform from the GitHub release pinned in src/native_manifest.rs, verifies their SHA-256 checksum, and caches them per user (~/.cache/opendaq-rs/ on Linux, ~/Library/Caches/opendaq-rs/ on macOS, %LOCALAPPDATA%\opendaq-rs\cache\ on Windows). Subsequent runs reuse the cache. The download needs curl and tar, which ship with Linux, macOS, and Windows 10+.
The loader looks for the libraries in this order:
- the directory named by the
OPENDAQ_RUST_NATIVE_DIRenvironment variable, bin/<platform>/inside the crate source directory (a local development build),- the per-user download cache, downloading into it when necessary.
Set OPENDAQ_NO_DOWNLOAD to forbid the automatic download (e.g. in offline or CI environments); you can still trigger it explicitly with opendaq::install_native_libraries(). Set OPENDAQ_NATIVE_ARCHIVE_URL to fetch the archive from a mirror instead of GitHub (checksum verification is skipped for mirrors). Call opendaq::init() to surface loading problems as a Result instead of a panic on first use.
Some details
The crate is composed of two layers:
opendaq::syscontains the low-level FFI surface that closely mirrors the C API: one typedunsafe extern "C"function pointer per C function (resolved at runtime from the loaded libraries), the enums, and the error-code constants. It is autogenerated from the C header files (inopenDAQ/bindings/c/) and not intended for direct use.- The crate root contains the high-level API that wraps the low level in safe, idiomatic Rust. This is the intended interface for end users.
The native libraries are loaded dynamically at runtime (COM-style flat C ABI), so no linking happens at build time; an ABI mismatch surfaces as a missing-symbol error rather than memory corruption, and symbols missing from older/newer library builds surface as OPENDAQ_ERR_NOTIMPLEMENTED errors only when actually called.
Every openDAQ object is a reference-counted interface. Wrappers release their reference on drop and add one on clone, so objects are never freed manually. The interface hierarchy is modeled with Deref: a Channel dereferences to FunctionBlock, then Folder, Component, PropertyObject, down to BaseObject, so parent-interface methods are directly available. Conversions:
obj.cast::<T>()— query the object for interfaceT(a checkedqueryInterface);try_cast::<T>()returns anOptioninstead of an error andis_a::<T>()just tests.obj.to_value()— cross into Rust: a boxed scalar yields its native value, a daq list aVec, a daq dict key/value pairs, recursively; an object with no Rust form stays anObjectwrapper.obj.component_kind()— the most-derived component interface (Channel,Signal,Device, ...), for discovering a component's concrete type before casting.
Boxing / unboxing
The high-level API insulates you from openDAQ's C types. Generic arguments accept plain Rust values (boxing, on input, via impl Into<Value>) and generic results come back as Value (unboxing, on output); the two directions are inverses:
| Rust value | openDAQ core type |
|---|---|
String / &str |
String |
i64 (and smaller ints) |
Int |
f64 / f32 |
Float |
bool |
Bool |
Ratio |
Ratio |
Complex |
ComplexNumber |
Vec<T: Into<Value>> |
List |
Value::Dict pairs |
Dict |
| any interface wrapper | its object |
Typed signatures don't need Value at all: strings cross as &str/String, typed lists as Vec<T>, dicts as HashMap, enums as Rust enums, and nullable objects as Option<T>. property_value already applies the unboxing for you.
Readers
Readers are generic over the sample types they decode into: StreamReader<V = f64, D = i64> reads values as V and domain stamps as D, so read returns typed data with no buffers in sight. read returns Samples<V>, which carries the per-sample width alongside the flat values: for scalar signals it behaves like a Vec, while dimensioned signals (e.g. FFT spectra) expose one row per sample via .rows(). TailReader, BlockReader, and MultiReader follow the same pattern.
When a signal's descriptor changes to a sample type the reader cannot convert, the reader is invalidated: read fails with an error whose is_reader_invalidated() is true, and reading is resumed by building a from_existing reader with matching read types.
Callbacks
openDAQ events and callables invoke plain Rust closures:
let handler = context.on_core_event?.expect.subscribe?;
let sum = from_fn?;
obj.set_property_value?;
Closures must be Send + Sync (openDAQ may invoke them from its worker threads); a panic or error inside one is reported to the native caller as an openDAQ error instead of unwinding across the C boundary. Since openDAQ has no destruction hook for callables, each closure-backed procedure/function keeps its closure alive for the rest of the process (event-handler slots are recycled by Event::unsubscribe).
Development
The Makefile handles cloning the pinned openDAQ source, building the native libraries into bin/<platform>/, and regenerating the generated Rust bindings.
Override OPENDAQ_REF in the Makefile for a different upstream version. The generator lives in tools/generate_bindings.py (with tools/parse_bindings.py); regenerate whenever the pinned openDAQ version changes.
Folder structure
src/— hand-written runtime (loader, object model, values, readers, callbacks, ...)src/sys/generated.rs,src/generated/— autogenerated low-level and high-level bindings (committed)examples/— stand-alone example programstools/— the binding generatortests/— integration testsbin/— locally built or downloaded native openDAQ libraries (not committed). End users instead get the release archives pinned insrc/native_manifest.rs, which theBuild native binariesGitHub Actions workflow publishes as release assets
License
MIT. openDAQ itself is licensed under Apache-2.0.