|
| 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