#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::undocumented_unsafe_blocks)]
#![allow(dead_code)]
use core::ffi::c_void;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, Once};
use libloading::Library;
use polyplug::Runtime;
use polyplug::error::LoaderError;
use polyplug::loader::{BundleLoader, ManifestData};
use polyplug_abi::HostApi;
use polyplug_abi::POLYPLUG_ABI_VERSION;
use polyplug_abi::StringView;
use polyplug_abi::SupportedLanguage;
use polyplug_abi::plugin::BundleInitContext;
use polyplug_abi::types::{AbiError, AbiErrorCode};
use polyplug_utils::BundleId;
pub struct TestNativeLoader {
libraries: Mutex<HashMap<BundleId, Library>>,
retired: Mutex<Vec<Library>>,
}
impl TestNativeLoader {
pub fn new() -> Self {
Self {
libraries: Mutex::new(HashMap::new()),
retired: Mutex::new(Vec::new()),
}
}
fn load_library(&self, manifest: &ManifestData, runtime: &Runtime) -> Result<(), LoaderError> {
if manifest.file.is_empty() {
return Err(LoaderError::ManifestMissingFile {
bundle: manifest.name.clone(),
});
}
let bundle_path: PathBuf = manifest.path.join(&manifest.file);
let path_str: String = bundle_path.to_string_lossy().into_owned();
let library: Library = unsafe {
Library::new(&bundle_path).map_err(|e| LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!("failed to load plugin library at {path_str}: {e}"),
})?
};
let abi_version_symbol: libloading::Symbol<'_, unsafe extern "C" fn() -> u32> = unsafe {
library
.get(b"polyplug_abi_version\0")
.map_err(|_| LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!("missing symbol 'polyplug_abi_version' in bundle '{path_str}'"),
})?
};
let found_version: u32 = unsafe { abi_version_symbol() };
if found_version != POLYPLUG_ABI_VERSION {
return Err(LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"ABI version mismatch in {path_str}: expected={POLYPLUG_ABI_VERSION}, found={found_version}"
),
});
}
let init_fn_ptr: unsafe extern "C" fn(
*const HostApi,
*const BundleInitContext,
) -> AbiError = {
let sym: libloading::Symbol<
'_,
unsafe extern "C" fn(*const HostApi, *const BundleInitContext) -> AbiError,
> = unsafe {
library
.get(b"polyplug_init\0")
.map_err(|_| LoaderError::InitSymbolMissing {
bundle: manifest.name.clone(),
})?
};
*sym
};
let bundle_dir: &std::path::Path =
bundle_path.parent().unwrap_or(std::path::Path::new("."));
let ctx: BundleInitContext = BundleInitContext {
bundle_id: BundleId::new(&manifest.name).id(),
bundle_path: StringView {
ptr: bundle_dir.as_os_str().as_encoded_bytes().as_ptr(),
len: bundle_dir.as_os_str().as_encoded_bytes().len(),
},
};
let expected_bundle_id: BundleId = BundleId::new(&manifest.name);
runtime.push_init_bundle_id(expected_bundle_id.id());
let host_abi: *const HostApi = runtime.host_abi();
let init_result: AbiError = unsafe { init_fn_ptr(host_abi, &ctx) };
runtime.pop_init_bundle_id();
if init_result.code != AbiErrorCode::Ok as u32 {
let error_msg: String = if init_result.message.ptr.is_null() {
format!("init returned error code {:?}", init_result.code)
} else {
let bytes: &[u8] = unsafe {
core::slice::from_raw_parts(init_result.message.ptr, init_result.message.len)
};
String::from_utf8_lossy(bytes).into_owned()
};
return Err(LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: error_msg,
});
}
if let Some(old_library) = self
.libraries
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(expected_bundle_id, library)
{
self.retired
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(old_library);
}
Ok(())
}
}
impl Default for TestNativeLoader {
fn default() -> Self {
Self::new()
}
}
impl BundleLoader for TestNativeLoader {
fn loader_name(&self) -> &'static str {
"native"
}
fn loader_language(&self) -> SupportedLanguage {
SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
true
}
fn load(
&self,
manifest: &ManifestData,
_source: &polyplug::loader::BundleSource,
runtime: &Runtime,
) -> Result<(), LoaderError> {
self.load_library(manifest, runtime)
}
fn reload(&self, manifest: &ManifestData, runtime: &Runtime) -> Result<(), LoaderError> {
self.load_library(manifest, runtime)
}
}
pub unsafe fn register_native_loader(host: *const HostApi) {
let loader: *mut c_void = Box::into_raw(Box::new(
Box::new(TestNativeLoader::new()) as Box<dyn BundleLoader>
)) as *mut c_void;
let mut rc: AbiError = AbiError::ok();
unsafe { ((*host).register_loader)(host, loader, &mut rc) };
assert_eq!(
rc.code,
AbiErrorCode::Ok as u32,
"register_loader must succeed for native loader"
);
}
pub fn polyplugc_bin() -> PathBuf {
let target_dir: PathBuf = workspace_target_dir();
let profile: &str = if cfg!(debug_assertions) {
"debug"
} else {
"release"
};
let bin_name: &str = if cfg!(windows) {
"polyplugc.exe"
} else {
"polyplugc"
};
let bin_path: PathBuf = target_dir.join(profile).join(bin_name);
if !bin_path.exists() {
build_polyplugc(profile);
}
bin_path
}
fn workspace_target_dir() -> PathBuf {
if let Ok(dir) = std::env::var("CARGO_TARGET_DIR") {
return PathBuf::from(dir);
}
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("parent of crates/polyplug")
.parent()
.expect("workspace root")
.join("target")
}
fn build_polyplugc(profile: &str) {
static BUILD: Once = Once::new();
BUILD.call_once(|| {
let mut cmd: Command = Command::new(env!("CARGO"));
cmd.args(["build", "-p", "polyplugc"]);
if profile == "release" {
cmd.arg("--release");
}
let status: std::process::ExitStatus = cmd
.status()
.expect("failed to spawn cargo build -p polyplugc");
assert!(status.success(), "cargo build -p polyplugc failed");
});
}