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    /// Takes ownership of the pointed to [`FFI_ArrowArrayStream`]
191    ///
192    /// This acts to [move] the data out of `raw_stream`, setting the release callback to NULL
193    ///
194    /// # Safety
195    ///
196    /// * `raw_stream` must be [valid] for reads and writes
197    /// * `raw_stream` must be properly aligned
198    /// * `raw_stream` must point to a properly initialized value of [`FFI_ArrowArrayStream`]
199    ///
200    /// [move]: https://arrow.apache.org/docs/format/CDataInterface.html#moving-an-array
201    /// [valid]: https://doc.rust-lang.org/std/ptr/index.html#safety
202    pub unsafe fn from_raw(raw_stream: *mut FFI_ArrowArrayStream) -> Self {
203        unsafe { std::ptr::replace(raw_stream, Self::empty()) }
204    }
205
206    /// Creates a new empty [FFI_ArrowArrayStream]. Used to import from the C Stream Interface.
207    pub fn empty() -> Self {
208        Self {
209            get_schema: None,
210            get_next: None,
211            get_last_error: None,
212            release: None,
213            private_data: std::ptr::null_mut(),
214        }
215    }
216
217    /// Returns the producer-provided release callback, if any.
218    pub fn release(&self) -> Option<unsafe extern "C" fn(arg1: *mut Self)> {
219        self.release
220    }
221
222    /// Returns the opaque producer-provided private data pointer.
223    pub fn private_data(&self) -> *mut c_void {
224        self.private_data
225    }
226
227    /// Replaces the release callback, returning the previous one.
228    ///
229    /// Lets a consumer wrap release: save the old callback, install its own, and
230    /// chain back on drop. See <https://github.com/apache/arrow-rs/issues/9771>.
231    ///
232    /// # Safety
233    ///
234    /// [`Drop`] calls this callback with a pointer to `self`. The new callback
235    /// must correctly release this stream (usually by chaining to the returned
236    /// one) and must match the [`FFI_ArrowArrayStream::private_data`] it reads.
237    /// A wrong callback is undefined behavior on drop.
238    pub unsafe fn set_release(
239        &mut self,
240        release: Option<unsafe extern "C" fn(arg1: *mut Self)>,
241    ) -> Option<unsafe extern "C" fn(arg1: *mut Self)> {
242        std::mem::replace(&mut self.release, release)
243    }
244
245    /// Replaces the private data pointer, returning the previous one.
246    ///
247    /// # Safety
248    ///
249    /// The old pointer is returned without being freed; the caller owns it from
250    /// here. The new pointer must match what the current
251    /// [`FFI_ArrowArrayStream::release`] callback expects.
252    pub unsafe fn set_private_data(&mut self, private_data: *mut c_void) -> *mut c_void {
253        std::mem::replace(&mut self.private_data, private_data)
254    }
255}
256
257struct ExportedArrayStream {
258    stream: *mut FFI_ArrowArrayStream,
259}
260
261impl ExportedArrayStream {
262    fn get_private_data(&mut self) -> &mut StreamPrivateData {
263        unsafe { &mut *(*self.stream).private_data.cast::<StreamPrivateData>() }
264    }
265
266    pub fn get_schema(&mut self, out: *mut FFI_ArrowSchema) -> i32 {
267        let private_data = self.get_private_data();
268        let reader = &private_data.batch_reader;
269
270        let schema = FFI_ArrowSchema::try_from(reader.schema().as_ref());
271
272        match schema {
273            Ok(schema) => {
274                unsafe { std::ptr::copy(addr_of!(schema), out, 1) };
275                std::mem::forget(schema);
276                0
277            }
278            Err(ref err) => {
279                private_data.last_error = Some(
280                    CString::new(err.to_string()).expect("Error string has a null byte in it."),
281                );
282                get_error_code(err)
283            }
284        }
285    }
286
287    pub fn get_next(&mut self, out: *mut FFI_ArrowArray) -> i32 {
288        let private_data = self.get_private_data();
289        let reader = &mut private_data.batch_reader;
290
291        match reader.next() {
292            None => {
293                // Marks ArrowArray released to indicate reaching the end of stream.
294                unsafe { std::ptr::write(out, FFI_ArrowArray::empty()) }
295                0
296            }
297            Some(next_batch) => {
298                if let Ok(batch) = next_batch {
299                    let struct_array = StructArray::from(batch);
300                    let array = FFI_ArrowArray::new(&struct_array.to_data());
301
302                    unsafe { std::ptr::write_unaligned(out, array) };
303                    0
304                } else {
305                    let err = &next_batch.unwrap_err();
306                    private_data.last_error = Some(
307                        CString::new(err.to_string()).expect("Error string has a null byte in it."),
308                    );
309                    get_error_code(err)
310                }
311            }
312        }
313    }
314
315    pub fn get_last_error(&mut self) -> Option<&CString> {
316        self.get_private_data().last_error.as_ref()
317    }
318}
319
320fn get_error_code(err: &ArrowError) -> i32 {
321    match err {
322        ArrowError::NotYetImplemented(_) => ENOSYS,
323        ArrowError::MemoryError(_) => ENOMEM,
324        ArrowError::IoError(_, _) => EIO,
325        _ => EINVAL,
326    }
327}
328
329/// A `RecordBatchReader` which imports Arrays from `FFI_ArrowArrayStream`.
330///
331/// Struct used to fetch `RecordBatch` from the C Stream Interface.
332/// Its main responsibility is to expose `RecordBatchReader` functionality
333/// that requires [FFI_ArrowArrayStream].
334#[derive(Debug)]
335pub struct ArrowArrayStreamReader {
336    stream: FFI_ArrowArrayStream,
337    schema: SchemaRef,
338}
339
340/// Returns the producer's message for the last failed call on a `FFI_ArrowArrayStream`.
341///
342/// Returns `None` when the producer supplies no message, either because it installs no
343/// `get_last_error` callback or because that callback returns NULL: the C Stream Interface
344/// lets `get_last_error` return NULL when no detailed description is available.
345///
346/// # Safety
347///
348/// `stream_ptr` must point to a valid, not yet released [`FFI_ArrowArrayStream`], and the last
349/// operation on it must have returned an error: the C Stream Interface forbids calling
350/// `get_last_error` in any other case.
351unsafe fn producer_error(stream_ptr: *mut FFI_ArrowArrayStream) -> Option<String> {
352    let get_last_error = unsafe { (*stream_ptr).get_last_error }?;
353
354    let error_str = unsafe { get_last_error(stream_ptr) };
355    if error_str.is_null() {
356        return None;
357    }
358
359    Some(
360        unsafe { CStr::from_ptr(error_str) }
361            .to_string_lossy()
362            .into_owned(),
363    )
364}
365
366/// Gets schema from a raw pointer of `FFI_ArrowArrayStream`. This is used when constructing
367/// `ArrowArrayStreamReader` to cache schema.
368fn get_stream_schema(stream_ptr: *mut FFI_ArrowArrayStream) -> Result<SchemaRef> {
369    let mut schema = FFI_ArrowSchema::empty();
370
371    let ret_code = unsafe { (*stream_ptr).get_schema.unwrap()(stream_ptr, &raw mut schema) };
372
373    if ret_code == 0 {
374        let schema = Schema::try_from(&schema)?;
375        Ok(Arc::new(schema))
376    } else {
377        let message = format!("Cannot get schema from input stream. Error code: {ret_code}");
378        // SAFETY: `stream_ptr` is valid and unreleased, and the `get_schema` call above
379        // returned a non-zero code.
380        let message = match unsafe { producer_error(stream_ptr) } {
381            Some(producer_message) => format!("{message}. Producer error: {producer_message}"),
382            None => message,
383        };
384        Err(ArrowError::CDataInterface(message))
385    }
386}
387
388impl ArrowArrayStreamReader {
389    /// Creates a new `ArrowArrayStreamReader` from a `FFI_ArrowArrayStream`.
390    /// This is used to import from the C Stream Interface.
391    pub fn try_new(mut stream: FFI_ArrowArrayStream) -> Result<Self> {
392        if stream.release.is_none() {
393            return Err(ArrowError::CDataInterface(
394                "input stream is already released".to_string(),
395            ));
396        }
397
398        let schema = get_stream_schema(&raw mut stream)?;
399
400        Ok(Self { stream, schema })
401    }
402
403    /// Creates a new `ArrowArrayStreamReader` from a raw pointer of `FFI_ArrowArrayStream`.
404    ///
405    /// Assumes that the pointer represents valid C Stream Interfaces.
406    /// This function copies the content from the raw pointer and cleans up it to prevent
407    /// double-dropping. The caller is responsible for freeing up the memory allocated for
408    /// the pointer.
409    ///
410    /// # Safety
411    ///
412    /// See [`FFI_ArrowArrayStream::from_raw`]
413    pub unsafe fn from_raw(raw_stream: *mut FFI_ArrowArrayStream) -> Result<Self> {
414        Self::try_new(unsafe { FFI_ArrowArrayStream::from_raw(raw_stream) })
415    }
416}
417
418impl Iterator for ArrowArrayStreamReader {
419    type Item = Result<RecordBatch>;
420
421    fn next(&mut self) -> Option<Self::Item> {
422        let mut array = FFI_ArrowArray::empty();
423
424        let ret_code =
425            unsafe { self.stream.get_next.unwrap()(&raw mut self.stream, &raw mut array) };
426
427        if ret_code == 0 {
428            // The end of stream has been reached
429            if array.is_released() {
430                return None;
431            }
432
433            let result = unsafe {
434                from_ffi_and_data_type(array, DataType::Struct(self.schema().fields().clone()))
435            };
436            Some(result.and_then(|data| {
437                let len = data.len();
438                RecordBatch::try_new_with_options(
439                    self.schema.clone(),
440                    StructArray::from(data).into_parts().1,
441                    &RecordBatchOptions::new().with_row_count(Some(len)),
442                )
443            }))
444        } else {
445            let message =
446                format!("Cannot get next batch from input stream. Error code: {ret_code}");
447            // SAFETY: `self.stream` is valid and unreleased by construction, and the
448            // `get_next` call above returned a non-zero code.
449            let message = match unsafe { producer_error(&raw mut self.stream) } {
450                Some(producer_message) => format!("{message}. Producer error: {producer_message}"),
451                None => message,
452            };
453            Some(Err(ArrowError::CDataInterface(message)))
454        }
455    }
456}
457
458impl RecordBatchReader for ArrowArrayStreamReader {
459    fn schema(&self) -> SchemaRef {
460        self.schema.clone()
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use std::collections::HashMap;
468
469    use arrow_schema::Field;
470
471    use crate::array::Int32Array;
472    use crate::ffi::from_ffi;
473
474    struct TestRecordBatchReader {
475        schema: SchemaRef,
476        iter: Box<dyn Iterator<Item = Result<RecordBatch>> + Send>,
477    }
478
479    impl TestRecordBatchReader {
480        pub fn new(
481            schema: SchemaRef,
482            iter: Box<dyn Iterator<Item = Result<RecordBatch>> + Send>,
483        ) -> TestRecordBatchReader {
484            TestRecordBatchReader { schema, iter }
485        }
486    }
487
488    impl Iterator for TestRecordBatchReader {
489        type Item = Result<RecordBatch>;
490
491        fn next(&mut self) -> Option<Self::Item> {
492            self.iter.next()
493        }
494    }
495
496    impl RecordBatchReader for TestRecordBatchReader {
497        fn schema(&self) -> SchemaRef {
498            self.schema.clone()
499        }
500    }
501
502    fn _test_round_trip_export(batch: RecordBatch, schema: Arc<Schema>) -> Result<()> {
503        let iter = Box::new(vec![batch.clone(), batch.clone()].into_iter().map(Ok)) as _;
504
505        let reader = Box::new(TestRecordBatchReader::new(schema.clone(), iter));
506
507        // Export a `RecordBatchReader` through `FFI_ArrowArrayStream`
508        let mut ffi_stream = FFI_ArrowArrayStream::new(reader);
509
510        // Get schema from `FFI_ArrowArrayStream`
511        let mut ffi_schema = FFI_ArrowSchema::empty();
512        let ret_code = unsafe { get_schema(&raw mut ffi_stream, &raw mut ffi_schema) };
513        assert_eq!(ret_code, 0);
514
515        let exported_schema = Schema::try_from(&ffi_schema).unwrap();
516        assert_eq!(&exported_schema, schema.as_ref());
517
518        // Get array from `FFI_ArrowArrayStream`
519        let mut produced_batches = vec![];
520        loop {
521            let mut ffi_array = FFI_ArrowArray::empty();
522            let ret_code = unsafe { get_next(&raw mut ffi_stream, &raw mut ffi_array) };
523            assert_eq!(ret_code, 0);
524
525            // The end of stream has been reached
526            if ffi_array.is_released() {
527                break;
528            }
529
530            let array = unsafe { from_ffi(ffi_array, &ffi_schema) }.unwrap();
531            let len = array.len();
532
533            let record_batch = RecordBatch::try_new_with_options(
534                SchemaRef::from(exported_schema.clone()),
535                StructArray::from(array).into_parts().1,
536                &RecordBatchOptions::new().with_row_count(Some(len)),
537            )
538            .unwrap();
539            produced_batches.push(record_batch);
540        }
541
542        assert_eq!(produced_batches, vec![batch.clone(), batch]);
543
544        Ok(())
545    }
546
547    fn _test_round_trip_import(batch: RecordBatch, schema: Arc<Schema>) -> Result<()> {
548        let iter = Box::new(vec![batch.clone(), batch.clone()].into_iter().map(Ok)) as _;
549
550        let reader = Box::new(TestRecordBatchReader::new(schema.clone(), iter));
551
552        // Import through `FFI_ArrowArrayStream` as `ArrowArrayStreamReader`
553        let stream = FFI_ArrowArrayStream::new(reader);
554        let stream_reader = ArrowArrayStreamReader::try_new(stream).unwrap();
555
556        let imported_schema = stream_reader.schema();
557        assert_eq!(imported_schema, schema);
558
559        let mut produced_batches = vec![];
560        for batch in stream_reader {
561            produced_batches.push(batch.unwrap());
562        }
563
564        assert_eq!(produced_batches, vec![batch.clone(), batch]);
565
566        Ok(())
567    }
568
569    #[test]
570    fn test_stream_round_trip() {
571        let array = Int32Array::from(vec![Some(2), None, Some(1), None]);
572        let array: Arc<dyn Array> = Arc::new(array);
573        let metadata = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
574
575        let schema = Arc::new(Schema::new_with_metadata(
576            vec![
577                Field::new("a", array.data_type().clone(), true).with_metadata(metadata.clone()),
578                Field::new("b", array.data_type().clone(), true).with_metadata(metadata.clone()),
579                Field::new("c", array.data_type().clone(), true).with_metadata(metadata.clone()),
580            ],
581            metadata,
582        ));
583        let batch = RecordBatch::try_new(schema.clone(), vec![array.clone(), array.clone(), array])
584            .unwrap();
585
586        _test_round_trip_export(batch.clone(), schema.clone()).unwrap();
587        _test_round_trip_import(batch, schema).unwrap();
588    }
589
590    #[test]
591    fn test_stream_round_trip_no_columns() {
592        let metadata = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
593
594        let schema = Arc::new(Schema::new_with_metadata(Vec::<Field>::new(), metadata));
595        let batch = RecordBatch::try_new_with_options(
596            schema.clone(),
597            Vec::<Arc<dyn Array>>::new(),
598            &RecordBatchOptions::new().with_row_count(Some(10)),
599        )
600        .unwrap();
601
602        _test_round_trip_export(batch.clone(), schema.clone()).unwrap();
603        _test_round_trip_import(batch, schema).unwrap();
604    }
605
606    #[test]
607    fn test_error_import() -> Result<()> {
608        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
609
610        let iter =
611            Box::new(vec![Err(ArrowError::MemoryError("out of memory".to_string()))].into_iter());
612
613        let reader = Box::new(TestRecordBatchReader::new(schema.clone(), iter));
614
615        // Import through `FFI_ArrowArrayStream` as `ArrowArrayStreamReader`
616        let stream = FFI_ArrowArrayStream::new(reader);
617        let stream_reader = ArrowArrayStreamReader::try_new(stream).unwrap();
618
619        let imported_schema = stream_reader.schema();
620        assert_eq!(imported_schema, schema);
621
622        let mut produced_batches = vec![];
623        for batch in stream_reader {
624            produced_batches.push(batch);
625        }
626
627        // The results should outlive the lifetime of the stream itself.
628        assert_eq!(produced_batches.len(), 1);
629        assert_eq!(
630            produced_batches[0].as_ref().unwrap_err().to_string(),
631            format!(
632                "C Data interface error: Cannot get next batch from input stream. \
633                 Error code: {ENOMEM}. Producer error: Memory error: out of memory"
634            )
635        );
636
637        Ok(())
638    }
639
640    unsafe extern "C" fn failing_get_schema(
641        _stream: *mut FFI_ArrowArrayStream,
642        _out: *mut FFI_ArrowSchema,
643    ) -> c_int {
644        EIO
645    }
646
647    unsafe extern "C" fn working_get_schema(
648        _stream: *mut FFI_ArrowArrayStream,
649        out: *mut FFI_ArrowSchema,
650    ) -> c_int {
651        let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
652        unsafe { std::ptr::write(out, FFI_ArrowSchema::try_from(&schema).unwrap()) };
653        0
654    }
655
656    unsafe extern "C" fn failing_get_next(
657        _stream: *mut FFI_ArrowArrayStream,
658        _out: *mut FFI_ArrowArray,
659    ) -> c_int {
660        EIO
661    }
662
663    unsafe extern "C" fn producer_last_error(_stream: *mut FFI_ArrowArrayStream) -> *const c_char {
664        c"the producer failed".as_ptr()
665    }
666
667    unsafe extern "C" fn null_last_error(_stream: *mut FFI_ArrowArrayStream) -> *const c_char {
668        std::ptr::null()
669    }
670
671    unsafe extern "C" fn mark_released(stream: *mut FFI_ArrowArrayStream) {
672        unsafe { (*stream).release = None };
673    }
674
675    fn failing_stream(
676        get_last_error: Option<unsafe extern "C" fn(*mut FFI_ArrowArrayStream) -> *const c_char>,
677    ) -> FFI_ArrowArrayStream {
678        let mut stream = FFI_ArrowArrayStream::empty();
679        stream.get_schema = Some(failing_get_schema);
680        stream.get_next = Some(failing_get_next);
681        stream.get_last_error = get_last_error;
682        stream.release = Some(mark_released);
683        stream
684    }
685
686    #[test]
687    fn test_import_schema_error_reports_producer_message() {
688        let err =
689            ArrowArrayStreamReader::try_new(failing_stream(Some(producer_last_error))).unwrap_err();
690        assert_eq!(
691            err.to_string(),
692            format!(
693                "C Data interface error: Cannot get schema from input stream. \
694                 Error code: {EIO}. Producer error: the producer failed"
695            )
696        );
697    }
698
699    #[test]
700    fn test_import_schema_error_without_producer_message() {
701        // A producer need not supply a message: `get_last_error` may return NULL when no
702        // detailed description is available.
703        let err =
704            ArrowArrayStreamReader::try_new(failing_stream(Some(null_last_error))).unwrap_err();
705        assert_eq!(
706            err.to_string(),
707            format!(
708                "C Data interface error: Cannot get schema from input stream. Error code: {EIO}"
709            )
710        );
711    }
712
713    #[test]
714    fn test_import_schema_error_without_error_callback() {
715        let err = ArrowArrayStreamReader::try_new(failing_stream(None)).unwrap_err();
716        assert_eq!(
717            err.to_string(),
718            format!(
719                "C Data interface error: Cannot get schema from input stream. Error code: {EIO}"
720            )
721        );
722    }
723
724    #[test]
725    fn test_import_next_error_without_producer_message() {
726        // Previously panicked: the message was unwrapped without checking that the producer
727        // supplied one.
728        let mut stream = failing_stream(Some(null_last_error));
729        stream.get_schema = Some(working_get_schema);
730
731        let err = ArrowArrayStreamReader::try_new(stream)
732            .unwrap()
733            .next()
734            .unwrap()
735            .unwrap_err();
736        assert_eq!(
737            err.to_string(),
738            format!(
739                "C Data interface error: Cannot get next batch from input stream. Error code: {EIO}"
740            )
741        );
742    }
743
744    // A consumer wraps the release callback with its own, then chains back to
745    // the original on drop. This is the same wrap-release pattern the
746    // release/private_data accessors exist for (#9771).
747    static STREAM_WRAPPER_RAN: std::sync::atomic::AtomicBool =
748        std::sync::atomic::AtomicBool::new(false);
749
750    struct StreamWrapperData {
751        original_release: Option<unsafe extern "C" fn(*mut FFI_ArrowArrayStream)>,
752        original_private_data: *mut c_void,
753    }
754
755    unsafe extern "C" fn wrapping_release(stream: *mut FFI_ArrowArrayStream) {
756        use std::sync::atomic::Ordering;
757        let stream = unsafe { &mut *stream };
758        let data = unsafe { Box::from_raw(stream.private_data().cast::<StreamWrapperData>()) };
759        STREAM_WRAPPER_RAN.store(true, Ordering::SeqCst);
760        unsafe { stream.set_release(data.original_release) };
761        unsafe { stream.set_private_data(data.original_private_data) };
762        if let Some(release) = stream.release() {
763            unsafe { release(stream) };
764        }
765    }
766
767    #[test]
768    fn test_wrap_release_callback() {
769        use std::sync::atomic::Ordering;
770
771        let batch_reader = Box::new(TestRecordBatchReader::new(
772            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)])),
773            Box::new(std::iter::empty()),
774        ));
775        let mut stream = FFI_ArrowArrayStream::new(batch_reader);
776
777        let data = Box::new(StreamWrapperData {
778            original_release: stream.release(),
779            original_private_data: stream.private_data(),
780        });
781        unsafe { stream.set_release(Some(wrapping_release)) };
782        unsafe { stream.set_private_data(Box::into_raw(data).cast::<c_void>()) };
783
784        drop(stream); // runs wrapping_release, which chains to the original
785        assert!(STREAM_WRAPPER_RAN.load(Ordering::SeqCst));
786    }
787}