faiss-next-sys 0.6.0

Raw FFI bindings to Faiss (Facebook AI Similarity Search)
Documentation
use std::env;
use std::path::PathBuf;

const FAISS_MIN_VERSION: (u32, u32, u32) = (1, 14, 0);
const FAISS_MAX_TESTED_VERSION: (u32, u32, u32) = (1, 14, 99);
const BINDING_VERSION: &str = "v1_14";

#[allow(dead_code)]
struct FaissPaths {
    include_dir: PathBuf,
    lib_dir: PathBuf,
    version: Option<(u32, u32, u32)>,
}

fn main() {
    #[cfg(all(feature = "cuda", target_os = "macos"))]
    compile_error!("CUDA feature is not supported on macOS. Please remove the 'cuda' feature.");

    println!("cargo:rustc-check-cfg=cfg(faiss_binding, values(\"v1_14\", \"v1_15\"))");
    println!(
        "cargo:rustc-check-cfg=cfg(faiss_version, values(\"1_14_0\", \"1_14_1\", \"1_15_0\"))"
    );

    if env::var("DOC_RS").is_ok() {
        return;
    }

    if let Some(paths) = find_faiss() {
        println!("cargo:rustc-link-search=native={}", paths.lib_dir.display());

        if let Some(version) = &paths.version {
            check_version(version);
            emit_version_cfg(version);
            select_binding_version(version);
        } else {
            println!(
                "cargo:warning=Could not detect Faiss version, using {} bindings",
                BINDING_VERSION
            );
            println!("cargo:rustc-cfg=faiss_binding=\"{}\"", BINDING_VERSION);
        }

        #[cfg(feature = "bindgen")]
        generate_bindings(&paths);
    } else {
        println!(
            "cargo:warning=Faiss not found via standard paths, using {} bindings",
            BINDING_VERSION
        );
        println!("cargo:rustc-cfg=faiss_binding=\"{}\"", BINDING_VERSION);
    }

    search_library_paths();

    println!("cargo:rustc-link-lib=dylib=faiss");
    println!("cargo:rustc-link-lib=dylib=faiss_c");

    println!("cargo:rerun-if-changed=wrapper.h");
}

fn find_faiss() -> Option<FaissPaths> {
    if let Ok(include_dir) = env::var("FAISS_INCLUDE_DIR") {
        if let Ok(lib_dir) = env::var("FAISS_LIB_DIR") {
            let include_path = PathBuf::from(include_dir);
            let lib_path = PathBuf::from(lib_dir);
            if include_path.is_dir() && lib_path.is_dir() {
                let version = detect_version_from_include(&include_path);
                return Some(FaissPaths {
                    include_dir: include_path,
                    lib_dir: lib_path,
                    version,
                });
            }
        }
    }

    if let Ok(dir) = env::var("FAISS_DIR") {
        let path = PathBuf::from(dir);
        if path.join("include").is_dir() && path.join("lib").is_dir() {
            let version = detect_version(&path);
            return Some(FaissPaths {
                include_dir: path.join("include"),
                lib_dir: path.join("lib"),
                version,
            });
        }
    }

    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    {
        let path = PathBuf::from("/opt/homebrew/opt/faiss");
        if path.join("include").is_dir() && path.join("lib").is_dir() {
            let version = detect_version(&path);
            return Some(FaissPaths {
                include_dir: path.join("include"),
                lib_dir: path.join("lib"),
                version,
            });
        }
    }

    #[cfg(target_os = "linux")]
    {
        let path = PathBuf::from("/usr/local");
        if path.join("include/faiss").is_dir() {
            let version = detect_version(&path);
            return Some(FaissPaths {
                include_dir: path.join("include"),
                lib_dir: path.join("lib"),
                version,
            });
        }

        #[cfg(feature = "bindgen")]
        if pkg_config::probe_library("faiss").is_ok() {
            return None;
        }
    }

    #[cfg(target_os = "windows")]
    {
        let path = PathBuf::from("C:\\tools\\faiss");
        if path.join("include").is_dir() && path.join("lib").is_dir() {
            let version = detect_version(&path);
            return Some(FaissPaths {
                include_dir: path.join("include"),
                lib_dir: path.join("lib"),
                version,
            });
        }

        let candidates = [
            "C:\\faiss",
            "C:\\Program Files\\faiss",
            "C:\\Program Files (x86)\\faiss",
        ];
        for candidate in candidates {
            let path = PathBuf::from(candidate);
            if path.join("include").is_dir() && path.join("lib").is_dir() {
                let version = detect_version(&path);
                return Some(FaissPaths {
                    include_dir: path.join("include"),
                    lib_dir: path.join("lib"),
                    version,
                });
            }
        }

        if let Ok(path) = env::var("PATH") {
            for entry in path.split(';') {
                let path = PathBuf::from(entry);
                if path.join("faiss.dll").exists() || path.join("faiss_c.dll").exists() {
                    let parent = path.parent()?;
                    if parent.join("include").is_dir() {
                        let version = detect_version(parent);
                        return Some(FaissPaths {
                            include_dir: parent.join("include"),
                            lib_dir: path,
                            version,
                        });
                    }
                }
            }
        }
    }

    None
}

