pub mod bundle;
pub mod host;
use std::collections::HashMap;
use std::ffi::{CStr, c_char, c_void};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context, Result, anyhow, bail};
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use reflow_actor::Actor;
use bundle::{
ABI_VERSION_SYMBOL, DEFAULT_ENTRYPOINT, PackManifest, extract_bundle, is_bundle_file,
read_manifest,
};
use host::{PackFactoryDropFn, PackFactoryFn, PackHostVtable, PackRegisterStatus};
pub const REFLOW_PACK_ABI_VERSION: u32 = {
let s = match option_env!("REFLOW_PACK_ABI_VERSION") {
Some(s) => s,
None => "0",
};
parse_u32(s.as_bytes())
};
pub const REFLOW_PACK_HOST_TRIPLE: &str = match option_env!("REFLOW_PACK_HOST_TRIPLE") {
Some(s) => s,
None => "unknown",
};
const fn parse_u32(bytes: &[u8]) -> u32 {
let mut out: u32 = 0;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b < b'0' || b > b'9' {
return 0;
}
out = out * 10 + (b - b'0') as u32;
i += 1;
}
out
}
struct FactoryRecord {
factory: PackFactoryFn,
drop: PackFactoryDropFn,
user_data: SendPtr,
_owner: Arc<LoadedPack>,
}
impl Drop for FactoryRecord {
fn drop(&mut self) {
if let Some(drop_fn) = self.drop {
unsafe { drop_fn(self.user_data.0) };
}
}
}
struct SendPtr(*mut c_void);
unsafe impl Send for SendPtr {}
unsafe impl Sync for SendPtr {}
struct LoadedPack {
name: String,
version: String,
#[allow(dead_code)]
manifest: Option<PackManifest>,
source_path: PathBuf,
templates: parking_lot::Mutex<Vec<String>>,
#[cfg(not(target_arch = "wasm32"))]
_lib: libloading::Library,
}
pub struct PackRegistry {
inner: RwLock<PackRegistryInner>,
}
struct PackRegistryInner {
templates: HashMap<String, Arc<FactoryRecord>>,
loaded: HashMap<String, Arc<LoadedPack>>,
}
impl PackRegistry {
fn new() -> Self {
Self {
inner: RwLock::new(PackRegistryInner {
templates: HashMap::new(),
loaded: HashMap::new(),
}),
}
}
pub fn instantiate(&self, template_id: &str) -> Option<Arc<dyn Actor>> {
let record = self.inner.read().templates.get(template_id).cloned()?;
let factory = record.factory?;
let ptr = unsafe { factory(record.user_data.0) };
if ptr.is_null() {
return None;
}
unsafe { host::PackActorHandle::unbox(ptr) }
}
pub fn template_ids(&self) -> Vec<String> {
let g = self.inner.read();
let mut v: Vec<String> = g.templates.keys().cloned().collect();
v.sort();
v
}
pub fn loaded_packs(&self) -> Vec<LoadedPackInfo> {
let g = self.inner.read();
g.loaded
.values()
.map(|p| LoadedPackInfo {
name: p.name.clone(),
version: p.version.clone(),
source_path: p.source_path.clone(),
templates: p.templates.lock().clone(),
})
.collect()
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct LoadedPackInfo {
pub name: String,
pub version: String,
pub source_path: PathBuf,
pub templates: Vec<String>,
}
pub static PACK_REGISTRY: Lazy<PackRegistry> = Lazy::new(PackRegistry::new);
#[cfg(not(target_arch = "wasm32"))]
pub fn load_pack<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let path = path.as_ref();
let canonical =
std::fs::canonicalize(path).with_context(|| format!("canonicalize {}", path.display()))?;
let (dylib_path, manifest): (PathBuf, Option<PackManifest>) =
if is_bundle_file(&canonical).unwrap_or(false) {
let extracted =
extract_bundle(&canonical, REFLOW_PACK_ABI_VERSION, REFLOW_PACK_HOST_TRIPLE)?;
(extracted.dylib_path, Some(extracted.manifest))
} else {
(canonical.clone(), None)
};
let pack_name = manifest
.as_ref()
.map(|m| m.name.clone())
.unwrap_or_else(|| {
canonical
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("pack")
.to_string()
});
let pack_version = manifest
.as_ref()
.map(|m| m.version.clone())
.unwrap_or_else(|| "0.0.0".to_string());
let entrypoint_name = manifest
.as_ref()
.map(|m| m.entrypoint.clone())
.unwrap_or_else(|| DEFAULT_ENTRYPOINT.to_string());
{
let g = PACK_REGISTRY.inner.read();
if let Some(existing) = g.loaded.get(&pack_name) {
return Ok(existing.templates.lock().clone());
}
}
let lib = unsafe {
libloading::Library::new(&dylib_path)
.with_context(|| format!("dlopen {}", dylib_path.display()))?
};
unsafe {
let sym: libloading::Symbol<unsafe extern "C" fn() -> u32> = lib
.get(ABI_VERSION_SYMBOL.as_bytes())
.with_context(|| format!("pack missing symbol `{ABI_VERSION_SYMBOL}`"))?;
let v = sym();
if v != REFLOW_PACK_ABI_VERSION {
bail!(
"pack `{pack_name}` ABI {v} != host ABI {} — rebuild pack against current toolchain",
REFLOW_PACK_ABI_VERSION
);
}
}
let owner = Arc::new(LoadedPack {
name: pack_name.clone(),
version: pack_version,
manifest,
source_path: canonical,
templates: parking_lot::Mutex::new(Vec::new()),
_lib: lib,
});
{
let lib = &owner._lib;
let register: libloading::Symbol<unsafe extern "C" fn(*mut PackHostVtable) -> i32> = unsafe {
lib.get(entrypoint_name.as_bytes())
.with_context(|| format!("pack missing symbol `{entrypoint_name}`"))?
};
let key = RegisterKey::new(Arc::clone(&owner));
let key_box = Box::into_raw(Box::new(key));
let mut vtable = PackHostVtable {
host_data: key_box as *mut c_void,
register_template: register_template_trampoline,
};
let status = unsafe { register(&mut vtable as *mut PackHostVtable) };
let _ = unsafe { Box::from_raw(key_box) };
if status != PackRegisterStatus::Ok as i32 {
bail!("pack `{pack_name}` register returned status {status} — registration aborted");
}
}
let templates = owner.templates.lock().clone();
PACK_REGISTRY
.inner
.write()
.loaded
.insert(pack_name, Arc::clone(&owner));
Ok(templates)
}
pub fn inspect_pack<P: AsRef<Path>>(path: P) -> Result<PackManifest> {
let path = path.as_ref();
if !is_bundle_file(path).unwrap_or(false) {
bail!("inspect_pack requires a .rflpack bundle — raw dylibs have no manifest");
}
read_manifest(path)
}
struct RegisterKey {
owner: Arc<LoadedPack>,
}
impl RegisterKey {
fn new(owner: Arc<LoadedPack>) -> Self {
Self { owner }
}
}
unsafe extern "C" fn register_template_trampoline(
host_data: *mut c_void,
template_id: *const c_char,
factory: PackFactoryFn,
drop: PackFactoryDropFn,
factory_user_data: *mut c_void,
) -> i32 {
if host_data.is_null() || template_id.is_null() || factory.is_none() {
return PackRegisterStatus::NullArg as i32;
}
let key = unsafe { &*(host_data as *const RegisterKey) };
let id = match unsafe { CStr::from_ptr(template_id) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return PackRegisterStatus::BadUtf8 as i32,
};
let record = Arc::new(FactoryRecord {
factory,
drop,
user_data: SendPtr(factory_user_data),
_owner: Arc::clone(&key.owner),
});
let mut g = PACK_REGISTRY.inner.write();
if g.templates.contains_key(&id) {
return PackRegisterStatus::Duplicate as i32;
}
g.templates.insert(id.clone(), record);
key.owner.templates.lock().push(id);
PackRegisterStatus::Ok as i32
}
pub fn has_template(template_id: &str) -> bool {
PACK_REGISTRY
.inner
.read()
.templates
.contains_key(template_id)
}
pub fn instantiate(template_id: &str) -> Option<Arc<dyn Actor>> {
PACK_REGISTRY.instantiate(template_id)
}
pub fn list_packs_json() -> Result<String> {
let list = PACK_REGISTRY.loaded_packs();
serde_json::to_string(&list).map_err(|e| anyhow!("serialize pack list: {e}"))
}
#[allow(dead_code)]
fn _types_used() {
let _: PackFactoryFn = None;
let _: PackFactoryDropFn = None;
}