use crate::error::Error;
use crate::ffi::{
symbols, KqlCleanupFn, KqlGetClassificationsFn, KqlGetCompletionsFn, KqlGetLastErrorFn,
KqlInitFn, KqlValidateSyntaxFn, KqlValidateWithSchemaFn,
};
use libloading::Library;
use once_cell::sync::OnceCell;
use std::path::PathBuf;
pub const LIB_PATH_ENV: &str = "KQL_LANGUAGE_TOOLS_PATH";
#[cfg(target_os = "macos")]
pub const LIB_NAME: &str = "KqlLanguageFfiNE.dylib";
#[cfg(target_os = "linux")]
pub const LIB_NAME: &str = "KqlLanguageFfiNE.so";
#[cfg(target_os = "windows")]
pub const LIB_NAME: &str = "KqlLanguageFfiNE.dll";
pub fn current_rid() -> &'static str {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
return "osx-arm64";
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
return "osx-x64";
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
return "linux-x64";
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
return "linux-arm64";
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
return "win-x64";
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
return "win-arm64";
}
pub fn find_library_path() -> Option<PathBuf> {
if let Ok(path) = std::env::var(LIB_PATH_ENV) {
let path = PathBuf::from(path);
if path.is_file() {
log::debug!("Found library via {LIB_PATH_ENV}: {}", path.display());
return Some(path);
}
if path.is_dir() {
let lib_path = path.join(LIB_NAME);
if lib_path.exists() {
log::debug!(
"Found library in {LIB_PATH_ENV} directory: {}",
lib_path.display()
);
return Some(lib_path);
}
}
}
if let Ok(exe_path) = std::env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
let lib_path = exe_dir.join(LIB_NAME);
if lib_path.exists() {
log::debug!("Found library next to executable: {}", lib_path.display());
return Some(lib_path);
}
}
}
let native_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("dotnet")
.join("native")
.join(current_rid());
let lib_path = native_dir.join(LIB_NAME);
if lib_path.exists() {
log::debug!("Found library in native directory: {}", lib_path.display());
return Some(lib_path);
}
let cwd_path = PathBuf::from(LIB_NAME);
if cwd_path.exists() {
log::debug!("Found library in current directory: {}", cwd_path.display());
return Some(cwd_path);
}
log::debug!("Native library not found");
None
}
pub fn searched_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(path) = std::env::var(LIB_PATH_ENV) {
paths.push(PathBuf::from(&path));
paths.push(PathBuf::from(path).join(LIB_NAME));
}
if let Ok(exe_path) = std::env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
paths.push(exe_dir.join(LIB_NAME));
}
}
let native_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("dotnet")
.join("native")
.join(current_rid());
paths.push(native_dir.join(LIB_NAME));
paths.push(PathBuf::from(LIB_NAME));
paths
}
static LIBRARY: OnceCell<LoadedLibrary> = OnceCell::new();
pub struct LoadedLibrary {
#[allow(dead_code)]
library: Library,
pub init: KqlInitFn,
#[allow(dead_code)]
pub cleanup: KqlCleanupFn,
pub validate_syntax: KqlValidateSyntaxFn,
pub get_last_error: KqlGetLastErrorFn,
pub validate_with_schema: Option<KqlValidateWithSchemaFn>,
pub get_completions: Option<KqlGetCompletionsFn>,
pub get_classifications: Option<KqlGetClassificationsFn>,
}
unsafe impl Send for LoadedLibrary {}
unsafe impl Sync for LoadedLibrary {}
impl LoadedLibrary {
fn load_from(path: &PathBuf) -> Result<Self, Error> {
log::info!("Loading KQL language library from {}", path.display());
let library =
unsafe { Library::new(path) }.map_err(|e| Error::library_load_failed(path, e))?;
let init: KqlInitFn = unsafe {
*library
.get(symbols::KQL_INIT.as_bytes())
.map_err(|_| Error::SymbolNotFound {
symbol: symbols::KQL_INIT.to_string(),
})?
};
let cleanup: KqlCleanupFn = unsafe {
*library
.get(symbols::KQL_CLEANUP.as_bytes())
.map_err(|_| Error::SymbolNotFound {
symbol: symbols::KQL_CLEANUP.to_string(),
})?
};
let validate_syntax: KqlValidateSyntaxFn = unsafe {
*library
.get(symbols::KQL_VALIDATE_SYNTAX.as_bytes())
.map_err(|_| Error::SymbolNotFound {
symbol: symbols::KQL_VALIDATE_SYNTAX.to_string(),
})?
};
let get_last_error: KqlGetLastErrorFn = unsafe {
*library
.get(symbols::KQL_GET_LAST_ERROR.as_bytes())
.map_err(|_| Error::SymbolNotFound {
symbol: symbols::KQL_GET_LAST_ERROR.to_string(),
})?
};
let validate_with_schema: Option<KqlValidateWithSchemaFn> = unsafe {
library
.get(symbols::KQL_VALIDATE_WITH_SCHEMA.as_bytes())
.ok()
.map(|s| *s)
};
let get_completions: Option<KqlGetCompletionsFn> = unsafe {
library
.get(symbols::KQL_GET_COMPLETIONS.as_bytes())
.ok()
.map(|s| *s)
};
let get_classifications: Option<KqlGetClassificationsFn> = unsafe {
library
.get(symbols::KQL_GET_CLASSIFICATIONS.as_bytes())
.ok()
.map(|s| *s)
};
log::debug!(
"Loaded symbols: validate_with_schema={}, get_completions={}, get_classifications={}",
validate_with_schema.is_some(),
get_completions.is_some(),
get_classifications.is_some()
);
Ok(Self {
library,
init,
cleanup,
validate_syntax,
get_last_error,
validate_with_schema,
get_completions,
get_classifications,
})
}
pub fn supports_schema_validation(&self) -> bool {
self.validate_with_schema.is_some()
}
pub fn supports_completion(&self) -> bool {
self.get_completions.is_some()
}
pub fn supports_classification(&self) -> bool {
self.get_classifications.is_some()
}
}
impl Drop for LoadedLibrary {
fn drop(&mut self) {
log::debug!("Calling kql_cleanup before unloading library");
unsafe { (self.cleanup)() };
}
}
fn ensure_dotnet_root() {
if std::env::var("DOTNET_ROOT").is_ok() {
return;
}
if let Some(dotnet_root) = find_dotnet_root() {
log::debug!("Auto-detected DOTNET_ROOT: {}", dotnet_root.display());
std::env::set_var("DOTNET_ROOT", &dotnet_root);
}
}
fn find_dotnet_root() -> Option<PathBuf> {
let candidates = [
"/opt/homebrew/Cellar/dotnet",
"/usr/local/Cellar/dotnet",
"/usr/share/dotnet",
"/usr/local/share/dotnet",
"C:\\Program Files\\dotnet",
];
if let Ok(output) = std::process::Command::new("dotnet")
.args(["--info"])
.output()
{
if output.status.success() {
let info = String::from_utf8_lossy(&output.stdout);
for line in info.lines() {
if line.trim().starts_with("Base Path:") {
if let Some(path_str) = line.split(':').nth(1) {
let path = PathBuf::from(path_str.trim());
if let Some(libexec) = path.ancestors().find(|p| p.ends_with("libexec")) {
return Some(libexec.to_path_buf());
}
if let Some(dotnet_dir) = path
.ancestors()
.find(|p| p.join("dotnet").exists() || p.join("shared").exists())
{
return Some(dotnet_dir.to_path_buf());
}
}
}
}
}
}
for candidate in candidates {
let path = PathBuf::from(candidate);
if path.exists() {
if candidate.contains("Cellar") {
if let Ok(entries) = std::fs::read_dir(&path) {
let mut versions: Vec<_> = entries
.filter_map(std::result::Result::ok)
.filter(|e| e.path().is_dir())
.collect();
versions.sort_by_key(|b| std::cmp::Reverse(b.path()));
if let Some(version_dir) = versions.first() {
let libexec = version_dir.path().join("libexec");
if libexec.exists() {
return Some(libexec);
}
}
}
} else if path.join("shared").exists() {
return Some(path);
}
}
}
None
}
pub fn load_library() -> Result<&'static LoadedLibrary, Error> {
LIBRARY.get_or_try_init(|| {
ensure_dotnet_root();
let path = find_library_path().ok_or_else(|| Error::LibraryNotFound {
searched_paths: searched_paths(),
})?;
let lib = LoadedLibrary::load_from(&path)?;
let result = unsafe { (lib.init)() };
if result != 0 {
let mut error_buf = vec![0u8; 1024];
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let error_len =
unsafe { (lib.get_last_error)(error_buf.as_mut_ptr(), error_buf.len() as i32) };
let message = if error_len > 0 {
#[allow(clippy::cast_sign_loss)]
let len = error_len as usize;
String::from_utf8_lossy(&error_buf[..len]).to_string()
} else {
format!("Initialization returned error code: {result}")
};
return Err(Error::InitializationFailed { message });
}
log::info!("KQL language library initialized successfully");
Ok(lib)
})
}
#[allow(dead_code)]
pub fn is_loaded() -> bool {
LIBRARY.get().is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_current_rid() {
let rid = current_rid();
assert!(!rid.is_empty());
#[cfg(target_os = "macos")]
assert!(rid.starts_with("osx-"));
#[cfg(target_os = "linux")]
assert!(rid.starts_with("linux-"));
#[cfg(target_os = "windows")]
assert!(rid.starts_with("win-"));
}
#[test]
fn test_searched_paths_not_empty() {
let paths = searched_paths();
assert!(!paths.is_empty());
}
}