Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FFI (C/C++ integration)

The delta_kernel_ffi crate exposes delta-kernel-rs to C and C++ through a stable FFI boundary. It uses cbindgen to generate header files (.h and .hpp) at build time. This matters because it lets you build a connector in any language that can call C functions, not only Rust.

Building the FFI crate

The crate can be built as a shared library (cdylib) or static library (staticlib):

# Shared library (e.g. libdelta_kernel_ffi.so / .dylib / .dll)
cargo build -p delta_kernel_ffi --release

# Generated headers are written to target/ffi-headers/
# - delta_kernel_ffi.h   (C)
# - delta_kernel_ffi.hpp (C++)

Feature flags

FeatureDefaultDescription
default-engine-rustlsyesIncludes the DefaultEngine with rustls TLS
default-engine-native-tlsnoIncludes the DefaultEngine with native TLS (instead of rustls)
arrowyesEnables Arrow integration (selects arrow-58 by default)
arrow-58yesPin to Arrow 58 explicitly (enabled transitively by arrow)
arrow-57noPin to Arrow 57 explicitly
delta-kernel-unity-catalognoEnables Unity Catalog integration for catalog-managed tables
tracingnoEnables tracing/logging support via tracing-subscriber

Note: You must enable exactly one of default-engine-rustls or default-engine-native-tls. The default-engine-base feature contains shared implementation details and is not meant to be enabled directly.

The handle system

Objects that cross the FFI boundary are wrapped in handles. These are opaque pointers that carry ownership semantics. There are two kinds:

  • Mutable handles (Box-like) represent exclusive ownership. Dropping the handle drops the underlying object. These are neither Copy nor Clone.
  • Shared handles (Arc-like) represent shared ownership. Dropping the handle only drops the underlying object if it was the last reference.

Every handle has a corresponding free_* function that you must call to release it. For example, free_engine, free_snapshot, free_scan, free_transaction.

Several FFI functions consume their handle argument and return a new handle. After calling such a function, you must not use the old handle. The function documentation notes this with “CONSUMES the handle.”

Core API surface

The FFI mirrors the Rust API. A typical read flow looks like (no transaction is needed for reads):

get_default_engine()        ->  Handle<SharedExternEngine>
        |
get_snapshot_builder()      ->  Handle<MutableFfiSnapshotBuilder>
        |
snapshot_builder_build()    ->  Handle<SharedSnapshot>
        |
      scan()                ->  Handle<SharedScan>
        |
scan_metadata_iter_init()   ->  Handle<SharedScanMetadataIterator>
        |
  (read parquet, apply transforms, apply selection vectors)

A typical write flow:

get_default_engine()  ->  Handle<SharedExternEngine>
        |
    transaction()     ->  Handle<ExclusiveTransaction>
        |
  with_engine_info()  ->  Handle<ExclusiveTransaction>
        |
    add_files()
        |
    commit()          ->  ExternResult<u64>  (committed version)

For more control over scans, you can use the scan builder API instead of the convenience scan() function:

scan_builder()                ->  Handle<ExclusiveScanBuilder>
        |
scan_builder_with_predicate() ->  Handle<ExclusiveScanBuilder>
        |
scan_builder_with_schema()    ->  Handle<ExclusiveScanBuilder>
        |
scan_builder_build()          ->  Handle<SharedScan>

Public FFI functions

The tables below group the stable FFI functions by purpose. Unless noted, each function is available with the default feature flags. For the full, authoritative list and signatures, consult the generated delta_kernel_ffi.h header.

Engine creation

FunctionPurpose
get_default_engineCreate an engine from a table path with default options
get_engine_builder / set_builder_option / builder_buildCreate an engine with custom storage options
set_builder_with_multithreaded_executorConfigure the builder to use a multi-threaded tokio executor
free_engineRelease the engine handle

Snapshots

FunctionPurpose
get_snapshot_builderCreate a snapshot builder from a table path
get_snapshot_builder_fromCreate a snapshot builder incrementally from an existing snapshot
snapshot_builder_set_versionPin the snapshot to a specific table version
snapshot_builder_set_log_tailProvide a log tail for catalog-managed tables
snapshot_builder_set_max_catalog_versionBound the snapshot to the version the catalog has ratified
snapshot_builder_buildConsume the builder and produce the snapshot
free_snapshot_builder / free_snapshotRelease snapshot-related handles

Snapshot inspection

Use these on a Handle<SharedSnapshot> to read table metadata, protocol, and partition columns without building a scan.

