Low-level and high-level Lua bindings for openDAQ
(data acquisition SDK), mirroring the architecture of the Dart bindings in
blueberry_flutter_native_frameworks/dart.
- Low level —
opendaq_bindings, one portable C extension: ~1,350 generatedlua_CFunctions, 1:1 with thelibcopendaqC ABI (C.daqSignal_getPublic(handle)→err, value), plus hand-written ownership/reader/callback plumbing. Compiles unchanged against Lua 5.1/LuaJIT through Lua 5.5;libcopendaqisdlopened at runtime. - High level —
opendaq, pure Lua: idiomatic wrapper classes (generated from the RTGen interface model + a hand-written core), with properties, inheritance,queryInterfacecasting, error objects, GC finalizers and deterministicdispose().
local opendaq = require('opendaq')
opendaq.init('/path/to/libcopendaq.dylib')
local instance = opendaq.Instance.create{ modulePaths = { '/path/to/modules' } }
local root = instance.rootDevice
for _, info in ipairs(root.availableDevices) do
print(info.name, info.connectionString) -- e.g. Simulator daqref://device0
end
local device = root:addDevice('daqref://device0')
local channel = device:getChannelsRecursive()[1]
channel:setPropertyValue('Frequency', 25.0)
local signal = device:getSignalsRecursive()[1]
local reader = opendaq.StreamReader.forSignal(signal)
reader:read(1000, 200) -- first read drains the initial event
local r = reader:readWithDomain(1000, 1000)
print(('%d samples, first tick %s'):format(r.count, tostring(r.domainTicks[1])))codegen/ the generator (pure Lua, self-contained)
main.lua orchestrator: parse headers + model → emit → write-if-changed
config.lua ALL policy: targets, base classes, excludes, overrides
lib/ header_parser, model, type_map, emit_c, emit_lua
model/*.json 189 RTGen interface dumps (copied from the Dart repo)
src/ the C extension (one translation unit)
opendaq_bindings.c handle/buffer userdata, dlopen loader, readers,
trampoline pools, event queue
compat.h Lua 5.1↔5.5 shims dl_compat.h dlopen/LoadLibrary
gen/ GENERATED: api table, dlsym loop, wrappers, enums
lua/opendaq/ the high-level package
core/ class system, DaqObject, errors, casting, conversions
generated/ GENERATED wrapper classes + manifest
opendaq/ hand-written: Instance, Context, callbacks, packets, ...
reader/ Stream/Tail/Block/Multi readers, sample types
examples/ example1..5 (tree walk, discovery+properties, stream
reading, producer path, core events + callbacks)
tests/ plain-Lua suite (tests/run.lua harness)
tools/ build.sh, test.sh, run.sh, check_symbols.sh
tools/fetch_opendaq.sh # clone + build openDAQ from GitHub (pinned release,
# default v3.40.0) into vendor/: libcopendaq +
# reference device/function-block modules
tools/build.sh # builds build/lua5.5/ and build/luajit/ variants
tools/test.sh # runs the suite under both interpreters
tools/run.sh examples/example1.lua # lua 5.5
tools/run.sh --luajit examples/example3.lua # LuaJITopenDAQ comes from github.com/openDAQ/openDAQ at the release tag pinned
in tools/fetch_opendaq.sh (OPENDAQ_TAG overrides). Headers land in
vendor/openDAQ/bindings/c/include, binaries in vendor/build/bin/ — the
default everywhere (codegen, CMake, tools, examples). Overrides:
OPENDAQ_LIB— path tolibcopendaq.dylib/.so(module plugins are discovered from the same directory)OPENDAQ_C_INCLUDE— copendaq header dir for the codegenLUA55_INC/LUAJIT_INC— Lua header dirs for the build
lua codegen/main.lua # regen src/gen + lua/opendaq/generated
lua codegen/main.lua --check # CI gate: exits 2 if anything would change
tools/check_symbols.sh # generated names ⊆ nm -gU libcopendaqInputs: the copendaq headers (--headers <dir> or OPENDAQ_C_INCLUDE,
default: the blueberry native-frameworks build tree) drive the low-level C
emission; codegen/model/*.json drives the high-level classes. All policy
(which interfaces, base classes, excludes) lives in codegen/config.lua.
- Errors — low-level returns
err, values...and never raises for openDAQ failures; high-level raises structured errors ({ code, symbol, operation },opendaq.isError).DAQ_ERR_NOINTERFACEdrivesobj:tryCast('Signal')returning nil. - Ownership — every object handle is a boxed userdata owning one native
reference;
__gcreleases it,obj:dispose()releases deterministically and neuters the box (further native use raises). Test gate:C.debug_handle_count() == 0after dispose + GC. - Events — openDAQ fires from arbitrary native threads; handlers never
run there. The C layer queues events (refs pre-added, bounded ring,
drop-oldest);
opendaq.pollEvents(timeoutMs)drains and dispatches on your thread. Cancel is race-free via slot generations. - Callbacks —
DaqFunction.fromLua(fn)/DaqProcedure.fromLua(fn)wrap closures via a static trampoline pool (the C typedefs have no context pointer). They call into Lua synchronously — only safe when openDAQ invokes them on the Lua thread; use events otherwise. - int64 — exact on Lua 5.3+; on 5.1/LuaJIT numbers are doubles (exact to 2^53 — ~285 years of 1 MHz domain ticks). Buffers store native int64.
- Enums — plain integers; name tables under
opendaq.enums.<daqEnum>, friendly aliasesopendaq.SampleType,opendaq.LogLevel.
| Scenario (Dart examples) | Lua |
|---|---|
| init(path) + Instance.create (modulePaths, logLevel, loggerSinks) | ✅ opendaq.init + Instance.create{} |
| rootDevice, availableDevices → addDevice (daqref reference device) | ✅ example2 |
| component tree walk, findComponent + cast, kind/specialize | ✅ example1, tests 03/04 |
| property get/set, hasProperty, visibleProperties (name/valueType/value) | ✅ example2, test 04 |
| device lock/unlock, operationMode + availableOperationModes | ✅ test 04 |
| function blocks: addFunctionBlock, input ports, connect | ✅ generated (Device:addFunctionBlock, InputPort:connect) |
| StreamReader (read / readWithDomain / readInto), read-twice idiom | ✅ example3, test 05 |
| TailReader, BlockReader, MultiReader (synchronized multi-signal) | ✅ test 05 |
| producer path: descriptors, linear rule, signals, packets, sendPacket | ✅ example4, test 06 |
| core events: context-wide + per-component subscribe/cancel | ✅ example5, test 07 |
| IFunction/IProcedure from closures (+ invoke received callables) | ✅ example5, test 07 |
| deterministic dispose + GC finalizers, leak gates | ✅ every test file |
Known gaps (deliberate, same as or narrower than Dart): *AndStealRef
members, ValueRange (bespoke), subscribeForDestructNotification,
Device.getTicksSinceOrigin (UInt), C++-only factories.