Skip to content

Commit d737639

Browse files
authored
ffi: create data source from memory buffer (#8426)
Match rust API: buffer is borrowed under the condition it won't be modified until data source is freed Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
1 parent 2528dca commit d737639

5 files changed

Lines changed: 144 additions & 17 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vortex-ffi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ all-features = true
2323
arrow-array = { workspace = true, features = ["ffi"] }
2424
arrow-schema = { workspace = true }
2525
async-fs = { workspace = true }
26+
bytes = { workspace = true }
2627
futures = { workspace = true }
2728
itertools = { workspace = true }
2829
mimalloc = { workspace = true, optional = true }

vortex-ffi/cinclude/vortex.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,21 @@ void vx_data_source_free(const vx_data_source *ptr);
884884
const vx_data_source *
885885
vx_data_source_new(const vx_session *session, const vx_data_source_options *options, vx_error **err);
886886

887+
/**
888+
* Create a data source from a single in-memory Vortex file.
889+
*
890+
* "buffer_len" is the length of "buffer" in bytes.
891+
* The bytes are borrowed, not copied: the caller must keep "buffer" alive and
892+
* unmodified until the data source is freed.
893+
*
894+
* The returned pointer is owned by the caller and must be freed with
895+
* vx_data_source_free.
896+
*
897+
* On error, returns NULL and sets "err".
898+
*/
899+
const vx_data_source *
900+
vx_data_source_new_buffer(const vx_session *session, const void *buffer, size_t buffer_len, vx_error **err);
901+
887902
/**
888903
* Return the schema of the data source as a non-owned dtype.
889904
* The returned pointer is valid as long as "ds" is alive. Do not free it.

vortex-ffi/src/data_source.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@
55

66
use std::ffi::CStr;
77
use std::ffi::c_char;
8+
use std::ffi::c_void;
89
use std::ptr;
10+
use std::slice;
911
use std::sync::Arc;
1012

13+
use bytes::Bytes;
14+
use vortex::buffer::ByteBuffer;
1115
use vortex::error::VortexResult;
1216
use vortex::error::vortex_ensure;
1317
use vortex::expr::stats::Precision::Absent;
1418
use vortex::expr::stats::Precision::Exact;
1519
use vortex::expr::stats::Precision::Inexact;
20+
use vortex::file::OpenOptionsSessionExt;
1621
use vortex::file::multi::MultiFileDataSource;
1722
use vortex::io::runtime::BlockingRuntime;
23+
use vortex::layout::scan::multi::MultiLayoutDataSource;
1824
use vortex::scan::DataSource;
1925
use vortex::scan::DataSourceRef;
2026

@@ -104,6 +110,38 @@ pub unsafe extern "C-unwind" fn vx_data_source_new(
104110
})
105111
}
106112

113+
/// Create a data source from a single in-memory Vortex file.
114+
///
115+
/// "buffer_len" is the length of "buffer" in bytes.
116+
/// The bytes are borrowed, not copied: the caller must keep "buffer" alive and
117+
/// unmodified until the data source is freed.
118+
///
119+
/// The returned pointer is owned by the caller and must be freed with
120+
/// vx_data_source_free.
121+
///
122+
/// On error, returns NULL and sets "err".
123+
#[unsafe(no_mangle)]
124+
pub unsafe extern "C-unwind" fn vx_data_source_new_buffer(
125+
session: *const vx_session,
126+
buffer: *const c_void,
127+
buffer_len: usize,
128+
err: *mut *mut vx_error,
129+
) -> *const vx_data_source {
130+
try_or(err, ptr::null(), || {
131+
vortex_ensure!(!session.is_null());
132+
vortex_ensure!(!buffer.is_null());
133+
134+
let session = vx_session::as_ref(session);
135+
let bytes: &'static [u8] =
136+
unsafe { slice::from_raw_parts(buffer.cast::<u8>(), buffer_len) };
137+
let buffer = ByteBuffer::from(Bytes::from_static(bytes));
138+
let file = session.open_options().open_buffer(buffer)?;
139+
let ds = MultiLayoutDataSource::new_with_first(file.layout_reader()?, Vec::new(), session);
140+
141+
Ok(vx_data_source::new(Arc::new(ds) as DataSourceRef))
142+
})
143+
}
144+
107145
/// Return the schema of the data source as a non-owned dtype.
108146
/// The returned pointer is valid as long as "ds" is alive. Do not free it.
109147
#[unsafe(no_mangle)]
@@ -140,12 +178,15 @@ pub unsafe extern "C-unwind" fn vx_data_source_get_row_count(
140178
#[cfg(test)]
141179
mod tests {
142180
use std::ffi::CString;
181+
use std::ffi::c_void;
182+
use std::fs::read;
143183
use std::ptr;
144184

145185
use crate::data_source::vx_data_source_dtype;
146186
use crate::data_source::vx_data_source_free;
147187
use crate::data_source::vx_data_source_get_row_count;
148188
use crate::data_source::vx_data_source_new;
189+
use crate::data_source::vx_data_source_new_buffer;
149190
use crate::data_source::vx_data_source_options;
150191
use crate::dtype::vx_dtype;
151192
use crate::scan::vx_estimate;
@@ -220,4 +261,39 @@ mod tests {
220261
vx_session_free(session);
221262
}
222263
}
264+
265+
#[test]
266+
#[cfg_attr(miri, ignore)]
267+
fn test_buffer() {
268+
unsafe {
269+
let session = vx_session_new();
270+
let (sample, struct_array) = write_sample(session);
271+
272+
let mut error = ptr::null_mut();
273+
let ds = vx_data_source_new_buffer(session, ptr::null(), 0, &raw mut error);
274+
assert_error(error);
275+
assert!(ds.is_null());
276+
277+
let file = read(sample).unwrap();
278+
let ds = vx_data_source_new_buffer(
279+
session,
280+
file.as_ptr() as *const c_void,
281+
file.len(),
282+
&raw mut error,
283+
);
284+
assert_no_error(error);
285+
assert!(!ds.is_null());
286+
287+
let dtype = vx_dtype::as_ref(vx_data_source_dtype(ds));
288+
assert_eq!(dtype, struct_array.dtype());
289+
290+
let mut row_count = vx_estimate::default();
291+
vx_data_source_get_row_count(ds, &raw mut row_count);
292+
assert_eq!(row_count.r#type, vx_estimate_type::VX_ESTIMATE_EXACT);
293+
assert_eq!(row_count.estimate, SAMPLE_ROWS as u64);
294+
295+
vx_data_source_free(ds);
296+
vx_session_free(session);
297+
}
298+
}
223299
}

vortex-ffi/test/scan.cpp

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <random>
1010
#include <thread>
1111
#include <unistd.h>
12+
#include <fstream>
1213

1314
using FFI_ArrowArrayStream = ArrowArrayStream;
1415
using FFI_ArrowArray = ArrowArray;
@@ -218,6 +219,11 @@ TEST_CASE("Creating datasources", "[datasource]") {
218219
REQUIRE(error != nullptr);
219220
vx_error_free(error);
220221

222+
ds = vx_data_source_new_buffer(session, nullptr, 0, &error);
223+
REQUIRE(ds == nullptr);
224+
REQUIRE(error != nullptr);
225+
vx_error_free(error);
226+
221227
TempPath file = write_sample(session);
222228
opts.paths = file.c_str();
223229
ds = vx_data_source_new(session, &opts, &error);
@@ -383,24 +389,8 @@ TEST_CASE("Requesting scans", "[datasource]") {
383389
}
384390
}
385391

386-
TEST_CASE("Basic scan", "[datasource]") {
387-
vx_session *session = vx_session_new();
388-
defer {
389-
vx_session_free(session);
390-
};
391-
TempPath path = write_sample(session);
392+
void basic_scan(const vx_data_source *ds) {
392393
vx_error *error = nullptr;
393-
394-
vx_data_source_options ds_options = {};
395-
ds_options.paths = path.c_str();
396-
397-
const vx_data_source *ds = vx_data_source_new(session, &ds_options, &error);
398-
require_no_error(error);
399-
REQUIRE(ds != nullptr);
400-
defer {
401-
vx_data_source_free(ds);
402-
};
403-
404394
vx_estimate estimate = {};
405395
vx_scan *scan = vx_data_source_scan(ds, nullptr, &estimate, &error);
406396
require_no_error(error);
@@ -439,6 +429,50 @@ TEST_CASE("Basic scan", "[datasource]") {
439429
verify_sample_array(array);
440430
}
441431

432+
TEST_CASE("Basic scan", "[datasource]") {
433+
vx_session *session = vx_session_new();
434+
defer {
435+
vx_session_free(session);
436+
};
437+
TempPath path = write_sample(session);
438+
vx_error *error = nullptr;
439+
440+
vx_data_source_options ds_options = {};
441+
ds_options.paths = path.c_str();
442+
const vx_data_source *ds = vx_data_source_new(session, &ds_options, &error);
443+
require_no_error(error);
444+
REQUIRE(ds != nullptr);
445+
defer {
446+
vx_data_source_free(ds);
447+
};
448+
449+
basic_scan(ds);
450+
}
451+
452+
TEST_CASE("Basic scan from memory", "[datasource]") {
453+
vx_session *session = vx_session_new();
454+
defer {
455+
vx_session_free(session);
456+
};
457+
TempPath path = write_sample(session);
458+
459+
std::ifstream file(path, std::ios::binary | std::ios::ate);
460+
const std::streamsize size = file.tellg();
461+
file.seekg(0, std::ios::beg);
462+
std::vector<char> buffer(size);
463+
REQUIRE(file.read(buffer.data(), size));
464+
465+
vx_error *error = nullptr;
466+
const vx_data_source *ds = vx_data_source_new_buffer(session, buffer.data(), size, &error);
467+
require_no_error(error);
468+
REQUIRE(ds != nullptr);
469+
defer {
470+
vx_data_source_free(ds);
471+
};
472+
473+
basic_scan(ds);
474+
}
475+
442476
TEST_CASE("Multithreaded scan", "[datasource]") {
443477
vx_session *session = vx_session_new();
444478
defer {

0 commit comments

Comments
 (0)