FunctionPurpose
versionReturn the version number of a snapshot
snapshot_timestampReturn the snapshot’s commit timestamp (milliseconds since epoch)
snapshot_table_rootReturn the table root URL as an engine-allocated string
logical_schemaReturn the table’s logical schema as a Handle<SharedSchema>
snapshot_get_metadataClone the table metadata into a Handle<SharedMetadata>
snapshot_get_protocolClone the protocol into a Handle<SharedProtocol>
get_partition_column_count / get_partition_columnsCount partition columns and iterate their names as a StringSliceIterator
string_slice_next / free_string_slice_dataIterate and release a StringSliceIterator (e.g. returned by get_partition_columns)
get_app_id_versionLook up the last committed transaction version for an app_id (the read side of idempotent writes)
free_metadata / free_protocol / free_schemaRelease the corresponding handles

Schema and metadata visitors

Kernel exposes schemas, protocols, and metadata through visitor callbacks so the engine can materialize them into its own types without Kernel allocating engine-owned memory.

FunctionPurpose
visit_schemaWalk a SharedSchema by invoking per-field callbacks on an EngineSchemaVisitor
visit_protocolInvoke a visit_versions callback, then a visit_feature callback per reader/writer feature
visit_metadataInvoke a single callback with (id, name, description, format_provider, has_created_time, created_time_ms)
visit_metadata_configurationIterate the configuration key/value map (takes a snapshot handle, not a metadata handle)
visit_string_map / get_from_string_mapIterate or look up entries in an opaque CStringMap (used by both metadata and scan-metadata surfaces)

See Visitor callbacks below for the pattern.

Schema construction (projection pushdown)

The build-side counterpart to visit_schema: per-field callbacks that let the engine construct a Kernel StructType from its own type system (for example, to pass to scan_builder_with_schema).

FunctionPurpose
visit_field_byte / visit_field_short / visit_field_integer / visit_field_long / visit_field_float / visit_field_double / visit_field_booleanBuild a numeric or boolean primitive StructField
visit_field_string / visit_field_binary / visit_field_date / visit_field_timestamp / visit_field_timestamp_ntzBuild a string, binary, or date/time primitive StructField
visit_field_decimalBuild a decimal StructField with explicit precision and scale
visit_field_struct / visit_field_array / visit_field_map / visit_field_variantBuild a complex StructField (struct, array, map, or variant) from previously created field or struct IDs

Reading (scans)

FunctionPurpose
scanCreate a scan with optional predicate and projection (convenience function)
scan_builder / scan_builder_with_predicate / scan_builder_with_schema / scan_builder_buildBuild a scan incrementally with the builder pattern
scan_logical_schema / scan_physical_schema / scan_table_rootInspect the scan’s logical/physical read schemas and table root
scan_metadata_iter_init / scan_metadata_nextIterate over scan metadata (per-file lists, deletion vectors, transforms)
scan_metadata_next_arrow / free_scan_metadata_arrow_resultPull the next scan-metadata batch as an Arrow RecordBatch and release it (requires default-engine-base)
visit_scan_metadataInvoke a callback for each scan file in a SharedScanMetadata batch
selection_vector_from_scan_metadataMaterialize the per-row selection bitmap from a scan-metadata batch
selection_vector_from_dv / row_indexes_from_dvMaterialize a selection bitmap or row-index array from a DvInfo
get_transform_for_rowLook up the per-file transform expression for a given row in a scan-metadata batch
free_scan / free_scan_builder / free_scan_metadata / free_scan_metadata_iter / free_bool_slice / free_row_indexesRelease scan-related handles and allocations

Engine data and Arrow interop

Rows marked (requires default-engine-base) are only compiled when that feature is enabled; the rest are always available.

FunctionPurpose
engine_data_lengthReturn the row count of an ExclusiveEngineData batch
get_engine_dataImport Arrow C Data Interface array + schema into an ExclusiveEngineData (requires default-engine-base)
get_raw_arrow_dataExport an ExclusiveEngineData batch as Arrow C Data Interface structs (requires default-engine-base)
read_result_next / free_read_result_iterIterate a scan’s parquet read iterator and release it
free_engine_dataDrop a single ExclusiveEngineData batch
read_parquet_fileDirectly read a single parquet file via the engine’s parquet handler

Warning: get_raw_engine_data is always exported (regardless of feature flags) but unimplemented. It calls todo!() and will panic. Do not use it.

Writing (transactions)

FunctionPurpose
transactionStart a write transaction on the latest snapshot
transaction_with_committerStart a transaction with a custom committer
with_engine_infoRecord a free-form engine identifier on the transaction (consumes and returns a new handle)
with_transaction_idSet an (app_id, version) pair for idempotent writes (consumes and returns a new handle; see Idempotent Writes)
with_domain_metadata / with_domain_metadata_removedAttach or remove a domain-metadata entry (each consumes and returns a new handle)
add_filesAppend file-level write metadata to the transaction
set_data_changeToggle the transaction’s data-change flag (does not consume the handle)
remove_filesRegister Remove actions for the files selected by a scan-metadata batch
commitCommit the transaction and return the new version number
free_transactionRelease the transaction handle without committing

