use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use std::path::PathBuf;
use anyhow::{Result, anyhow};
use crate::backend::Backend;
pub trait Engine {
fn id(&self) -> &str;
fn navigate(&mut self, url: &str) -> Result<()>;
fn evaluate(&mut self, js: &str) -> Result<String>;
fn screenshot(&mut self) -> Result<Vec<u8>>;
}
#[repr(C)]
struct EngineVtable {
id: unsafe extern "C" fn() -> *const c_char,
new: unsafe extern "C" fn(*const c_char) -> *mut std::ffi::c_void,
destroy: unsafe extern "C" fn(*mut std::ffi::c_void),
navigate: unsafe extern "C" fn(*mut std::ffi::c_void, *const c_char) -> c_int,
evaluate: unsafe extern "C" fn(*mut std::ffi::c_void, *const c_char) -> *mut c_char,
free_string: unsafe extern "C" fn(*mut std::ffi::c_void, *mut c_char),
screenshot: unsafe extern "C" fn(*mut std::ffi::c_void, *mut *mut u8, *mut usize) -> c_int,
free_pixels: unsafe extern "C" fn(*mut std::ffi::c_void, *mut u8, usize),
}
pub struct CdylibEngine {
#[allow(dead_code)]
lib: libloading::Library,
vt: EngineVtable,
handle: *mut std::ffi::c_void,
id: String,
}
unsafe impl Send for CdylibEngine {}
impl CdylibEngine {
pub fn open(backend: Backend, options_json: &str) -> Result<Self> {
let id = backend.engine_id().ok_or_else(|| {
anyhow!(
"backend {} is CDP-driven in-process; it has no foreign engine lib",
backend.label()
)
})?;
let path = locate_engine_lib(id)?;
let lib = unsafe { libloading::Library::new(&path) }
.map_err(|e| anyhow!("dlopen {} failed: {e}", path.display()))?;
macro_rules! sym {
($name:literal) => {{
let label = std::str::from_utf8($name).unwrap_or("<symbol>");
*unsafe { lib.get($name) }.map_err(|e| anyhow!("missing {label}: {e}"))?
}};
}
let vt = EngineVtable {
id: sym!(b"shirabe_engine_id\0"),
new: sym!(b"shirabe_engine_new\0"),
destroy: sym!(b"shirabe_engine_destroy\0"),
navigate: sym!(b"shirabe_engine_navigate\0"),
evaluate: sym!(b"shirabe_engine_evaluate\0"),
free_string: sym!(b"shirabe_engine_free_string\0"),
screenshot: sym!(b"shirabe_engine_screenshot\0"),
free_pixels: sym!(b"shirabe_engine_free_pixels\0"),
};
let opts_c = CString::new(options_json)
.map_err(|e| anyhow!("embedded NUL in engine options: {e}"))?;
let handle = unsafe { (vt.new)(opts_c.as_ptr()) };
if handle.is_null() {
return Err(anyhow!("vendor engine {} refused to start", id));
}
let id_str = unsafe { CStr::from_ptr((vt.id)()) }
.to_string_lossy()
.into_owned();
Ok(CdylibEngine {
lib,
vt,
handle,
id: id_str,
})
}
}
impl Engine for CdylibEngine {
fn id(&self) -> &str {
&self.id
}
fn navigate(&mut self, url: &str) -> Result<()> {
let c = CString::new(url).map_err(|e| anyhow!("embedded NUL in navigate URL: {e}"))?;
let rc = unsafe { (self.vt.navigate)(self.handle, c.as_ptr()) };
if rc == 0 {
Ok(())
} else {
Err(anyhow!("vendor navigate failed (code {rc})"))
}
}
fn evaluate(&mut self, js: &str) -> Result<String> {
let c = CString::new(js).map_err(|e| anyhow!("embedded NUL in JS expression: {e}"))?;
let ptr = unsafe { (self.vt.evaluate)(self.handle, c.as_ptr()) };
if ptr.is_null() {
return Err(anyhow!("vendor evaluate returned null"));
}
let out = unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned();
unsafe { (self.vt.free_string)(self.handle, ptr) };
Ok(out)
}
fn screenshot(&mut self) -> Result<Vec<u8>> {
let mut buf: *mut u8 = std::ptr::null_mut();
let mut len: usize = 0;
let rc = unsafe { (self.vt.screenshot)(self.handle, &mut buf, &mut len) };
if rc != 0 || buf.is_null() {
return Err(anyhow!("vendor screenshot failed (code {rc})"));
}
let out = unsafe { std::slice::from_raw_parts(buf, len) }.to_vec();
unsafe { (self.vt.free_pixels)(self.handle, buf, len) };
Ok(out)
}
}
impl Drop for CdylibEngine {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe { (self.vt.destroy)(self.handle) };
self.handle = std::ptr::null_mut();
}
}
}
pub fn locate_engine_lib(id: &str) -> Result<PathBuf> {
let file = lib_name(id);
if let Ok(p) = std::env::var("SHIRABE_ENGINE_PATH") {
let path = PathBuf::from(&p);
if path.exists() {
return Ok(path);
}
}
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
let cand = dir.join(&file);
if cand.exists() {
return Ok(cand);
}
}
}
let cached = cache_engine_path(id).join(&file);
if cached.exists() {
return Ok(cached);
}
Err(anyhow!(
"no vendor engine lib for `{id}` found. Set SHIRABE_ENGINE_PATH, drop \
`{file}` next to the executable, or let shirabe fetch it from a release \
into {}",
cache_engine_path(id).display()
))
}
pub fn cache_engine_path(id: &str) -> PathBuf {
cache_dir()
.unwrap_or_else(|| std::env::temp_dir().join("shirabe-cache"))
.join("shirabe")
.join("engines")
.join(id)
}
fn cache_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
std::env::var_os("LOCALAPPDATA").map(PathBuf::from)
}
#[cfg(target_os = "macos")]
{
std::env::var_os("HOME").map(|h| PathBuf::from(h).join("Library/Caches"))
}
#[cfg(all(unix, not(target_os = "macos")))]
{
std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
}
#[cfg(not(any(target_os = "windows", target_os = "macos", unix)))]
{
None
}
}
fn lib_name(id: &str) -> String {
if cfg!(target_os = "windows") {
format!("shirabe_engine_{id}.dll")
} else if cfg!(target_os = "macos") {
format!("libshirabe_engine_{id}.dylib")
} else {
format!("libshirabe_engine_{id}.so")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lib_name_is_platform_appropriate() {
let n = lib_name("firefox");
if cfg!(target_os = "windows") {
assert_eq!(n, "shirabe_engine_firefox.dll");
} else if cfg!(target_os = "macos") {
assert_eq!(n, "libshirabe_engine_firefox.dylib");
} else {
assert_eq!(n, "libshirabe_engine_firefox.so");
}
}
#[test]
fn missing_engine_lib_is_a_clean_error() {
unsafe { std::env::remove_var("SHIRABE_ENGINE_PATH") };
assert!(locate_engine_lib("definitely_no_such_engine").is_err());
}
}