C ABI

The ABI is designed around opaque host handles. Rust owns the control lifetime; the host owns the underlying object behind each handle.

When a C entry point fails with a DagMlError, error_out carries the ADR-11 descriptor JSON shape: category, code, severity, message, remediation_hint and context. Null-pointer and malformed-input preflight errors still use the legacy human string because callers often branch on the status code before parsing a payload.

Current Scaffold

crates/dag-ml-capi/include/dag_ml.h exposes:

  • version and string-free helpers;

  • owned byte release helper for JSON outputs returned by Rust;

  • owned row-major DagMlF64Tensor and column-major DagMlF64ColumnarTensor release helpers for Rust-allocated prediction buffers returned to host bindings;

  • dagml_graph_spec_contract_json and dagml_graph_validate_json for GraphSpec contract discovery and graph validation before plan building;

  • dagml_model_input_spec_contract_json, dagml_model_input_spec_validate_json, dagml_data_plan_contract_json and dagml_data_plan_validate_json for non-Rust bindings that need to exchange neutral data/model compatibility requests and data-planner answers;

  • dagml_controller_manifest_validate_json and dagml_controller_manifest_list_validate_json so bindings can preflight controller manifests and registry uniqueness before plan building;

  • custom aggregation-controller task/result contract discovery and validation, including result-vs-task checks that preserve requested sample/unit order, fold scope, prediction level and target names before a host aggregation controller result can be trusted by the coordinator;

  • the generic controller vtable invoke path is also used for runtime custom aggregation dispatch: bindings receive an AggregationControllerTask JSON and return an AggregationControllerResult JSON, after which Rust validates the result against the exact task;

  • dagml_data_output_provenance_contract_json and dagml_data_output_provenance_validate_json so bindings can discover the reserved DataProviderViewSpec.extra key and validate propagated data-view provenance before using it;

  • dagml_node_result_validate_for_task_json so host bindings can preflight a controller-produced NodeResult against the exact NodeTask before handing the JSON back to the scheduler;

  • dagml_pipeline_dsl_contract_json and dagml_pipeline_dsl_validate_json so bindings can discover and preflight the canonical PipelineDslSpec and serialized nirs4all-compatible list/dict DSL input profiles;

  • dagml_pipeline_dsl_compile_json for pure compilation of that DSL input surface into canonical GraphSpec JSON, plus dagml_pipeline_dsl_compile_artifact_json when bindings also need the extracted GenerationSpec including coordinated override dimensions, validated shape-plan fragments, validated data-binding fragments, a campaign template and search-space fingerprint, and dagml_pipeline_dsl_execution_plan_build_json when bindings want the Rust planner to build a validated ExecutionPlan directly from DSL and controller manifests;

  • dagml_graph_parallel_levels_json for deterministic node batches that bindings can use to prepare parallel schedulers;

  • dagml_execution_plan_build_json for compiling graph/campaign/controller manifests into an ExecutionPlan while Rust owns planner validation;

  • dagml_execution_plan_schedule_json for exporting deterministic phase/variant/fold node-level schedules from a compiled ExecutionPlan;

  • selection policy/decision validation and candidate selection JSON helpers;

  • sample-level and target/group aggregated prediction block conversion helpers that validate canonical JSON blocks and return contiguous row-major double buffers with explicit rows, cols, len and capacity;

  • prediction-cache payload tensor export that validates a cache payload set against its bundle before returning contiguous row-major F64 values plus JSON metadata for requirement key, prediction level, block offsets, folds and sample/unit ids. The metadata JSON is versioned by DAG_ML_PREDICTION_CACHE_TENSOR_METADATA_SCHEMA_VERSION and documented in docs/contracts/prediction_cache_tensor_metadata.schema.json;

  • prediction-cache payload columnar tensor export that returns contiguous column-major F64 values with explicit column_offsets, versioned by DAG_ML_PREDICTION_CACHE_COLUMNAR_TENSOR_METADATA_SCHEMA_VERSION and documented in docs/contracts/prediction_cache_columnar_tensor_metadata.schema.json;

  • execution bundle validation, replay-envelope validation, replay-request validation and prediction-cache payload validation helpers;

  • dagml_research_provenance_export_json for building the standards-facing ResearchProvenanceExport JSON over validated execution plans, bundles, optional lineage records, replay envelopes, prediction-cache manifests and artifact manifests; null pointer plus zero length denotes an omitted optional input;

  • dagml_openlineage_run_event_json for building the same validated provenance evidence as an OpenLineage RunEvent JSON with explicit namespace and event time byte views;

  • vtable replay execution helper that composes host controllers, host data provider, host artifact store and optional host prediction-cache store while Rust owns scheduling and validation;

  • replay-request validation can optionally include an OOF prediction-cache payload set, which is required for OOF-dependent REFIT replay;

  • mock replay execution helper that returns a JSON summary while exercising Rust-side data handle materialization, data view creation and artifact handle materialization;

  • Arrow C Data ArrowArray and ArrowSchema structs for controller predictions and data-provider identity/target/feature exports;

  • DagMlControllerVTable for host operator controllers, including generic invoke over NodeTask/NodeResult JSON and explicit returned-byte release. Controller vtable ABI v2 keeps borrowed user_data semantics; v3 is opt-in owned semantics where Rust calls destroy(user_data) after releasing controller-owned result handles. The planned multitask extension keeps the same ownership shape: an optional invoke_batch carries a validated TaskBatchRequest JSON and returns one logical NodeResult per member task plus optional parent batch provenance;

  • DagMlDataVTable for host data providers, including materialize, make_view, view_identity, target_arrow and feature_arrow. feature_arrow remains ABI-compatible: hosts may receive either a plain feature-set id or a JSON fusion selector understood by dag-ml-data providers. The vtable uses the shared DAG_ML_DATA_PROVIDER_VTABLE_ABI_VERSION macro and guarded DagMlDataVTable definition so dag_ml.h and dag_ml_data.h can be included together by bindings.

  • DagMlArtifactStoreVTable for host replay artifact stores, returning typed DagMlHandleRef values for model/artifact handles. Artifact references are JSON-level Rust contracts with optional typed backend, URI, content fingerprint and plugin/version metadata; the ABI still transports them inside owned JSON payloads so C structs do not freeze a storage layout too early. Artifact-store vtable ABI v1 is borrowed; v2 is opt-in owned lifecycle with destroy(user_data) after materialized artifact handles are released.

  • DagMlPredictionCacheVTable for host prediction-cache stores, including load_blocks, materialize and explicit returned-byte release. load_blocks is the single JSON load callback for replay prediction requirements: sample-level requirements return PredictionBlock[], while target/group requirements return AggregatedPredictionBlock[] keyed by typed PredictionUnitId values. Rust selects and validates the expected block shape from the bundle requirement before materializing a prediction handle. Prediction-cache vtable ABI v1 is borrowed; v2 is opt-in owned lifecycle with destroy(user_data) after materialized prediction handles are released.

  • docs/contracts/process_adapter_description.schema.json documents the required process-adapter --describe JSON used by CLI-managed host adapters. The C ABI exposes its schema version and schema id through dagml_process_adapter_description_contract_json.

  • docs/contracts/process_adapter_frame.schema.json documents the control_frames_v1 JSONL protocol used by persistent process adapters: coordinator init/task/close request frames and adapter ack/result/error response frames wrapping the published NodeTask/NodeResult contracts. The C ABI exposes the frame schema version and schema id through dagml_process_adapter_frame_contract_json.

  • docs/contracts/graph_spec.schema.json documents the canonical graph JSON consumed by the planner and C ABI.

  • docs/contracts/model_input_spec.schema.json and docs/contracts/data_plan.schema.json document the neutral data-shape contracts exchanged between controller descriptions, data planners and host bindings.

