ZHAL provides both client and server libraries that provide IPC interfaces which projects can easily integrate with in order to both make requests and receive events from a Zigbee stack (the client side) as well as receive receive those requests and generate events (the server side). Clients and servers are hosted in separate processes.
Note that this interface was extracted from a mature product and has a few concepts that will not make sense to other projects (such as managing an integrated piezo or low power mode). These should be refactored out as extensions in a future revision.
A ZHAL server initializes the zhal-server library providing the IP address and port to listen to for connections as well as a callback structure with all supported callbacks. Multiple connections are allowed at a time to service requests. Events are always sent to all connected clients. Client connections are persistent and transmit requests, responses, and events on the same connection.
A ZHAL client similarly initializes the zhal-client library providing the IP address and port of the ZHAL server. Callbacks for events sent from the ZHAL server are registered during initialization. Once initialized successfully, which indicates a connection to the server, the client can receive asynchronous events from the server as well as make requests through simple C function invocations.
The internals of this project handle marshalling and unmarshalling of the C function calls (requests from the client side, events from the server side) and their related C structures to and from JSON-RPC 2.0. Complete test implementations of both the client and server sides are provided and driven by comprehensive tests to ensure there are no bugs with the encoding to and from JSON-RPC.
All structs are GLib atomic reference-counted (RC-box) objects. They should be created with the
corresponding *_new() functions and their refcounts managed with the corresponding
*_acquire() / *_release() helpers.
Important caller guidance:
- Do NOT call
free()on objects returned by the*_new()family. These objects are allocated using GLib's atomic rc-box APIs and must be released with the type-specific*_release()helper (for exampleZhalAttributeData_release) or by usingg_list_free_full(list, (GDestroyNotify)Type_release)when the elements are boxed objects. - Some APIs still return raw
malloc-allocated buffers or raw pointers (for exampleuint64_t*elements returned byzhalFreeSourceRouteor attributedatabuffers insideZhalAttributeData). Those remain the caller's responsibility to free withfree(); helper functions are provided for common list-returning APIs (see "Memory Ownership" below).
Example: freeing a list of boxed attribute entries:
GList *list = zhalGetSomeAttributeList(...); // returns GList* of ZhalAttributeData*
g_list_free_full(list, (GDestroyNotify)ZhalAttributeData_release);
The codebase uses the atomic variants of the rc-box APIs (g_atomic_rc_box_new0,
g_atomic_rc_box_acquire, g_atomic_rc_box_release[_full]) so reference counting is thread-safe.
The following artifacts represent the outputs of this project once successfully built. Shared libraries shall be versioned using standard "soname" methodology.
- zhal-server: server shared library and related C header file
- zhal-client: client shared library and related C header file
- zhal-test-server: an executable that uses zhal-server to host a server that can be driven by tests.
- zhal-test-runner: an executable that uses zhal-client to run the tests against zhal-test-server.
Out-of-tree build is recommended:
mkdir -p build
cd build
cmake -DZHAL_BUILD_TESTS=ON -DZHAL_BUILD_SHARED=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
cmake --build . --parallel
After building with ZHAL_BUILD_TESTS=ON:
ctest --output-on-failure
AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan) are supported via
-DZHAL_ENABLE_SANITIZERS=ON. The project adds the sanitizer flags automatically
for non-Release builds when the option is enabled.
Quick run (out-of-tree):
cmake -S . -B build -DZHAL_BUILD_TESTS=ON -DZHAL_ENABLE_SANITIZERS=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel
ctest --test-dir build --output-on-failure
Convenience target: the top-level CMake defines a custom target zhal-sanitize-test
that configures, builds, and runs tests in build-sanitize/. From the repository root:
cmake --build . --target zhal-sanitize-test
Capturing sanitizer output and logs:
- ASan/UBSan write diagnostics to stderr by default. To collect a full sanitizer log,
redirect stdout/stderr when running
ctestor run the test binary directly. - You can set sanitizer options via environment variables. For example:
export ASAN_OPTIONS="detect_leaks=1:halt_on_error=1:log_path=asan.log"
export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1"
cmake --build . --target zhal-sanitize-test
Notes & debugging tips:
- If ASan reports leaks, inspect the sanitizer stack traces to find the allocation site. The trace often points to the library (e.g., Jansson) and the caller site in ZHAL code.
- Running a single failing test (instead of the entire suite) can make investigation
faster. You can run the test binary directly from
build-sanitize/bin/zhal-test-runner(ctest runs the registered CMocka tests). Example:
build-sanitize/bin/zhal-test-runner -- -v
- For heavy-weight leak checks, consider using Valgrind with the test binary:
valgrind --leak-check=full --show-leak-kinds=all build-sanitize/bin/zhal-test-runner
CI integration:
- Add a matrix entry in CI that configures with
-DZHAL_ENABLE_SANITIZERS=ONand runsctestto fail on sanitizer errors.
Produced in CI for main. Locally:
cmake -S . -B build-cov -DZHAL_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="--coverage" -DCMAKE_EXE_LINKER_FLAGS="--coverage"
cmake --build build-cov --parallel
ctest --test-dir build-cov --output-on-failure
lcov --directory build-cov --capture --output-file coverage.info
lcov --remove coverage.info '/usr/*' '*/_deps/*' --output-file coverage.info
genhtml coverage.info --output-directory coverage-html
When not using sanitizers:
valgrind --leak-check=full --show-leak-kinds=all ctest -j1 --output-on-failure
Install into a prefix (default /usr/local):
cmake --install . --prefix /your/prefix
This installs:
- Libraries:
libzhal-common.so,libzhal-client.so,libzhal-server.so(with SONAME0for version${PROJECT_VERSION_MAJOR}) - Headers under
include/ - CMake package config:
lib/cmake/zhal/{zhalConfig.cmake,zhalConfigVersion.cmake} - pkg-config file:
lib/pkgconfig/zhal.pc
find_package(zhal REQUIRED)
add_executable(app main.c)
target_link_libraries(app PRIVATE zhal::zhal-client)
pkg-config --cflags --libs zhal
Most APIs that return dynamically allocated data follow a simple ownership rule: if you receive a non-NULL pointer or
list, you own it and must free it. For lists returned via GLib GList*, helper free functions are provided:
zhalFreeSourceRoute(routeList); // elements: uint64_t*
zhalFreeLqiTable(lqiList); // elements: ZhalLqiData*
zhalFreeMonitoredDevices(monList); // elements: ZhalLpmMonitoredDeviceInfo*
zhalFreeEnergyScanResults(scanResults); // elements: ZhalEnergyScanResult*
zhalFreeAddressTable(addressTableList); // elements: ZhalAddressTableEntry*
Attribute read responses allocate data buffers inside each ZhalAttributeData entry which the caller must free.
zhalTerm() cleans up the client transport, pending responses, and synchronization primitives. The server side
zhalServerShutdown() stops the listening socket, clears tracked client FDs, and resets state.
Current status:
- Complex multi-record responses implemented (energy scan, LQI/source route/address tables, monitored devices)
- Negative tests include invalid argument, timeout, and unknown method dispatch (
zhalInvokeRawhelper) - Optional sanitizers (
-DZHAL_ENABLE_SANITIZERS=ON) and CI workflow building matrix variants - Helper free APIs for all composite list-returning functions
- Graceful shutdown on both client (
zhalTerm) and server (zhalServerShutdown)
Future potential improvements:
- Malformed JSON / forced disconnect simulation tests
- Fuzzing of JSON parser for events & responses
- Optional build mode without GLib dependency
- Additional tracing / metrics hooks
(Add license details here.)