Write context and file writing

Use a WriteContext to learn where to write parquet files and what schema to write. For unpartitioned writes, one context serves the whole transaction. Partitioned writes (which would use one context per partition) are tracked in #2355.

Engines must append their own <uuid>.parquet filename (and any subdirectory layout) onto the returned table root. The kernel-side WriteContext::write_dir helper – which produces the recommended directory (Hive-style partition paths for partitioned tables when column mapping is off, or a random 2-char prefix when column mapping is on) – is internal and has no FFI binding.

FunctionPurpose
get_unpartitioned_write_contextGet a SharedWriteContext covering all rows in the transaction
get_write_pathReturn the table root URL from a SharedWriteContext (engines append their own subdirectory and filename)
get_write_schemaReturn the logical (user-facing) write schema from a SharedWriteContext
free_write_contextRelease the write-context handle

Domain metadata

FunctionPurpose
get_domain_metadataLook up the configuration string for a specific domain on a snapshot
visit_domain_metadataIterate all domain metadata entries on a snapshot

Table creation

FunctionPurpose
get_create_table_builderCreate a builder for a new Delta table with a schema
create_table_builder_with_table_propertyAdd a table property to the builder
create_table_builder_buildConsume the builder and produce a create-table transaction using the default (filesystem) committer
create_table_builder_build_with_committerConsume the builder and produce a create-table transaction with a custom committer
create_table_with_engine_infoAttach a free-form engine identifier to a create-table transaction (consumes and returns a new handle)
create_table_set_data_changeToggle the data-change flag on a create-table transaction (does not consume the handle)
create_table_get_unpartitioned_write_contextGet a WriteContext to stage initial data files during table creation
create_table_add_filesRegister file metadata for initial data being written alongside the CREATE TABLE commit
create_table_commitCommit the create-table transaction
free_create_table_builderRelease a create-table builder handle (before it is consumed by create_table_builder_build*)
create_table_free_transactionRelease a create-table transaction handle (after build, before commit)

Change data feed (table changes)

Incremental log reads for change data feed. Mirrors the Rust TableChanges API. The entire table_changes module is gated behind the default-engine-base feature.

FunctionPurpose
table_changes_from_versionOpen a TableChanges from start_version to the latest
table_changes_between_versionsOpen a TableChanges between start_version and end_version (inclusive)
table_changes_start_version / table_changes_end_versionRead the requested start/end versions
table_changes_schema / table_changes_table_rootInspect the CDF schema and table root
table_changes_scanApply an optional predicate and produce a SharedTableChangesScan
table_changes_scan_logical_schema / table_changes_scan_physical_schema / table_changes_scan_table_rootInspect the resulting scan
table_changes_scan_executeProduce an iterator of CDF data
scan_table_changes_nextPull the next *mut ArrowFFIData batch from the CDF iterator; the engine must release each non-null batch via free_arrow_ffi_data
free_table_changes / free_table_changes_scan / free_scan_table_changes_iterRelease CDF-related handles

Warning

scan_table_changes_next returns *mut ArrowFFIData (a heap-allocated Arrow C Data Interface batch). C callers must release each non-null result with free_arrow_ffi_data exactly once. This is an ABI-breaking change from earlier releases that used a different return shape, so connectors upgrading across that boundary need to update both the type and the cleanup path.

Checkpointing

FunctionPurpose
checkpoint_snapshotWrite a checkpoint for the given snapshot

Unity Catalog integration

Requires the delta-kernel-unity-catalog feature. Lets the engine provide a catalog-aware committer without implementing the committer trait from scratch.

FunctionPurpose
get_uc_commit_clientWrap an engine-provided CCommit callback in a SharedFfiUCCommitClient
get_uc_committerProduce a MutableCommitter bound to a specific table_id, ready to pass to transaction_with_committer
free_uc_commit_client / free_uc_committerRelease the corresponding handles

Expressions and predicates

Kernel’s expression system is exposed through two parallel surfaces:

  • Build (engine AST -> Kernel expression): the engine calls visit_engine_expression / visit_engine_predicate, providing an engine-side iterator. From inside that callback, the engine uses the visit_expression_* / visit_predicate_* builder functions to append Kernel-side nodes (columns, literals, operators) into an internal KernelExpressionVisitorState. The final handle is a SharedExpression or SharedPredicate.
  • Walk (Kernel expression -> engine type): the engine calls visit_expression / visit_predicate, supplying an EngineExpressionVisitor struct whose function pointers Kernel invokes as it traverses the tree.

Most engines need only one of the two surfaces.

