Skip to main content

weft_core/package/
native.rs

1use anyhow::{anyhow, Context, Result};
2use libloading::Library;
3use serde_json::Value;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex as StdMutex};
7
8type NativePackageFn = unsafe extern "C" fn(*const u8, usize, *mut usize) -> *mut u8;
9type NativePackageFreeFn = unsafe extern "C" fn(*mut u8, usize);
10
11#[derive(Clone)]
12pub(crate) struct NativePackageHandle {
13    name: String,
14    library: Arc<Library>,
15    call_fn: NativePackageFn,
16    free_fn: Option<NativePackageFreeFn>,
17}
18
19pub struct NativePackageHost {
20    packages: HashMap<String, NativePackageHandle>,
21}
22
23impl Default for NativePackageHost {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl NativePackageHost {
30    pub fn new() -> Self {
31        Self {
32            packages: HashMap::new(),
33        }
34    }
35
36    pub fn has_package(&self, name: &str) -> bool {
37        self.packages.contains_key(name)
38    }
39
40    pub fn package_names(&self) -> Vec<String> {
41        self.packages.keys().cloned().collect()
42    }
43
44    pub fn load_package(&mut self, load_info: &NativePackageLoadInfo) -> Result<()> {
45        if self.packages.contains_key(&load_info.name) {
46            return Ok(());
47        }
48
49        let library = unsafe { Library::new(&load_info.library_path) }.with_context(|| {
50            format!(
51                "Failed to load native library '{}')",
52                load_info.library_path.display()
53            )
54        })?;
55
56        let call_fn = unsafe {
57            *library
58                .get::<NativePackageFn>(b"weft_plugin_handle_json")
59                .context("Missing required symbol 'weft_plugin_handle_json'")?
60        };
61        let free_fn = unsafe {
62            library
63                .get::<NativePackageFreeFn>(b"weft_plugin_free")
64                .ok()
65                .map(|symbol| *symbol)
66        };
67
68        let handle = NativePackageHandle {
69            name: load_info.name.clone(),
70            library: Arc::new(library),
71            call_fn,
72            free_fn,
73        };
74
75        self.packages.insert(load_info.name.clone(), handle);
76        Ok(())
77    }
78
79    pub fn unload_package(&mut self, name: &str) -> Result<()> {
80        self.packages
81            .remove(name)
82            .ok_or_else(|| anyhow!("Native package '{}' not loaded", name))?;
83        Ok(())
84    }
85
86    pub fn reload_package(&mut self, load_info: &NativePackageLoadInfo) -> Result<()> {
87        let _ = self.packages.remove(&load_info.name);
88        self.load_package(load_info)
89    }
90
91    pub fn call_json(&self, name: &str, payload: &Value) -> Result<Value> {
92        let handle = self
93            .packages
94            .get(name)
95            .ok_or_else(|| anyhow!("Native package '{}' not loaded", name))?;
96
97        let _keep_library_alive = handle.library.clone();
98        let _package_name = &handle.name;
99
100        let input = serde_json::to_vec(payload)?;
101        let mut out_len: usize = 0;
102        let out_ptr =
103            unsafe { (handle.call_fn)(input.as_ptr(), input.len(), &mut out_len as *mut usize) };
104        if out_ptr.is_null() {
105            return Err(anyhow!("Native package '{}' returned null response", name));
106        }
107
108        let bytes = unsafe { std::slice::from_raw_parts(out_ptr, out_len).to_vec() };
109        if let Some(free_fn) = handle.free_fn {
110            unsafe { free_fn(out_ptr, out_len) };
111        }
112
113        serde_json::from_slice(&bytes)
114            .with_context(|| format!("Native package '{}' returned invalid JSON", name))
115    }
116}
117
118#[derive(Debug, Clone)]
119pub struct NativePackageLoadInfo {
120    pub name: String,
121    pub dir: PathBuf,
122    pub library_path: PathBuf,
123}
124
125#[derive(Clone)]
126pub struct NativeHandle {
127    host: Arc<StdMutex<NativePackageHost>>,
128}
129
130impl NativeHandle {
131    pub fn new(host: NativePackageHost) -> Self {
132        Self {
133            host: Arc::new(StdMutex::new(host)),
134        }
135    }
136
137    pub fn has_package(&self, name: &str) -> bool {
138        self.host
139            .lock()
140            .map(|host| host.has_package(name))
141            .unwrap_or(false)
142    }
143
144    pub fn package_names(&self) -> Vec<String> {
145        self.host
146            .lock()
147            .map(|host| host.package_names())
148            .unwrap_or_default()
149    }
150
151    pub fn load_package(&self, info: &NativePackageLoadInfo) -> Result<()> {
152        let mut host = self
153            .host
154            .lock()
155            .map_err(|e| anyhow!("NativeHandle lock poisoned: {}", e))?;
156        host.load_package(info)
157    }
158
159    pub fn unload_package(&self, name: &str) -> Result<()> {
160        let mut host = self
161            .host
162            .lock()
163            .map_err(|e| anyhow!("NativeHandle lock poisoned: {}", e))?;
164        host.unload_package(name)
165    }
166
167    pub fn reload_package(&self, info: &NativePackageLoadInfo) -> Result<()> {
168        let mut host = self
169            .host
170            .lock()
171            .map_err(|e| anyhow!("NativeHandle lock poisoned: {}", e))?;
172        host.reload_package(info)
173    }
174
175    pub fn call_json(&self, name: &str, payload: &Value) -> Result<Value> {
176        let host = self
177            .host
178            .lock()
179            .map_err(|e| anyhow!("NativeHandle lock poisoned: {}", e))?;
180        host.call_json(name, payload)
181    }
182
183    pub fn from_test_package(
184        name: String,
185        call_fn: NativePackageFn,
186        free_fn: Option<NativePackageFreeFn>,
187    ) -> Result<Self> {
188        let library = unsafe { Library::new(std::env::current_exe()?) }
189            .with_context(|| "Failed to open current executable for test native handle")?;
190        let mut host = NativePackageHost::new();
191        host.packages.insert(
192            name.clone(),
193            NativePackageHandle {
194                name,
195                library: Arc::new(library),
196                call_fn,
197                free_fn,
198            },
199        );
200        Ok(Self::new(host))
201    }
202}
203
204pub fn native_library_candidates(entry_path: &Path) -> Vec<PathBuf> {
205    let stem = entry_path
206        .file_stem()
207        .and_then(|s| s.to_str())
208        .unwrap_or("plugin");
209    let mut candidates = Vec::new();
210    let parent = entry_path.parent().unwrap_or(Path::new("."));
211    if entry_path.exists() {
212        candidates.push(entry_path.to_path_buf());
213    }
214    #[cfg(target_os = "windows")]
215    {
216        candidates.push(parent.join(format!("{}.dll", stem)));
217        candidates.push(
218            parent
219                .join("target")
220                .join("debug")
221                .join(format!("{}.dll", stem)),
222        );
223    }
224    #[cfg(target_os = "linux")]
225    {
226        candidates.push(parent.join(format!("lib{}.so", stem)));
227        candidates.push(parent.join(format!("{}.so", stem)));
228        candidates.push(
229            parent
230                .join("target")
231                .join("debug")
232                .join(format!("lib{}.so", stem)),
233        );
234        candidates.push(
235            parent
236                .join("target")
237                .join("debug")
238                .join(format!("{}.so", stem)),
239        );
240    }
241    #[cfg(target_os = "macos")]
242    {
243        candidates.push(parent.join(format!("lib{}.dylib", stem)));
244        candidates.push(parent.join(format!("{}.dylib", stem)));
245        candidates.push(
246            parent
247                .join("target")
248                .join("debug")
249                .join(format!("lib{}.dylib", stem)),
250        );
251        candidates.push(
252            parent
253                .join("target")
254                .join("debug")
255                .join(format!("{}.dylib", stem)),
256        );
257    }
258    candidates.sort();
259    candidates.dedup();
260    candidates
261}