master
Colby Knox Restore 25 files to their state before PR #61: the wiki's pr revert left them truncated to the diff's context 4f3a0f4 8d ago
//! A SPICE simulation through the ngspice shared library KiCad installs (`<kicad>/bin/ngspice.dll`), the
//! engine behind KiCad's own simulator (wiki #89). ngspice keeps global state and can bring its host down
//! on a bad circuit, so the bridge runs this in a child process of itself (`kicad-bridge --ngspice-worker`)
//! and only ever once per process. Windows only for now: the library names its API in sharedspice.h.

use serde_json::{json, Value};

/// Run `lines` (a whole circuit, analysis lines and `.end` included) in the ngspice library at `dll`, with
/// `cwd` as the working folder (so `.include` paths resolve), and return every vector of the last plot:
/// `{plot, vectors: [{name, type, real: [..]} | {name, type, re: [..], im: [..]}], log: [..]}`.
/// `codemodels` are the XSPICE code model libraries (`<kicad>/lib/ngspice/*.cm`) loaded first, as KiCad's own
/// simulator does: KiCad ships no spinit to load them.
/// `commands` run after the circuit is loaded and before `run` (a sweep's `alter R4 = 300`); a command ngspice
/// reports an error for fails the run.
pub fn run(dll: &std::path::Path, lines: &[String], cwd: &std::path::Path, codemodels: &[std::path::PathBuf], commands: &[String]) -> Result<Value, String> {
    imp::run(dll, lines, cwd, codemodels, commands)
}

