blob: 60b7921c68655061851e88a3a1ef3dc3df0b4ee6 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#![allow(clippy::missing_safety_doc)]
use paimon_ftindex_core::io::{ReadRequest, SeekRead, SeekWrite};
use paimon_ftindex_core::{
FullTextIndexConfig, FullTextIndexReader, FullTextIndexWriter, FullTextReadMetrics,
};
use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::io;
use std::os::raw::{c_char, c_int, c_void};
use std::panic::{self, AssertUnwindSafe};
use std::{ptr, slice};
thread_local! {
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}
fn set_error(msg: impl Into<String>) {
let msg = msg.into().replace('\0', "\\0");
LAST_ERROR.with(|e| {
*e.borrow_mut() = CString::new(msg).ok();
});
}
fn panic_message(e: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = e.downcast_ref::<String>() {
format!("native panic: {s}")
} else if let Some(s) = e.downcast_ref::<&str>() {
format!("native panic: {s}")
} else {
"native panic: unknown".to_string()
}
}
fn ffi_status<F>(f: F) -> c_int
where
F: FnOnce() -> Result<(), String>,
{
match panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => 0,
Ok(Err(e)) => {
set_error(e);
-1
}
Err(e) => {
set_error(panic_message(&e));
-1
}
}
}
fn ffi_ptr<T, F>(f: F) -> *mut T
where
F: FnOnce() -> Result<*mut T, String>,
{
match panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(value)) => value,
Ok(Err(e)) => {
set_error(e);
ptr::null_mut()
}
Err(e) => {
set_error(panic_message(&e));
ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn paimon_ftindex_last_error() -> *const c_char {
LAST_ERROR.with(|e| match &*e.borrow() {
Some(msg) => msg.as_ptr(),
None => ptr::null(),
})
}
#[repr(C)]
pub struct PaimonFtindexOutputFile {
pub ctx: *mut c_void,
pub write_fn: Option<unsafe extern "C" fn(*mut c_void, *const u8, usize) -> c_int>,
pub flush_fn: Option<unsafe extern "C" fn(*mut c_void) -> c_int>,
}
struct FfiOutputFile {
raw: PaimonFtindexOutputFile,
}
unsafe impl Send for FfiOutputFile {}
impl SeekWrite for FfiOutputFile {
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
let write_fn = self
.raw
.write_fn
.ok_or_else(|| io::Error::other("write_fn is null"))?;
let status = unsafe { write_fn(self.raw.ctx, buf.as_ptr(), buf.len()) };
if status == 0 {
Ok(())
} else {
Err(io::Error::other("write callback failed"))
}
}
fn flush(&mut self) -> io::Result<()> {
if let Some(flush_fn) = self.raw.flush_fn {
let status = unsafe { flush_fn(self.raw.ctx) };
if status != 0 {
return Err(io::Error::other("flush callback failed"));
}
}
Ok(())
}
}
#[repr(C)]
pub struct PaimonFtindexInputFile {
pub ctx: *mut c_void,
pub pread_fn: Option<unsafe extern "C" fn(*mut c_void, u64, *mut u8, usize) -> c_int>,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
pub struct PaimonFtindexReadMetrics {
pub pread_calls: u64,
pub pread_ranges: u64,
pub pread_bytes: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub cache_evictions: u64,
pub cached_blocks: u64,
}
impl From<FullTextReadMetrics> for PaimonFtindexReadMetrics {
fn from(metrics: FullTextReadMetrics) -> Self {
Self {
pread_calls: metrics.pread_calls,
pread_ranges: metrics.pread_ranges,
pread_bytes: metrics.pread_bytes,
cache_hits: metrics.cache_hits,
cache_misses: metrics.cache_misses,
cache_evictions: metrics.cache_evictions,
cached_blocks: metrics.cached_blocks,
}
}
}
struct FfiInputFile {
raw: PaimonFtindexInputFile,
}
unsafe impl Send for FfiInputFile {}
unsafe impl Sync for FfiInputFile {}
impl SeekRead for FfiInputFile {
fn pread(&self, ranges: &mut [ReadRequest<'_>]) -> io::Result<()> {
let pread_fn = self
.raw
.pread_fn
.ok_or_else(|| io::Error::other("pread_fn is null"))?;
for range in ranges {
let status = unsafe {
pread_fn(
self.raw.ctx,
range.pos,
range.buf.as_mut_ptr(),
range.buf.len(),
)
};
if status != 0 {
return Err(io::Error::other(format!(
"pread callback failed at offset {} length {}",
range.pos,
range.buf.len()
)));
}
}
Ok(())
}
}
pub struct PaimonFtindexWriterHandle {
inner: FullTextIndexWriter,
}
pub struct PaimonFtindexReaderHandle {
inner: FullTextIndexReader<FfiInputFile>,
}
struct SearchRequest<'a> {
query: *const c_char,
limit: usize,
roaring_filter: Option<&'a [u8]>,
row_ids: *mut i64,
scores: *mut f32,
capacity: usize,
result_len: *mut usize,
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_writer_open(
keys: *const *const c_char,
values: *const *const c_char,
len: usize,
) -> *mut PaimonFtindexWriterHandle {
ffi_ptr(|| {
let options = options_from_raw(keys, values, len)?;
let config = FullTextIndexConfig::from_options(&options).map_err(|e| e.to_string())?;
let writer = FullTextIndexWriter::new(config).map_err(|e| e.to_string())?;
Ok(Box::into_raw(Box::new(PaimonFtindexWriterHandle {
inner: writer,
})))
})
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_writer_add_document(
writer: *mut PaimonFtindexWriterHandle,
row_id: i64,
text: *const c_char,
) -> c_int {
ffi_status(|| {
let writer = require_mut(writer, "writer")?;
let text = cstr_to_string(text, "text")?;
writer
.inner
.add_document(row_id, text)
.map_err(|e| e.to_string())
})
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_writer_add_document_fields(
writer: *mut PaimonFtindexWriterHandle,
row_id: i64,
field_names: *const *const c_char,
texts: *const *const c_char,
len: usize,
) -> c_int {
ffi_status(|| {
let writer = require_mut(writer, "writer")?;
let fields = fields_from_raw(field_names, texts, len)?;
writer
.inner
.add_document_fields(row_id, fields)
.map_err(|e| e.to_string())
})
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_writer_write_index(
writer: *mut PaimonFtindexWriterHandle,
output: PaimonFtindexOutputFile,
) -> c_int {
ffi_status(|| {
let writer = require_mut(writer, "writer")?;
let mut output = FfiOutputFile { raw: output };
writer.inner.write(&mut output).map_err(|e| e.to_string())
})
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_writer_free(writer: *mut PaimonFtindexWriterHandle) {
if !writer.is_null() {
drop(Box::from_raw(writer));
}
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_reader_open(
input: PaimonFtindexInputFile,
) -> *mut PaimonFtindexReaderHandle {
ffi_ptr(|| {
let input = FfiInputFile { raw: input };
let reader = FullTextIndexReader::open(input).map_err(|e| e.to_string())?;
Ok(Box::into_raw(Box::new(PaimonFtindexReaderHandle {
inner: reader,
})))
})
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_reader_search(
reader: *mut PaimonFtindexReaderHandle,
query: *const c_char,
limit: usize,
row_ids: *mut i64,
scores: *mut f32,
capacity: usize,
result_len: *mut usize,
) -> c_int {
ffi_status(|| {
let reader = require_ref(reader, "reader")?;
search_impl(
reader,
SearchRequest {
query,
limit,
roaring_filter: None,
row_ids,
scores,
capacity,
result_len,
},
)
})
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_reader_search_with_roaring_filter(
reader: *mut PaimonFtindexReaderHandle,
query: *const c_char,
limit: usize,
roaring_filter: *const u8,
roaring_filter_len: usize,
row_ids: *mut i64,
scores: *mut f32,
capacity: usize,
result_len: *mut usize,
) -> c_int {
ffi_status(|| {
let reader = require_ref(reader, "reader")?;
let roaring_filter = const_slice(roaring_filter, roaring_filter_len, "roaring_filter")?;
search_impl(
reader,
SearchRequest {
query,
limit,
roaring_filter: Some(roaring_filter),
row_ids,
scores,
capacity,
result_len,
},
)
})
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_reader_prewarm(
reader: *mut PaimonFtindexReaderHandle,
) -> c_int {
ffi_status(|| {
let reader = require_ref(reader, "reader")?;
reader.inner.prewarm().map_err(|e| e.to_string())
})
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_reader_read_metrics(
reader: *mut PaimonFtindexReaderHandle,
metrics: *mut PaimonFtindexReadMetrics,
) -> c_int {
ffi_status(|| {
let reader = require_ref(reader, "reader")?;
let metrics = require_mut(metrics, "metrics")?;
*metrics = reader.inner.read_metrics().into();
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn paimon_ftindex_reader_free(reader: *mut PaimonFtindexReaderHandle) {
if !reader.is_null() {
drop(Box::from_raw(reader));
}
}
unsafe fn options_from_raw(
keys: *const *const c_char,
values: *const *const c_char,
len: usize,
) -> Result<HashMap<String, String>, String> {
if len == 0 {
return Ok(HashMap::new());
}
if keys.is_null() {
return Err("keys is null".to_string());
}
if values.is_null() {
return Err("values is null".to_string());
}
let keys = slice::from_raw_parts(keys, len);
let values = slice::from_raw_parts(values, len);
let mut options = HashMap::with_capacity(len);
for i in 0..len {
let key = cstr_to_string(keys[i], "option key")?;
let value = cstr_to_string(values[i], "option value")?;
options.insert(key, value);
}
Ok(options)
}
unsafe fn fields_from_raw(
field_names: *const *const c_char,
texts: *const *const c_char,
len: usize,
) -> Result<Vec<(String, String)>, String> {
if len == 0 {
return Err("document fields must not be empty".to_string());
}
if field_names.is_null() {
return Err("field_names is null".to_string());
}
if texts.is_null() {
return Err("texts is null".to_string());
}
let field_names = slice::from_raw_parts(field_names, len);
let texts = slice::from_raw_parts(texts, len);
let mut fields = Vec::with_capacity(len);
for i in 0..len {
let field_name = cstr_to_string(field_names[i], "field name")?;
let text = cstr_to_string(texts[i], "field text")?;
fields.push((field_name, text));
}
Ok(fields)
}
unsafe fn require_mut<'a, T>(ptr: *mut T, name: &str) -> Result<&'a mut T, String> {
ptr.as_mut().ok_or_else(|| format!("{name} is null"))
}
unsafe fn require_ref<'a, T>(ptr: *const T, name: &str) -> Result<&'a T, String> {
ptr.as_ref().ok_or_else(|| format!("{name} is null"))
}
unsafe fn const_slice<'a, T>(ptr: *const T, len: usize, name: &str) -> Result<&'a [T], String> {
if len == 0 {
Ok(&[])
} else if ptr.is_null() {
Err(format!("{name} is null"))
} else {
Ok(slice::from_raw_parts(ptr, len))
}
}
unsafe fn cstr_to_string(ptr: *const c_char, name: &str) -> Result<String, String> {
if ptr.is_null() {
return Err(format!("{name} is null"));
}
CStr::from_ptr(ptr)
.to_str()
.map(|s| s.to_string())
.map_err(|e| format!("{name} is not valid UTF-8: {e}"))
}
fn search_impl(
reader: &PaimonFtindexReaderHandle,
request: SearchRequest<'_>,
) -> Result<(), String> {
if request.result_len.is_null() {
return Err("result_len is null".to_string());
}
let query = unsafe { cstr_to_string(request.query, "query") }?;
let result = if let Some(roaring_filter) = request.roaring_filter {
reader
.inner
.search_with_roaring_filter(&query, request.limit, roaring_filter)
.map_err(|e| e.to_string())?
} else {
reader
.inner
.search(&query, request.limit)
.map_err(|e| e.to_string())?
};
unsafe {
*request.result_len = result.row_ids.len();
}
if result.row_ids.len() > request.capacity {
return Err(format!(
"result capacity {} is smaller than result length {}",
request.capacity,
result.row_ids.len()
));
}
if !result.row_ids.is_empty() {
if request.row_ids.is_null() {
return Err("row_ids is null".to_string());
}
if request.scores.is_null() {
return Err("scores is null".to_string());
}
unsafe {
ptr::copy_nonoverlapping(
result.row_ids.as_ptr(),
request.row_ids,
result.row_ids.len(),
);
ptr::copy_nonoverlapping(result.scores.as_ptr(), request.scores, result.scores.len());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use roaring::RoaringTreemap;
use std::ffi::CString;
unsafe extern "C" fn write_vec(ctx: *mut c_void, buf: *const u8, len: usize) -> c_int {
let out = &mut *(ctx as *mut Vec<u8>);
out.extend_from_slice(slice::from_raw_parts(buf, len));
0
}
unsafe extern "C" fn read_vec(ctx: *mut c_void, pos: u64, buf: *mut u8, len: usize) -> c_int {
let input = &*(ctx as *const Vec<u8>);
let start = pos as usize;
let end = start + len;
if end > input.len() {
return -1;
}
ptr::copy_nonoverlapping(input[start..end].as_ptr(), buf, len);
0
}
#[test]
fn ffi_round_trip_search() {
unsafe {
let writer = paimon_ftindex_writer_open(ptr::null(), ptr::null(), 0);
assert!(!writer.is_null());
let text = CString::new("Apache Paimon full text").unwrap();
assert_eq!(
paimon_ftindex_writer_add_document(writer, 7, text.as_ptr()),
0
);
let mut bytes = Vec::new();
let output = PaimonFtindexOutputFile {
ctx: &mut bytes as *mut Vec<u8> as *mut c_void,
write_fn: Some(write_vec),
flush_fn: None,
};
assert_eq!(paimon_ftindex_writer_write_index(writer, output), 0);
paimon_ftindex_writer_free(writer);
let input = PaimonFtindexInputFile {
ctx: &bytes as *const Vec<u8> as *mut c_void,
pread_fn: Some(read_vec),
};
let reader = paimon_ftindex_reader_open(input);
assert!(!reader.is_null());
let mut metrics = PaimonFtindexReadMetrics::default();
assert_eq!(paimon_ftindex_reader_read_metrics(reader, &mut metrics), 0);
assert!(metrics.pread_calls >= 2);
assert!(metrics.pread_bytes > 16);
assert_eq!(paimon_ftindex_reader_prewarm(reader), 0);
let mut after_prewarm = PaimonFtindexReadMetrics::default();
assert_eq!(
paimon_ftindex_reader_read_metrics(reader, &mut after_prewarm),
0
);
assert!(after_prewarm.pread_calls > metrics.pread_calls);
let query = CString::new(r#"{"match":{"query":"paimon","column":"text"}}"#).unwrap();
let mut row_ids = [0i64; 4];
let mut scores = [0f32; 4];
let mut result_len = 0usize;
assert_eq!(
paimon_ftindex_reader_search(
reader,
query.as_ptr(),
4,
row_ids.as_mut_ptr(),
scores.as_mut_ptr(),
row_ids.len(),
&mut result_len,
),
0
);
assert_eq!(result_len, 1);
assert_eq!(row_ids[0], 7);
assert!(scores[0] > 0.0);
let mut after_search = PaimonFtindexReadMetrics::default();
assert_eq!(
paimon_ftindex_reader_read_metrics(reader, &mut after_search),
0
);
assert!(after_search.pread_calls >= after_prewarm.pread_calls);
assert!(after_search.cache_misses >= metrics.cache_misses);
paimon_ftindex_reader_free(reader);
}
}
#[test]
fn ffi_round_trip_search_with_roaring_filter() {
unsafe {
let writer = paimon_ftindex_writer_open(ptr::null(), ptr::null(), 0);
assert!(!writer.is_null());
let text = CString::new("Apache Paimon full text").unwrap();
assert_eq!(
paimon_ftindex_writer_add_document(writer, 7, text.as_ptr()),
0
);
let text = CString::new("Paimon filtered row").unwrap();
assert_eq!(
paimon_ftindex_writer_add_document(writer, 9, text.as_ptr()),
0
);
let mut bytes = Vec::new();
let output = PaimonFtindexOutputFile {
ctx: &mut bytes as *mut Vec<u8> as *mut c_void,
write_fn: Some(write_vec),
flush_fn: None,
};
assert_eq!(paimon_ftindex_writer_write_index(writer, output), 0);
paimon_ftindex_writer_free(writer);
let input = PaimonFtindexInputFile {
ctx: &bytes as *const Vec<u8> as *mut c_void,
pread_fn: Some(read_vec),
};
let reader = paimon_ftindex_reader_open(input);
assert!(!reader.is_null());
let query = CString::new(r#"{"match":{"query":"paimon","column":"text"}}"#).unwrap();
let mut allowed = RoaringTreemap::new();
allowed.insert(9);
let mut filter_bytes = Vec::new();
allowed.serialize_into(&mut filter_bytes).unwrap();
let mut row_ids = [0i64; 4];
let mut scores = [0f32; 4];
let mut result_len = 0usize;
assert_eq!(
paimon_ftindex_reader_search_with_roaring_filter(
reader,
query.as_ptr(),
4,
filter_bytes.as_ptr(),
filter_bytes.len(),
row_ids.as_mut_ptr(),
scores.as_mut_ptr(),
row_ids.len(),
&mut result_len,
),
0
);
assert_eq!(result_len, 1);
assert_eq!(row_ids[0], 9);
assert!(scores[0] > 0.0);
paimon_ftindex_reader_free(reader);
}
}
#[test]
fn ffi_add_document_fields_searches_multi_field_index() {
unsafe {
let key = CString::new("text-fields").unwrap();
let value = CString::new("title,body").unwrap();
let keys = [key.as_ptr()];
let values = [value.as_ptr()];
let writer = paimon_ftindex_writer_open(keys.as_ptr(), values.as_ptr(), keys.len());
assert!(!writer.is_null());
let title = CString::new("title").unwrap();
let body = CString::new("body").unwrap();
let title_text = CString::new("Apache Paimon").unwrap();
let body_text = CString::new("lake storage").unwrap();
let field_names = [title.as_ptr(), body.as_ptr()];
let texts = [title_text.as_ptr(), body_text.as_ptr()];
assert_eq!(
paimon_ftindex_writer_add_document_fields(
writer,
17,
field_names.as_ptr(),
texts.as_ptr(),
field_names.len(),
),
0
);
let mut bytes = Vec::new();
let output = PaimonFtindexOutputFile {
ctx: &mut bytes as *mut Vec<u8> as *mut c_void,
write_fn: Some(write_vec),
flush_fn: None,
};
assert_eq!(paimon_ftindex_writer_write_index(writer, output), 0);
paimon_ftindex_writer_free(writer);
let input = PaimonFtindexInputFile {
ctx: &bytes as *const Vec<u8> as *mut c_void,
pread_fn: Some(read_vec),
};
let reader = paimon_ftindex_reader_open(input);
assert!(!reader.is_null());
let query =
CString::new(r#"{"multi_match":{"query":"paimon","columns":["title","body"]}}"#)
.unwrap();
let mut row_ids = [0i64; 4];
let mut scores = [0f32; 4];
let mut result_len = 0usize;
assert_eq!(
paimon_ftindex_reader_search(
reader,
query.as_ptr(),
4,
row_ids.as_mut_ptr(),
scores.as_mut_ptr(),
row_ids.len(),
&mut result_len,
),
0
);
assert_eq!(result_len, 1);
assert_eq!(row_ids[0], 17);
paimon_ftindex_reader_free(reader);
}
}
}