Skip to main content

arrow_array/
ffi_stream.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Contains declarations to bind to the [C Stream Interface](https://arrow.apache.org/docs/format/CStreamInterface.html).
19//!
20//! This module has two main interfaces:
21//! One interface maps C ABI to native Rust types, i.e. convert c-pointers, c_char, to native rust.
22//! This is handled by [FFI_ArrowArrayStream].
23//!
24//! The second interface is used to import `FFI_ArrowArrayStream` as Rust implementation `RecordBatch` reader.
25//! This is handled by `ArrowArrayStreamReader`.
26//!
27//! ```ignore
28//! # use std::fs::File;
29//! # use std::sync::Arc;
30//! # use arrow::error::Result;
31//! # use arrow::ffi_stream::{export_reader_into_raw, ArrowArrayStreamReader, FFI_ArrowArrayStream};
32//! # use arrow::ipc::reader::FileReader;
33//! # use arrow::record_batch::RecordBatchReader;
34//! # fn main() -> Result<()> {
35//! // create an record batch reader natively
36//! let file = File::open("arrow_file").unwrap();
37//! let reader = Box::new(FileReader::try_new(file).unwrap());
38//!
39//! // export it
40//! let mut stream = FFI_ArrowArrayStream::empty();
41//! unsafe { export_reader_into_raw(reader, &mut stream) };
42//!
43//! // consumed and used by something else...
44//!
45//! // import it
46//! let stream_reader = unsafe { ArrowArrayStreamReader::from_raw(&mut stream).unwrap() };
47//! let imported_schema = stream_reader.schema();
48//!
49//! let mut produced_batches = vec![];
50//! for batch in stream_reader {
51//!      produced_batches.push(batch.unwrap());
52//! }
53//! Ok(())
54//! }
55//! ```
56
57use arrow_schema::DataType;
58use std::ffi::CStr;
59use std::ptr::addr_of;
60use std::{
61    ffi::CString,
62    os::raw::{c_char, c_int, c_void},
63    sync::Arc,
64};
65
66use arrow_data::ffi::FFI_ArrowArray;
67use arrow_schema::{ArrowError, Schema, SchemaRef, ffi::FFI_ArrowSchema};
68
69use crate::RecordBatchOptions;
70use crate::array::Array;
71use crate::array::StructArray;
72use crate::ffi::from_ffi_and_data_type;
73use crate::record_batch::{RecordBatch, RecordBatchReader};
74
75type Result<T> = std::result::Result<T, ArrowError>;
76
77// Errno values returned through the C stream interface, taken from libc so they match
78// the platform the consumer interprets them against.
79#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
80use libc::{EINVAL, EIO, ENOMEM, ENOSYS};
81
82// wasm32-unknown-unknown has no libc, and no OS to interpret the codes either — any
83// non-zero value works there, so use Linux's.
84#[cfg(all(target_family = "wasm", target_os = "unknown"))]
85const ENOMEM: i32 = 12;
86#[cfg(all(target_family = "wasm", target_os = "unknown"))]
87const EIO: i32 = 5;
88#[cfg(all(target_family = "wasm", target_os = "unknown"))]
89const EINVAL: i32 = 22;
90#[cfg(all(target_family = "wasm", target_os = "unknown"))]
91const ENOSYS: i32 = 38;
92
93/// ABI-compatible struct for `ArrayStream` from C Stream Interface
94/// See <https://arrow.apache.org/docs/format/CStreamInterface.html#structure-definitions>
95/// This was created by bindgen
96#[repr(C)]
97#[derive(Debug)]
98pub struct FFI_ArrowArrayStream {
99    // Fields are intentionally private so safety guarantees can be upheld via
100    // explicit unsafe functions.
101    /// C function to get schema from the stream
102    get_schema: Option<unsafe extern "C" fn(arg1: *mut Self, out: *mut FFI_ArrowSchema) -> c_int>,
103    /// C function to get next array from the stream
104    get_next: Option<unsafe extern "C" fn(arg1: *mut Self, out: *mut FFI_ArrowArray) -> c_int>,
105    /// C function to get the error from last operation on the stream
106    get_last_error: Option<unsafe extern "C" fn(arg1: *mut Self) -> *const c_char>,
107    /// C function to release the stream
108    release: Option<unsafe extern "C" fn(arg1: *mut Self)>,
109    /// Private data used by the stream, owned by the release callback.
110    private_data: *mut c_void,
111}
112
113unsafe impl Send for FFI_ArrowArrayStream {}
114
115// callback used to drop [FFI_ArrowArrayStream] when it is exported.
116unsafe extern "C" fn release_stream(stream: *mut FFI_ArrowArrayStream) {
117    if stream.is_null() {
118        return;
119    }
120    let stream = unsafe { &mut *stream };
121
122    stream.get_schema = None;
123    stream.get_next = None;
124    stream.get_last_error = None;
125
126    let private_data = unsafe { Box::from_raw(stream.private_data.cast::<StreamPrivateData>()) };
127    drop(private_data);
128
129    stream.release = None;
130}
131
132struct StreamPrivateData {
133    batch_reader: Box<dyn RecordBatchReader + Send>,
134    last_error: Option<CString>,
135}
136
137// The callback used to get array schema
138unsafe extern "C" fn get_schema(
139    stream: *mut FFI_ArrowArrayStream,
140    schema: *mut FFI_ArrowSchema,
141) -> c_int {
142    ExportedArrayStream { stream }.get_schema(schema)
143}
144
145// The callback used to get next array
146unsafe extern "C" fn get_next(
147    stream: *mut FFI_ArrowArrayStream,
148    array: *mut FFI_ArrowArray,
149) -> c_int {
150    ExportedArrayStream { stream }.get_next(array)
151}
152
153// The callback used to get the error from last operation on the `FFI_ArrowArrayStream`
154unsafe extern "C" fn get_last_error(stream: *mut FFI_ArrowArrayStream) -> *const c_char {
155    let mut ffi_stream = ExportedArrayStream { stream };
156    // The consumer should not take ownership of this string, we should return
157    // a const pointer to it.
158    match ffi_stream.get_last_error() {
159        Some(err_string) => err_string.as_ptr(),
160        None => std::ptr::null(),
161    }
162}
163
164impl Drop for FFI_ArrowArrayStream {
165    fn drop(&mut self) {
166        match self.release {
167            None => (),
168            Some(release) => unsafe { release(self) },
169        }
170    }
171}
172
173impl FFI_ArrowArrayStream {
174    /// Creates a new [`FFI_ArrowArrayStream`].
175    pub fn new(batch_reader: Box<dyn RecordBatchReader + Send>) -> Self {
176        let private_data = Box::new(StreamPrivateData {
177            batch_reader,
178            last_error: None,
179        });
180
181        Self {
182            get_schema: Some(get_schema),
183            get_next: Some(get_next),
184            get_last_error: Some(get_last_error),
185            release: Some(release_stream),
186            private_data: Box::into_raw(private_data).cast::<c_void>(),
187        }
188    }
189
190    /// Creates a new [`FFI_ArrowArrayStream`] from its raw parts.
191    ///
192    /// # Safety
193    ///
194    /// The caller takes responsibility for the [C stream interface] contract:
195    ///
196    /// * Each callback that is `Some` must be sound to invoke with a pointer to
197    ///   this struct, and must implement the semantics the C stream interface
198    ///   specifies for it.
199    /// * `private_data` must own everything the callbacks rely on, and must
200    ///   remain valid until `release` is called.
201    /// * `release`, if `Some`, is invoked by [`Drop`] with a pointer to this
202    ///   struct. It must free `private_data` exactly once and then set the
203    ///   release callback to `None` to mark the stream released.
204    /// * If `release` is `None` the stream is already released, so dropping it
205    ///   must not leak: nothing may be left for the callback to free.
206    ///
207    /// [C stream interface]: https://arrow.apache.org/docs/format/CStreamInterface.html
208    pub unsafe fn new_unchecked(
209        get_schema: Option<
210            unsafe extern "C" fn(arg1: *mut Self, out: *mut FFI_ArrowSchema) -> c_int,
211        >,
212        get_next: Option<unsafe extern "C" fn(arg1: *mut Self, out: *mut FFI_ArrowArray) -> c_int>,
213        get_last_error: Option<unsafe extern "C" fn(arg1: *mut Self) -> *const c_char>,
214        release: Option<unsafe extern "C" fn(arg1: *mut Self)>,
215        private_data: *mut c_void,
216    ) -> Self {
217        Self {
218            get_schema,
219            get_next,
220            get_last_error,
221            release,
222            private_data,
223        }
224    }
225
226    /// Takes ownership of the pointed to [`FFI_ArrowArrayStream`]
227    ///
228    /// This acts to [move] the data out of `raw_stream`, setting the release callback to NULL
229    ///
230    /// # Safety
231    ///
232    /// * `raw_stream` must be [valid] for reads and writes
233    /// * `raw_stream` must be properly aligned
234    /// * `raw_stream` must point to a properly initialized value of [`FFI_ArrowArrayStream`]
235    ///
236    /// [move]: https://arrow.apache.org/docs/format/CDataInterface.html#moving-an-array
237    /// [valid]: https://doc.rust-lang.org/std/ptr/index.html#safety
238    pub unsafe fn from_raw(raw_stream: *mut FFI_ArrowArrayStream) -> Self {
239        unsafe { std::ptr::replace(raw_stream, Self::empty()) }
240    }
241
242    /// Creates a new empty [FFI_ArrowArrayStream]. Used to import from the C Stream Interface.
243    pub fn empty() -> Self {
244        Self {
245            get_schema: None,
246            get_next: None,
247            get_last_error: None,
248            release: None,
249            private_data: std::ptr::null_mut(),
250        }
251    }
252
253    #[expect(clippy::unnecessary_safety_doc)]
254    /// Returns the producer-provided callback that writes this stream's schema, if any.
255    ///
256    /// # Safety
257    ///
258    /// The callback must be invoked with a pointer to the stream it was read from.
259    pub fn get_schema(
260        &self,
261    ) -> Option<unsafe extern "C" fn(arg1: *mut Self, out: *mut FFI_ArrowSchema) -> c_int> {
262        self.get_schema
263    }
264
265    #[expect(clippy::unnecessary_safety_doc)]
266    /// Returns the producer-provided callback that yields the next array, if any.
267    ///
268    /// # Safety
269    ///
270    /// The callback must be invoked with a pointer to the stream it was read from.
271    pub fn get_next(
272        &self,
273    ) -> Option<unsafe extern "C" fn(arg1: *mut Self, out: *mut FFI_ArrowArray) -> c_int> {
274        self.get_next
275    }
276
277    #[expect(clippy::unnecessary_safety_doc)]
278    /// Returns the producer-provided callback that describes the last error, if any.
279    ///
280    /// # Safety
281    ///
282    /// The callback must be invoked with a pointer to the stream it was read from.
283    /// The string it returns is owned by the producer and valid only until the next
284    /// call on the stream.
285    pub fn get_last_error(&self) -> Option<unsafe extern "C" fn(arg1: *mut Self) -> *const c_char> {
286        self.get_last_error
287    }
288
289    /// Returns the producer-provided release callback, if any.
290    pub fn release(&self) -> Option<unsafe extern "C" fn(arg1: *mut Self)> {
291        self.release
292    }
293
294    /// Returns the opaque producer-provided private data pointer.
295    pub fn private_data(&self) -> *mut c_void {
296        self.private_data
297    }
298
299    /// Replaces the release callback, returning the previous one.
300    ///
301    /// Lets a consumer wrap release: save the old callback, install its own, and
302    /// chain back on drop. See <https://github.com/apache/arrow-rs/issues/9771>.
303    ///
304    /// # Safety
305    ///
306    /// [`Drop`] calls this callback with a pointer to `self`. The new callback
307    /// must correctly release this stream (usually by chaining to the returned
308    /// one) and must match the [`FFI_ArrowArrayStream::private_data`] it reads.
309    /// A wrong callback is undefined behavior on drop.
310    pub unsafe fn set_release(
311        &mut self,
312        release: Option<unsafe extern "C" fn(arg1: *mut Self)>,
313    ) -> Option<unsafe extern "C" fn(arg1: *mut Self)> {
314        std::mem::replace(&mut self.release, release)
315    }
316
317    /// Replaces the private data pointer, returning the previous one.
318    ///
319    /// # Safety
320    ///
321    /// The old pointer is returned without being freed; the caller owns it from
322    /// here. The new pointer must match what the current
323    /// [`FFI_ArrowArrayStream::release`] callback expects.
324    pub unsafe fn set_private_data(&mut self, private_data: *mut c_void) -> *mut c_void {
325        std::mem::replace(&mut self.private_data, private_data)
326    }
327}
328
329struct ExportedArrayStream {
330    stream: *mut FFI_ArrowArrayStream,
331}
332
333impl ExportedArrayStream {
334    fn get_private_data(&mut self) -> &mut StreamPrivateData {
335        unsafe { &mut *(*self.stream).private_data.cast::<StreamPrivateData>() }
336    }
337
338    pub fn get_schema(&mut self, out: *mut FFI_ArrowSchema) -> i32 {
339        let private_data = self.get_private_data();
340        let reader = &private_data.batch_reader;
341
342        let schema = FFI_ArrowSchema::try_from(reader.schema().as_ref());
343
344        match schema {
345            Ok(schema) => {
346                unsafe { std::ptr::copy(addr_of!(schema), out, 1) };
347                std::mem::forget(schema);
348                0
349            }
350            Err(ref err) => {
351                private_data.last_error = Some(
352                    CString::new(err.to_string()).expect("Error string has a null byte in it."),
353                );
354                get_error_code(err)
355            }
356        }
357    }
358
359    pub fn get_next(&mut self, out: *mut FFI_ArrowArray) -> i32 {
360        let private_data = self.get_private_data();
361        let reader = &mut private_data.batch_reader;
362
363        match reader.next() {
364            None => {
365                // Marks ArrowArray released to indicate reaching the end of stream.
366                unsafe { std::ptr::write(out, FFI_ArrowArray::empty()) }
367                0
368            }
369            Some(next_batch) => {
370                if let Ok(batch) = next_batch {
371                    let struct_array = StructArray::from(batch);
372                    let array = FFI_ArrowArray::new(&struct_array.to_data());
373
374                    unsafe { std::ptr::write_unaligned(out, array) };
375                    0
376                } else {
377                    let err = &next_batch.unwrap_err();
378                    private_data.last_error = Some(
379                        CString::new(err.to_string()).expect("Error string has a null byte in it."),
380                    );
381                    get_error_code(err)
382                }
383            }
384        }
385    }
386
387    pub fn get_last_error(&mut self) -> Option<&CString> {
388        self.get_private_data().last_error.as_ref()
389    }
390}
391
392fn get_error_code(err: &ArrowError) -> i32 {
393    match err {
394        ArrowError::NotYetImplemented(_) => ENOSYS,
395        ArrowError::MemoryError(_) => ENOMEM,
396        ArrowError::IoError(_, _) => EIO,
397        _ => EINVAL,
398    }
399}
400
401/// A `RecordBatchReader` which imports Arrays from `FFI_ArrowArrayStream`.
402///
403/// Struct used to fetch `RecordBatch` from the C Stream Interface.
404/// Its main responsibility is to expose `RecordBatchReader` functionality
405/// that requires [FFI_ArrowArrayStream].
406#[derive(Debug)]
407pub struct ArrowArrayStreamReader {
408    stream: FFI_ArrowArrayStream,
409    schema: SchemaRef,
410}
411
412/// Returns the producer's message for the last failed call on a `FFI_ArrowArrayStream`.
413///
414/// Returns `None` when the producer supplies no message, either because it installs no
415/// `get_last_error` callback or because that callback returns NULL: the C Stream Interface
416/// lets `get_last_error` return NULL when no detailed description is available.
417///
418/// # Safety
419///
420/// `stream_ptr` must point to a valid, not yet released [`FFI_ArrowArrayStream`], and the last
421/// operation on it must have returned an error: the C Stream Interface forbids calling
422/// `get_last_error` in any other case.
423unsafe fn producer_error(stream_ptr: *mut FFI_ArrowArrayStream) -> Option<String> {
424    let get_last_error = unsafe { (*stream_ptr).get_last_error }?;
425
426    let error_str = unsafe { get_last_error(stream_ptr) };
427    if error_str.is_null() {
428        return None;
429    }
430
431    Some(
432        unsafe { CStr::from_ptr(error_str) }
433            .to_string_lossy()
434            .into_owned(),
435    )
436}
437
438/// Gets schema from a raw pointer of `FFI_ArrowArrayStream`. This is used when constructing
439/// `ArrowArrayStreamReader` to cache schema.
440fn get_stream_schema(stream_ptr: *mut FFI_ArrowArrayStream) -> Result<SchemaRef> {
441    let mut schema = FFI_ArrowSchema::empty();
442
443    let ret_code = unsafe { (*stream_ptr).get_schema.unwrap()(stream_ptr, &raw mut schema) };
444
445    if ret_code == 0 {
446        let schema = Schema::try_from(&schema)?;
447        Ok(Arc::new(schema))
448    } else {
449        let message = format!("Cannot get schema from input stream. Error code: {ret_code}");
450        // SAFETY: `stream_ptr` is valid and unreleased, and the `get_schema` call above
451        // returned a non-zero code.
452        let message = match unsafe { producer_error(stream_ptr) } {
453            Some(producer_message) => format!("{message}. Producer error: {producer_message}"),
454            None => message,
455        };
456        Err(ArrowError::CDataInterface(message))
457    }
458}
459
460impl ArrowArrayStreamReader {
461    /// Creates a new `ArrowArrayStreamReader` from a `FFI_ArrowArrayStream`.
462    /// This is used to import from the C Stream Interface.
463    pub fn try_new(mut stream: FFI_ArrowArrayStream) -> Result<Self> {
464        if stream.release.is_none() {
465            return Err(ArrowError::CDataInterface(
466                "input stream is already released".to_string(),
467            ));
468        }
469
470        let schema = get_stream_schema(&raw mut stream)?;
471
472        Ok(Self { stream, schema })
473    }
474
475    /// Creates a new `ArrowArrayStreamReader` from a raw pointer of `FFI_ArrowArrayStream`.
476    ///
477    /// Assumes that the pointer represents valid C Stream Interfaces.
478    /// This function copies the content from the raw pointer and cleans up it to prevent
479    /// double-dropping. The caller is responsible for freeing up the memory allocated for
480    /// the pointer.
481    ///
482    /// # Safety
483    ///
484    /// See [`FFI_ArrowArrayStream::from_raw`]
485    pub unsafe fn from_raw(raw_stream: *mut FFI_ArrowArrayStream) -> Result<Self> {
486        Self::try_new(unsafe { FFI_ArrowArrayStream::from_raw(raw_stream) })
487    }
488}
489
490impl Iterator for ArrowArrayStreamReader {
491    type Item = Result<RecordBatch>;
492
493    fn next(&mut self) -> Option<Self::Item> {
494        let mut array = FFI_ArrowArray::empty();
495
496        let ret_code =
497            unsafe { self.stream.get_next.unwrap()(&raw mut self.stream, &raw mut array) };
498
499        if ret_code == 0 {
500            // The end of stream has been reached
501            if array.is_released() {
502                return None;
503            }
504
505            let result = unsafe {
506                from_ffi_and_data_type(array, DataType::Struct(self.schema().fields().clone()))
507            };
508            Some(result.and_then(|data| {
509                let len = data.len();
510                RecordBatch::try_new_with_options(
511                    self.schema.clone(),
512                    StructArray::from(data).into_parts().1,
513                    &RecordBatchOptions::new().with_row_count(Some(len)),
514                )
515            }))
516        } else {
517            let message =
518                format!("Cannot get next batch from input stream. Error code: {ret_code}");
519            // SAFETY: `self.stream` is valid and unreleased by construction, and the
520            // `get_next` call above returned a non-zero code.
521            let message = match unsafe { producer_error(&raw mut self.stream) } {
522                Some(producer_message) => format!("{message}. Producer error: {producer_message}"),
523                None => message,
524            };
525            Some(Err(ArrowError::CDataInterface(message)))
526        }
527    }
528}
529
530impl RecordBatchReader for ArrowArrayStreamReader {
531    fn schema(&self) -> SchemaRef {
532        self.schema.clone()
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use std::collections::HashMap;
540
541    use arrow_schema::Field;
542
543    use crate::array::Int32Array;
544    use crate::ffi::from_ffi;
545
546    struct TestRecordBatchReader {
547        schema: SchemaRef,
548        iter: Box<dyn Iterator<Item = Result<RecordBatch>> + Send>,
549    }
550
551    impl TestRecordBatchReader {
552        pub fn new(
553            schema: SchemaRef,
554            iter: Box<dyn Iterator<Item = Result<RecordBatch>> + Send>,
555        ) -> TestRecordBatchReader {
556            TestRecordBatchReader { schema, iter }
557        }
558    }
559
560    impl Iterator for TestRecordBatchReader {
561        type Item = Result<RecordBatch>;
562
563        fn next(&mut self) -> Option<Self::Item> {
564            self.iter.next()
565        }
566    }
567
568    impl RecordBatchReader for TestRecordBatchReader {
569        fn schema(&self) -> SchemaRef {
570            self.schema.clone()
571        }
572    }
573
574    fn _test_round_trip_export(batch: RecordBatch, schema: Arc<Schema>) -> Result<()> {
575        let iter = Box::new(vec![batch.clone(), batch.clone()].into_iter().map(Ok)) as _;
576
577        let reader = Box::new(TestRecordBatchReader::new(schema.clone(), iter));
578
579        // Export a `RecordBatchReader` through `FFI_ArrowArrayStream`
580        let mut ffi_stream = FFI_ArrowArrayStream::new(reader);
581
582        // Get schema from `FFI_ArrowArrayStream`
583        let mut ffi_schema = FFI_ArrowSchema::empty();
584        let ret_code = unsafe { get_schema(&raw mut ffi_stream, &raw mut ffi_schema) };
585        assert_eq!(ret_code, 0);
586
587        let exported_schema = Schema::try_from(&ffi_schema).unwrap();
588        assert_eq!(&exported_schema, schema.as_ref());
589
590        // Get array from `FFI_ArrowArrayStream`
591        let mut produced_batches = vec![];
592        loop {
593            let mut ffi_array = FFI_ArrowArray::empty();
594            let ret_code = unsafe { get_next(&raw mut ffi_stream, &raw mut ffi_array) };
595            assert_eq!(ret_code, 0);
596
597            // The end of stream has been reached
598            if ffi_array.is_released() {
599                break;
600            }
601
602            let array = unsafe { from_ffi(ffi_array, &ffi_schema) }.unwrap();
603            let len = array.len();
604
605            let record_batch = RecordBatch::try_new_with_options(
606                SchemaRef::from(exported_schema.clone()),
607                StructArray::from(array).into_parts().1,
608                &RecordBatchOptions::new().with_row_count(Some(len)),
609            )
610            .unwrap();
611            produced_batches.push(record_batch);
612        }
613
614        assert_eq!(produced_batches, vec![batch.clone(), batch]);
615
616        Ok(())
617    }
618
619    fn _test_round_trip_import(batch: RecordBatch, schema: Arc<Schema>) -> Result<()> {
620        let iter = Box::new(vec![batch.clone(), batch.clone()].into_iter().map(Ok)) as _;
621
622        let reader = Box::new(TestRecordBatchReader::new(schema.clone(), iter));
623
624        // Import through `FFI_ArrowArrayStream` as `ArrowArrayStreamReader`
625        let stream = FFI_ArrowArrayStream::new(reader);
626        let stream_reader = ArrowArrayStreamReader::try_new(stream).unwrap();
627
628        let imported_schema = stream_reader.schema();
629        assert_eq!(imported_schema, schema);
630
631        let mut produced_batches = vec![];
632        for batch in stream_reader {
633            produced_batches.push(batch.unwrap());
634        }
635
636        assert_eq!(produced_batches, vec![batch.clone(), batch]);
637
638        Ok(())
639    }
640
641    #[test]
642    fn test_stream_round_trip() {
643        let array = Int32Array::from(vec![Some(2), None, Some(1), None]);
644        let array: Arc<dyn Array> = Arc::new(array);
645        let metadata = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
646
647        let schema = Arc::new(Schema::new_with_metadata(
648            vec![
649                Field::new("a", array.data_type().clone(), true).with_metadata(metadata.clone()),
650                Field::new("b", array.data_type().clone(), true).with_metadata(metadata.clone()),
651                Field::new("c", array.data_type().clone(), true).with_metadata(metadata.clone()),
652            ],
653            metadata,
654        ));
655        let batch = RecordBatch::try_new(schema.clone(), vec![array.clone(), array.clone(), array])
656            .unwrap();
657
658        _test_round_trip_export(batch.clone(), schema.clone()).unwrap();
659        _test_round_trip_import(batch, schema).unwrap();
660    }
661
662    #[test]
663    fn test_stream_round_trip_no_columns() {
664        let metadata = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
665
666        let schema = Arc::new(Schema::new_with_metadata(Vec::<Field>::new(), metadata));
667        let batch = RecordBatch::try_new_with_options(
668            schema.clone(),
669            Vec::<Arc<dyn Array>>::new(),
670            &RecordBatchOptions::new().with_row_count(Some(10)),
671        )
672        .unwrap();
673
674        _test_round_trip_export(batch.clone(), schema.clone()).unwrap();
675        _test_round_trip_import(batch, schema).unwrap();
676    }
677
678    #[test]
679    fn test_error_import() -> Result<()> {
680        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
681
682        let iter =
683            Box::new(vec![Err(ArrowError::MemoryError("out of memory".to_string()))].into_iter());
684
685        let reader = Box::new(TestRecordBatchReader::new(schema.clone(), iter));
686
687        // Import through `FFI_ArrowArrayStream` as `ArrowArrayStreamReader`
688        let stream = FFI_ArrowArrayStream::new(reader);
689        let stream_reader = ArrowArrayStreamReader::try_new(stream).unwrap();
690
691        let imported_schema = stream_reader.schema();
692        assert_eq!(imported_schema, schema);
693
694        let mut produced_batches = vec![];
695        for batch in stream_reader {
696            produced_batches.push(batch);
697        }
698
699        // The results should outlive the lifetime of the stream itself.
700        assert_eq!(produced_batches.len(), 1);
701        assert_eq!(
702            produced_batches[0].as_ref().unwrap_err().to_string(),
703            format!(
704                "C Data interface error: Cannot get next batch from input stream. \
705                 Error code: {ENOMEM}. Producer error: Memory error: out of memory"
706            )
707        );
708
709        Ok(())
710    }
711
712    unsafe extern "C" fn failing_get_schema(
713        _stream: *mut FFI_ArrowArrayStream,
714        _out: *mut FFI_ArrowSchema,
715    ) -> c_int {
716        EIO
717    }
718
719    unsafe extern "C" fn working_get_schema(
720        _stream: *mut FFI_ArrowArrayStream,
721        out: *mut FFI_ArrowSchema,
722    ) -> c_int {
723        let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
724        unsafe { std::ptr::write(out, FFI_ArrowSchema::try_from(&schema).unwrap()) };
725        0
726    }
727
728    unsafe extern "C" fn failing_get_next(
729        _stream: *mut FFI_ArrowArrayStream,
730        _out: *mut FFI_ArrowArray,
731    ) -> c_int {
732        EIO
733    }
734
735    unsafe extern "C" fn producer_last_error(_stream: *mut FFI_ArrowArrayStream) -> *const c_char {
736        c"the producer failed".as_ptr()
737    }
738
739    unsafe extern "C" fn null_last_error(_stream: *mut FFI_ArrowArrayStream) -> *const c_char {
740        std::ptr::null()
741    }
742
743    unsafe extern "C" fn mark_released(stream: *mut FFI_ArrowArrayStream) {
744        unsafe { (*stream).release = None };
745    }
746
747    fn failing_stream(
748        get_last_error: Option<unsafe extern "C" fn(*mut FFI_ArrowArrayStream) -> *const c_char>,
749    ) -> FFI_ArrowArrayStream {
750        let mut stream = FFI_ArrowArrayStream::empty();
751        stream.get_schema = Some(failing_get_schema);
752        stream.get_next = Some(failing_get_next);
753        stream.get_last_error = get_last_error;
754        stream.release = Some(mark_released);
755        stream
756    }
757
758    #[test]
759    fn test_import_schema_error_reports_producer_message() {
760        let err =
761            ArrowArrayStreamReader::try_new(failing_stream(Some(producer_last_error))).unwrap_err();
762        assert_eq!(
763            err.to_string(),
764            format!(
765                "C Data interface error: Cannot get schema from input stream. \
766                 Error code: {EIO}. Producer error: the producer failed"
767            )
768        );
769    }
770
771    #[test]
772    fn test_import_schema_error_without_producer_message() {
773        // A producer need not supply a message: `get_last_error` may return NULL when no
774        // detailed description is available.
775        let err =
776            ArrowArrayStreamReader::try_new(failing_stream(Some(null_last_error))).unwrap_err();
777        assert_eq!(
778            err.to_string(),
779            format!(
780                "C Data interface error: Cannot get schema from input stream. Error code: {EIO}"
781            )
782        );
783    }
784
785    #[test]
786    fn test_import_schema_error_without_error_callback() {
787        let err = ArrowArrayStreamReader::try_new(failing_stream(None)).unwrap_err();
788        assert_eq!(
789            err.to_string(),
790            format!(
791                "C Data interface error: Cannot get schema from input stream. Error code: {EIO}"
792            )
793        );
794    }
795
796    #[test]
797    fn test_import_next_error_without_producer_message() {
798        // Previously panicked: the message was unwrapped without checking that the producer
799        // supplied one.
800        let mut stream = failing_stream(Some(null_last_error));
801        stream.get_schema = Some(working_get_schema);
802
803        let err = ArrowArrayStreamReader::try_new(stream)
804            .unwrap()
805            .next()
806            .unwrap()
807            .unwrap_err();
808        assert_eq!(
809            err.to_string(),
810            format!(
811                "C Data interface error: Cannot get next batch from input stream. Error code: {EIO}"
812            )
813        );
814    }
815
816    // A consumer wraps the release callback with its own, then chains back to
817    // the original on drop. This is the same wrap-release pattern the
818    // release/private_data accessors exist for (#9771).
819    static STREAM_WRAPPER_RAN: std::sync::atomic::AtomicBool =
820        std::sync::atomic::AtomicBool::new(false);
821
822    struct StreamWrapperData {
823        original_release: Option<unsafe extern "C" fn(*mut FFI_ArrowArrayStream)>,
824        original_private_data: *mut c_void,
825    }
826
827    unsafe extern "C" fn wrapping_release(stream: *mut FFI_ArrowArrayStream) {
828        use std::sync::atomic::Ordering;
829        let stream = unsafe { &mut *stream };
830        let data = unsafe { Box::from_raw(stream.private_data().cast::<StreamWrapperData>()) };
831        STREAM_WRAPPER_RAN.store(true, Ordering::SeqCst);
832        unsafe { stream.set_release(data.original_release) };
833        unsafe { stream.set_private_data(data.original_private_data) };
834        if let Some(release) = stream.release() {
835            unsafe { release(stream) };
836        }
837    }
838
839    #[test]
840    fn test_wrap_release_callback() {
841        use std::sync::atomic::Ordering;
842
843        let batch_reader = Box::new(TestRecordBatchReader::new(
844            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)])),
845            Box::new(std::iter::empty()),
846        ));
847        let mut stream = FFI_ArrowArrayStream::new(batch_reader);
848
849        let data = Box::new(StreamWrapperData {
850            original_release: stream.release(),
851            original_private_data: stream.private_data(),
852        });
853        unsafe { stream.set_release(Some(wrapping_release)) };
854        unsafe { stream.set_private_data(Box::into_raw(data).cast::<c_void>()) };
855
856        drop(stream); // runs wrapping_release, which chains to the original
857        assert!(STREAM_WRAPPER_RAN.load(Ordering::SeqCst));
858    }
859
860    // A producer that exports a stream of plain arrays rather than record batches.
861    // `FFI_ArrowArrayStream::new` cannot express this, since it takes a
862    // `RecordBatchReader`. See <https://github.com/apache/arrow-rs/issues/6586>.
863    struct ArrayStreamPrivateData {
864        field: Field,
865        arrays: std::vec::IntoIter<Int32Array>,
866    }
867
868    unsafe extern "C" fn array_stream_get_schema(
869        stream: *mut FFI_ArrowArrayStream,
870        out: *mut FFI_ArrowSchema,
871    ) -> c_int {
872        let private_data = unsafe { &*(*stream).private_data().cast::<ArrayStreamPrivateData>() };
873        let schema = FFI_ArrowSchema::try_from(&private_data.field).unwrap();
874        unsafe { std::ptr::write(out, schema) };
875        0
876    }
877
878    unsafe extern "C" fn array_stream_get_next(
879        stream: *mut FFI_ArrowArrayStream,
880        out: *mut FFI_ArrowArray,
881    ) -> c_int {
882        let private_data =
883            unsafe { &mut *(*stream).private_data().cast::<ArrayStreamPrivateData>() };
884        match private_data.arrays.next() {
885            // Marks ArrowArray released to indicate reaching the end of stream.
886            None => unsafe { std::ptr::write(out, FFI_ArrowArray::empty()) },
887            Some(array) => unsafe {
888                std::ptr::write_unaligned(out, FFI_ArrowArray::new(&array.to_data()))
889            },
890        }
891        0
892    }
893
894    unsafe extern "C" fn array_stream_get_last_error(
895        _stream: *mut FFI_ArrowArrayStream,
896    ) -> *const c_char {
897        std::ptr::null()
898    }
899
900    unsafe extern "C" fn array_stream_release(stream: *mut FFI_ArrowArrayStream) {
901        let private_data =
902            unsafe { Box::from_raw((*stream).private_data().cast::<ArrayStreamPrivateData>()) };
903        drop(private_data);
904        // Clears every callback and marks the stream released, without dropping the
905        // value being overwritten.
906        unsafe { std::ptr::write(stream, FFI_ArrowArrayStream::empty()) };
907    }
908
909    fn array_stream(field: Field, arrays: Vec<Int32Array>) -> FFI_ArrowArrayStream {
910        let private_data = Box::new(ArrayStreamPrivateData {
911            field,
912            arrays: arrays.into_iter(),
913        });
914
915        unsafe {
916            FFI_ArrowArrayStream::new_unchecked(
917                Some(array_stream_get_schema),
918                Some(array_stream_get_next),
919                Some(array_stream_get_last_error),
920                Some(array_stream_release),
921                Box::into_raw(private_data).cast::<c_void>(),
922            )
923        }
924    }
925
926    #[test]
927    fn test_new_unchecked_exports_stream_of_arrays() {
928        let field = Field::new("a", DataType::Int32, true);
929        let arrays = vec![
930            Int32Array::from(vec![1, 2, 3]),
931            Int32Array::from(vec![4, 5]),
932        ];
933        let mut stream = array_stream(field.clone(), arrays.clone());
934
935        // Drive the stream through the callbacks the constructor stored, so a
936        // field the constructor put in the wrong place would surface here.
937        let get_schema_fn = stream.get_schema().unwrap();
938        let get_next_fn = stream.get_next().unwrap();
939        let get_last_error_fn = stream.get_last_error().unwrap();
940
941        let mut ffi_schema = FFI_ArrowSchema::empty();
942        let ret_code = unsafe { get_schema_fn(&raw mut stream, &raw mut ffi_schema) };
943        assert_eq!(ret_code, 0);
944        assert_eq!(Field::try_from(&ffi_schema).unwrap(), field);
945
946        let mut produced = vec![];
947        loop {
948            let mut ffi_array = FFI_ArrowArray::empty();
949            let ret_code = unsafe { get_next_fn(&raw mut stream, &raw mut ffi_array) };
950            assert_eq!(ret_code, 0);
951
952            // The end of stream has been reached
953            if ffi_array.is_released() {
954                break;
955            }
956
957            let data = unsafe { from_ffi(ffi_array, &ffi_schema) }.unwrap();
958            produced.push(Int32Array::from(data));
959        }
960
961        assert_eq!(produced, arrays);
962
963        // This producer never fails, so it reports no error message.
964        assert!(unsafe { get_last_error_fn(&raw mut stream) }.is_null());
965
966        // Runs the stored release callback, freeing the private data exactly once.
967        drop(stream);
968    }
969
970    #[test]
971    fn test_accessors_report_an_empty_stream_as_released() {
972        let stream = FFI_ArrowArrayStream::empty();
973        assert!(stream.get_schema().is_none());
974        assert!(stream.get_next().is_none());
975        assert!(stream.get_last_error().is_none());
976        assert!(stream.release().is_none());
977        assert!(stream.private_data().is_null());
978    }
979}