fn detect_version(faiss_path: &std::path::Path) -> Option<(u32, u32, u32)> {
    detect_version_from_include(&faiss_path.join("include"))
}

fn detect_version_from_include(include_path: &std::path::Path) -> Option<(u32, u32, u32)> {
    let index_h = include_path.join("faiss/Index.h");
    if let Ok(content) = std::fs::read_to_string(&index_h) {
        let mut major: Option<u32> = None;
        let mut minor: Option<u32> = None;
        let mut patch: Option<u32> = None;

        for line in content.lines() {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 3 {
                if parts.get(1) == Some(&"FAISS_VERSION_MAJOR") {
                    major = parts.get(2).and_then(|s| s.parse().ok());
                }
                if parts.get(1) == Some(&"FAISS_VERSION_MINOR") {
                    minor = parts.get(2).and_then(|s| s.parse().ok());
                }
                if parts.get(1) == Some(&"FAISS_VERSION_PATCH") {
                    patch = parts.get(2).and_then(|s| s.parse().ok());
                }
            }
        }

        if let (Some(m), Some(mi), Some(p)) = (major, minor, patch) {
            return Some((m, mi, p));
        }
    }

    let version_file = include_path.join("faiss/impl/platform_macros.h");
    if let Ok(content) = std::fs::read_to_string(&version_file) {
        if let Some(line) = content.lines().find(|l| l.contains("FAISS_VERSION")) {
            let parts: Vec<&str> = line.split('"').collect();
            if parts.len() >= 2 {
                let version_str = parts[1];
                let nums: Vec<u32> = version_str
                    .split('.')
                    .filter_map(|s| s.parse().ok())
                    .collect();
                if nums.len() >= 3 {
                    return Some((nums[0], nums[1], nums[2]));
                }
            }
        }
    }
    None
}

fn check_version(version: &(u32, u32, u32)) {
    let (maj, min, patch) = *version;

    if version < &FAISS_MIN_VERSION {
        panic!(
            "Faiss version {}.{}.{} is not supported. Minimum required: {}.{}.{}",
            maj, min, patch, FAISS_MIN_VERSION.0, FAISS_MIN_VERSION.1, FAISS_MIN_VERSION.2
        );
    }

    if version > &FAISS_MAX_TESTED_VERSION {
        println!(
            "cargo:warning=Faiss version {}.{}.{} is newer than tested versions ({}.{}.x).",
            maj, min, patch, FAISS_MIN_VERSION.0, FAISS_MIN_VERSION.1
        );
        println!(
            "cargo:warning=Using {} bindings. Compatibility not guaranteed.",
            BINDING_VERSION
        );
    }
}

fn emit_version_cfg(version: &(u32, u32, u32)) {
    let (maj, min, patch) = *version;
    println!(
        "cargo:rustc-cfg=faiss_version=\"{}_{}_{}\"",
        maj, min, patch
    );
    println!("cargo:rustc-env=FAISS_VERSION={}.{}.{}", maj, min, patch);
}

