# C++ API Reference Dora provides C++ bindings for both standalone nodes and in-process operators via [CXX](https://cxx.rs/) (Rust-C++ interop). The CXX bridge generates type-safe C++ headers from Rust definitions -- no raw FFI or manual `extern "C"` declarations are needed. Two crates provide the C++ surface: | Crate | Library | Use case | |-------|---------|----------| | `dora-node-api-cxx` | `libdora_node_api_cxx.a` | Standalone node executable | | `dora-operator-api-cxx` | `libdora_operator_api_cxx.a` | Shared-library operator loaded by the runtime | Generated headers: `dora-node-api.h` and `dora-operator-api.h`. --- ## Node API (`dora-node-api-cxx`) ### Initialization ```cpp #include "dora-node-api.h" // Initialize a node from environment variables set by the Dora daemon. // Returns an DoraNode struct containing the event stream and output sender. // Throws on failure. DoraNode init_dora_node(); ``` ### DoraNode Returned by `init_dora_node()`. Owns the event stream and the output sender for the lifetime of the node. ```cpp struct DoraNode { rust::Box events; // event stream (blocking receiver) rust::Box send_output; // output sender }; ``` ### Events Opaque Rust type exposed to C++. Provides blocking iteration over the node's incoming events. ```cpp // Member function -- call on the boxed object directly. rust::Box Events::next(); // Free function form -- equivalent to events->next(). rust::Box next_event(rust::Box& events); ``` Both forms block until the next event arrives and return an owned `DoraEvent`. ### Non-blocking and timed receive For nodes that need to do work between events, or react to deadlines, the `Events` stream also offers non-blocking and timed variants of `next_event()`: ```cpp // Block up to `timeout_ms` milliseconds for the next event. Returns // an event with `event_type() == Timeout` if the deadline elapses // before one arrives, or `AllInputsClosed` if the stream closed. rust::Box next_event_timeout(rust::Box& events, uint64_t timeout_ms); // Non-blocking poll. Returns `event_type() == Empty` if no event is // immediately available, or `AllInputsClosed` if the stream is closed. rust::Box try_next_event(rust::Box& events); // Hint: true when the event queue is currently empty. Treat as // advisory only -- the daemon can produce a new event between the // check and a subsequent receive. bool events_is_empty(const rust::Box& events); ``` For draining buffered events into a snapshot (useful when you want to process a batch all at once without racing the daemon): ```cpp // Take a snapshot of all currently-buffered events. Subsequent // `next_event` / `try_next_event` calls only see events that arrive // after this point. rust::Box drain_events(rust::Box& events); size_t drained_events_len(const rust::Box& drained); // Pop the next event from a drained snapshot. Returns // `event_type() == Empty` once the snapshot is exhausted. rust::Box drained_events_next(rust::Box& drained); ``` ### DoraEvent Opaque Rust type. Inspect its kind with `event_type()`, then downcast with one of the variant extractors below. ```cpp // Determine the event kind. DoraEventType event_type(const rust::Box& event); // Downcast to a raw-byte input. Throws if the event is not Input. DoraInput event_as_input(rust::Box event); // Downcast to an Arrow FFI input (writes Arrow C Data Interface structs). // out_array and out_schema must point to valid ArrowArray / ArrowSchema structs. // Returns DoraResult with empty error on success. DoraResult event_as_arrow_input( rust::Box event, uint8_t* out_array, uint8_t* out_schema); // Same as above, but also returns the input ID and metadata. ArrowInputInfo event_as_arrow_input_with_info( rust::Box event, uint8_t* out_array, uint8_t* out_schema); // Downcast to a NodeFailed payload. Throws if the event is not NodeFailed. DoraNodeFailed event_as_node_failed(rust::Box event); ``` ### DoraEventType ```cpp enum class DoraEventType : uint8_t { Stop, // graceful shutdown requested Input, // new data arrived on an input InputClosed, // a single input was closed Error, // an error occurred Unknown, // unrecognized event variant AllInputsClosed, // all inputs closed (stream ended) Empty, // try_next_event / drained_events_next found no event ready Timeout, // next_event_timeout deadline elapsed without an event NodeFailed, // an upstream node failed (use event_as_node_failed to inspect) Reload, // hot-reload notification for an operator }; ``` `Empty` is distinct from `Timeout`: `Empty` means "no event available right now" (the caller did not request a timeout); `Timeout` means "the caller-supplied deadline elapsed before an event arrived". A non-blocking poll never returns `Timeout`; a timed receive returns either `Timeout` or `AllInputsClosed` if no event arrived in time. ### DoraInput Returned by `event_as_input()`. Contains raw bytes. ```cpp struct DoraInput { rust::String id; // input identifier (e.g. "tick", "image") rust::Vec data; // raw payload bytes }; ``` ### DoraNodeFailed Returned by `event_as_node_failed()`. Carries the failure information from an upstream node that exited unexpectedly. Use it to log the cause, flush downstream state, or trigger graceful shutdown. ```cpp struct DoraNodeFailed { rust::Vec affected_input_ids; // inputs on this node that will stop receiving data rust::String error; // human-readable error message from the failed node rust::String source_node_id; // id of the node that failed }; ``` Example: ```cpp auto event = next_event(dora_node.events); if (event_type(event) == DoraEventType::NodeFailed) { auto failed = event_as_node_failed(std::move(event)); std::cerr << "Upstream node `" << std::string(failed.source_node_id) << "` failed: " << std::string(failed.error) << std::endl; } ``` ### ArrowInputInfo Returned by `event_as_arrow_input_with_info()`. Contains the input ID, metadata, and an error string. ```cpp struct ArrowInputInfo { rust::String id; // input identifier rust::Box metadata; // attached metadata rust::String error; // empty on success }; ``` ### DoraResult Returned by output-sending functions. Check the `error` field -- empty means success. ```cpp struct DoraResult { rust::String error; // empty string on success }; ``` ### OutputSender Opaque Rust type. All methods take `rust::Box&` as the first argument (the sender from `DoraNode::send_output`). #### send_output Send raw bytes on a named output. ```cpp DoraResult send_output( rust::Box& sender, rust::String id, rust::Slice data); ``` #### send_output_with_metadata Send raw bytes with attached metadata. ```cpp DoraResult send_output_with_metadata( rust::Box& sender, rust::String id, rust::Slice data, rust::Box metadata); ``` #### send_arrow_output Send an Arrow array via the C Data Interface. The pointers must reference valid `ArrowArray` and `ArrowSchema` structs. Ownership of the Arrow data transfers to Rust on success. ```cpp DoraResult send_arrow_output( rust::Box& sender, rust::String id, uint8_t* array_ptr, uint8_t* schema_ptr); // Overload with metadata (same C++ name via cxx_name attribute). DoraResult send_arrow_output( rust::Box& sender, rust::String id, uint8_t* array_ptr, uint8_t* schema_ptr, rust::Box metadata); ``` #### close_outputs Selectively close one or more of this node's outputs without shutting the whole node down. Downstream subscribers see the corresponding `InputClosed` event for each closed output. ```cpp DoraResult close_outputs( rust::Box& sender, rust::Vec output_ids); ``` `output_ids` are validated as `DataId`s; an invalid id (e.g. one containing characters not allowed by the data-id grammar) returns a `DoraResult` whose `error` names the offending id and the underlying validation message, instead of aborting the process. #### node_config_json Return this node's `NodeRunConfig` (inputs, outputs, type annotations, framing overrides, etc. — the per-node block from the dataflow descriptor) serialized as JSON. Lets a C++ node reason about its own declared interface at runtime without re-parsing the dataflow yaml. ```cpp rust::String node_config_json(const rust::Box& sender); ``` Throws `rust::Error` if serialization fails (rare — `NodeRunConfig` has no fields that can produce serde errors in practice). #### dataflow_descriptor_json Return the full parsed `Descriptor` (the entire dataflow yaml) serialized as JSON. Useful for introspecting peer nodes, listing all topics, etc. ```cpp rust::String dataflow_descriptor_json(const rust::Box& sender); ``` Throws `rust::Error` if the daemon hasn't delivered the descriptor yet, or on serialization failure. > **Caution:** the returned JSON includes any `env` blocks declared on nodes in the dataflow yaml. Inline literals (`API_KEY: "..."`) **and** host-environment substitutions (`API_KEY: "${HOST_VAR}"`, expanded at descriptor parse time) both appear as plain strings in the output. Don't pipe the result into shared logs or telemetry without sanitizing if your dataflow can carry secrets in env vars. #### log_message Send a log message through the Dora logging system. ```cpp DoraResult log_message( const rust::Box& sender, rust::String level, // e.g. "info", "warn", "error" rust::String message); ``` ### Metadata Opaque Rust type for attaching typed key-value pairs to outputs. #### Construction ```cpp rust::Box new_metadata(); ``` #### Reading ```cpp uint64_t Metadata::timestamp() const; bool Metadata::get_bool(const rust::Str key) const; // throws on missing/wrong type int64_t Metadata::get_int(const rust::Str key) const; double Metadata::get_float(const rust::Str key) const; rust::String Metadata::get_str(const rust::Str key) const; rust::Vec Metadata::get_list_int(const rust::Str key) const; rust::Vec Metadata::get_list_float(const rust::Str key) const; rust::Vec Metadata::get_list_string(const rust::Str key) const; int64_t Metadata::get_timestamp(const rust::Str key) const; // nanoseconds since epoch rust::String Metadata::get_json(const rust::Str key) const; // single value as JSON string ``` #### Writing All setters throw on failure. ```cpp void Metadata::set_bool(const rust::Str key, bool value); void Metadata::set_int(const rust::Str key, int64_t value); void Metadata::set_float(const rust::Str key, double value); void Metadata::set_string(const rust::Str key, rust::String value); void Metadata::set_list_int(const rust::Str key, rust::Vec value); void Metadata::set_list_float(const rust::Str key, rust::Vec value); void Metadata::set_list_string(const rust::Str key, rust::Vec value); void Metadata::set_timestamp(const rust::Str key, int64_t nanos); // nanoseconds since epoch ``` #### Introspection ```cpp MetadataValueType Metadata::type(const rust::Str key) const; // throws if key missing rust::String Metadata::to_json() const; // full metadata as JSON rust::Vec Metadata::list_keys() const; ``` ### MetadataValueType ```cpp enum class MetadataValueType : uint8_t { Bool, Integer, Float, String, ListInt, ListFloat, ListString, Timestamp, }; ``` ### Service, Action, and Streaming Patterns C++ nodes can implement [communication patterns](patterns.md) using the metadata API. The well-known metadata keys are: | Key | Description | |-----|-------------| | `"request_id"` | Service request/response correlation (UUID v7) | | `"goal_id"` | Action goal identification (UUID v7) | | `"goal_status"` | Action result status: `"succeeded"`, `"aborted"`, or `"canceled"` | | `"session_id"` | Streaming session identifier | | `"segment_id"` | Streaming segment within a session (integer) | | `"seq"` | Streaming chunk sequence number (integer) | | `"fin"` | Last chunk of a streaming segment (bool) | | `"flush"` | Discard older queued messages on input (bool) | ```cpp // Service server: pass through request_id from input metadata auto input_metadata = event_as_arrow_input_with_info(event); send_output_with_metadata(sender, "response", result, std::move(input_metadata.metadata)); // Action server: set goal_id and goal_status on result auto meta = new_metadata(); meta->set_string("goal_id", goal_id); meta->set_string("goal_status", "succeeded"); send_output_with_metadata(sender, "result", result_data, std::move(meta)); ``` ### CombinedEvents (ROS2 integration) When using the optional `ros2-bridge` feature, node events and ROS2 subscription events can be merged into a single stream. ```cpp // Convert Dora events into a combined stream. CombinedEvents dora_events_into_combined(rust::Box events); // Create an empty combined stream (for ROS2-only nodes). CombinedEvents empty_combined_events(); ``` #### CombinedEvents struct ```cpp struct CombinedEvents { rust::Box events; CombinedEvent next(); // blocking -- returns the next merged event }; ``` #### CombinedEvent struct ```cpp struct CombinedEvent { rust::Box event; bool is_dora() const; // true if this is a standard Dora event }; // Downcast a combined event back to an DoraEvent. Throws if not an Dora event. rust::Box downcast_dora(CombinedEvent event); ``` ROS2 subscriptions add their own events to the merged stream. Use `subscription->matches(event)` and `subscription->downcast(event)` to handle ROS2-specific events (see the [ROS2 Bridge docs](ros2-bridge.md)). --- ## Operator API (`dora-operator-api-cxx`) Operators are shared libraries loaded by the Dora runtime. The C++ side implements five functions that the CXX bridge calls into. > **Breaking change vs. earlier dora releases:** the operator API used to require only `new_operator` + `on_input`; `Event::InputClosed` and `Event::Stop` were silently dropped on the C++ side. As of dora #1849 the bridge calls `on_input_closed` and `on_stop` instead. As of dora #1879 the bridge also calls `on_input_parse_error`. Existing operators must add a no-op stub for each new function — `return { rust::String(), false };` is sufficient. ### Required C++ interface You must provide a header `operator.h` and an implementation file. The header declares an `Operator` class and five free functions: ```cpp // operator.h #pragma once #include #include "dora-operator-api.h" class Operator { public: Operator(); // Add any state your operator needs. }; std::unique_ptr new_operator(); DoraOnInputResult on_input( Operator& op, rust::Str id, rust::Slice data, OutputSender& output_sender); DoraOnInputResult on_input_closed(Operator& op, rust::Str id, OutputSender& output_sender); DoraOnInputResult on_stop(Operator& op, OutputSender& output_sender); DoraOnInputResult on_input_parse_error(Operator& op, rust::Str id, rust::Str error, OutputSender& output_sender); ``` - `new_operator()` -- called once at startup; returns the operator instance. - `on_input()` -- called for every input event; process data and optionally send outputs. - `on_input_closed()` -- called when an upstream input stream closes (the daemon delivers `Event::InputClosed { id }`). Operator can log, flush per-input state, or `send_output(output_sender, ...)` to emit a final/status message in response. Set `result.stop = true` to request shutdown. - `on_stop()` -- called on graceful shutdown (the daemon delivers `Event::Stop`). Operator can drain output queues, persist final state, or `send_output(output_sender, ...)` to flush buffered data before returning. - `on_input_parse_error()` -- called when an input's Arrow data fails to deserialize (`Event::InputParseError { id, error }`). `id` identifies the affected input; `error` is the deserialization error string. Operator can log the error, surface it as health state, or request shutdown. Previously these events were silently dropped. Default implementations that simply log + return success are sufficient for operators that don't need to react to these events. See `examples/c++-dataflow/operator-rust-api/operator.cc` for a minimal reference. ### OutputSender (operator) Available inside all operator callbacks. Sends data on a named output. ```cpp DoraSendOutputResult send_output( OutputSender& sender, rust::Str id, rust::Slice data); ``` ### Result types ```cpp struct DoraOnInputResult { rust::String error; // empty on success bool stop; // true to request graceful shutdown }; ``` > **`error` and `stop` are mutually exclusive.** If `error` is non-empty the runtime treats the result as a fatal failure regardless of the `stop` field. Use `stop = true` only when `error` is empty. ```cpp struct DoraSendOutputResult { rust::String error; // empty on success }; ``` --- ## Quick Start: Node Example A minimal node that receives timer ticks and sends a counter. ```cpp #include "dora-node-api.h" #include #include int main() { auto dora_node = init_dora_node(); unsigned char counter = 0; for (;;) { auto event = next_event(dora_node.events); auto ty = event_type(event); if (ty == DoraEventType::AllInputsClosed) { break; } if (ty == DoraEventType::Stop) { break; } if (ty == DoraEventType::Input) { auto input = event_as_input(std::move(event)); counter += 1; std::cout << "Input: " << std::string(input.id) << " counter=" << (int)counter << std::endl; std::vector out{counter}; rust::Slice slice{out.data(), out.size()}; auto result = send_output(dora_node.send_output, "counter", slice); if (!result.error.empty()) { std::cerr << "Send error: " << std::string(result.error) << std::endl; return 1; } } } return 0; } ``` Dataflow YAML: ```yaml nodes: - id: cxx-node path: build/my_node inputs: tick: dora/timer/millis/300 outputs: - counter ``` --- ## Quick Start: Arrow Node Example A node that receives and sends Arrow arrays via the C Data Interface, with metadata. ```cpp #include "dora-node-api.h" #include #include #include int main() { auto dora_node = init_dora_node(); for (int i = 0; i < 10; i++) { auto event = dora_node.events->next(); auto ty = event_type(event); if (ty == DoraEventType::AllInputsClosed || ty == DoraEventType::Stop) { break; } if (ty == DoraEventType::Input) { // Receive Arrow input with metadata struct ArrowArray c_array; struct ArrowSchema c_schema; auto info = event_as_arrow_input_with_info( std::move(event), reinterpret_cast(&c_array), reinterpret_cast(&c_schema)); if (!info.error.empty()) { std::cerr << std::string(info.error) << std::endl; continue; } std::cout << "Input: " << std::string(info.id) << " ts=" << info.metadata->timestamp() << std::endl; auto imported = arrow::ImportArray(&c_array, &c_schema); auto array = imported.ValueOrDie(); std::cout << "Arrow: " << array->ToString() << std::endl; // Build an output Arrow array arrow::Int32Builder builder; builder.Append(i * 10); std::shared_ptr out_array; builder.Finish(&out_array); // Export and send with metadata struct ArrowArray out_c_array; struct ArrowSchema out_c_schema; arrow::ExportArray(*out_array, &out_c_array, &out_c_schema); auto meta = new_metadata(); meta->set_string("source", "cpp-arrow-node"); meta->set_int("iteration", i); auto result = send_arrow_output( dora_node.send_output, "counter", reinterpret_cast(&out_c_array), reinterpret_cast(&out_c_schema), std::move(meta)); if (!result.error.empty()) { std::cerr << "Send error: " << std::string(result.error) << std::endl; } } } return 0; } ``` --- ## Quick Start: Operator Example A minimal operator shared library. ```cpp // operator.cc #include "operator.h" #include #include Operator::Operator() {} std::unique_ptr new_operator() { return std::make_unique(); } DoraOnInputResult on_input( Operator& op, rust::Str id, rust::Slice data, OutputSender& output_sender) { op.counter += 1; std::vector out{op.counter}; rust::Slice slice{out.data(), out.size()}; auto send_result = send_output(output_sender, rust::Str("status"), slice); return DoraOnInputResult{send_result.error, false}; } ``` Dataflow YAML: ```yaml nodes: - id: runtime-node operators: - id: my-operator shared-library: build/my_operator inputs: data: some-node/output outputs: - status ``` --- ## Build Integration (CMake) The recommended build approach uses CMake with the `DoraTargets.cmake` helper (see `examples/cmake-dataflow/`). ### Project structure ``` my-project/ CMakeLists.txt DoraTargets.cmake # copied from examples/cmake-dataflow/ node/main.cc operator/operator.h operator/operator.cc dataflow.yml ``` ### CMakeLists.txt ```cmake cmake_minimum_required(VERSION 3.21) project(my-dataflow LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_FLAGS "-fPIC") include(DoraTargets.cmake) link_directories(${dora_link_dirs}) # Standalone node (executable) add_executable(my_node node/main.cc ${node_bridge}) add_dependencies(my_node Dora_cxx) target_include_directories(my_node PRIVATE ${dora_cxx_include_dir}) target_link_libraries(my_node dora_node_api_cxx) # Operator (shared library) add_library(my_operator SHARED operator/operator.cc ${operator_bridge}) add_dependencies(my_operator Dora_cxx) target_include_directories(my_operator PRIVATE ${dora_cxx_include_dir} ${dora_c_include_dir} ${CMAKE_CURRENT_SOURCE_DIR}/operator) target_link_libraries(my_operator dora_operator_api_cxx) install(TARGETS my_node DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/bin) install(TARGETS my_operator DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/lib) ``` ### What DoraTargets.cmake provides | Variable | Description | |----------|-------------| | `dora_cxx_include_dir` | Path to generated CXX headers (`dora-node-api.h`, `dora-operator-api.h`) | | `dora_c_include_dir` | Path to C API headers (for mixed C/C++ projects) | | `dora_link_dirs` | Library search path for `libdora_node_api_cxx.a` / `libdora_operator_api_cxx.a` | | `node_bridge` | Generated CXX bridge source file for nodes (`node_bridge.cc`) | | `operator_bridge` | Generated CXX bridge source file for operators (`operator_bridge.cc`) | | `Dora_cxx` | CMake target dependency that builds the CXX crates | ### Build steps ```bash # Option A: Build against local Dora source mkdir build && cd build cmake .. -DDORA_ROOT_DIR=/path/to/dora cmake --build . # Option B: Build against Dora from GitHub (cloned automatically) mkdir build && cd build cmake .. cmake --build . ``` ### Requirements - C++20 compiler - Rust toolchain (for building the Dora static libraries via Cargo) - CMake 3.21+ - For Arrow integration: Apache Arrow C++ library --- ## CXX Bridge Notes - All Rust opaque types (`Events`, `OutputSender`, `DoraEvent`, `Metadata`, `MergedEvents`, `MergedDoraEvent`) are accessed through `rust::Box`. - `rust::String`, `rust::Vec`, and `rust::Slice` are CXX bridge types that interoperate with their C++ standard library counterparts. See the [CXX type reference](https://cxx.rs/binding/box.html). - Functions that return `Result` in Rust throw C++ exceptions on the error path. - Arrow FFI functions (`event_as_arrow_input`, `send_arrow_output`) are `unsafe` on the Rust side. The caller must pass valid pointers to `ArrowArray` / `ArrowSchema` structs cast to `uint8_t*`. - The node library is a static archive (`staticlib`). Link it into your executable with `-ldora_node_api_cxx`. - The operator library is also a static archive. Link it into your shared library with `-ldora_operator_api_cxx`.