Skip to content

Commit 37006d6

Browse files
committed
Add native C ABI for .NET query interop
1 parent 905ad61 commit 37006d6

4 files changed

Lines changed: 279 additions & 1 deletion

File tree

‎Cargo.lock‎

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

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["miniexcel", "miniexcel-cli", "miniexcel-wasm"]
2+
members = ["miniexcel", "miniexcel-cli", "miniexcel-ffi", "miniexcel-wasm"]
33
resolver = "2"
44

55
[workspace.package]

‎miniexcel-ffi/Cargo.toml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "miniexcel-ffi"
3+
version.workspace = true
4+
edition.workspace = true
5+
rust-version.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
homepage.workspace = true
9+
description = "Native C ABI for MiniExcel .NET interop"
10+
publish = false
11+
12+
[lib]
13+
crate-type = ["cdylib"]
14+
15+
[dependencies]
16+
miniexcel = { path = "../miniexcel" }

‎miniexcel-ffi/src/lib.rs‎

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
use std::cell::RefCell;
2+
use std::ffi::{CStr, c_char};
3+
use std::panic::{AssertUnwindSafe, catch_unwind};
4+
use std::ptr;
5+
use std::str::FromStr;
6+
7+
use miniexcel::{CellReference, CellValue, DynamicRow, HeaderMode, MiniExcel, ReadOptions};
8+
9+
const ABI_VERSION: u32 = 1;
10+
const RESULT_END: i32 = 0;
11+
const RESULT_BATCH: i32 = 1;
12+
const ERROR_INVALID_ARGUMENT: i32 = -1;
13+
const ERROR_QUERY: i32 = -2;
14+
const ERROR_PANIC: i32 = -3;
15+
16+
thread_local! {
17+
static LAST_ERROR: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
18+
}
19+
20+
pub struct QueryHandle {
21+
rows: Box<dyn Iterator<Item = miniexcel::Result<DynamicRow>> + Send>,
22+
frame: Vec<u8>,
23+
}
24+
25+
#[unsafe(no_mangle)]
26+
pub extern "C" fn miniexcel_abi_version() -> u32 {
27+
ABI_VERSION
28+
}
29+
30+
/// Opens a path-based XLSX query and returns an opaque native handle.
31+
///
32+
/// # Safety
33+
///
34+
/// String pointers must be null-terminated UTF-8. `path`, `start_cell`, and `out_handle` must be
35+
/// non-null and valid for the duration of the call. `sheet_name` may be null.
36+
#[unsafe(no_mangle)]
37+
pub unsafe extern "C" fn miniexcel_query_open(
38+
path: *const c_char,
39+
use_header_row: u8,
40+
sheet_name: *const c_char,
41+
start_cell: *const c_char,
42+
out_handle: *mut *mut QueryHandle,
43+
) -> i32 {
44+
ffi_result(|| {
45+
if path.is_null() || start_cell.is_null() || out_handle.is_null() {
46+
set_last_error("path, start_cell, and out_handle are required");
47+
return Err(ERROR_INVALID_ARGUMENT);
48+
}
49+
50+
let path = unsafe { read_utf8(path) }?;
51+
let start_cell = unsafe { read_utf8(start_cell) }?;
52+
let start_cell = CellReference::from_str(start_cell).map_err(|error| {
53+
set_last_error(error.to_string());
54+
ERROR_INVALID_ARGUMENT
55+
})?;
56+
57+
let mut options = ReadOptions::new()
58+
.with_header_mode(if use_header_row == 0 {
59+
HeaderMode::None
60+
} else {
61+
HeaderMode::FirstRow
62+
})
63+
.with_start_cell(start_cell);
64+
65+
if !sheet_name.is_null() {
66+
let sheet_name = unsafe { read_utf8(sheet_name) }?;
67+
if !sheet_name.is_empty() {
68+
options = options.with_sheet_name(sheet_name);
69+
}
70+
}
71+
72+
let rows = MiniExcel::query_with_options(path, &options).map_err(|error| {
73+
set_last_error(error.to_string());
74+
ERROR_QUERY
75+
})?;
76+
let handle = Box::new(QueryHandle { rows, frame: Vec::new() });
77+
unsafe { ptr::write(out_handle, Box::into_raw(handle)) };
78+
Ok(RESULT_BATCH)
79+
})
80+
}
81+
82+
/// Writes the next bounded batch into memory owned by the query handle.
83+
///
84+
/// # Safety
85+
///
86+
/// `handle` must have been returned by `miniexcel_query_open` and not yet closed. Output pointers
87+
/// must be non-null and writable. Returned data is valid until the next call using the handle.
88+
#[unsafe(no_mangle)]
89+
pub unsafe extern "C" fn miniexcel_query_next_batch(
90+
handle: *mut QueryHandle,
91+
max_rows: u32,
92+
out_data: *mut *const u8,
93+
out_length: *mut usize,
94+
) -> i32 {
95+
ffi_result(|| {
96+
if handle.is_null() || max_rows == 0 || out_data.is_null() || out_length.is_null() {
97+
set_last_error("handle, max_rows, out_data, and out_length are required");
98+
return Err(ERROR_INVALID_ARGUMENT);
99+
}
100+
101+
let handle = unsafe { &mut *handle };
102+
handle.frame.clear();
103+
write_u32(&mut handle.frame, 0);
104+
105+
let mut row_count = 0_u32;
106+
while row_count < max_rows {
107+
let Some(row) = handle.rows.next() else {
108+
break;
109+
};
110+
let row = row.map_err(|error| {
111+
set_last_error(error.to_string());
112+
ERROR_QUERY
113+
})?;
114+
write_row(&mut handle.frame, &row)?;
115+
row_count += 1;
116+
}
117+
118+
if row_count == 0 {
119+
unsafe {
120+
ptr::write(out_data, ptr::null());
121+
ptr::write(out_length, 0);
122+
}
123+
return Ok(RESULT_END);
124+
}
125+
126+
handle.frame[0..4].copy_from_slice(&row_count.to_le_bytes());
127+
unsafe {
128+
ptr::write(out_data, handle.frame.as_ptr());
129+
ptr::write(out_length, handle.frame.len());
130+
}
131+
Ok(RESULT_BATCH)
132+
})
133+
}
134+
135+
/// Closes a query handle and releases its worker and temporary resources.
136+
///
137+
/// # Safety
138+
///
139+
/// `handle` must be null or a handle returned by `miniexcel_query_open` that has not been closed.
140+
#[unsafe(no_mangle)]
141+
pub unsafe extern "C" fn miniexcel_query_close(handle: *mut QueryHandle) {
142+
if !handle.is_null() {
143+
let _ = catch_unwind(AssertUnwindSafe(|| drop(unsafe { Box::from_raw(handle) })));
144+
}
145+
}
146+
147+
/// Returns the last error recorded on the current native thread.
148+
///
149+
/// # Safety
150+
///
151+
/// `out_length` may be null; otherwise it must be writable. The returned data remains valid until
152+
/// the next MiniExcel FFI error on this thread.
153+
#[unsafe(no_mangle)]
154+
pub unsafe extern "C" fn miniexcel_last_error(out_length: *mut usize) -> *const u8 {
155+
LAST_ERROR.with(|error| {
156+
let error = error.borrow();
157+
if !out_length.is_null() {
158+
unsafe { ptr::write(out_length, error.len()) };
159+
}
160+
error.as_ptr()
161+
})
162+
}
163+
164+
fn ffi_result(operation: impl FnOnce() -> Result<i32, i32>) -> i32 {
165+
match catch_unwind(AssertUnwindSafe(operation)) {
166+
Ok(Ok(result)) => result,
167+
Ok(Err(code)) => code,
168+
Err(_) => {
169+
set_last_error("Rust panic crossed the MiniExcel FFI boundary");
170+
ERROR_PANIC
171+
}
172+
}
173+
}
174+
175+
unsafe fn read_utf8<'a>(value: *const c_char) -> Result<&'a str, i32> {
176+
unsafe { CStr::from_ptr(value) }.to_str().map_err(|error| {
177+
set_last_error(error.to_string());
178+
ERROR_INVALID_ARGUMENT
179+
})
180+
}
181+
182+
fn set_last_error(message: impl AsRef<str>) {
183+
LAST_ERROR.with(|error| {
184+
let mut error = error.borrow_mut();
185+
error.clear();
186+
error.extend_from_slice(message.as_ref().as_bytes());
187+
});
188+
}
189+
190+
fn write_row(frame: &mut Vec<u8>, row: &DynamicRow) -> Result<(), i32> {
191+
write_length(frame, row.len())?;
192+
for (name, value) in row {
193+
write_string(frame, name)?;
194+
match value {
195+
CellValue::Empty => frame.push(0),
196+
CellValue::Bool(value) => {
197+
frame.push(1);
198+
frame.push(u8::from(*value));
199+
}
200+
CellValue::Int(value) => {
201+
frame.push(2);
202+
frame.extend_from_slice(&value.to_le_bytes());
203+
}
204+
CellValue::Float(value) => {
205+
frame.push(3);
206+
frame.extend_from_slice(&value.to_le_bytes());
207+
}
208+
CellValue::String(value) => {
209+
frame.push(4);
210+
write_string(frame, value)?;
211+
}
212+
CellValue::Date(value) => {
213+
frame.push(5);
214+
write_string(frame, value.format("%Y-%m-%d").to_string())?;
215+
}
216+
CellValue::Time(value) => {
217+
frame.push(6);
218+
write_string(frame, value.format("%H:%M:%S%.f").to_string())?;
219+
}
220+
CellValue::DateTime(value) => {
221+
frame.push(7);
222+
write_string(frame, value.format("%Y-%m-%dT%H:%M:%S%.f").to_string())?;
223+
}
224+
CellValue::Duration(value) => {
225+
frame.push(8);
226+
frame.extend_from_slice(&value.num_milliseconds().to_le_bytes());
227+
}
228+
CellValue::Error(value) => {
229+
frame.push(9);
230+
write_string(frame, value)?;
231+
}
232+
}
233+
}
234+
Ok(())
235+
}
236+
237+
fn write_string(frame: &mut Vec<u8>, value: impl AsRef<str>) -> Result<(), i32> {
238+
let bytes = value.as_ref().as_bytes();
239+
write_length(frame, bytes.len())?;
240+
frame.extend_from_slice(bytes);
241+
Ok(())
242+
}
243+
244+
fn write_length(frame: &mut Vec<u8>, length: usize) -> Result<(), i32> {
245+
let length = u32::try_from(length).map_err(|_| {
246+
set_last_error("FFI frame value exceeds the 4 GiB format limit");
247+
ERROR_QUERY
248+
})?;
249+
write_u32(frame, length);
250+
Ok(())
251+
}
252+
253+
fn write_u32(frame: &mut Vec<u8>, value: u32) {
254+
frame.extend_from_slice(&value.to_le_bytes());
255+
}

0 commit comments

Comments
 (0)