blob: 03a9e7b02ffba78929e6ff87d1fd2bf632db4fbf [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build cgo
// +build cgo
package cdata
import (
"context"
"errors"
"fmt"
"unsafe"
"github.com/apache/arrow-go/v18/arrow"
"github.com/apache/arrow-go/v18/arrow/array"
"github.com/apache/arrow-go/v18/arrow/arrio"
"github.com/apache/arrow-go/v18/arrow/memory"
)
// SchemaFromPtr is a simple helper function to cast a uintptr to a *CArrowSchema
func SchemaFromPtr(ptr uintptr) *CArrowSchema { return (*CArrowSchema)(unsafe.Pointer(ptr)) }
// ArrayFromPtr is a simple helper function to cast a uintptr to a *CArrowArray
func ArrayFromPtr(ptr uintptr) *CArrowArray { return (*CArrowArray)(unsafe.Pointer(ptr)) }
// ImportCArrowField takes in an ArrowSchema from the C Data interface, it
// will copy the metadata and type definitions rather than keep direct references
// to them. It is safe to call C.ArrowSchemaRelease after receiving the field
// from this function.
func ImportCArrowField(out *CArrowSchema) (arrow.Field, error) {
return importSchema(out)
}
// ImportCArrowSchema takes in the ArrowSchema from the C Data Interface, it
// will copy the metadata and schema definitions over from the C object rather
// than keep direct references to them. This function will call ArrowSchemaRelease
// on the passed in schema regardless of whether or not there is an error returned.
//
// This version is intended to take in a schema for a record batch, which means
// that the top level of the schema should be a struct of the schema fields. If
// importing a single array's schema, then use ImportCArrowField instead.
func ImportCArrowSchema(out *CArrowSchema) (*arrow.Schema, error) {
ret, err := importSchema(out)
if err != nil {
return nil, err
}
structType, ok := ret.Type.(*arrow.StructType)
if !ok {
return nil, fmt.Errorf("%w: record batch schema must have a top-level struct type", arrow.ErrInvalid)
}
return arrow.NewSchema(structType.Fields(), &ret.Metadata), nil
}
// ImportCArrayWithType takes a pointer to a C Data ArrowArray and interprets the values
// as an array with the given datatype. If err is not nil, then ArrowArrayRelease must still
// be called on arr to release the memory.
//
// The underlying buffers will not be copied, but will instead be referenced directly
// by the resulting array interface object. The passed in ArrowArray will have it's ownership
// transferred to the resulting arrow.Array via ArrowArrayMove. The underlying array.Data
// object that is owned by the Array will now be the owner of the memory pointer and
// will call ArrowArrayRelease when it is released and garbage collected via runtime.SetFinalizer.
//
// NOTE: The array takes ownership of the underlying memory buffers via ArrowArrayMove,
// it does not take ownership of the actual arr object itself.
func ImportCArrayWithType(arr *CArrowArray, dt arrow.DataType) (arrow.Array, error) {
imp, err := importCArrayAsType(arr, dt)
if err != nil {
return nil, err
}
defer imp.data.Release()
return array.MakeFromData(imp.data), nil
}
// ImportCArray takes a pointer to both a C Data ArrowArray and C Data ArrowSchema in order
// to import them into usable Go Objects. If err is not nil, then ArrowArrayRelease must still
// be called on arr to release the memory. The ArrowSchemaRelease will be called on the passed in
// schema regardless of whether there is an error or not.
//
// The Schema will be copied with the information used to populate the returned Field, complete
// with metadata. The array will reference the same memory that is referred to by the ArrowArray
// object and take ownership of it as per ImportCArrayWithType. The returned arrow.Array will
// own the C memory and call ArrowArrayRelease when the array.Data object is cleaned up.
//
// NOTE: The array takes ownership of the underlying memory buffers via ArrowArrayMove,
// it does not take ownership of the actual arr object itself.
func ImportCArray(arr *CArrowArray, schema *CArrowSchema) (arrow.Field, arrow.Array, error) {
field, err := importSchema(schema)
if err != nil {
return field, nil, err
}
ret, err := ImportCArrayWithType(arr, field.Type)
return field, ret, err
}
// ImportCRecordBatchWithSchema is used for importing a Record Batch array when the schema
// is already known such as when receiving record batches through a stream.
//
// All of the semantics regarding memory ownership are the same as when calling
// ImportCRecordBatch directly with a schema.
//
// NOTE: The array takes ownership of the underlying memory buffers via ArrowArrayMove,
// it does not take ownership of the actual arr object itself.
func ImportCRecordBatchWithSchema(arr *CArrowArray, sc *arrow.Schema) (arrow.RecordBatch, error) {
imp, err := importCArrayAsType(arr, arrow.StructOf(sc.Fields()...))
if err != nil {
return nil, err
}
defer imp.data.Release()
st := array.NewStructData(imp.data)
defer st.Release()
// now that we have our fields, we can split them out into the slice of arrays
// and construct a record batch from them to return.
cols := make([]arrow.Array, st.NumField())
for i := 0; i < st.NumField(); i++ {
cols[i] = st.Field(i)
}
return array.NewRecordBatch(sc, cols, int64(st.Len())), nil
}
// ImportCRecordBatch imports an ArrowArray from C as a record batch. If err is not nil,
// then ArrowArrayRelease must still be called to release the memory.
//
// A record batch is represented in the C Data Interface as a Struct Array whose fields
// are the columns of the record batch. Thus after importing the schema passed in here,
// if it is not a Struct type, this will return an error. As with ImportCArray, the
// columns in the record batch will take ownership of the CArrowArray memory if successful.
// Since ArrowArrayMove is used, it's still safe to call ArrowArrayRelease on the source
// regardless. But if there is an error, it *MUST* be called to ensure there is no memory leak.
//
// NOTE: The array takes ownership of the underlying memory buffers via ArrowArrayMove,
// it does not take ownership of the actual arr object itself.
func ImportCRecordBatch(arr *CArrowArray, sc *CArrowSchema) (arrow.RecordBatch, error) {
field, err := importSchema(sc)
if err != nil {
return nil, err
}
if field.Type.ID() != arrow.STRUCT {
return nil, errors.New("recordbatch array import must be of struct type")
}
return ImportCRecordBatchWithSchema(arr, arrow.NewSchema(field.Type.(*arrow.StructType).Fields(), &field.Metadata))
}
// ImportCArrayStream creates an arrio.Reader from an ArrowArrayStream taking ownership
// of the underlying stream object via ArrowArrayStreamMove.
//
// The records returned by this reader must be released manually after they are returned.
// The reader itself will release the stream via SetFinalizer when it is garbage collected.
// It will return (nil, io.EOF) from the Read function when there are no more records to return.
//
// NOTE: The reader takes ownership of the underlying memory buffers via ArrowArrayStreamMove,
// it does not take ownership of the actual stream object itself.
//
// Deprecated: This will panic if importing the schema fails (which is possible).
// Prefer ImportCRecordReader instead.
func ImportCArrayStream(stream *CArrowArrayStream, schema *arrow.Schema) arrio.Reader {
reader, err := ImportCRecordReader(stream, schema)
if err != nil {
panic(err)
}
return reader
}
// ImportCStreamReader creates an arrio.Reader from an ArrowArrayStream taking ownership
// of the underlying stream object via ArrowArrayStreamMove.
//
// The records returned by this reader must be released manually after they are returned.
// The reader itself will release the stream via SetFinalizer when it is garbage collected.
// It will return (nil, io.EOF) from the Read function when there are no more records to return.
//
// NOTE: The reader takes ownership of the underlying memory buffers via ArrowArrayStreamMove,
// it does not take ownership of the actual stream object itself.
func ImportCRecordReader(stream *CArrowArrayStream, schema *arrow.Schema) (arrio.Reader, error) {
out := &nativeCRecordBatchReader{schema: schema}
if err := initReader(out, stream); err != nil {
out.Release()
return nil, err
}
return out, nil
}
// ExportArrowSchema populates the passed in CArrowSchema with the schema passed in so
// that it can be passed to some consumer of the C Data Interface. The `release` function
// is tied to a callback in order to properly release any memory that was allocated during
// the populating of the struct. Any memory allocated will be allocated using malloc
// which means that it is invisible to the Go Garbage Collector and must be freed manually
// using the callback on the CArrowSchema object.
//
// WARNING: the output ArrowSchema MUST BE ZERO INITIALIZED, or the Go garbage collector
// may error at runtime, due to CGO rules ("the current implementation may sometimes
// cause a runtime error if the contents of the C memory appear to be a Go pointer").
// You have been warned!
func ExportArrowSchema(schema *arrow.Schema, out *CArrowSchema) {
dummy := arrow.Field{Type: arrow.StructOf(schema.Fields()...), Metadata: schema.Metadata()}
exportField(dummy, out)
}
// ExportArrowRecordBatch populates the passed in CArrowArray (and optionally the schema too)
// by sharing the memory used for the buffers of each column's arrays. It does not
// copy the data, and will internally increment the reference counters so that releasing
// the record will not free the memory prematurely.
//
// When using CGO, memory passed to C is pinned so that the Go garbage collector won't
// move where it is allocated out from under the C pointer locations, ensuring the C pointers
// stay valid. This is only true until the CGO call returns, at which point the garbage collector
// is free to move things around again. As a result, if the function you're calling is going to
// hold onto the pointers or otherwise continue to reference the memory *after* the call returns,
// you should use the CgoArrowAllocator rather than the GoAllocator (or DefaultAllocator) so that
// the memory which is allocated for the record batch in the first place is allocated in C,
// not by the Go runtime and is therefore not subject to the Garbage collection.
//
// The release function on the populated CArrowArray will properly decrease the reference counts,
// and release the memory if the record has already been released. But since this must be explicitly
// done, make sure it is released so that you do not create a memory leak.
//
// WARNING: the output ArrowArray MUST BE ZERO INITIALIZED, or the Go garbage collector
// may error at runtime, due to CGO rules ("the current implementation may sometimes
// cause a runtime error if the contents of the C memory appear to be a Go pointer").
// You have been warned!
func ExportArrowRecordBatch(rb arrow.RecordBatch, out *CArrowArray, outSchema *CArrowSchema) {
children := make([]arrow.ArrayData, rb.NumCols())
for i := range rb.Columns() {
children[i] = rb.Column(i).Data()
}
data := array.NewData(arrow.StructOf(rb.Schema().Fields()...), int(rb.NumRows()), []*memory.Buffer{nil},
children, 0, 0)
defer data.Release()
arr := array.NewStructData(data)
defer arr.Release()
if outSchema != nil {
ExportArrowSchema(rb.Schema(), outSchema)
}
exportArray(arr, out, nil)
}
// ExportArrowArray populates the CArrowArray that is passed in with the pointers to the memory
// being used by the arrow.Array passed in, in order to share with zero-copy across the C
// Data Interface. See the documentation for ExportArrowRecordBatch for details on how to ensure
// you do not leak memory and prevent unwanted, undefined or strange behaviors.
//
// WARNING: the output ArrowArray MUST BE ZERO INITIALIZED, or the Go garbage collector
// may error at runtime, due to CGO rules ("the current implementation may sometimes
// cause a runtime error if the contents of the C memory appear to be a Go pointer").
// You have been warned!
func ExportArrowArray(arr arrow.Array, out *CArrowArray, outSchema *CArrowSchema) {
exportArray(arr, out, outSchema)
}
// ExportRecordReader populates the CArrowArrayStream that is passed in with the appropriate
// callbacks to be a working ArrowArrayStream utilizing the passed in RecordReader. The
// CArrowArrayStream takes ownership of the RecordReader until the consumer calls the release
// callback, as such it is unnecessary to call Release on the passed in reader unless it has
// previously been retained. To call that release callback and prevent a memory leak, you can
// call ReleaseCArrowArrayStream on the CArrowArrayStream once it is no longer needed.
//
// WARNING: the output ArrowArrayStream MUST BE ZERO INITIALIZED, or the Go garbage
// collector may error at runtime, due to CGO rules ("the current implementation may
// sometimes cause a runtime error if the contents of the C memory appear to be a Go
// pointer"). You have been warned!
func ExportRecordReader(reader array.RecordReader, out *CArrowArrayStream) {
exportStream(reader, out)
}
// ReleaseCArrowArray calls ArrowArrayRelease on the passed in cdata array
func ReleaseCArrowArray(arr *CArrowArray) { releaseArr(arr) }
// ReleaseCArrowSchema calls ArrowSchemaRelease on the passed in cdata schema
func ReleaseCArrowSchema(schema *CArrowSchema) { releaseSchema(schema) }
// ReleaseCArrowArrayStream calls ArrowArrayStreamRelease on the passed in cdata stream
func ReleaseCArrowArrayStream(stream *CArrowArrayStream) { releaseStream(stream) }
// RecordMessage is a simple container for a record batch channel to stream for
// using the Async C Data Interface via ExportAsyncRecordBatchStream.
type RecordMessage struct {
Record arrow.RecordBatch
AdditionalMetadata arrow.Metadata
Err error
}
// AsyncRecordBatchStream represents a stream of record batches being read in
// from an ArrowAsyncDeviceStreamHandler's callbacks. If an error was encountered
// before the call to on_schema, then this will contain the error as Err. Otherwise
// the Schema will be valid and the Stream is a channel of RecordMessages being
// propagated via on_next_task and extract_data.
type AsyncRecordBatchStream struct {
Schema *arrow.Schema
AdditionalMetadata arrow.Metadata
Err error
Stream <-chan RecordMessage
}
// AsyncStreamError represents an error encountered via a call to the on_error
// callback of an ArrowAsyncDeviceStreamHandler. The Code is the error code that
// should be errno compatible.
type AsyncStreamError struct {
Code int
Msg string
Metadata string
}
func (e AsyncStreamError) Error() string { return e.Msg }
// CreateAsyncDeviceStreamHandler populates a given ArrowAsyncDeviceStreamHandler's callbacks
// and waits for the on_schema callback to be called before passing the AsyncRecordBatchStream
// object across the returned channel.
//
// The provided queueSize is the number of records that will be requested at a time to be passed
// along the Stream in the returned AsyncRecordBatchStream. See the documentation on
// https://arrow.apache.org/docs/format/CDeviceDataInterface.html for more information as to the
// expected semantics of that size.
//
// The populated ArrowAsyncDeviceStreamHandler can then be given to any compatible provider for
// async record batch streams via the C Device interface.
func CreateAsyncDeviceStreamHandler(ctx context.Context, queueSize uint64, out *CArrowAsyncDeviceStreamHandler) <-chan AsyncRecordBatchStream {
ch := make(chan AsyncRecordBatchStream)
exportAsyncHandler(cAsyncState{ctx: ctx, ch: ch, queueSize: queueSize}, out)
return ch
}
// ExportAsyncRecordBatchStream takes in a schema and a channel of RecordMessages along with a
// ArrowAsyncDeviceStreamHandler to export the records as they come across the channel and call
// the appropriate callbacks on the handler. This function will block until the stream is closed
// or a message containing an error comes across the channel.
//
// The returned error will be nil if everything is successful, otherwise it will be the error which
// is encountered on the stream or an AsyncError if one of the handler callbacks returns an error.
func ExportAsyncRecordBatchStream(schema *arrow.Schema, stream <-chan RecordMessage, handler *CArrowAsyncDeviceStreamHandler) error {
return exportAsyncProducer(schema, stream, handler)
}