1use std::ffi::{c_char, c_void, CStr, CString};
19use std::panic::catch_unwind;
20use std::path::Path;
21
22use abi_stable::sabi_trait::TD_Opaque;
23use abi_stable::std_types::{RResult, RStr, RString};
24use apiplant_abi::c as cabi;
25use apiplant_abi::{BoxedFunction, Function, FunctionManifest, HostApi_TO, LogLevel};
26use libloading::{Library, Symbol};
27use serde_json::Value;
28
29struct CFunction {
31 manifest: FunctionManifest,
32 name: CString,
34 invoke: cabi::InvokeFn,
35 free: cabi::FreeFn,
36 _library: &'static Library,
40}
41
42unsafe impl Send for CFunction {}
46unsafe impl Sync for CFunction {}
47
48impl Function for CFunction {
49 fn manifest(&self) -> FunctionManifest {
50 self.manifest.clone()
51 }
52
53 fn invoke(
54 &self,
55 host: HostApi_TO<'_, abi_stable::std_types::RBox<()>>,
56 input: RStr<'_>,
57 ) -> RResult<RString, RString> {
58 let Ok(input) = CString::new(input.as_str()) else {
61 return RResult::RErr(RString::from("input contains a NUL byte"));
62 };
63
64 let mut bridge = Bridge { host: &host };
67 let c_host = cabi::Host {
68 ctx: &mut bridge as *mut Bridge<'_, '_> as *mut c_void,
69 query: Some(host_query),
70 log: Some(host_log),
71 config: Some(host_config),
72 principal_id: Some(host_principal_id),
73 hook: Some(host_hook),
74 free_string: Some(host_free_string),
75 send_email: Some(host_send_email),
76 cache: Some(host_cache),
77 };
78
79 let mut out: *mut c_char = std::ptr::null_mut();
80 let status =
83 unsafe { (self.invoke)(self.name.as_ptr(), input.as_ptr(), &c_host, &mut out) };
84
85 let message = self.take_string(out);
86 match status {
87 cabi::OK => RResult::ROk(RString::from(message.unwrap_or_default())),
88 cabi::ERR_REQUEST => RResult::RErr(RString::from(
89 message.unwrap_or_else(|| "function rejected the request".to_string()),
90 )),
91 _ => RResult::RErr(RString::from(format!(
94 "{}{}",
95 apiplant_abi::INTERNAL_ERROR_PREFIX,
96 message.unwrap_or_else(|| format!("function returned status {status}"))
97 ))),
98 }
99 }
100}
101
102impl CFunction {
103 fn take_string(&self, ptr: *mut c_char) -> Option<String> {
107 if ptr.is_null() {
108 return None;
109 }
110 let owned = unsafe { CStr::from_ptr(ptr) }
113 .to_string_lossy()
114 .into_owned();
115 unsafe { (self.free)(ptr) };
117 Some(owned)
118 }
119}
120
121struct Bridge<'a, 'b> {
123 host: &'a HostApi_TO<'b, abi_stable::std_types::RBox<()>>,
124}
125
126unsafe fn bridge<'a>(ctx: *mut c_void) -> Option<&'a Bridge<'a, 'a>> {
132 (ctx as *const Bridge<'a, 'a>).as_ref()
133}
134
135fn to_c(s: &str) -> *mut c_char {
141 CString::new(s).unwrap_or_default().into_raw()
142}
143
144fn guard_string<F: FnOnce() -> *mut c_char>(f: F) -> *mut c_char {
146 match catch_unwind(std::panic::AssertUnwindSafe(f)) {
147 Ok(ptr) => ptr,
148 Err(_) => {
149 tracing::error!("panic in a host callback serving a C function");
150 std::ptr::null_mut()
151 }
152 }
153}
154
155extern "C" fn host_query(ctx: *mut c_void, request: *const c_char) -> *mut c_char {
156 guard_string(|| {
157 let (Some(bridge), Some(request)) = (unsafe { bridge(ctx) }, unsafe { cstr(request) })
160 else {
161 return to_c(r#"{"error":"invalid query request"}"#);
162 };
163 match bridge.host.query(RStr::from_str(&request)) {
164 RResult::ROk(rows) => to_c(rows.as_str()),
165 RResult::RErr(e) => {
168 let body = serde_json::json!({ "error": e.as_str() });
169 to_c(&body.to_string())
170 }
171 }
172 })
173}
174
175extern "C" fn host_send_email(ctx: *mut c_void, request: *const c_char) -> *mut c_char {
176 guard_string(|| {
177 let (Some(bridge), Some(request)) = (unsafe { bridge(ctx) }, unsafe { cstr(request) })
179 else {
180 return to_c(r#"{"error":"invalid email request"}"#);
181 };
182 in_band(bridge.host.send_email(RStr::from_str(&request)))
183 })
184}
185
186extern "C" fn host_cache(ctx: *mut c_void, request: *const c_char) -> *mut c_char {
187 guard_string(|| {
188 let (Some(bridge), Some(request)) = (unsafe { bridge(ctx) }, unsafe { cstr(request) })
190 else {
191 return to_c(r#"{"error":"invalid cache request"}"#);
192 };
193 in_band(bridge.host.cache(RStr::from_str(&request)))
194 })
195}
196
197fn in_band(result: RResult<RString, RString>) -> *mut c_char {
201 match result {
202 RResult::ROk(reply) => to_c(reply.as_str()),
203 RResult::RErr(e) => to_c(&serde_json::json!({ "error": e.as_str() }).to_string()),
204 }
205}
206
207extern "C" fn host_log(ctx: *mut c_void, level: i32, message: *const c_char) {
208 let _ = catch_unwind(std::panic::AssertUnwindSafe(|| {
209 let (Some(bridge), Some(message)) = (unsafe { bridge(ctx) }, unsafe { cstr(message) })
211 else {
212 return;
213 };
214 let level = match level {
215 cabi::log_level::TRACE => LogLevel::Trace,
216 cabi::log_level::DEBUG => LogLevel::Debug,
217 cabi::log_level::WARN => LogLevel::Warn,
218 cabi::log_level::ERROR => LogLevel::Error,
219 _ => LogLevel::Info,
221 };
222 bridge.host.log(level, RStr::from_str(&message));
223 }));
224}
225
226extern "C" fn host_config(ctx: *mut c_void) -> *mut c_char {
227 guard_string(|| match unsafe { bridge(ctx) } {
229 Some(b) => to_c(b.host.config().as_str()),
230 None => to_c("{}"),
231 })
232}
233
234extern "C" fn host_principal_id(ctx: *mut c_void) -> *mut c_char {
235 guard_string(|| match unsafe { bridge(ctx) } {
237 Some(b) => to_c(b.host.principal_id().as_str()),
238 None => to_c(""),
239 })
240}
241
242extern "C" fn host_hook(ctx: *mut c_void) -> *mut c_char {
243 guard_string(|| match unsafe { bridge(ctx) } {
245 Some(b) => to_c(b.host.hook().as_str()),
246 None => to_c(""),
247 })
248}
249
250extern "C" fn host_free_string(_ctx: *mut c_void, string: *mut c_char) {
251 if string.is_null() {
252 return;
253 }
254 drop(unsafe { CString::from_raw(string) });
258}
259
260unsafe fn cstr(ptr: *const c_char) -> Option<String> {
265 if ptr.is_null() {
266 return None;
267 }
268 Some(CStr::from_ptr(ptr).to_string_lossy().into_owned())
269}
270
271pub fn load(path: &Path) -> Result<Option<Vec<BoxedFunction>>, String> {
277 let library = match unsafe { Library::new(path) } {
280 Ok(l) => l,
281 Err(e) => return Err(format!("cannot open library: {e}")),
282 };
283
284 let version = unsafe { library.get::<cabi::AbiVersionFn>(cabi::SYM_ABI_VERSION) };
288 let Ok(version) = version else {
289 return Ok(None);
290 };
291 let version = unsafe { version() };
293 if version != cabi::ABI_VERSION {
294 return Err(format!(
295 "library targets apiplant C ABI version {version}, this host speaks {}",
296 cabi::ABI_VERSION
297 ));
298 }
299
300 let symbol = |name: &[u8]| -> Result<*const (), String> {
301 unsafe {
304 library
305 .get::<*const ()>(name)
306 .map(|s: Symbol<'_, *const ()>| *s)
307 .map_err(|e| {
308 format!(
309 "library exports `apiplant_abi_version` but not `{}`: {e}",
310 String::from_utf8_lossy(&name[..name.len() - 1])
311 )
312 })
313 }
314 };
315
316 let manifest_ptr = symbol(cabi::SYM_MANIFEST)?;
317 let invoke_ptr = symbol(cabi::SYM_INVOKE)?;
318 let free_ptr = symbol(cabi::SYM_FREE)?;
319
320 let (manifest_fn, invoke, free): (cabi::ManifestFn, cabi::InvokeFn, cabi::FreeFn) = unsafe {
323 (
324 std::mem::transmute::<*const (), cabi::ManifestFn>(manifest_ptr),
325 std::mem::transmute::<*const (), cabi::InvokeFn>(invoke_ptr),
326 std::mem::transmute::<*const (), cabi::FreeFn>(free_ptr),
327 )
328 };
329
330 let library: &'static Library = Box::leak(Box::new(library));
334
335 let json = unsafe { cstr(manifest_fn()) }
337 .ok_or_else(|| "`apiplant_manifest` returned NULL".to_string())?;
338 let entries: Vec<Value> = serde_json::from_str::<Value>(&json)
339 .map_err(|e| format!("`apiplant_manifest` is not valid JSON: {e}"))?
340 .as_array()
341 .cloned()
342 .ok_or_else(|| "`apiplant_manifest` must return a JSON array".to_string())?;
343 if entries.is_empty() {
344 return Err("`apiplant_manifest` returned an empty array".to_string());
345 }
346
347 let mut functions = Vec::with_capacity(entries.len());
348 for entry in &entries {
349 let manifest = parse_manifest(entry)?;
350 let name = CString::new(manifest.name.as_str())
351 .map_err(|_| "a function name contains a NUL byte".to_string())?;
352 functions.push(BoxedFunction::from_value(
353 CFunction {
354 manifest,
355 name,
356 invoke,
357 free,
358 _library: library,
359 },
360 TD_Opaque,
361 ));
362 }
363 Ok(Some(functions))
364}
365
366fn parse_manifest(entry: &Value) -> Result<FunctionManifest, String> {
372 apiplant_abi::manifest_from_json(entry)
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use apiplant_abi::{FunctionAccess, HttpMethod, Visibility};
379
380 fn entry(json: &str) -> Result<FunctionManifest, String> {
381 parse_manifest(&serde_json::from_str(json).unwrap())
382 }
383
384 #[test]
385 fn a_name_is_the_only_required_field() {
386 let m = entry(r#"{"name":"hello"}"#).unwrap();
387 assert_eq!(m.name.as_str(), "hello");
388 assert_eq!(m.version.as_str(), "0.0.0");
389 assert_eq!(m.method, HttpMethod::Post);
390 assert!(m.description.is_empty());
391 }
392
393 #[test]
394 fn a_missing_or_unreadable_name_is_an_error() {
395 assert!(entry(r#"{"description":"no name"}"#).is_err());
396 assert!(entry(r#"{"name":""}"#).is_err());
397 assert!(entry(r#"{"name":42}"#).is_err());
398 }
399
400 #[test]
402 fn visibility_defaults_to_private() {
403 assert_eq!(
404 entry(r#"{"name":"h"}"#).unwrap().visibility,
405 Visibility::Private
406 );
407 }
408
409 #[test]
410 fn visibility_uses_the_same_strings_as_resource_permissions() {
411 let vis = |v: &str| entry(&format!(r#"{{"name":"h","visibility":"{v}"}}"#)).unwrap();
412 assert_eq!(vis("public").visibility, Visibility::Public);
413 assert_eq!(vis("authenticated").visibility, Visibility::Authenticated);
414 assert_eq!(vis("private").visibility, Visibility::Private);
415
416 let gated = vis("role:admin");
417 assert_eq!(gated.visibility, Visibility::RoleGated);
418 assert_eq!(gated.role.as_str(), "admin");
419 }
420
421 #[test]
424 fn an_unknown_visibility_is_rejected() {
425 let err = entry(r#"{"name":"h","visibility":"pubic"}"#).unwrap_err();
426 assert!(err.contains("unknown permission"), "{err}");
427 assert!(entry(r#"{"name":"h","visibility":"role:"}"#).is_err());
428 }
429
430 #[test]
433 fn permission_is_read_and_outranks_visibility() {
434 let member = entry(r#"{"name":"h","permission":"member"}"#).unwrap();
435 assert_eq!(member.access(), FunctionAccess::Member);
436 assert_eq!(member.visibility, Visibility::Authenticated);
439
440 let both = entry(r#"{"name":"h","visibility":"public","permission":"role:ops"}"#).unwrap();
441 assert_eq!(both.access(), FunctionAccess::Role("ops".into()));
442 assert_eq!(both.visibility, Visibility::RoleGated);
443 assert_eq!(both.role.as_str(), "ops");
444
445 assert_eq!(
447 entry(r#"{"name":"h"}"#).unwrap().access(),
448 FunctionAccess::Private
449 );
450 }
451
452 #[test]
455 fn the_admin_block_survives_as_an_object_or_a_string() {
456 let inline = entry(r#"{"name":"h","admin":{"label":"Do it","order":2}}"#).unwrap();
457 let parsed: Value = serde_json::from_str(inline.admin.as_str()).unwrap();
458 assert_eq!(parsed["label"], "Do it");
459 assert_eq!(parsed["order"], 2);
460
461 let preserialised = entry(r#"{"name":"h","admin":"{\"label\":\"Do it\"}"}"#).unwrap();
462 assert_eq!(preserialised.admin.as_str(), r#"{"label":"Do it"}"#);
463
464 assert!(entry(r#"{"name":"h"}"#).unwrap().admin.is_empty());
465 }
466
467 #[test]
468 fn methods_are_case_insensitive_and_validated() {
469 let m = |v: &str| entry(&format!(r#"{{"name":"h","method":"{v}"}}"#));
470 assert_eq!(m("get").unwrap().method, HttpMethod::Get);
471 assert_eq!(m("Put").unwrap().method, HttpMethod::Put);
472 assert_eq!(m("DELETE").unwrap().method, HttpMethod::Delete);
473
474 let err = m("PATCH").unwrap_err();
475 assert!(err.contains("unsupported method"), "{err}");
476 }
477
478 #[test]
481 fn schemas_accept_an_object_or_a_string() {
482 let inline = entry(r#"{"name":"h","input_schema":{"type":"object"}}"#).unwrap();
483 assert_eq!(
484 serde_json::from_str::<Value>(inline.input_schema.as_str()).unwrap(),
485 serde_json::json!({"type":"object"})
486 );
487
488 let preserialised =
489 entry(r#"{"name":"h","input_schema":"{\"type\":\"string\"}"}"#).unwrap();
490 assert_eq!(
491 serde_json::from_str::<Value>(preserialised.input_schema.as_str()).unwrap(),
492 serde_json::json!({"type":"string"})
493 );
494
495 assert!(entry(r#"{"name":"h"}"#).unwrap().input_schema.is_empty());
496 assert!(entry(r#"{"name":"h","input_schema":null}"#)
497 .unwrap()
498 .input_schema
499 .is_empty());
500 }
501
502 #[test]
505 fn a_library_that_is_not_c_abi_is_not_an_error() {
506 let dir = std::env::temp_dir().join(format!("apiplant-cabi-{}", std::process::id()));
507 std::fs::create_dir_all(&dir).unwrap();
508 let path = dir.join("libgarbage.so");
509 std::fs::write(&path, b"not an elf file").unwrap();
510
511 assert!(load(&path).is_err());
513
514 std::fs::remove_dir_all(&dir).unwrap();
515 }
516}