C API Reference
This document covers the two C APIs provided by the Adora framework: the Node API for standalone C processes and the Operator API for shared-library operators loaded by the Adora runtime.
Table of Contents
- Node API (adora-node-api-c)
- Operator API (adora-operator-api-c)
- Node Example
- Operator Example
- Building and Linking
Node API (adora-node-api-c)
Header: apis/c/node/node_api.h
Crate: adora-node-api-c (builds as staticlib)
The Node API is used by standalone C executables that participate in an Adora dataflow as external processes. The daemon spawns the process and sets environment variables that the node reads during initialization.
Initialization
init_adora_context_from_env
void *init_adora_context_from_env();
Initializes an Adora node context from environment variables set by the daemon. Returns an opaque pointer to the context on success, or NULL on failure.
The returned pointer must be passed to all subsequent Node API calls that expect a context argument. When the node is finished, free it with free_adora_context.
free_adora_context
void free_adora_context(void *adora_context);
Frees a context previously created by init_adora_context_from_env. Each context must be freed exactly once. After freeing, the pointer must not be used again.
Event Loop
adora_next_event
void *adora_next_event(void *adora_context);
Blocks until the next event is available for this node. Returns an opaque pointer to the event, or NULL when all event streams have closed (indicating the node should exit).
The returned pointer must not be dereferenced directly. Use the read_adora_* functions to extract the event type and payload. Free the event with free_adora_event when done.
free_adora_event
void free_adora_event(void *adora_event);
Frees an event previously returned by adora_next_event. Each event must be freed exactly once. After freeing, the event pointer and all derived pointers (from read_adora_input_id, read_adora_input_data) become invalid.
Event Inspection
read_adora_event_type
enum AdoraEventType read_adora_event_type(void *adora_event);
Returns the type of the given event. See AdoraEventType for possible values.
read_adora_input_id
void read_adora_input_id(void *adora_event, char **out_ptr, size_t *out_len);
Reads the input ID from an AdoraEventType_Input event. Writes the string start pointer to *out_ptr and its byte length to *out_len. The string is valid UTF-8 but not null-terminated; use out_len to determine its bounds.
If the event is not an input event, sets *out_ptr = NULL and *out_len = 0.
The returned pointer borrows from the event. It becomes invalid after free_adora_event is called.
read_adora_input_data
void read_adora_input_data(void *adora_event, char **out_ptr, size_t *out_len);
Reads the raw data bytes from an AdoraEventType_Input event. Writes the data start pointer to *out_ptr and its byte length to *out_len.
Sets *out_ptr = NULL and *out_len = 0 if the event is not an input event or the input carries no data.
Currently only UInt8 Arrow arrays are supported. Other Arrow data types will cause a runtime panic. Future versions will use the Arrow C Data Interface for full type support.
The returned pointer borrows from the event. It becomes invalid after free_adora_event is called.
read_adora_input_timestamp
unsigned long long read_adora_input_timestamp(void *adora_event);
Returns the hybrid logical clock timestamp from an input event’s metadata as a uint64 value. Returns 0 if the event is not an input event.
Output
adora_send_output
int adora_send_output(
void *adora_context,
const char *id_ptr,
size_t id_len,
const char *data_ptr,
size_t data_len
);
Sends output data to all downstream subscribers. The output ID (id_ptr/id_len) must be a valid UTF-8 string matching one of the node’s declared outputs in the dataflow YAML. The data (data_ptr/data_len) is sent as raw bytes (UInt8 Arrow array).
Returns 0 on success, -1 on error. Errors are logged via tracing.
Returns -1 immediately if any pointer argument is NULL.
Logging
adora_log
int adora_log(
void *adora_context,
const char *level_ptr,
size_t level_len,
const char *msg_ptr,
size_t msg_len
);
Sends a structured log message through the Adora logging pipeline. Both level and msg must be valid UTF-8 strings.
Valid log levels: "error", "warn", "info", "debug", "trace".
Returns 0 on success, -1 on error. Returns -1 immediately if any pointer argument is NULL.
Enums
AdoraEventType
enum AdoraEventType {
AdoraEventType_Stop, // Graceful shutdown requested
AdoraEventType_Input, // New input data available
AdoraEventType_InputClosed, // An input stream was closed
AdoraEventType_Error, // An error occurred
AdoraEventType_Unknown, // Unrecognized event type
};
Operator API (adora-operator-api-c)
Headers: apis/c/operator/operator_api.h, apis/c/operator/operator_types.h
Crate: adora-operator-api-c
The Operator API is used by shared libraries (.so/.dylib/.dll) loaded into the Adora runtime process. Unlike nodes, operators do not have their own main function. Instead, they export three functions that the runtime calls at the appropriate lifecycle points.
The operator_types.h header is auto-generated by safer-ffi and defines all C-compatible struct and enum types.
Lifecycle Functions
adora_init_operator
AdoraInitResult_t adora_init_operator(void);
Called once when the runtime loads the operator. Allocate and initialize any operator state, then return it via the operator_context field. The runtime passes this pointer back on every subsequent call.
Return an AdoraInitResult_t with .result.error = NULL on success.
adora_drop_operator
AdoraResult_t adora_drop_operator(void *operator_context);
Called once when the operator is being unloaded. Free all resources associated with operator_context.
Return an AdoraResult_t with .error = NULL on success.
Event Handling
adora_on_event
OnEventResult_t adora_on_event(
RawEvent_t *event,
const SendOutput_t *send_output,
void *operator_context
);
Called by the runtime each time an event arrives for this operator. Inspect the event fields to determine the event type:
| Field | Meaning |
|---|---|
event->input != NULL | New input available |
event->stop == true | Graceful shutdown requested |
event->error.ptr != NULL | An error occurred (UTF-8 string in error.ptr/error.len) |
event->input_closed.ptr != NULL | An input stream closed (input ID in input_closed.ptr/input_closed.len) |
Use send_output to emit data to downstream nodes (see adora_send_operator_output). Return an OnEventResult_t with the appropriate AdoraStatus_t to control the operator lifecycle.
Input Reading
adora_read_input_id
char *adora_read_input_id(const Input_t *input);
Returns a newly allocated null-terminated string containing the input ID. The caller must free it with adora_free_input_id.
adora_read_data
Vec_uint8_t adora_read_data(Input_t *input);
Reads the input data as a byte array. Consumes the underlying Arrow array from the input (the data can only be read once per event). Returns a Vec_uint8_t with .ptr = NULL if the input has no data or the data has already been consumed.
The caller must free the returned data with adora_free_data.
Output Sending
adora_send_operator_output
AdoraResult_t adora_send_operator_output(
const SendOutput_t *send_output,
const char *id,
const uint8_t *data_ptr,
size_t data_len
);
Sends output data to downstream subscribers. The id must be a null-terminated string matching one of the operator’s declared outputs. The data (data_ptr/data_len) is converted to a UInt8 Arrow array internally.
Returns an AdoraResult_t with .error = NULL on success.
Memory Management
The Operator API allocates memory that the caller must free using the corresponding functions:
| Allocation source | Free function |
|---|---|
adora_read_input_id | adora_free_input_id |
adora_read_data | adora_free_data |
void adora_free_input_id(char *input_id);
void adora_free_data(Vec_uint8_t data);
Failing to call these functions will leak memory. Do not use free() on these allocations – they are allocated by the Rust runtime and must be freed through the API.
Structs
Vec_uint8_t
typedef struct Vec_uint8 {
uint8_t *ptr;
size_t len;
size_t cap;
} Vec_uint8_t;
A Rust-allocated byte vector. Access len bytes starting at ptr. Do not modify cap. Free with adora_free_data.
AdoraResult_t
typedef struct AdoraResult {
Vec_uint8_t *error; // NULL on success, points to error string on failure
} AdoraResult_t;
Generic result type. A NULL error pointer indicates success. When non-NULL, the error pointer contains a UTF-8 error message.
AdoraInitResult_t
typedef struct AdoraInitResult {
AdoraResult_t result;
void *operator_context; // opaque pointer to operator state
} AdoraInitResult_t;
Returned by adora_init_operator. On success, result.error is NULL and operator_context holds the operator state pointer.
OnEventResult_t
typedef struct OnEventResult {
AdoraResult_t result;
AdoraStatus_t status;
} OnEventResult_t;
Returned by adora_on_event. Contains both an error/success result and a status code controlling the operator lifecycle.
RawEvent_t
typedef struct RawEvent {
Input_t *input; // non-NULL when this is an input event
Vec_uint8_t input_closed; // non-empty when an input stream closed
bool stop; // true when shutdown is requested
Vec_uint8_t error; // non-empty on error
} RawEvent_t;
Represents an event delivered to the operator. Multiple fields may be set simultaneously; check them in order of priority.
Input_t
typedef struct Input Input_t; // opaque
Opaque type representing an input event’s data. Use adora_read_input_id and adora_read_data to extract its contents.
Output_t
typedef struct Output Output_t; // opaque
Opaque type used internally by adora_send_operator_output. Not created directly by user code.
SendOutput_t
typedef struct SendOutput {
ArcDynFn1_AdoraResult_Output_t send_output;
} SendOutput_t;
Callback handle passed to adora_on_event. Pass it to adora_send_operator_output to emit data. Do not store it beyond the scope of the current adora_on_event call.
Metadata_t
typedef struct Metadata {
Vec_uint8_t open_telemetry_context;
} Metadata_t;
Event metadata containing an OpenTelemetry trace context string.
Operator Enums
AdoraStatus_t
enum AdoraStatus {
ADORA_STATUS_CONTINUE = 0, // Keep running
ADORA_STATUS_STOP = 1, // Stop this operator
ADORA_STATUS_STOP_ALL = 2, // Stop the entire dataflow
};
typedef uint8_t AdoraStatus_t;
Returned in OnEventResult_t to control operator lifecycle after processing an event.
Node Example
A complete C node that receives timer ticks and sends output messages:
#include <stdio.h>
#include <string.h>
#include "node_api.h"
int main() {
void *ctx = init_adora_context_from_env();
if (ctx == NULL) {
fprintf(stderr, "failed to init adora context\n");
return 1;
}
for (int i = 0; i < 100; i++) {
void *event = adora_next_event(ctx);
if (event == NULL)
break; // all streams closed
enum AdoraEventType ty = read_adora_event_type(event);
if (ty == AdoraEventType_Input) {
char *id;
size_t id_len;
read_adora_input_id(event, &id, &id_len);
// Send a response
char out_id[] = "message";
char out_data[64];
int out_len = snprintf(out_data, sizeof(out_data),
"iteration %d", i);
adora_send_output(ctx, out_id, strlen(out_id),
out_data, out_len);
} else if (ty == AdoraEventType_Stop) {
free_adora_event(event);
break;
}
free_adora_event(event);
}
free_adora_context(ctx);
return 0;
}
Dataflow YAML for the node:
nodes:
- id: c_node
path: build/c_node
inputs:
timer: adora/timer/millis/100
outputs:
- message
Operator Example
A complete C operator that reads input, maintains state, and sends output:
#include "operator_api.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
AdoraInitResult_t adora_init_operator(void) {
// Allocate operator state (a simple counter)
int *counter = (int *)calloc(1, sizeof(int));
AdoraInitResult_t result = {.operator_context = counter};
return result;
}
AdoraResult_t adora_drop_operator(void *operator_context) {
free(operator_context);
AdoraResult_t result = {.error = NULL};
return result;
}
OnEventResult_t adora_on_event(
RawEvent_t *event,
const SendOutput_t *send_output,
void *operator_context)
{
OnEventResult_t result = {.status = ADORA_STATUS_CONTINUE};
int *counter = (int *)operator_context;
if (event->input != NULL) {
char *id = adora_read_input_id(event->input);
Vec_uint8_t data = adora_read_data(event->input);
if (data.ptr != NULL) {
*counter += 1;
printf("received input '%s', counter: %d\n", id, *counter);
// Send counter value as string
char buf[64];
int len = snprintf(buf, sizeof(buf), "count=%d", *counter);
result.result = adora_send_operator_output(
send_output, "counter", (uint8_t *)buf, len);
adora_free_data(data);
}
adora_free_input_id(id);
}
if (event->stop) {
result.status = ADORA_STATUS_STOP;
}
return result;
}
Dataflow YAML for the operator:
nodes:
- id: runtime-node
operators:
- id: c_operator
shared-library: build/operator
inputs:
data: source_node/output
outputs:
- counter
Building and Linking
Node (static library)
C nodes link against adora-node-api-c, which builds as a static library.
Step 1: Build the static library
cargo build -p adora-node-api-c --release
This produces target/release/libadora_node_api_c.a (or .lib on Windows).
Step 2: Compile and link
clang node.c -ladora_node_api_c -L ../../target/release -o build/c_node <FLAGS>
Platform-specific linker flags:
| Platform | Flags |
|---|---|
| Linux | -lm -lrt -ldl -pthread |
| macOS | -framework CoreServices -framework Security -lSystem -lresolv -lpthread -lc -lm |
| Windows | -ladvapi32 -luserenv -lkernel32 -lws2_32 -lbcrypt -lncrypt -lschannel -lntdll -liphlpapi -lcfgmgr32 -lcredui -lcrypt32 -lcryptnet -lfwpuclnt -lgdi32 -lmsimg32 -lmswsock -lole32 -lopengl32 -lsecur32 -lshell32 -lsynchronization -luser32 -lwinspool -Wl,-nodefaultlib:libcmt -D_DLL -lmsvcrt |
On Windows, add the .exe extension to the output file.
Operator (shared library)
C operators are compiled into shared libraries that the Adora runtime loads at startup.
Step 1: Compile to object file
clang -c operator.c -o build/operator.o -fdeclspec -fPIC
Omit -fPIC on Windows.
Step 2: Link as shared library
# Linux
clang -shared build/operator.o -o build/liboperator.so
# macOS
clang -shared build/operator.o -o build/liboperator.dylib
# Windows
clang -shared build/operator.o -o build/operator.dll
Step 3: Reference in dataflow YAML
operators:
- id: c_operator
shared-library: build/operator # without lib prefix or extension
inputs:
data: source/output
outputs:
- result
The shared-library path omits the platform-specific prefix (lib) and extension (.so/.dylib/.dll). The runtime resolves the correct file for the current platform.
Include Paths
The Node API header is at apis/c/node/node_api.h. The Operator API headers are at apis/c/operator/operator_api.h and apis/c/operator/operator_types.h. Adjust your include paths accordingly:
# Node
clang -I path/to/adora/apis/c/node node.c ...
# Operator
clang -I path/to/adora/apis/c/operator operator.c ...
C++ Compatibility
Both headers include extern "C" guards (in the operator headers) or use C-compatible declarations (in the node header), so they can be included directly from C++ source files.