weft_core/package/
native.rs1use 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}