The vtables are intentionally small in this scaffold. They establish shape, ownership and naming before full execution is implemented.

DagMlStatusCode is a fixed uint32_t ABI value rather than a C/Rust enum boundary type. Unknown host status codes are treated as runtime validation errors instead of being decoded as Rust enum discriminants.

Vtable user_data lifetime is explicit per ABI surface. Controller vtable v2 is borrowed for backwards compatibility, while controller vtable v3 opts into Rust-owned lifecycle and calls destroy(user_data) after handle release on drop. Artifact-store and prediction-cache vtable v1 are also borrowed; their v2 surfaces opt into Rust-owned lifecycle with destroy(user_data) after materialized handles are released. Data-provider vtables remain borrowed in the current replay API because that ABI is shared with dag-ml-data. Rust releases controller-result, data/view, replay-artifact and prediction-cache handles that it receives or materializes through the vtables.

Ownership Rules

Object

Owner

Release path

Host data block

Host

DataVTable.release

Host controller result JSON

Host allocation returned through controller vtable

ControllerVTable.release_bytes

Host fitted model

Host

ControllerVTable.release

Host replay artifact handle

Host

ArtifactStoreVTable.release

Host prediction cache handle

Host

PredictionCacheVTable.release

Rust error string

Rust allocation returned through ABI

dagml_string_free

Rust JSON byte output

Rust allocation returned through ABI

dagml_owned_bytes_free

Rust row-major F64 tensor output

Rust allocation returned through ABI

dagml_f64_tensor_free

Rust column-major F64 tensor output

Rust allocation returned through ABI

dagml_f64_columnar_tensor_free

Host JSON byte output

Host allocation returned through prediction-cache vtable

PredictionCacheVTable.release_bytes

Arrow arrays

Producer of the Arrow array

Arrow C Data Interface release callback

JSON blobs

Caller-provided view unless returned as owned bytes

ABI-specific free function

ABI Roadmap

  1. Freeze DagMlBytesView, DagMlOwnedBytes, DagMlF64Tensor, DagMlF64ColumnarTensor, handle and status conventions.

  2. Add schema coverage for the remaining execution-plan and bundle sub-blobs that are still Rust-implicit.

  3. Add conformance tests that call the C ABI from a small C program.

  4. Add C conformance tests that drive non-mock replay through the vtable surface.

  5. Keep shared dag-ml-data header inclusion and vtable ABI conformance in CI.

  6. Add host adapters for Python and native C++ controllers and artifact stores.