use std::cell::Cell;
use std::collections::HashMap;
use std::ffi::CStr;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use super::answer::{Native, Rows};
use super::cancel::{self, CancelToken};
use super::{Error, Query, Result, handshake, sys};
pub(super) struct Vtable {
runs: HashMap<&'static str, sys::AnalyticRunFn>,
pub(super) next: sys::RowsNextFn,
pub(super) next_batch: sys::RowsNextBatchFn,
pub(super) stats: sys::RowsStatsFn,
pub(super) close: sys::RowsCloseFn,
engine_open: sys::EngineOpenFn,
engine_close: sys::EngineCloseFn,
last_fault: Option<sys::LastFaultFn>,
cancel: Option<cancel::Vtable>,
}
enum State {
Absent,
Drifted(String),
Ready(Vtable),
}
fn resolve<F: Copy>(name: &CStr) -> Option<F> {
const { assert!(size_of::<F>() == size_of::<*mut std::ffi::c_void>()) };
let p = sys::symbol(name)?;
Some(unsafe { std::mem::transmute_copy::<*mut std::ffi::c_void, F>(&p) })
}
fn probe() -> State {
let (Some(next), Some(next_batch), Some(stats), Some(close)) = (
resolve::<sys::RowsNextFn>(c"irgx_rows_next"),
resolve::<sys::RowsNextBatchFn>(c"irgx_rows_next_batch"),
resolve::<sys::RowsStatsFn>(c"irgx_rows_stats"),
resolve::<sys::RowsCloseFn>(c"irgx_rows_close"),
) else {
return State::Absent;
};
let (Some(engine_open), Some(engine_close)) = (
resolve::<sys::EngineOpenFn>(ENGINE_OPENER),
resolve::<sys::EngineCloseFn>(c"irgx_engine_close"),
) else {
return State::Absent;
};
let runs = entries();
if runs.is_empty() {
return State::Absent;
}
if let Some(digest) = resolve::<sys::SchemaDigestFn>(c"irgx_schema_digest") {
let introspect =
resolve::<sys::SchemaCountFn>(c"irgx_schema_count")
.zip(resolve::<sys::SchemaGetFn>(c"irgx_schema_get"));
if let Some(why) = handshake::drift(digest, introspect) {
return State::Drifted(why);
}
}
State::Ready(Vtable {
runs,
next,
next_batch,
stats,
close,
engine_open,
engine_close,
last_fault: resolve::<sys::LastFaultFn>(c"irgx_last_fault"),
cancel: cancel::Vtable::resolve(sys::symbol),
})
}
pub(super) fn cancellation() -> Option<cancel::Vtable> {
match state() {
State::Ready(vt) => vt.cancel,
State::Absent | State::Drifted(_) => None,
}
}
const ENGINE_OPENER: &CStr = c"irgx_engine_open";
fn entries() -> HashMap<&'static str, sys::AnalyticRunFn> {
let mut out = HashMap::new();
for verb in crate::contract::schema::VERBS {
if out.contains_key(verb.entry) {
continue;
}
let Ok(name) = std::ffi::CString::new(verb.entry) else {
continue;
};
if !sys::shares_engine(&name, ENGINE_OPENER) {
continue;
}
if let Some(f) = resolve::<sys::AnalyticRunFn>(&name) {
out.insert(verb.entry, f);
}
}
out
}
fn state() -> &'static State {
static STATE: OnceLock<State> = OnceLock::new();
STATE.get_or_init(probe)
}
#[must_use]
pub fn available() -> bool {
matches!(state(), State::Ready(_))
}
pub(super) struct EngineHandle {
ptr: *mut sys::irgx_engine,
close: sys::EngineCloseFn,
}
unsafe impl Send for EngineHandle {}
unsafe impl Sync for EngineHandle {}
impl Drop for EngineHandle {
fn drop(&mut self) {
unsafe { (self.close)(self.ptr) };
}
}
fn engine(vt: &Vtable, roots: &[PathBuf]) -> Result<Arc<EngineHandle>> {
static CACHE: OnceLock<Mutex<HashMap<Vec<PathBuf>, Arc<EngineHandle>>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let Ok(mut map) = cache.lock() else {
return Err(Error::Failed("engine cache poisoned".to_owned()));
};
if let Some(found) = map.get(roots) {
return Ok(Arc::clone(found));
}
let owned = roots
.iter()
.map(|p| cstring(p))
.collect::<Result<Vec<_>>>()?;
let ptrs: Vec<*const std::os::raw::c_char> = owned.iter().map(|c| c.as_ptr()).collect();
let mut out: *mut sys::irgx_engine = std::ptr::null_mut();
let status = unsafe {
(vt.engine_open)(
if ptrs.is_empty() {
std::ptr::null()
} else {
ptrs.as_ptr()
},
ptrs.len(),
&raw mut out,
)
};
if status != sys::OK {
return Err(fault(vt, status, "analytic engine open"));
}
let handle = Arc::new(EngineHandle {
ptr: out,
close: vt.engine_close,
});
map.insert(roots.to_vec(), Arc::clone(&handle));
Ok(handle)
}
fn cstring(p: &Path) -> Result<std::ffi::CString> {
#[cfg(unix)]
let bytes = {
use std::os::unix::ffi::OsStrExt;
p.as_os_str().as_bytes().to_vec()
};
#[cfg(not(unix))]
let bytes = p.to_string_lossy().into_owned().into_bytes();
std::ffi::CString::new(bytes)
.map_err(|_| Error::Unrepresentable(format!("root path has interior NUL: {p:?}")))
}
pub(super) fn fault(vt: &Vtable, status: i32, what: &str) -> Error {
match vt.last_fault.and_then(incident) {
Some(d) => Error::Failed(format!("{what}: {d} (status {status})")),
None => Error::Failed(format!("{what}: native status {status}")),
}
}
fn incident(pull: sys::LastFaultFn) -> Option<String> {
let mut f = sys::Fault {
struct_size: super::struct_size::<sys::Fault>(),
status: 0,
at_space: 0,
name: std::ptr::null(),
path: std::ptr::null(),
path_len: 0,
at: 0,
};
if unsafe { pull(&raw mut f) } != sys::MATCH || f.name.is_null() {
return None;
}
let mut msg = unsafe { CStr::from_ptr(f.name) }
.to_string_lossy()
.into_owned();
if !f.path.is_null() {
let path = unsafe { super::cell::slice(f.path, f.path_len) };
msg.push_str(&format!(" at {}", String::from_utf8_lossy(path)));
if f.at_space == crate::contract::AT_FILE {
msg.push_str(&format!("+{}", f.at));
}
}
Some(msg)
}
fn producer(vt: &Vtable, op: u32) -> Option<sys::AnalyticRunFn> {
let verb = crate::contract::schema::VERBS.get(usize::try_from(op).ok()?.checked_sub(1)?)?;
vt.runs.get(verb.entry).copied()
}
pub fn run(query: &impl Query) -> Result<Option<Rows>> {
run_until(query, None)
}
pub fn run_until(query: &impl Query, token: Option<&CancelToken>) -> Result<Option<Rows>> {
let vt = match state() {
State::Absent => return Ok(None),
State::Drifted(why) => return Err(Error::SchemaDrift(why.clone())),
State::Ready(vt) => vt,
};
let Some(run) = producer(vt, query.op()) else {
return Ok(None);
};
let engine = engine(vt, query.roots())?;
let patterns = query.texts();
let views: Vec<sys::Text> = patterns
.iter()
.map(|p| sys::Text {
ptr: p.as_ptr(),
len: p.len(),
})
.collect();
let mut wire = query.wire();
wire.bind(&views);
let mut out: *mut sys::irgx_rows = std::ptr::null_mut();
let status = unsafe {
run(
engine.ptr,
query.op(),
wire.as_ptr(),
token.map_or(std::ptr::null_mut(), CancelToken::raw),
&raw mut out,
)
};
match status {
sys::OK | sys::MATCH => Ok(Some(Rows::native(Native {
ptr: out,
vt,
done: Cell::new(false),
_engine: engine,
}))),
sys::STALE => Ok(None),
other => Err(fault(vt, other, "analytic run")),
}
}
#[cfg(test)]
mod incidents {
use super::*;
struct Staged {
name: &'static CStr,
path: Option<&'static [u8]>,
at_space: i32,
at: u64,
status: i32,
}
thread_local! {
static STAGED: Cell<Option<Staged>> = const { Cell::new(None) };
}
unsafe extern "C" fn pull(out: *mut sys::Fault) -> i32 {
let Some(s) = STAGED.take() else {
return sys::OK;
};
let f = unsafe { &mut *out };
f.name = s.name.as_ptr();
f.path = s.path.map_or(std::ptr::null(), <[u8]>::as_ptr);
f.path_len = s.path.map_or(0, <[u8]>::len);
f.at_space = s.at_space;
f.at = s.at;
s.status
}
fn rendered(s: Option<Staged>) -> Option<String> {
STAGED.set(s);
incident(pull)
}
#[test]
fn a_written_fault_is_the_message() {
let msg = rendered(Some(Staged {
name: c"Corrupt",
path: Some(b"a/b.gist"),
at_space: crate::contract::AT_FILE,
at: 42,
status: sys::MATCH,
}));
assert_eq!(msg.as_deref(), Some("Corrupt at a/b.gist+42"));
}
#[test]
fn an_empty_slot_adds_nothing() {
assert_eq!(rendered(None), None);
}
#[test]
fn ok_is_an_empty_slot_not_a_fault() {
let msg = rendered(Some(Staged {
name: c"Corrupt",
path: Some(b"a/b.gist"),
at_space: crate::contract::AT_FILE,
at: 42,
status: sys::OK,
}));
assert_eq!(msg, None);
}
#[test]
fn only_a_file_offset_follows_the_path() {
for (space, want) in [
(crate::contract::AT_PATTERN, "BadPattern at (?=x)"),
(crate::contract::AT_NONE, "BadPattern at (?=x)"),
(crate::contract::AT_FILE, "BadPattern at (?=x)+3"),
] {
let msg = rendered(Some(Staged {
name: c"BadPattern",
path: Some(b"(?=x)"),
at_space: space,
at: 3,
status: sys::MATCH,
}));
assert_eq!(msg.as_deref(), Some(want), "at_space {space}");
}
}
}
#[cfg(test)]
mod routing {
use super::*;
unsafe extern "C" fn stub(
_e: *mut sys::irgx_engine,
_op: u32,
_p: *const std::ffi::c_void,
_c: *mut sys::irgx_cancel,
_o: *mut *mut sys::irgx_rows,
) -> i32 {
sys::OK
}
fn with(entries: &[&'static str]) -> Vtable {
Vtable {
runs: entries
.iter()
.map(|e| (*e, stub as sys::AnalyticRunFn))
.collect(),
next: {
unsafe extern "C" fn f(_: *mut sys::irgx_rows, _: *mut sys::Row) -> i32 {
sys::OK
}
f
},
next_batch: {
unsafe extern "C" fn f(
_: *mut sys::irgx_rows,
_: *mut sys::Row,
_: usize,
_: *mut usize,
) -> i32 {
sys::OK
}
f
},
stats: {
unsafe extern "C" fn f(_: *mut sys::irgx_rows, _: *mut sys::Stats) -> i32 {
sys::OK
}
f
},
close: {
unsafe extern "C" fn f(_: *mut sys::irgx_rows) {}
f
},
engine_open: {
unsafe extern "C" fn f(
_: *const *const std::os::raw::c_char,
_: usize,
_: *mut *mut sys::irgx_engine,
) -> i32 {
sys::OK
}
f
},
engine_close: {
unsafe extern "C" fn f(_: *mut sys::irgx_engine) {}
f
},
last_fault: None,
cancel: None,
}
}
fn op(verb: &str) -> u32 {
crate::contract::schema::VERBS
.iter()
.find(|v| v.name == verb)
.expect("verb")
.op
}
#[test]
fn a_verb_reaches_only_its_own_producer() {
let all = with(&["gist_run", "relate_run", "blast_run"]);
for v in crate::contract::schema::VERBS {
assert!(producer(&all, v.op).is_some(), "`{}` did not route", v.name);
}
let thin = with(&["gist_run"]);
assert!(producer(&thin, op("rank")).is_some());
for v in crate::contract::schema::VERBS {
if v.entry != "gist_run" {
assert!(
producer(&thin, v.op).is_none(),
"`{}` reached libgist",
v.name
);
}
}
}
#[test]
fn an_op_the_table_lacks_declines() {
let vt = with(&["gist_run", "relate_run", "blast_run"]);
let past = u32::try_from(crate::contract::schema::VERBS.len()).unwrap() + 1;
for bad in [0, past, u32::MAX] {
assert!(producer(&vt, bad).is_none(), "op {bad} routed somewhere");
}
}
}