fn select_binding_version(version: &(u32, u32, u32)) {
    let (_, minor, _) = *version;

    let binding_version = match minor {
        14 => "v1_14",
        15 => "v1_15",
        _ if minor > 15 => {
            println!(
                "cargo:warning=Faiss 1.{}.x detected, using latest available bindings (v1_14)",
                minor
            );
            "v1_14"
        }
        _ => {
            panic!("Unsupported Faiss version 1.{}.x", minor);
        }
    };

    println!("cargo:rustc-cfg=faiss_binding=\"{}\"", binding_version);
}

fn search_library_paths() {
    let env_vars = if cfg!(target_os = "windows") {
        vec!["PATH"]
    } else {
        vec!["LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH"]
    };

    for env_var in env_vars {
        if let Ok(paths) = env::var(env_var) {
            let separator = if cfg!(target_os = "windows") {
                ';'
            } else {
                ':'
            };
            for path in paths.split(separator) {
                let path = PathBuf::from(path);
                let has_faiss = path.join("libfaiss.so").exists()
                    || path.join("libfaiss.dylib").exists()
                    || path.join("faiss.dll").exists();
                let has_faiss_c = path.join("libfaiss_c.so").exists()
                    || path.join("libfaiss_c.dylib").exists()
                    || path.join("faiss_c.dll").exists();

                if has_faiss || has_faiss_c {
                    println!("cargo:rustc-link-search=native={}", path.display());
                }
            }
        }
    }
}

#[cfg(all(feature = "cuda", feature = "bindgen"))]
fn cuda_dir() -> Option<PathBuf> {
    #[cfg(target_os = "linux")]
    {
        let path = PathBuf::from("/usr/local/cuda");
        if path.exists() {
            return Some(path);
        }
    }

    #[cfg(target_os = "windows")]
    {
        if let Ok(cuda_path) = env::var("CUDA_PATH") {
            let path = PathBuf::from(cuda_path);
            if path.join("include").exists() {
                return Some(path);
            }
        }

        let base = PathBuf::from("C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA");
        if base.exists() {
            if let Ok(entries) = std::fs::read_dir(&base) {
                let mut versions: Vec<_> = entries
                    .filter_map(|e| e.ok())
                    .filter(|e| e.path().join("include").exists())
                    .collect();
                versions.sort_by_key(|e| e.file_name());
                if let Some(latest) = versions.last() {
                    return Some(latest.path());
                }
            }
        }
    }

    None
}

#[cfg(feature = "bindgen")]
fn generate_bindings(paths: &FaissPaths) {
    let os = env::var("CARGO_CFG_TARGET_OS").unwrap();
    let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();

    let out_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
        .join("src")
        .join("bindings")
        .join(BINDING_VERSION);

    std::fs::create_dir_all(&out_dir).unwrap();

    let output_file = if cfg!(feature = "cuda") {
        match os.as_str() {
            "linux" => out_dir.join(format!("linux_{}_cuda.rs", arch)),
            "windows" => out_dir.join(format!("windows_{}_cuda.rs", arch)),
            _ => panic!("CUDA is only supported on Linux and Windows"),
        }
    } else {
        match os.as_str() {
            "macos" => out_dir.join(format!("macos_{}.rs", arch)),
            "linux" => out_dir.join(format!("linux_{}.rs", arch)),
            "windows" => out_dir.join(format!("windows_{}.rs", arch)),
            _ => out_dir.join(format!("{}_{}.rs", os, arch)),
        }
    };

    let mut builder = bindgen::builder()
        .header("wrapper.h")
        .derive_default(true)
        .default_enum_style(bindgen::EnumVariation::Rust {
            non_exhaustive: true,
        })
        .layout_tests(false)
        .allowlist_function("faiss_.*")
        .allowlist_type("idx_t|Faiss.*")
        .opaque_type("FILE");

    builder = builder.clang_arg(format!("-I{}", paths.include_dir.display()));

    #[cfg(all(feature = "cuda", feature = "bindgen"))]
    {
        if let Some(cuda_path) = cuda_dir() {
            builder = builder
                .clang_arg("-DUSE_CUDA")
                .clang_arg(format!("-I{}", cuda_path.join("include").display()));
        }
    }

    builder
        .generate()
        .expect("Failed to generate bindings")
        .write_to_file(output_file)
        .expect("Failed to write bindings");
}