新增 8 个 _separate 函数: - push_reading_event_separate - update_reading_position_separate - export_pending_events_separate - create_note_anchor_separate - search_markdown_blocks_separate - search_text_content_separate 新增 lift_from_raw! 宏用于从原始 buffer 参数反序列化复杂类型。 原 #[uniffi::export] 函数保留(macOS/Android 路径可用)。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
448 lines
16 KiB
Rust
448 lines
16 KiB
Rust
// FFI functions are called from generated UniFFI bindings (C-ABI),
|
|
// so Rust's dead_code analysis doesn't see the calls.
|
|
#![allow(dead_code)]
|
|
|
|
uniffi::setup_scaffolding!();
|
|
|
|
pub use zx_document_core::material_type::{MaterialType, PreviewMode};
|
|
pub use zx_document_core::image_meta::ImageMeta;
|
|
pub use zx_document_core::text::TextStats;
|
|
pub use zx_document_core::search::SearchResult;
|
|
pub use zx_document_core::anchors::NoteAnchor;
|
|
pub use zx_document_core::progress::ReadingPosition;
|
|
pub use zx_document_core::events::ReadingEvent;
|
|
|
|
use zx_document_core::blocks as core_blocks;
|
|
|
|
// FFI-compatible DocumentBlock (tuple variants, UniFFI proc-macro)
|
|
#[derive(Debug, uniffi::Enum)]
|
|
pub enum DocumentBlock {
|
|
Heading(String, u8, String),
|
|
Paragraph(String, String),
|
|
List(String, bool, Vec<String>),
|
|
CodeBlock(String, Option<String>, String),
|
|
Quote(String, String),
|
|
Table(String, Vec<String>, Vec<Vec<String>>),
|
|
ImageBlock(String, String, Option<String>),
|
|
HorizontalRule(String),
|
|
}
|
|
|
|
impl From<core_blocks::DocumentBlock> for DocumentBlock {
|
|
fn from(b: core_blocks::DocumentBlock) -> Self {
|
|
match b {
|
|
core_blocks::DocumentBlock::Heading { id, level, text } => {
|
|
DocumentBlock::Heading(id, level, text)
|
|
}
|
|
core_blocks::DocumentBlock::Paragraph { id, text } => {
|
|
DocumentBlock::Paragraph(id, text)
|
|
}
|
|
core_blocks::DocumentBlock::List { id, ordered, items } => {
|
|
DocumentBlock::List(id, ordered, items)
|
|
}
|
|
core_blocks::DocumentBlock::CodeBlock { id, language, code } => {
|
|
DocumentBlock::CodeBlock(id, language, code)
|
|
}
|
|
core_blocks::DocumentBlock::Quote { id, text } => {
|
|
DocumentBlock::Quote(id, text)
|
|
}
|
|
core_blocks::DocumentBlock::Table { id, headers, rows } => {
|
|
DocumentBlock::Table(id, headers, rows)
|
|
}
|
|
core_blocks::DocumentBlock::Image { id, src, alt } => {
|
|
DocumentBlock::ImageBlock(id, src, alt)
|
|
}
|
|
core_blocks::DocumentBlock::HorizontalRule { id } => {
|
|
DocumentBlock::HorizontalRule(id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, uniffi::Error)]
|
|
pub enum DocumentError {
|
|
FileNotFound,
|
|
UnsupportedFormat,
|
|
ParseError,
|
|
InvalidEncoding,
|
|
IoError,
|
|
}
|
|
|
|
impl std::fmt::Display for DocumentError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::FileNotFound => write!(f, "File not found"),
|
|
Self::UnsupportedFormat => write!(f, "Unsupported format"),
|
|
Self::ParseError => write!(f, "Parse error"),
|
|
Self::InvalidEncoding => write!(f, "Invalid encoding"),
|
|
Self::IoError => write!(f, "IO error"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for DocumentError {}
|
|
|
|
impl From<zx_document_core::error::DocumentError> for DocumentError {
|
|
fn from(e: zx_document_core::error::DocumentError) -> Self {
|
|
match e {
|
|
zx_document_core::error::DocumentError::FileNotFound(_) => Self::FileNotFound,
|
|
zx_document_core::error::DocumentError::UnsupportedFormat(_) => Self::UnsupportedFormat,
|
|
zx_document_core::error::DocumentError::ParseError(_) => Self::ParseError,
|
|
zx_document_core::error::DocumentError::InvalidEncoding => Self::InvalidEncoding,
|
|
zx_document_core::error::DocumentError::IoError(_) => Self::IoError,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn detect_material_type(file_path: String) -> Result<MaterialType, DocumentError> {
|
|
zx_document_core::material_type::detect_material_type(&file_path).map_err(Into::into)
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn read_image_meta(file_path: String) -> Result<ImageMeta, DocumentError> {
|
|
zx_document_core::image_meta::read_image_meta(&file_path).map_err(Into::into)
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn read_text_stats(file_path: String) -> Result<TextStats, DocumentError> {
|
|
let content = std::fs::read_to_string(&file_path).map_err(|_| DocumentError::FileNotFound)?;
|
|
Ok(zx_document_core::text::text_stats(&content))
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn parse_text(content: String) -> Result<Vec<DocumentBlock>, DocumentError> {
|
|
let blocks = zx_document_core::text::parse_text_content(&content);
|
|
let result: Vec<DocumentBlock> = blocks.into_iter().map(Into::into).collect();
|
|
Ok(result)
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn parse_markdown(content: String) -> Result<Vec<DocumentBlock>, DocumentError> {
|
|
let blocks = zx_document_core::markdown::parse_markdown(&content).map_err(|e| match e {
|
|
zx_document_core::error::DocumentError::ParseError(_) => DocumentError::ParseError,
|
|
_ => DocumentError::ParseError,
|
|
})?;
|
|
let result: Vec<DocumentBlock> = blocks.into_iter().map(Into::into).collect();
|
|
Ok(result)
|
|
}
|
|
|
|
/// Out-pointer free: avoids struct-passing ABI issues
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_rustbuffer_free_separate(
|
|
capacity: u64,
|
|
len: u64,
|
|
data: *mut u8,
|
|
) {
|
|
if data.is_null() { return; }
|
|
unsafe {
|
|
let _v = Vec::from_raw_parts(data, len as usize, capacity as usize);
|
|
// _v drops here, freeing the memory
|
|
}
|
|
}
|
|
|
|
/// Workaround: receive raw bytes via separate len/data args, return via out-pointers
|
|
/// Avoids all struct-passing ABI issues on ARM64 iOS.
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_rustbuffer_from_bytes_separate(
|
|
len: i32,
|
|
data: *const u8,
|
|
out_capacity: *mut u64,
|
|
out_len: *mut u64,
|
|
out_data: *mut *mut u8,
|
|
) {
|
|
let mut call_status = uniffi::RustCallStatus::default();
|
|
let buf = unsafe {
|
|
uniffi::ffi::uniffi_rustbuffer_from_bytes(
|
|
uniffi::ForeignBytes::from_raw_parts(data, len),
|
|
&mut call_status,
|
|
)
|
|
};
|
|
unsafe {
|
|
*out_capacity = buf.capacity() as u64;
|
|
*out_len = buf.len() as u64;
|
|
*out_data = buf.data_pointer() as *mut u8;
|
|
}
|
|
// Transfer ownership to caller — don't drop the buffer
|
|
std::mem::forget(buf);
|
|
}
|
|
|
|
/// Full parse_markdown via raw bytes, result via out-pointers — avoids all struct-passing ABI issues
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_parse_markdown_separate(
|
|
len: i32,
|
|
data: *const u8,
|
|
out_capacity: *mut u64,
|
|
out_len: *mut u64,
|
|
out_data: *mut *mut u8,
|
|
out_error_code: *mut i8,
|
|
) {
|
|
let slice = unsafe { std::slice::from_raw_parts(data, len as usize) };
|
|
let content = match std::str::from_utf8(slice) {
|
|
Ok(s) => s.to_string(),
|
|
Err(_) => {
|
|
unsafe { *out_error_code = -1; }
|
|
return;
|
|
}
|
|
};
|
|
|
|
let result = crate::parse_markdown(content);
|
|
|
|
// Serialize result using UniFFI
|
|
use uniffi::LowerReturn;
|
|
let lowered = <Result<Vec<DocumentBlock>, DocumentError> as LowerReturn<UniFfiTag>>::lower_return(result);
|
|
match lowered {
|
|
Ok(buf) => {
|
|
unsafe {
|
|
*out_capacity = buf.capacity() as u64;
|
|
*out_len = buf.len() as u64;
|
|
*out_data = buf.data_pointer() as *mut u8;
|
|
*out_error_code = 0;
|
|
}
|
|
std::mem::forget(buf);
|
|
}
|
|
Err(e) => {
|
|
unsafe { *out_error_code = -1; }
|
|
}
|
|
}
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn search_markdown_blocks(blocks: Vec<DocumentBlock>, query: String) -> Vec<SearchResult> {
|
|
let core_blocks: Vec<core_blocks::DocumentBlock> = blocks.into_iter().map(core_block_from_ffi).collect();
|
|
zx_document_core::search::search_blocks(&core_blocks, &query)
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn search_text_content(content: String, query: String) -> Vec<SearchResult> {
|
|
zx_document_core::search::search_text(&content, &query)
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn create_note_anchor(material_id: String, position: Option<ReadingPosition>) -> NoteAnchor {
|
|
zx_document_core::anchors::NoteAnchor::from_position(&material_id, position.as_ref())
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn push_reading_event(event: ReadingEvent) {
|
|
zx_document_core::events::push_reading_event(event)
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn update_reading_position(material_id: String, position: ReadingPosition) {
|
|
zx_document_core::events::update_reading_position(&material_id, position)
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn export_pending_events() -> Vec<ReadingEvent> {
|
|
zx_document_core::events::export_pending_events()
|
|
}
|
|
|
|
#[uniffi::export]
|
|
fn clear_exported_events(count: u32) {
|
|
zx_document_core::events::clear_exported_events(count as usize)
|
|
}
|
|
|
|
/// Helper: serialize a Result<T, DocumentError> into out-pointers.
|
|
/// Generic over T; the RustBuffer methods are accessed via the concrete type
|
|
/// after the `lower_return` call.
|
|
macro_rules! write_result_to_out {
|
|
($result:expr, $out_capacity:ident, $out_len:ident, $out_data:ident, $out_error_code:ident) => {{
|
|
use uniffi::LowerReturn;
|
|
match <Result<_, DocumentError> as LowerReturn<UniFfiTag>>::lower_return($result) {
|
|
Ok(buf) => {
|
|
unsafe {
|
|
*$out_capacity = buf.capacity() as u64;
|
|
*$out_len = buf.len() as u64;
|
|
*$out_data = buf.data_pointer() as *mut u8;
|
|
*$out_error_code = 0;
|
|
}
|
|
std::mem::forget(buf);
|
|
}
|
|
Err(_) => {
|
|
unsafe { *$out_error_code = -1; }
|
|
}
|
|
}
|
|
}};
|
|
}
|
|
|
|
/// Helper: read a UTF-8 string from raw bytes, or set error and return false.
|
|
unsafe fn read_str_input(len: i32, data: *const u8, out_error_code: *mut i8) -> Option<String> {
|
|
let slice = std::slice::from_raw_parts(data, len as usize);
|
|
match std::str::from_utf8(slice) {
|
|
Ok(s) => Some(s.to_string()),
|
|
Err(_) => { *out_error_code = -1; None },
|
|
}
|
|
}
|
|
|
|
// ─── Batch 1 out-pointer functions: String input → Result<T, E> output ───
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_detect_material_type_separate(
|
|
len: i32, data: *const u8,
|
|
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
|
) {
|
|
let file_path = match unsafe { read_str_input(len, data, out_error_code) } {
|
|
Some(s) => s, None => return,
|
|
};
|
|
let result = crate::detect_material_type(file_path);
|
|
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_read_image_meta_separate(
|
|
len: i32, data: *const u8,
|
|
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
|
) {
|
|
let file_path = match unsafe { read_str_input(len, data, out_error_code) } {
|
|
Some(s) => s, None => return,
|
|
};
|
|
let result = crate::read_image_meta(file_path);
|
|
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_read_text_stats_separate(
|
|
len: i32, data: *const u8,
|
|
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
|
) {
|
|
let file_path = match unsafe { read_str_input(len, data, out_error_code) } {
|
|
Some(s) => s, None => return,
|
|
};
|
|
let result = crate::read_text_stats(file_path);
|
|
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_parse_text_separate(
|
|
len: i32, data: *const u8,
|
|
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
|
) {
|
|
let content = match unsafe { read_str_input(len, data, out_error_code) } {
|
|
Some(s) => s, None => return,
|
|
};
|
|
let result = crate::parse_text(content);
|
|
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
|
}
|
|
|
|
// ─── Helper: lift from raw buffer fields ───
|
|
|
|
macro_rules! lift_from_raw {
|
|
($T:ty, $capacity:expr, $len:expr, $data:expr) => {{
|
|
let v = unsafe { Vec::from_raw_parts($data as *mut u8, $len as usize, $capacity as usize) };
|
|
let buf = uniffi::RustBuffer::from_vec(v);
|
|
<$T as uniffi::Lift<UniFfiTag>>::try_lift(buf).expect(concat!("failed to lift ", stringify!($T)))
|
|
}};
|
|
}
|
|
|
|
// ─── Batch 2: complex type input/output ───
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_push_reading_event_separate(
|
|
event_cap: u64, event_len: u64, event_data: *const u8,
|
|
) {
|
|
let event: ReadingEvent = lift_from_raw!(ReadingEvent, event_cap, event_len, event_data);
|
|
crate::push_reading_event(event);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_update_reading_position_separate(
|
|
mid_len: i32, mid_data: *const u8,
|
|
pos_cap: u64, pos_len: u64, pos_data: *const u8,
|
|
out_error_code: *mut i8,
|
|
) {
|
|
let material_id = match unsafe { read_str_input(mid_len, mid_data, out_error_code) } {
|
|
Some(s) => s, None => return,
|
|
};
|
|
unsafe { *out_error_code = 0; }
|
|
let position: ReadingPosition = lift_from_raw!(ReadingPosition, pos_cap, pos_len, pos_data);
|
|
crate::update_reading_position(material_id, position);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_export_pending_events_separate(
|
|
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
|
) {
|
|
let result: Vec<ReadingEvent> = crate::export_pending_events();
|
|
write_result_to_out!(Ok::<_, DocumentError>(result), out_capacity, out_len, out_data, out_error_code);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_create_note_anchor_separate(
|
|
mid_len: i32, mid_data: *const u8,
|
|
pos_cap: u64, pos_len: u64, pos_data: *const u8, pos_has_value: i8,
|
|
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
|
) {
|
|
let material_id = match unsafe { read_str_input(mid_len, mid_data, out_error_code) } {
|
|
Some(s) => s, None => return,
|
|
};
|
|
let position: Option<ReadingPosition> = if pos_has_value != 0 {
|
|
Some(lift_from_raw!(ReadingPosition, pos_cap, pos_len, pos_data))
|
|
} else {
|
|
None
|
|
};
|
|
let result: Result<NoteAnchor, DocumentError> = Ok(crate::create_note_anchor(material_id, position));
|
|
write_result_to_out!(result, out_capacity, out_len, out_data, out_error_code);
|
|
}
|
|
|
|
// ─── Batch 3: search functions ───
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_search_markdown_blocks_separate(
|
|
blocks_cap: u64, blocks_len: u64, blocks_data: *const u8,
|
|
query_len: i32, query_data: *const u8,
|
|
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
|
) {
|
|
let query = match unsafe { read_str_input(query_len, query_data, out_error_code) } {
|
|
Some(s) => s, None => return,
|
|
};
|
|
let blocks: Vec<DocumentBlock> = lift_from_raw!(Vec<DocumentBlock>, blocks_cap, blocks_len, blocks_data);
|
|
let result: Vec<SearchResult> = crate::search_markdown_blocks(blocks, query);
|
|
write_result_to_out!(Ok::<_, DocumentError>(result), out_capacity, out_len, out_data, out_error_code);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ffi_zx_document_ffi_search_text_content_separate(
|
|
content_len: i32, content_data: *const u8,
|
|
query_len: i32, query_data: *const u8,
|
|
out_capacity: *mut u64, out_len: *mut u64, out_data: *mut *mut u8, out_error_code: *mut i8,
|
|
) {
|
|
let content = match unsafe { read_str_input(content_len, content_data, out_error_code) } {
|
|
Some(s) => s, None => return,
|
|
};
|
|
let query = match unsafe { read_str_input(query_len, query_data, out_error_code) } {
|
|
Some(s) => s, None => return,
|
|
};
|
|
let result: Vec<SearchResult> = crate::search_text_content(content, query);
|
|
write_result_to_out!(Ok::<_, DocumentError>(result), out_capacity, out_len, out_data, out_error_code);
|
|
}
|
|
|
|
// Reverse conversion: FFI DocumentBlock → core DocumentBlock, used by search.
|
|
fn core_block_from_ffi(block: DocumentBlock) -> core_blocks::DocumentBlock {
|
|
match block {
|
|
DocumentBlock::Heading(id, level, text) => {
|
|
core_blocks::DocumentBlock::Heading { id, level, text }
|
|
}
|
|
DocumentBlock::Paragraph(id, text) => {
|
|
core_blocks::DocumentBlock::Paragraph { id, text }
|
|
}
|
|
DocumentBlock::List(id, ordered, items) => {
|
|
core_blocks::DocumentBlock::List { id, ordered, items }
|
|
}
|
|
DocumentBlock::CodeBlock(id, language, code) => {
|
|
core_blocks::DocumentBlock::CodeBlock { id, language, code }
|
|
}
|
|
DocumentBlock::Quote(id, text) => {
|
|
core_blocks::DocumentBlock::Quote { id, text }
|
|
}
|
|
DocumentBlock::Table(id, headers, rows) => {
|
|
core_blocks::DocumentBlock::Table { id, headers, rows }
|
|
}
|
|
DocumentBlock::ImageBlock(id, src, alt) => {
|
|
core_blocks::DocumentBlock::Image { id, src, alt }
|
|
}
|
|
DocumentBlock::HorizontalRule(id) => {
|
|
core_blocks::DocumentBlock::HorizontalRule { id }
|
|
}
|
|
}
|
|
}
|