reflow_pack_loader/
lib.rs1pub mod bundle;
13pub mod host;
14
15use std::collections::HashMap;
16use std::ffi::{CStr, c_char, c_void};
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19
20use anyhow::{Context, Result, anyhow, bail};
21use once_cell::sync::Lazy;
22use parking_lot::RwLock;
23use reflow_actor::Actor;
24
25use bundle::{
26 ABI_VERSION_SYMBOL, DEFAULT_ENTRYPOINT, PackManifest, extract_bundle, is_bundle_file,
27 read_manifest,
28};
29use host::{PackFactoryDropFn, PackFactoryFn, PackHostVtable, PackRegisterStatus};
30
31pub const REFLOW_PACK_ABI_VERSION: u32 = {
34 let s = match option_env!("REFLOW_PACK_ABI_VERSION") {
36 Some(s) => s,
37 None => "0",
38 };
39 parse_u32(s.as_bytes())
40};
41
42pub const REFLOW_PACK_HOST_TRIPLE: &str = match option_env!("REFLOW_PACK_HOST_TRIPLE") {
45 Some(s) => s,
46 None => "unknown",
47};
48
49const fn parse_u32(bytes: &[u8]) -> u32 {
50 let mut out: u32 = 0;
51 let mut i = 0;
52 while i < bytes.len() {
53 let b = bytes[i];
54 if b < b'0' || b > b'9' {
55 return 0;
56 }
57 out = out * 10 + (b - b'0') as u32;
58 i += 1;
59 }
60 out
61}
62
63struct FactoryRecord {
66 factory: PackFactoryFn,
67 drop: PackFactoryDropFn,
68 user_data: SendPtr,
69 _owner: Arc<LoadedPack>,
71}
72
73impl Drop for FactoryRecord {
74 fn drop(&mut self) {
75 if let Some(drop_fn) = self.drop {
76 unsafe { drop_fn(self.user_data.0) };
77 }
78 }
79}
80
81struct SendPtr(*mut c_void);
82unsafe impl Send for SendPtr {}
85unsafe impl Sync for SendPtr {}
86
87struct LoadedPack {
88 name: String,
89 version: String,
90 #[allow(dead_code)]
91 manifest: Option<PackManifest>,
92 source_path: PathBuf,
93 templates: parking_lot::Mutex<Vec<String>>,
94 #[cfg(not(target_arch = "wasm32"))]
98 _lib: libloading::Library,
99}
100
101pub struct PackRegistry {
102 inner: RwLock<PackRegistryInner>,
103}
104
105struct PackRegistryInner {
106 templates: HashMap<String, Arc<FactoryRecord>>,
109 loaded: HashMap<String, Arc<LoadedPack>>,
111}
112
113impl PackRegistry {
114 fn new() -> Self {
115 Self {
116 inner: RwLock::new(PackRegistryInner {
117 templates: HashMap::new(),
118 loaded: HashMap::new(),
119 }),
120 }
121 }
122
123 pub fn instantiate(&self, template_id: &str) -> Option<Arc<dyn Actor>> {
127 let record = self.inner.read().templates.get(template_id).cloned()?;
128 let factory = record.factory?;
129 let ptr = unsafe { factory(record.user_data.0) };
130 if ptr.is_null() {
131 return None;
132 }
133 unsafe { host::PackActorHandle::unbox(ptr) }
134 }
135
136 pub fn template_ids(&self) -> Vec<String> {
138 let g = self.inner.read();
139 let mut v: Vec<String> = g.templates.keys().cloned().collect();
140 v.sort();
141 v
142 }
143
144 pub fn loaded_packs(&self) -> Vec<LoadedPackInfo> {
146 let g = self.inner.read();
147 g.loaded
148 .values()
149 .map(|p| LoadedPackInfo {
150 name: p.name.clone(),
151 version: p.version.clone(),
152 source_path: p.source_path.clone(),
153 templates: p.templates.lock().clone(),
154 })
155 .collect()
156 }
157}
158
159#[derive(Debug, Clone, serde::Serialize)]
160pub struct LoadedPackInfo {
161 pub name: String,
162 pub version: String,
163 pub source_path: PathBuf,
164 pub templates: Vec<String>,
165}
166
167pub static PACK_REGISTRY: Lazy<PackRegistry> = Lazy::new(PackRegistry::new);
171
172#[cfg(not(target_arch = "wasm32"))]
184pub fn load_pack<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
185 let path = path.as_ref();
186 let canonical =
187 std::fs::canonicalize(path).with_context(|| format!("canonicalize {}", path.display()))?;
188
189 let (dylib_path, manifest): (PathBuf, Option<PackManifest>) =
190 if is_bundle_file(&canonical).unwrap_or(false) {
191 let extracted =
192 extract_bundle(&canonical, REFLOW_PACK_ABI_VERSION, REFLOW_PACK_HOST_TRIPLE)?;
193 (extracted.dylib_path, Some(extracted.manifest))
194 } else {
195 (canonical.clone(), None)
196 };
197
198 let pack_name = manifest
199 .as_ref()
200 .map(|m| m.name.clone())
201 .unwrap_or_else(|| {
202 canonical
203 .file_stem()
204 .and_then(|s| s.to_str())
205 .unwrap_or("pack")
206 .to_string()
207 });
208 let pack_version = manifest
209 .as_ref()
210 .map(|m| m.version.clone())
211 .unwrap_or_else(|| "0.0.0".to_string());
212 let entrypoint_name = manifest
213 .as_ref()
214 .map(|m| m.entrypoint.clone())
215 .unwrap_or_else(|| DEFAULT_ENTRYPOINT.to_string());
216
217 {
219 let g = PACK_REGISTRY.inner.read();
220 if let Some(existing) = g.loaded.get(&pack_name) {
221 return Ok(existing.templates.lock().clone());
222 }
223 }
224
225 let lib = unsafe {
228 libloading::Library::new(&dylib_path)
229 .with_context(|| format!("dlopen {}", dylib_path.display()))?
230 };
231
232 unsafe {
235 let sym: libloading::Symbol<unsafe extern "C" fn() -> u32> = lib
236 .get(ABI_VERSION_SYMBOL.as_bytes())
237 .with_context(|| format!("pack missing symbol `{ABI_VERSION_SYMBOL}`"))?;
238 let v = sym();
239 if v != REFLOW_PACK_ABI_VERSION {
240 bail!(
241 "pack `{pack_name}` ABI {v} != host ABI {} — rebuild pack against current toolchain",
242 REFLOW_PACK_ABI_VERSION
243 );
244 }
245 }
246
247 let owner = Arc::new(LoadedPack {
248 name: pack_name.clone(),
249 version: pack_version,
250 manifest,
251 source_path: canonical,
252 templates: parking_lot::Mutex::new(Vec::new()),
253 _lib: lib,
254 });
255
256 {
260 let lib = &owner._lib;
261 let register: libloading::Symbol<unsafe extern "C" fn(*mut PackHostVtable) -> i32> = unsafe {
262 lib.get(entrypoint_name.as_bytes())
263 .with_context(|| format!("pack missing symbol `{entrypoint_name}`"))?
264 };
265 let key = RegisterKey::new(Arc::clone(&owner));
266 let key_box = Box::into_raw(Box::new(key));
267 let mut vtable = PackHostVtable {
268 host_data: key_box as *mut c_void,
269 register_template: register_template_trampoline,
270 };
271 let status = unsafe { register(&mut vtable as *mut PackHostVtable) };
272 let _ = unsafe { Box::from_raw(key_box) };
274 if status != PackRegisterStatus::Ok as i32 {
275 bail!("pack `{pack_name}` register returned status {status} — registration aborted");
276 }
277 }
278
279 let templates = owner.templates.lock().clone();
281 PACK_REGISTRY
282 .inner
283 .write()
284 .loaded
285 .insert(pack_name, Arc::clone(&owner));
286 Ok(templates)
287}
288
289pub fn inspect_pack<P: AsRef<Path>>(path: P) -> Result<PackManifest> {
292 let path = path.as_ref();
293 if !is_bundle_file(path).unwrap_or(false) {
294 bail!("inspect_pack requires a .rflpack bundle — raw dylibs have no manifest");
295 }
296 read_manifest(path)
297}
298
299struct RegisterKey {
304 owner: Arc<LoadedPack>,
305}
306
307impl RegisterKey {
308 fn new(owner: Arc<LoadedPack>) -> Self {
309 Self { owner }
310 }
311}
312
313unsafe extern "C" fn register_template_trampoline(
314 host_data: *mut c_void,
315 template_id: *const c_char,
316 factory: PackFactoryFn,
317 drop: PackFactoryDropFn,
318 factory_user_data: *mut c_void,
319) -> i32 {
320 if host_data.is_null() || template_id.is_null() || factory.is_none() {
321 return PackRegisterStatus::NullArg as i32;
322 }
323 let key = unsafe { &*(host_data as *const RegisterKey) };
324 let id = match unsafe { CStr::from_ptr(template_id) }.to_str() {
325 Ok(s) => s.to_string(),
326 Err(_) => return PackRegisterStatus::BadUtf8 as i32,
327 };
328
329 let record = Arc::new(FactoryRecord {
330 factory,
331 drop,
332 user_data: SendPtr(factory_user_data),
333 _owner: Arc::clone(&key.owner),
334 });
335
336 let mut g = PACK_REGISTRY.inner.write();
337 if g.templates.contains_key(&id) {
338 return PackRegisterStatus::Duplicate as i32;
339 }
340 g.templates.insert(id.clone(), record);
341 key.owner.templates.lock().push(id);
342 PackRegisterStatus::Ok as i32
343}
344
345pub fn has_template(template_id: &str) -> bool {
351 PACK_REGISTRY
352 .inner
353 .read()
354 .templates
355 .contains_key(template_id)
356}
357
358pub fn instantiate(template_id: &str) -> Option<Arc<dyn Actor>> {
361 PACK_REGISTRY.instantiate(template_id)
362}
363
364pub fn list_packs_json() -> Result<String> {
367 let list = PACK_REGISTRY.loaded_packs();
368 serde_json::to_string(&list).map_err(|e| anyhow!("serialize pack list: {e}"))
369}
370
371#[allow(dead_code)]
374fn _types_used() {
375 let _: PackFactoryFn = None;
376 let _: PackFactoryDropFn = None;
377}