#[cfg(windows)]
mod imp {
    use super::*;
    use std::ffi::{c_char, c_int, c_void, CStr, CString};
    use std::sync::Mutex;
    use windows::core::PCWSTR;
    use windows::Win32::Foundation::HMODULE;
    use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryExW, LOAD_WITH_ALTERED_SEARCH_PATH};

    #[repr(C)]
    struct Complex {
        re: f64,
        im: f64,
    }

    #[repr(C)]
    struct VectorInfo {
        name: *const c_char,
        v_type: c_int,
        flags: i16,
        real: *const f64,
        comp: *const Complex,
        length: c_int,
    }

    type SendChar = extern "C" fn(*const c_char, c_int, *mut c_void) -> c_int;
    type SendStat = extern "C" fn(*const c_char, c_int, *mut c_void) -> c_int;
    type Exit = extern "C" fn(c_int, bool, bool, c_int, *mut c_void) -> c_int;
    type Init = unsafe extern "C" fn(Option<SendChar>, Option<SendStat>, Option<Exit>, *mut c_void, *mut c_void, *mut c_void, *mut c_void) -> c_int;
    type Circ = unsafe extern "C" fn(*mut *mut c_char) -> c_int;
    type Command = unsafe extern "C" fn(*const c_char) -> c_int;
    type CurPlot = unsafe extern "C" fn() -> *const c_char;
    type AllVecs = unsafe extern "C" fn(*const c_char) -> *mut *mut c_char;
    type VecInfo = unsafe extern "C" fn(*const c_char) -> *const VectorInfo;

    static LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());

    extern "C" fn on_print(s: *const c_char, _id: c_int, _u: *mut c_void) -> c_int {
        if !s.is_null() {
            let t = unsafe { CStr::from_ptr(s) }.to_string_lossy().into_owned();
            if let Ok(mut l) = LOG.lock() {
                if l.len() < 400 {
                    l.push(t);
                }
            }
        }
        0
    }
    extern "C" fn on_stat(_s: *const c_char, _id: c_int, _u: *mut c_void) -> c_int {
        0
    }
    extern "C" fn on_exit(_status: c_int, _unload: bool, _quit: bool, _id: c_int, _u: *mut c_void) -> c_int {
        0
    }

    fn sym<T: Copy>(lib: HMODULE, name: &str) -> Result<T, String> {
        let c = CString::new(name).unwrap();
        let p = unsafe { GetProcAddress(lib, windows::core::PCSTR(c.as_ptr() as *const u8)) }.ok_or(format!("ngspice has no {name}"))?;
        Ok(unsafe { std::mem::transmute_copy(&p) })
    }

    pub fn run(dll: &std::path::Path, lines: &[String], cwd: &std::path::Path, codemodels: &[std::path::PathBuf], commands: &[String]) -> Result<Value, String> {
        std::env::set_current_dir(cwd).map_err(|e| format!("cannot work in {}: {e}", cwd.display()))?;
        let wide: Vec<u16> = dll.as_os_str().to_string_lossy().encode_utf16().chain(std::iter::once(0)).collect();
        // the altered search path finds the library's own dependencies beside it, in KiCad's bin folder
        let lib = unsafe { LoadLibraryExW(PCWSTR(wide.as_ptr()), None, LOAD_WITH_ALTERED_SEARCH_PATH) }.map_err(|e| format!("cannot load {}: {e}", dll.display()))?;
        let init: Init = sym(lib, "ngSpice_Init")?;
        let circ: Circ = sym(lib, "ngSpice_Circ")?;
        let command: Command = sym(lib, "ngSpice_Command")?;
        let cur_plot: CurPlot = sym(lib, "ngSpice_CurPlot")?;
        let all_vecs: AllVecs = sym(lib, "ngSpice_AllVecs")?;
        let vec_info: VecInfo = sym(lib, "ngGet_Vec_Info")?;
        let null = std::ptr::null_mut();
        unsafe { init(Some(on_print), Some(on_stat), Some(on_exit), null, null, null, null) };
        for cm in codemodels {
            let c = CString::new(format!("codemodel {}", cm.to_string_lossy().replace('\\', "/"))).unwrap();
            unsafe { command(c.as_ptr()) };
        }
        let owned: Vec<CString> = lines.iter().map(|l| CString::new(l.replace('\0', "")).unwrap()).collect();
        let mut ptrs: Vec<*mut c_char> = owned.iter().map(|c| c.as_ptr() as *mut c_char).collect();
        ptrs.push(std::ptr::null_mut());
        if unsafe { circ(ptrs.as_mut_ptr()) } != 0 {
            return Err(format!("ngspice did not take the circuit: {}", errors()));
        }
        for c in commands {
            let seen = LOG.lock().map(|l| l.len()).unwrap_or(0);
            let cs = CString::new(c.replace('\0', "")).unwrap();
            let rc = unsafe { command(cs.as_ptr()) };
            let new: Vec<String> = LOG.lock().map(|l| l.iter().skip(seen).cloned().collect()).unwrap_or_default();
            if let Some(e) = new.iter().find(|l| { let t = l.to_lowercase(); t.contains("error") || t.contains("no such") }) {
                return Err(format!("ngspice refused \"{c}\": {e}"));
            }
            if rc != 0 {
                return Err(format!("ngspice refused \"{c}\": {}", errors()));
            }
        }
        let run = CString::new("run").unwrap();
        if unsafe { command(run.as_ptr()) } != 0 {
            return Err(format!("the analysis did not run: {}", errors()));
        }
        let plot_ptr = unsafe { cur_plot() };
        let plot = if plot_ptr.is_null() { String::new() } else { unsafe { CStr::from_ptr(plot_ptr) }.to_string_lossy().into_owned() };
        // an aborted analysis leaves the constants plot current: that is a failure, not a result
        let aborted = LOG.lock().map(|l| l.iter().any(|s| { let t = s.to_lowercase(); t.contains("aborted") || t.contains("fatal error") })).unwrap_or(false);
        if aborted || plot.is_empty() || plot == "const" {
            return Err(format!("the analysis did not run: {}", errors()));
        }
        // every vector of one plot by name; a noise analysis leaves two plots (noiseN: the spectra over frequency,
        // noiseN+1: the integrated totals), so the other one is read too
        let read_plot = |name: &str| -> Vec<Value> {
            let Ok(cname) = CString::new(name) else { return Vec::new() };
            let mut names = Vec::new();
            let list = unsafe { all_vecs(cname.as_ptr()) };
            if !list.is_null() {
                let mut i = 0;
                loop {
                    let p = unsafe { *list.add(i) };
                    if p.is_null() {
                        break;
                    }
                    names.push(unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned());
                    i += 1;
                }
            }
            let mut vectors = Vec::new();
            for n in &names {
                let full = CString::new(format!("{name}.{n}")).unwrap();
                let info = unsafe { vec_info(full.as_ptr()) };
                if info.is_null() {
                    continue;
                }
                let v = unsafe { &*info };
                let len = v.length.max(0) as usize;
                if !v.real.is_null() {
                    let data = unsafe { std::slice::from_raw_parts(v.real, len) };
                    vectors.push(json!({"name": n, "type": v.v_type, "real": data}));
                } else if !v.comp.is_null() {
                    let data = unsafe { std::slice::from_raw_parts(v.comp, len) };
                    vectors.push(json!({"name": n, "type": v.v_type, "re": data.iter().map(|c| c.re).collect::<Vec<_>>(), "im": data.iter().map(|c| c.im).collect::<Vec<_>>()}));
                }
            }
            vectors
        };
        let mut vectors = read_plot(&plot);
        let mut plot = plot;
        if let Some(n) = plot.strip_prefix("noise").and_then(|d| d.parse::<u32>().ok()) {
            let (spectra, totals) = if vectors.iter().any(|v| v["name"] == "frequency") { (n, n + 1) } else { (n.saturating_sub(1), n) };
            let other = if spectra == n { totals } else { spectra };
            let names: Vec<Value> = vectors.iter().map(|v| v["name"].clone()).collect();
            for v in read_plot(&format!("noise{other}")) {
                if !names.contains(&v["name"]) {
                    vectors.push(v);
                }
            }
            // the spectra plot leads: its frequency is the sweep
            plot = format!("noise{spectra}");
        }
        if vectors.is_empty() {
            return Err(format!("the analysis gave no vectors: {}", errors()));
        }
        Ok(json!({"plot": plot, "vectors": vectors, "log": LOG.lock().map(|l| l.clone()).unwrap_or_default()}))
    }

    fn errors() -> String {
        let l = LOG.lock().map(|l| l.clone()).unwrap_or_default();
        let e: Vec<&String> = l.iter().filter(|s| { let t = s.to_lowercase(); (t.contains("error") || t.contains("aborted")) && !t.contains("spinit") }).take(8).collect();
        if e.is_empty() { l.iter().rev().take(6).cloned().collect::<Vec<_>>().join(" | ") } else { e.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" | ") }
    }
}

#[cfg(not(windows))]
mod imp {
    use super::*;
    pub fn run(_dll: &std::path::Path, _lines: &[String], _cwd: &std::path::Path, _codemodels: &[std::path::PathBuf], _commands: &[String]) -> Result<Value, String> {
        Err("simulation runs through KiCad's ngspice.dll, on Windows for now".into())
    }
}