use std::ffi::c_int;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Sender, channel};
use std::thread;
use idakit_sys as sys;
use crate::Database;
use crate::claim;
use crate::error::{CallError, InitError, Result, panic_payload_str};
static KERNEL_LIVE: AtomicBool = AtomicBool::new(false);
static KERNEL_INITED: AtomicBool = AtomicBool::new(false);
pub(crate) struct KernelClaim;
impl KernelClaim {
fn acquire() -> Result<Self, InitError> {
if KERNEL_LIVE.swap(true, Ordering::AcqRel) {
Err(InitError::AlreadyRunning)
} else {
Ok(Self)
}
}
}
impl Drop for KernelClaim {
fn drop(&mut self) {
KERNEL_LIVE.store(false, Ordering::Release);
}
}
type Job = Box<dyn FnOnce(&mut Database) + Send>;
#[derive(Clone)]
pub struct Ida {
tx: Sender<Job>,
}
const KERNEL_STACK_DEFAULT: usize = 8 << 20;
impl Ida {
#[expect(
clippy::new_ret_no_self,
reason = "deliberate builder entry (Command::new style), not a constructor returning Self"
)]
pub fn new() -> IdaConfigBuilder {
IdaConfig::builder()
}
#[doc(alias("init_library"))]
pub fn here() -> Result<Database, InitError> {
IdaConfig::builder().here()
}
#[doc(alias("init_library"))]
pub fn run<R, F>(app: F) -> Result<R, InitError>
where
F: FnOnce(Self) -> R,
{
IdaConfig::builder().run(app)
}
pub fn run_with_stack<R, F>(stack_size: usize, app: F) -> Result<R, InitError>
where
F: FnOnce(Self) -> R,
{
IdaConfig::builder().stack_size(stack_size).run(app)
}
pub fn call<R, F>(&self, f: F) -> Result<R, CallError>
where
F: FnOnce(&mut Database) -> R + Send + 'static,
R: Send + 'static,
{
let (rtx, rrx) = channel::<thread::Result<R>>();
if self
.tx
.send(Box::new(move |idb| {
let _ = rtx.send(catch_unwind(AssertUnwindSafe(|| f(idb))));
}))
.is_err()
{
return Err(CallError::Disconnected);
}
match rrx.recv() {
Ok(Ok(value)) => Ok(value),
Ok(Err(payload)) => Err(CallError::Panicked(payload)),
Err(_) => Err(CallError::Disconnected),
}
}
}
#[derive(bon::Builder)]
pub struct IdaConfig {
#[builder(default = KERNEL_STACK_DEFAULT)]
stack_size: usize,
#[builder(default = true)]
batch: bool,
}
impl IdaConfig {
pub fn run<R, F>(self, app: F) -> Result<R, InitError>
where
F: FnOnce(Ida) -> R,
{
let _claim = KernelClaim::acquire()?;
let (tx, rx) = channel::<Job>();
let (setup_tx, setup_rx) = channel::<Result<(), InitError>>();
let stack_size = self.stack_size;
let kernel = thread::Builder::new()
.name("idakit-kernel".into())
.stack_size(stack_size)
.spawn(move || {
let setup = bring_up_kernel(&self);
let ok = setup.is_ok();
let _ = setup_tx.send(setup);
if !ok {
return;
}
let mut idb = Database::new();
while let Ok(job) = rx.recv() {
let _ = catch_unwind(AssertUnwindSafe(|| job(&mut idb)));
}
})
.expect("spawn kernel thread");
match setup_rx.recv() {
Ok(Ok(())) => {}
Ok(Err(e)) => {
let _ = kernel.join();
return Err(e);
}
Err(_) => {
let _ = kernel.join();
return Err(InitError::KernelGone);
}
}
let result = app(Ida { tx });
if let Err(payload) = kernel.join() {
let reason = panic_payload_str(&*payload).unwrap_or("<non-string panic payload>");
tracing::error!("idakit: kernel thread panicked after init: {reason}");
}
Ok(result)
}
pub fn here(self) -> Result<Database, InitError> {
let claim = KernelClaim::acquire()?;
bring_up_kernel(&self)?;
Ok(Database::owned(claim))
}
}
use ida_config_builder::{IsComplete, State};
impl<S: State> IdaConfigBuilder<S> {
pub fn run<R, F>(self, app: F) -> Result<R, InitError>
where
S: IsComplete,
F: FnOnce(Ida) -> R,
{
self.build().run(app)
}
pub fn here(self) -> Result<Database, InitError>
where
S: IsComplete,
{
self.build().here()
}
}
fn bring_up_kernel(cfg: &IdaConfig) -> Result<(), InitError> {
claim::steal_main().map_err(|reason| InitError::Claim { reason })?;
if !KERNEL_INITED.swap(true, Ordering::AcqRel) {
let rc = unsafe { sys::init_headless() };
if rc != 0 {
KERNEL_INITED.store(false, Ordering::Release); return Err(InitError::InitLibrary { code: rc });
}
}
unsafe { sys::set_batch(cfg.batch as c_int) };
Ok(())
}