carrow.export_c hands one array across the boundary. This hands a sequence
of them, which is the shape every consumer wants for a scan — DuckDB, pyarrow
and polars all take an ArrowArrayStream and pull batches until it ends.
Why this is the local answer
Flight moves Arrow between processes. In one process there is nothing to move:
the consumer reads our buffers where they already are, and the only thing that
crosses is a struct of pointers. A DuckDB extension loading a Mojo shared
library is exactly that case, and reaching for a wire protocol there would add
an encode and a decode to a problem with neither.
The contract, and who frees what
A stream is five fields: four C function pointers and a private_data the
producer owns. The consumer calls get_next until it receives an array whose
release is NULL — that, not an error, is how a stream ends — and then calls
release on the stream itself.
Ownership is strict and worth stating because getting it wrong is a
double-free rather than a wrong answer:
- Every array handed out becomes the consumer's to release. This exports
each batch up front and then moves it out on
get_next, clearing our copy
so nothing is released twice.
- The schema is handed out the same way, once per
get_schema call, so a
consumer that asks twice gets two independently-releasable copies.
- Whatever the consumer never asked for is freed by
release.
The callbacks are abi("C") and take raw addresses because that is what the
other side of the boundary can call. Everything they touch is heap-allocated
and outlives the Mojo frame that built it — a stream handed to C and then
pointing at a stack is the mistake this file exists to not make.