C Foreign Function Interface bindings for the Zerobus Rust SDK.
# Build both static and dynamic libraries
cargo build -p zerobus-ffi --release
# Output:
# target/release/libzerobus_ffi.a (static library)
# target/release/libzerobus_ffi.so (Linux dynamic library)
# target/release/libzerobus_ffi.dylib (macOS dynamic library)
# target/release/zerobus_ffi.dll (Windows dynamic library)# Linux ARM64
rustup target add aarch64-unknown-linux-gnu
cargo build -p zerobus-ffi --release --target aarch64-unknown-linux-gnu
# Linux musl (Alpine), x86_64 and ARM64 — cross-compiled with cargo-zigbuild.
# Built with -C target-feature=-crt-static so the libs link musl dynamically,
# which is correct for dynamically-linked Alpine hosts.
cargo install cargo-zigbuild
rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl
RUSTFLAGS="-C target-feature=-crt-static" \
cargo zigbuild -p zerobus-ffi --release --target x86_64-unknown-linux-musl
RUSTFLAGS="-C target-feature=-crt-static" \
cargo zigbuild -p zerobus-ffi --release --target aarch64-unknown-linux-musl
# macOS ARM64 (Apple Silicon)
rustup target add aarch64-apple-darwin
cargo build -p zerobus-ffi --release --target aarch64-apple-darwin
# Windows
rustup target add x86_64-pc-windows-gnu
cargo build -p zerobus-ffi --release --target x86_64-pc-windows-gnu/*
#cgo LDFLAGS: -L${SRCDIR}/lib -lzerobus_ffi -ldl -lpthread -lm
#include "zerobus.h"
*/
import "C"[DllImport("zerobus_ffi", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr zerobus_sdk_new(string endpoint, string ucUrl, ref CResult result);#include "zerobus.h"
// Link with -lzerobus_ffiFor callers that do not want to block a thread in the synchronous stream
creation functions, use zerobus_sdk_create_stream_async or
zerobus_sdk_create_stream_with_headers_provider_async. Each returns once the
request has been validated and queued on the Rust runtime, then invokes a
one-shot CreateStreamAsyncCallback with either the created
CZerobusStream* or a null stream plus a failure CResult.
The callback receives a const CResult* that is valid only for the duration of
the call, so copy error_message if you need to keep it. All string,
descriptor, and config inputs are copied before the async create call returns,
but the CZerobusSdk* itself must remain valid until the callback fires.
Build a protobuf descriptor and encode records straight from Unity Catalog
table metadata — no pre-generated .proto file and no second Rust crate:
CResult r = {0};
/* init: fetch GET /api/2.1/unity-catalog/tables/{name} and pass its JSON body */
CZerobusProtoSchema *schema = zerobus_proto_schema_from_uc_json(uc_table_json, &r);
/* on error schema == NULL; read r.error_message then zerobus_free_error_message(r.error_message) */
uintptr_t dlen;
const uint8_t *desc = zerobus_proto_schema_descriptor_bytes(schema, &dlen);
CZerobusStream *stream = zerobus_sdk_create_stream(sdk, table_name, desc, dlen,
client_id, client_secret, &opts, &r);
/* per record, at flush time */
uint8_t *buf; uintptr_t len;
if (zerobus_proto_schema_encode_json(schema, record_json, &buf, &len, &r)) {
/* collect buf/len into a batch, ingest via zerobus_stream_ingest_proto_records(...) */
zerobus_free_proto_bytes(buf, len);
}
/* shutdown */
zerobus_proto_schema_free(schema);Encoding contract: record object keys are matched to column names; unknown keys are ignored (upstream records often carry extra non-column metadata). Records are encoded through protobuf's JSON mapping, so a few column types need their JSON value shaped accordingly:
| Unity Catalog type | Proto type | JSON value to supply |
|---|---|---|
STRING |
string |
string |
INT/INTEGER, SHORT/SMALLINT |
int32 |
number |
LONG/BIGINT |
int64 |
number or string — use a string above 2^53 (see below) |
FLOAT, DOUBLE |
float/double |
number |
BOOLEAN |
bool |
boolean |
BINARY |
bytes |
base64-encoded string (not a JSON array of byte values) |
DATE |
int32 |
integer — days since the Unix epoch (not an ISO-8601 string) |
TIMESTAMP |
int64 |
integer — microseconds since the Unix epoch (not an ISO-8601 string) |
VARIANT |
string |
JSON-encoded string — a string whose contents are the variant's JSON (objects, arrays, or primitives) |
ARRAY<T> |
repeated T |
JSON array of T values |
MAP<K,V> |
map<K,V> |
JSON object (K must be an integral, bool, or string type) |
STRUCT<...> |
nested message |
JSON object |
This table mirrors the supported set in the Zerobus type-support
reference.
Beyond that reference, the encoder additionally accepts BYTE/TINYINT (→
int32), TIMESTAMP_NTZ (→ int64 micros, same wire shape as TIMESTAMP),
and DECIMAL (→ string, e.g. "123.45", to preserve precision/scale).
Complex columns (ARRAY/MAP/STRUCT) require the column's type_json from
the Unity Catalog REST response to be present in the input.
Two precision pitfalls worth calling out:
- 64-bit integers above 2^53 (large
BIGINTvalues, andTIMESTAMPmicros past year 2255) lose precision when emitted as a JSON number by most encoders. Pass them as JSON strings (the canonical protobuf-JSON form for 64-bit ints), which round-trip exactly. DATE/TIMESTAMP*unit mismatches are silent: writing milliseconds where microseconds are expected shifts every row by 10³.
Non-nullable scalar and struct columns become proto2 required fields; a record
missing one is rejected rather than encoded. Presence is checked recursively, so
a required field missing inside a STRUCT, inside an ARRAY<STRUCT> element, or
inside a MAP value is also rejected (the error names the full path, e.g.
addr.zip, items[2].id, props[home].zip). Non-nullable ARRAY/MAP columns
map to repeated, which has no presence, so an omitted one encodes as empty
rather than being rejected.
A handle may be shared by concurrent readers: worker threads may call
zerobus_proto_schema_encode_json and zerobus_proto_schema_descriptor_bytes
on the same handle concurrently. zerobus_proto_schema_free must be called
exactly once, after all in-flight calls on the handle have returned — it must
not race any other use of the handle.
See zerobus.h for the complete C API documentation.