#![warn(clippy::pedantic)]
use std::path::Path;
use dlopen::symbor::Library;
use libc::{c_int, c_void as void};
mod error;
pub use error::Error;
use serde::Deserialize;
use crate::error::LibYAMLScriptError;
const LIBYAMLSCRIPT_BASENAME: &str = "libyamlscript";
const LIBYAMLSCRIPT_VERSION: &str = "0.1.96";
#[cfg(target_os = "linux")]
const LIBYAMLSCRIPT_EXTENSION: &str = "so";
#[cfg(target_os = "macos")]
const LIBYAMLSCRIPT_EXTENSION: &str = "dylib";
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
compile_error!(
"Unsupported platform {} for yamlscript.",
std::env::consts::OS
);
pub struct YAMLScript {
_handle: Library,
_isolate: *mut void,
isolate_thread: *mut void,
_create_isolate_fn: CreateIsolateFn,
tear_down_isolate_fn: TearDownIsolateFn,
load_ys_to_json_fn: LoadYsToJsonFn,
}
type CreateIsolateFn = unsafe extern "C" fn(*mut void, *const *mut void, *const *mut void) -> c_int;
type TearDownIsolateFn = unsafe extern "C" fn(*mut void) -> c_int;
type LoadYsToJsonFn = unsafe extern "C" fn(*mut void, *const u8) -> *mut i8;
impl YAMLScript {
#[allow(clippy::crosspointer_transmute)]
pub fn new() -> Result<Self, Error> {
let handle = Self::open_library()?;
let isolate = std::ptr::null_mut();
let isolate_thread = std::ptr::null_mut();
let create_isolate_fn =
unsafe { handle.ptr_or_null::<CreateIsolateFn>("graal_create_isolate")? };
let tear_down_isolate_fn =
unsafe { handle.ptr_or_null::<TearDownIsolateFn>("graal_tear_down_isolate")? };
let load_ys_to_json_fn =
unsafe { handle.ptr_or_null::<LoadYsToJsonFn>("load_ys_to_json")? };
if create_isolate_fn.is_null()
|| tear_down_isolate_fn.is_null()
|| load_ys_to_json_fn.is_null()
{
return Err(Error::Load(dlopen::Error::NullSymbol));
}
let create_isolate_fn: CreateIsolateFn = unsafe { std::mem::transmute(*create_isolate_fn) };
let tear_down_isolate_fn: TearDownIsolateFn =
unsafe { std::mem::transmute(*tear_down_isolate_fn) };
let load_ys_to_json_fn: LoadYsToJsonFn =
unsafe { std::mem::transmute(*load_ys_to_json_fn) };
let x = unsafe { (create_isolate_fn)(std::ptr::null_mut(), &isolate, &isolate_thread) };
if x != 0 {
return Err(Error::GraalVM(x));
}
Ok(Self {
_handle: handle,
_isolate: isolate,
isolate_thread,
_create_isolate_fn: create_isolate_fn,
tear_down_isolate_fn,
load_ys_to_json_fn,
})
}
pub fn load<T>(&self, ys: &str) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
{
let raw = unsafe { std::ffi::CStr::from_ptr(self.load_raw(ys, self.isolate_thread)?) }
.to_str()?;
let response = serde_json::from_str::<YsResponse<T>>(raw)?;
match response {
YsResponse::Data(value) => Ok(value),
YsResponse::Error(err) => Err(Error::YAMLScript(err)),
}
}
fn load_raw(&self, ys: &str, isolate_thread: *mut void) -> Result<*mut i8, Error> {
let input = std::ffi::CString::new(ys)
.map_err(|_| Error::Ffi("load: input contains a nil-byte".to_string()))?;
let json = unsafe { (self.load_ys_to_json_fn)(isolate_thread, input.as_bytes().as_ptr()) };
if json.is_null() {
Err(Error::Ffi(
"load_ys_to_json: returned a null pointer".to_string(),
))
} else {
Ok(json)
}
}
fn open_library() -> Result<Library, Error> {
let mut first_error = None;
let library_path = std::env::var("LD_LIBRARY_PATH").map_err(|_| Error::NotFound)?;
let mut additional_paths = vec!["/usr/local/lib"];
let home_path = std::env::var("HOME")
.ok()
.map(|home| format!("{home}/.local/lib"));
if let Some(path) = &home_path {
additional_paths.push(path.as_str());
}
for path in library_path.split(':').chain(additional_paths.into_iter()) {
let path = Path::new(path).join(format!(
"{LIBYAMLSCRIPT_BASENAME}.{LIBYAMLSCRIPT_EXTENSION}.{LIBYAMLSCRIPT_VERSION}"
));
if !path.is_file() {
continue;
}
let library = Library::open(path);
match library {
Ok(x) => return Ok(x),
Err(x) => {
if first_error.is_none() {
first_error = Some(x);
}
}
}
}
match first_error {
Some(x) => Err(x.into()),
None => Err(Error::NotFound),
}
}
}
impl Drop for YAMLScript {
fn drop(&mut self) {
let res = unsafe { (self.tear_down_isolate_fn)(self.isolate_thread) };
if res != 0 {
eprintln!("Warning: Failed to tear down yamlscript's GraalVM isolate");
}
}
}
#[derive(Deserialize)]
enum YsResponse<T> {
#[serde(rename = "data")]
Data(T),
#[serde(rename = "error")]
Error(LibYAMLScriptError),
}