FunctionPurpose
visit_engine_expression / visit_engine_predicateBuild a Kernel SharedExpression / SharedPredicate from an engine-side AST (Build surface)
visit_expression / visit_predicateWalk a Kernel expression or predicate with an EngineExpressionVisitor (Walk surface)
visit_expression_ref / visit_predicate_refWalk a pre-interned expression or predicate reference (Walk surface)
visit_expression_column / visit_expression_struct / visit_expression_plus / visit_expression_literal_* / …Builder functions the engine calls from inside visit_engine_expression to construct Kernel expression nodes; see delta_kernel_ffi.h for the full list
visit_predicate_eq / visit_predicate_and / visit_predicate_or / …Builder functions the engine calls from inside visit_engine_predicate to construct Kernel predicate nodes
visit_expression_unknown / visit_predicate_unknownBuilder helpers that bridge an opaque engine operator through Kernel unchanged
visit_kernel_opaque_expression_op_name / visit_kernel_opaque_predicate_op_nameInspect the name of an opaque op carried through by the above
new_expression_evaluator / evaluate_expression / free_expression_evaluatorCompile and invoke an expression against an EngineData batch
expressions_are_equal / predicates_are_equalCompare two expressions or predicates for structural equality
free_kernel_expression / free_kernel_predicate / free_kernel_opaque_expression_op / free_kernel_opaque_predicate_opRelease the corresponding handles

Tracing

Enable Kernel’s internal tracing instrumentation. Requires the tracing feature flag. Call at most one of these during a process lifetime; later calls return false.

FunctionPurpose
enable_log_line_tracingForward each log line to a TracingLogLineFn callback
enable_formatted_log_line_tracingForward fully formatted log lines to a TracingLogLineFn callback
enable_event_tracingForward structured TracingEvent records instead of log lines

Common utilities

FunctionPurpose
allocate_kernel_stringCreate a Kernel-owned string from a KernelStringSlice

Visitor callbacks

Several FFI entry points take a visitor struct (for example, EngineSchemaVisitor for visit_schema, EngineExpressionVisitor for visit_expression) or individual callbacks (as in visit_metadata). The pattern is the same in every case:

  1. You allocate a context (a NullableCvoid you own) that the callbacks can write into. Kernel does not interpret this value.
  2. You fill in one callback per node kind. Kernel invokes the callbacks passing the context plus node-specific arguments (names, types, literal values, child handles). For tree-structured inputs (schemas, expressions, predicates), callbacks fire in depth-first order; for flat inputs (e.g. visit_metadata), the callback fires once.
  3. When visit_* returns, the context holds your engine-side representation.

Callbacks run synchronously on the same thread that called visit_*. Strings passed to callbacks (KernelStringSlice) are borrowed for the duration of the call; copy them if you need to retain them beyond the callback.

Error handling

Kernel functions that can fail return an ExternResult<T>, which is a tagged union:

// C representation (simplified)
typedef enum { Ok, Err } ExternResultTag;
typedef struct {
    ExternResultTag tag;
    union {
        T ok;
        EngineError* err;
    };
} ExternResult;

You provide an allocate_error callback when creating the engine. Kernel calls this callback to allocate error objects in your memory space whenever an operation fails. Because the engine allocates these errors, the engine is also responsible for freeing them. Kernel returns the error pointer immediately and does not retain it.

The EngineError struct contains a KernelError enum that classifies the error type (e.g., GenericError, FileNotFoundError, InvalidUrlError). The error message string passed to allocate_error is only valid for the duration of the callback, so you must copy it if you need to keep it.

C examples

The repository ships four runnable C examples under ffi/examples/. Each is a complete program that links against delta_kernel_ffi and exercises a different slice of the API.

ExampleDemonstrates
read-tableThe full read path: schema visiting, scan-metadata iteration, and Arrow data handling. Pass -a to switch from the callback-based scan-metadata path to the Arrow batch-mode path (scan_metadata_next_arrow).
read-table-changesReading a change data feed using table_changes_* and consuming ArrowFFIData batches from scan_table_changes_next.
create-tableCreating a new Delta table via the get_create_table_builder / create_table_builder_build / create_table_commit flow.
write-tableAppending data to an existing table via the transaction / add_files / commit flow.

The high-level flow in the read-table example:

// 1. Create an engine
ExternResultHandleSharedExternEngine engine_res =
    get_default_engine(table_path, allocate_error);

// 2. Build a snapshot
ExternResultHandleMutableFfiSnapshotBuilder builder_res =
    get_snapshot_builder(table_path, engine);
ExternResultHandleSharedSnapshot snap_res =
    snapshot_builder_build(builder);

// 3. Create a scan
ExternResultHandleSharedScan scan_res =
    scan(snap, engine, NULL, NULL);

// 4. Iterate over scan metadata and read data
// ... (see the full example for details)

// 5. Clean up
free_scan(the_scan);
free_snapshot(snap);
free_engine(engine);

What’s next