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)]
98#[allow(non_camel_case_types)]
99pub struct FFI_ArrowArrayStream {
100    /// C function to get schema from the stream
101    pub get_schema:
102        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    pub 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    pub get_last_error: Option<unsafe extern "C" fn(arg1: *mut Self) -> *const c_char>,
107    /// C function to release the stream
108    pub release: Option<unsafe extern "C" fn(arg1: *mut Self)>,
109    /// Private data used by the stream
110    pub 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 as *mut 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) as *mut 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
218struct ExportedArrayStream {
219    stream: *mut FFI_ArrowArrayStream,
220}
221
222impl ExportedArrayStream {
223    fn get_private_data(&mut self) -> &mut StreamPrivateData {
224        unsafe { &mut *((*self.stream).private_data as *mut StreamPrivateData) }
225    }
226
227    pub fn get_schema(&mut self, out: *mut FFI_ArrowSchema) -> i32 {
228        let private_data = self.get_private_data();
229        let reader = &private_data.batch_reader;
230
231        let schema = FFI_ArrowSchema::try_from(reader.schema().as_ref());
232
233        match schema {
234            Ok(schema) => {
235                unsafe { std::ptr::copy(addr_of!(schema), out, 1) };
236                std::mem::forget(schema);
237                0
238            }
239            Err(ref err) => {
240                private_data.last_error = Some(
241                    CString::new(err.to_string()).expect("Error string has a null byte in it."),
242                );
243                get_error_code(err)
244            }
245        }
246    }
247
248    pub fn get_next(&mut self, out: *mut FFI_ArrowArray) -> i32 {
249        let private_data = self.get_private_data();
250        let reader = &mut private_data.batch_reader;
251
252        match reader.next() {
253            None => {
254                // Marks ArrowArray released to indicate reaching the end of stream.
255                unsafe { std::ptr::write(out, FFI_ArrowArray::empty()) }
256                0
257            }
258            Some(next_batch) => {
259                if let Ok(batch) = next_batch {
260                    let struct_array = StructArray::from(batch);
261                    let array = FFI_ArrowArray::new(&struct_array.to_data());
262
263                    unsafe { std::ptr::write_unaligned(out, array) };
264                    0
265                } else {
266                    let err = &next_batch.unwrap_err();
267                    private_data.last_error = Some(
268                        CString::new(err.to_string()).expect("Error string has a null byte in it."),
269                    );
270                    get_error_code(err)
271                }
272            }
273        }
274    }
275
276    pub fn get_last_error(&mut self) -> Option<&CString> {
277        self.get_private_data().last_error.as_ref()
278    }
279}
280
281fn get_error_code(err: &ArrowError) -> i32 {
282    match err {
283        ArrowError::NotYetImplemented(_) => ENOSYS,
284        ArrowError::MemoryError(_) => ENOMEM,
285        ArrowError::IoError(_, _) => EIO,
286        _ => EINVAL,
287    }
288}
289
290/// A `RecordBatchReader` which imports Arrays from `FFI_ArrowArrayStream`.
291///
292/// Struct used to fetch `RecordBatch` from the C Stream Interface.
293/// Its main responsibility is to expose `RecordBatchReader` functionality
294/// that requires [FFI_ArrowArrayStream].
295#[derive(Debug)]
296pub struct ArrowArrayStreamReader {
297    stream: FFI_ArrowArrayStream,
298    schema: SchemaRef,
299}
300
301/// Gets schema from a raw pointer of `FFI_ArrowArrayStream`. This is used when constructing
302/// `ArrowArrayStreamReader` to cache schema.
303fn get_stream_schema(stream_ptr: *mut FFI_ArrowArrayStream) -> Result<SchemaRef> {
304    let mut schema = FFI_ArrowSchema::empty();
305
306    let ret_code = unsafe { (*stream_ptr).get_schema.unwrap()(stream_ptr, &mut schema) };
307
308    if ret_code == 0 {
309        let schema = Schema::try_from(&schema)?;
310        Ok(Arc::new(schema))
311    } else {
312        Err(ArrowError::CDataInterface(format!(
313            "Cannot get schema from input stream. Error code: {ret_code:?}"
314        )))
315    }
316}
317
318impl ArrowArrayStreamReader {
319    /// Creates a new `ArrowArrayStreamReader` from a `FFI_ArrowArrayStream`.
320    /// This is used to import from the C Stream Interface.
321    #[allow(dead_code)]
322    pub fn try_new(mut stream: FFI_ArrowArrayStream) -> Result<Self> {
323        if stream.release.is_none() {
324            return Err(ArrowError::CDataInterface(
325                "input stream is already released".to_string(),
326            ));
327        }
328
329        let schema = get_stream_schema(&mut stream)?;
330
331        Ok(Self { stream, schema })
332    }
333
334    /// Creates a new `ArrowArrayStreamReader` from a raw pointer of `FFI_ArrowArrayStream`.
335    ///
336    /// Assumes that the pointer represents valid C Stream Interfaces.
337    /// This function copies the content from the raw pointer and cleans up it to prevent
338    /// double-dropping. The caller is responsible for freeing up the memory allocated for
339    /// the pointer.
340    ///
341    /// # Safety
342    ///
343    /// See [`FFI_ArrowArrayStream::from_raw`]
344    pub unsafe fn from_raw(raw_stream: *mut FFI_ArrowArrayStream) -> Result<Self> {
345        Self::try_new(unsafe { FFI_ArrowArrayStream::from_raw(raw_stream) })
346    }
347
348    /// Get the last error from `ArrowArrayStreamReader`
349    fn get_stream_last_error(&mut self) -> Option<String> {
350        let get_last_error = self.stream.get_last_error?;
351
352        let error_str = unsafe { get_last_error(&mut self.stream) };
353        if error_str.is_null() {
354            return None;
355        }
356
357        let error_str = unsafe { CStr::from_ptr(error_str) };
358        Some(error_str.to_string_lossy().to_string())
359    }
360}
361
362impl Iterator for ArrowArrayStreamReader {
363    type Item = Result<RecordBatch>;
364
365    fn next(&mut self) -> Option<Self::Item> {
366        let mut array = FFI_ArrowArray::empty();
367
368        let ret_code = unsafe { self.stream.get_next.unwrap()(&mut self.stream, &mut array) };
369
370        if ret_code == 0 {
371            // The end of stream has been reached
372            if array.is_released() {
373                return None;
374            }
375
376            let result = unsafe {
377                from_ffi_and_data_type(array, DataType::Struct(self.schema().fields().clone()))
378            };
379            Some(result.and_then(|data| {
380                let len = data.len();
381                RecordBatch::try_new_with_options(
382                    self.schema.clone(),
383                    StructArray::from(data).into_parts().1,
384                    &RecordBatchOptions::new().with_row_count(Some(len)),
385                )
386            }))
387        } else {
388            let last_error = self.get_stream_last_error();
389            let err = ArrowError::CDataInterface(last_error.unwrap());
390            Some(Err(err))
391        }
392    }
393}
394
395impl RecordBatchReader for ArrowArrayStreamReader {
396    fn schema(&self) -> SchemaRef {
397        self.schema.clone()
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use std::collections::HashMap;
405
406    use arrow_schema::Field;
407
408    use crate::array::Int32Array;
409    use crate::ffi::from_ffi;
410
411    struct TestRecordBatchReader {
412        schema: SchemaRef,
413        iter: Box<dyn Iterator<Item = Result<RecordBatch>> + Send>,
414    }
415
416    impl TestRecordBatchReader {
417        pub fn new(
418            schema: SchemaRef,
419            iter: Box<dyn Iterator<Item = Result<RecordBatch>> + Send>,
420        ) -> Box<TestRecordBatchReader> {
421            Box::new(TestRecordBatchReader { schema, iter })
422        }
423    }
424
425    impl Iterator for TestRecordBatchReader {
426        type Item = Result<RecordBatch>;
427
428        fn next(&mut self) -> Option<Self::Item> {
429            self.iter.next()
430        }
431    }
432
433    impl RecordBatchReader for TestRecordBatchReader {
434        fn schema(&self) -> SchemaRef {
435            self.schema.clone()
436        }
437    }
438
439    fn _test_round_trip_export(batch: RecordBatch, schema: Arc<Schema>) -> Result<()> {
440        let iter = Box::new(vec![batch.clone(), batch.clone()].into_iter().map(Ok)) as _;
441
442        let reader = TestRecordBatchReader::new(schema.clone(), iter);
443
444        // Export a `RecordBatchReader` through `FFI_ArrowArrayStream`
445        let mut ffi_stream = FFI_ArrowArrayStream::new(reader);
446
447        // Get schema from `FFI_ArrowArrayStream`
448        let mut ffi_schema = FFI_ArrowSchema::empty();
449        let ret_code = unsafe { get_schema(&mut ffi_stream, &mut ffi_schema) };
450        assert_eq!(ret_code, 0);
451
452        let exported_schema = Schema::try_from(&ffi_schema).unwrap();
453        assert_eq!(&exported_schema, schema.as_ref());
454
455        // Get array from `FFI_ArrowArrayStream`
456        let mut produced_batches = vec![];
457        loop {
458            let mut ffi_array = FFI_ArrowArray::empty();
459            let ret_code = unsafe { get_next(&mut ffi_stream, &mut ffi_array) };
460            assert_eq!(ret_code, 0);
461
462            // The end of stream has been reached
463            if ffi_array.is_released() {
464                break;
465            }
466
467            let array = unsafe { from_ffi(ffi_array, &ffi_schema) }.unwrap();
468            let len = array.len();
469
470            let record_batch = RecordBatch::try_new_with_options(
471                SchemaRef::from(exported_schema.clone()),
472                StructArray::from(array).into_parts().1,
473                &RecordBatchOptions::new().with_row_count(Some(len)),
474            )
475            .unwrap();
476            produced_batches.push(record_batch);
477        }
478
479        assert_eq!(produced_batches, vec![batch.clone(), batch]);
480
481        Ok(())
482    }
483
484    fn _test_round_trip_import(batch: RecordBatch, schema: Arc<Schema>) -> Result<()> {
485        let iter = Box::new(vec![batch.clone(), batch.clone()].into_iter().map(Ok)) as _;
486
487        let reader = TestRecordBatchReader::new(schema.clone(), iter);
488
489        // Import through `FFI_ArrowArrayStream` as `ArrowArrayStreamReader`
490        let stream = FFI_ArrowArrayStream::new(reader);
491        let stream_reader = ArrowArrayStreamReader::try_new(stream).unwrap();
492
493        let imported_schema = stream_reader.schema();
494        assert_eq!(imported_schema, schema);
495
496        let mut produced_batches = vec![];
497        for batch in stream_reader {
498            produced_batches.push(batch.unwrap());
499        }
500
501        assert_eq!(produced_batches, vec![batch.clone(), batch]);
502
503        Ok(())
504    }
505
506    #[test]
507    fn test_stream_round_trip() {
508        let array = Int32Array::from(vec![Some(2), None, Some(1), None]);
509        let array: Arc<dyn Array> = Arc::new(array);
510        let metadata = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
511
512        let schema = Arc::new(Schema::new_with_metadata(
513            vec![
514                Field::new("a", array.data_type().clone(), true).with_metadata(metadata.clone()),
515                Field::new("b", array.data_type().clone(), true).with_metadata(metadata.clone()),
516                Field::new("c", array.data_type().clone(), true).with_metadata(metadata.clone()),
517            ],
518            metadata,
519        ));
520        let batch = RecordBatch::try_new(schema.clone(), vec![array.clone(), array.clone(), array])
521            .unwrap();
522
523        _test_round_trip_export(batch.clone(), schema.clone()).unwrap();
524        _test_round_trip_import(batch, schema).unwrap();
525    }
526
527    #[test]
528    fn test_stream_round_trip_no_columns() {
529        let metadata = HashMap::from([("foo".to_owned(), "bar".to_owned())]);
530
531        let schema = Arc::new(Schema::new_with_metadata(Vec::<Field>::new(), metadata));
532        let batch = RecordBatch::try_new_with_options(
533            schema.clone(),
534            Vec::<Arc<dyn Array>>::new(),
535            &RecordBatchOptions::new().with_row_count(Some(10)),
536        )
537        .unwrap();
538
539        _test_round_trip_export(batch.clone(), schema.clone()).unwrap();
540        _test_round_trip_import(batch, schema).unwrap();
541    }
542
543    #[test]
544    fn test_error_import() -> Result<()> {
545        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
546
547        let iter = Box::new(vec![Err(ArrowError::MemoryError("".to_string()))].into_iter());
548
549        let reader = TestRecordBatchReader::new(schema.clone(), iter);
550
551        // Import through `FFI_ArrowArrayStream` as `ArrowArrayStreamReader`
552        let stream = FFI_ArrowArrayStream::new(reader);
553        let stream_reader = ArrowArrayStreamReader::try_new(stream).unwrap();
554
555        let imported_schema = stream_reader.schema();
556        assert_eq!(imported_schema, schema);
557
558        let mut produced_batches = vec![];
559        for batch in stream_reader {
560            produced_batches.push(batch);
561        }
562
563        // The results should outlive the lifetime of the stream itself.
564        assert_eq!(produced_batches.len(), 1);
565        assert!(produced_batches[0].is_err());
566
567        Ok(())
568    }
569}