export_c turns one ArrayData tree into the two C structs every Arrow
implementation understands:
struct ArrowSchema { const char* format; const char* name; const char* metadata;
int64_t flags; int64_t n_children; ArrowSchema** children;
ArrowSchema* dictionary; void (*release)(ArrowSchema*);
void* private_data; };
struct ArrowArray { int64_t length; int64_t null_count; int64_t offset;
int64_t n_buffers; int64_t n_children; const void** buffers;
ArrowArray** children; ArrowArray* dictionary;
void (*release)(ArrowArray*); void* private_data; };
Each export is two allocations — one for the whole schema tree, one for the
whole array tree — laid out as: the structs, then the pointer arrays, then the
format and name strings and the metadata blocks, then copies of every buffer.
The root's private_data is the base of its block and its release frees the
lot; children carry a release that only clears itself, because a consumer
releases the root and the root owns everything. Both callbacks are abi("C"),
so they can be called from C, Python or Rust.
Buffers are copied rather than borrowed, which is what makes the export
outlive the RecordBatch it came from and makes ExportedArray safe to hand
to a runtime that will release it whenever it likes.
var e = export_c(batch.arena, batch.roots[0])
print(e.array, e.schema) # two addresses, ready for _import_from_c
e.into_raw() # hand ownership to the consumer
arrow_mlake.carrow_import is the other direction — a foreign ArrowSchema
and ArrowArray read back into an ArrayData, and ArrowArrayStream
iterated — and it implements the consumer's half of the same release
convention.