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 payments: Some(host_payments),
78 ai: Some(host_ai),
79 emit: Some(host_emit),
80 };
81
82 let mut out: *mut c_char = std::ptr::null_mut();
83 let status =
86 unsafe { (self.invoke)(self.name.as_ptr(), input.as_ptr(), &c_host, &mut out) };
87
88 let message = self.take_string(out);
89 match status {
90 cabi::OK => RResult::ROk(RString::from(message.unwrap_or_default())),
91 cabi::ERR_REQUEST => RResult::RErr(RString::from(
92 message.unwrap_or_else(|| "function rejected the request".to_string()),
93 )),
94 _ => RResult::RErr(RString::from(format!(
97 "{}{}",
98 apiplant_abi::INTERNAL_ERROR_PREFIX,
99 message.unwrap_or_else(|| format!("function returned status {status}"))
100 ))),
101 }
102 }
103}
104
105impl CFunction {
106 fn take_string(&self, ptr: *mut c_char) -> Option<String> {
110 if ptr.is_null() {
111 return None;
112 }
113 let owned = unsafe { CStr::from_ptr(ptr) }
116 .to_string_lossy()
117 .into_owned();
118 unsafe { (self.free)(ptr) };
120 Some(owned)
121 }
122}
123
124struct Bridge<'a, 'b> {
126 host: &'a HostApi_TO<'b, abi_stable::std_types::RBox<()>>,
127}
128
129unsafe fn bridge<'a>(ctx: *mut c_void) -> Option<&'a Bridge<'a, 'a>> {
135 (ctx as *const Bridge<'a, 'a>).as_ref()
136}
137
138fn to_c(s: &str) -> *mut c_char {
144 CString::new(s).unwrap_or_default().into_raw()
145}
146
147fn guard_string<F: FnOnce() -> *mut c_char>(f: F) -> *mut c_char {
149 match catch_unwind(std::panic::AssertUnwindSafe(f)) {
150 Ok(ptr) => ptr,
151 Err(_) => {
152 tracing::error!("panic in a host callback serving a C function");
153 std::ptr::null_mut()
154 }
155 }
156}
157
158extern "C" fn host_query(ctx: *mut c_void, request: *const c_char) -> *mut c_char {
159 guard_string(|| {
160 let (Some(bridge), Some(request)) = (unsafe { bridge(ctx) }, unsafe { cstr(request) })
163 else {
164 return to_c(r#"{"error":"invalid query request"}"#);
165 };
166 match bridge.host.query(RStr::from_str(&request)) {
167 RResult::ROk(rows) => to_c(rows.as_str()),
168 RResult::RErr(e) => {
171 let body = serde_json::json!({ "error": e.as_str() });
172 to_c(&body.to_string())
173 }
174 }
175 })
176}
177
178extern "C" fn host_send_email(ctx: *mut c_void, request: *const c_char) -> *mut c_char {
179 guard_string(|| {
180 let (Some(bridge), Some(request)) = (unsafe { bridge(ctx) }, unsafe { cstr(request) })
182 else {
183 return to_c(r#"{"error":"invalid email request"}"#);
184 };
185 in_band(bridge.host.send_email(RStr::from_str(&request)))
186 })
187}
188
189extern "C" fn host_payments(ctx: *mut c_void, request: *const c_char) -> *mut c_char {
190 guard_string(|| {
191 let (Some(bridge), Some(request)) = (unsafe { bridge(ctx) }, unsafe { cstr(request) })
193 else {
194 return to_c(r#"{"error":"invalid payments request"}"#);
195 };
196 in_band(bridge.host.payments(RStr::from_str(&request)))
197 })
198}
199
200extern "C" fn host_cache(ctx: *mut c_void, request: *const c_char) -> *mut c_char {
201 guard_string(|| {
202 let (Some(bridge), Some(request)) = (unsafe { bridge(ctx) }, unsafe { cstr(request) })
204 else {
205 return to_c(r#"{"error":"invalid cache request"}"#);
206 };
207 in_band(bridge.host.cache(RStr::from_str(&request)))
208 })
209}
210
211extern "C" fn host_ai(ctx: *mut c_void, request: *const c_char) -> *mut c_char {
212 guard_string(|| {
213 let (Some(bridge), Some(request)) = (unsafe { bridge(ctx) }, unsafe { cstr(request) })
215 else {
216 return to_c(r#"{"error":"invalid ai request"}"#);
217 };
218 in_band(bridge.host.ai(RStr::from_str(&request)))
219 })
220}
221
222extern "C" fn host_emit(ctx: *mut c_void, chunk: *const c_char) -> i32 {
226 let delivered = catch_unwind(std::panic::AssertUnwindSafe(|| {
227 let (Some(bridge), Some(chunk)) = (unsafe { bridge(ctx) }, unsafe { cstr(chunk) }) else {
229 return false;
230 };
231 bridge.host.emit(RStr::from_str(&chunk))
232 }));
233 match delivered {
234 Ok(true) => 1,
235 Ok(false) => 0,
236 Err(_) => {
237 tracing::error!("panic in a host callback serving a C function");
238 0
239 }
240 }
241}
242
243fn in_band(result: RResult<RString, RString>) -> *mut c_char {
247 match result {
248 RResult::ROk(reply) => to_c(reply.as_str()),
249 RResult::RErr(e) => to_c(&serde_json::json!({ "error": e.as_str() }).to_string()),
250 }
251}
252
253extern "C" fn host_log(ctx: *mut c_void, level: i32, message: *const c_char) {
254 let _ = catch_unwind(std::panic::AssertUnwindSafe(|| {
255 let (Some(bridge), Some(message)) = (unsafe { bridge(ctx) }, unsafe { cstr(message) })
257 else {
258 return;
259 };
260 let level = match level {
261 cabi::log_level::TRACE => LogLevel::Trace,
262 cabi::log_level::DEBUG => LogLevel::Debug,
263 cabi::log_level::WARN => LogLevel::Warn,
264 cabi::log_level::ERROR => LogLevel::Error,
265 _ => LogLevel::Info,
267 };
268 bridge.host.log(level, RStr::from_str(&message));
269 }));
270}
271
272extern "C" fn host_config(ctx: *mut c_void) -> *mut c_char {
273 guard_string(|| match unsafe { bridge(ctx) } {
275 Some(b) => to_c(b.host.config().as_str()),
276 None => to_c("{}"),
277 })
278}
279
280extern "C" fn host_principal_id(ctx: *mut c_void) -> *mut c_char {
281 guard_string(|| match unsafe { bridge(ctx) } {
283 Some(b) => to_c(b.host.principal_id().as_str()),
284 None => to_c(""),
285 })
286}
287
288extern "C" fn host_hook(ctx: *mut c_void) -> *mut c_char {
289 guard_string(|| match unsafe { bridge(ctx) } {
291 Some(b) => to_c(b.host.hook().as_str()),
292 None => to_c(""),
293 })
294}
295
296extern "C" fn host_free_string(_ctx: *mut c_void, string: *mut c_char) {
297 if string.is_null() {
298 return;
299 }
300 drop(unsafe { CString::from_raw(string) });
304}
305
306unsafe fn cstr(ptr: *const c_char) -> Option<String> {
311 if ptr.is_null() {
312 return None;
313 }
314 Some(CStr::from_ptr(ptr).to_string_lossy().into_owned())
315}
316
317pub fn load(path: &Path) -> Result<Option<Vec<BoxedFunction>>, String> {
323 let library = match unsafe { Library::new(path) } {
326 Ok(l) => l,
327 Err(e) => return Err(format!("cannot open library: {e}")),
328 };
329
330 let version = unsafe { library.get::<cabi::AbiVersionFn>(cabi::SYM_ABI_VERSION) };
334 let Ok(version) = version else {
335 return Ok(None);
336 };
337 let version = unsafe { version() };
339 if version != cabi::ABI_VERSION {
340 return Err(format!(
341 "library targets apiplant C ABI version {version}, this host speaks {}",
342 cabi::ABI_VERSION
343 ));
344 }
345
346 let symbol = |name: &[u8]| -> Result<*const (), String> {
347 unsafe {
350 library
351 .get::<*const ()>(name)
352 .map(|s: Symbol<'_, *const ()>| *s)
353 .map_err(|e| {
354 format!(
355 "library exports `apiplant_abi_version` but not `{}`: {e}",
356 String::from_utf8_lossy(&name[..name.len() - 1])
357 )
358 })
359 }
360 };
361
362 let manifest_ptr = symbol(cabi::SYM_MANIFEST)?;
363 let invoke_ptr = symbol(cabi::SYM_INVOKE)?;
364 let free_ptr = symbol(cabi::SYM_FREE)?;
365
366 let (manifest_fn, invoke, free): (cabi::ManifestFn, cabi::InvokeFn, cabi::FreeFn) = unsafe {
369 (
370 std::mem::transmute::<*const (), cabi::ManifestFn>(manifest_ptr),
371 std::mem::transmute::<*const (), cabi::InvokeFn>(invoke_ptr),
372 std::mem::transmute::<*const (), cabi::FreeFn>(free_ptr),
373 )
374 };
375
376 let library: &'static Library = Box::leak(Box::new(library));
380
381 let json = unsafe { cstr(manifest_fn()) }
383 .ok_or_else(|| "`apiplant_manifest` returned NULL".to_string())?;
384 let entries: Vec<Value> = serde_json::from_str::<Value>(&json)
385 .map_err(|e| format!("`apiplant_manifest` is not valid JSON: {e}"))?
386 .as_array()
387 .cloned()
388 .ok_or_else(|| "`apiplant_manifest` must return a JSON array".to_string())?;
389 if entries.is_empty() {
390 return Err("`apiplant_manifest` returned an empty array".to_string());
391 }
392
393 let mut functions = Vec::with_capacity(entries.len());
394 for entry in &entries {
395 let manifest = parse_manifest(entry)?;
396 let name = CString::new(manifest.name.as_str())
397 .map_err(|_| "a function name contains a NUL byte".to_string())?;
398 functions.push(BoxedFunction::from_value(
399 CFunction {
400 manifest,
401 name,
402 invoke,
403 free,
404 _library: library,
405 },
406 TD_Opaque,
407 ));
408 }
409 Ok(Some(functions))
410}
411
412fn parse_manifest(entry: &Value) -> Result<FunctionManifest, String> {
418 apiplant_abi::manifest_from_json(entry)
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use apiplant_abi::{FunctionAccess, HttpMethod, Visibility};
425
426 fn entry(json: &str) -> Result<FunctionManifest, String> {
427 parse_manifest(&serde_json::from_str(json).unwrap())
428 }
429
430 #[test]
431 fn a_name_is_the_only_required_field() {
432 let m = entry(r#"{"name":"hello"}"#).unwrap();
433 assert_eq!(m.name.as_str(), "hello");
434 assert_eq!(m.version.as_str(), "0.0.0");
435 assert_eq!(m.method, HttpMethod::Post);
436 assert!(m.description.is_empty());
437 }
438
439 #[test]
440 fn a_missing_or_unreadable_name_is_an_error() {
441 assert!(entry(r#"{"description":"no name"}"#).is_err());
442 assert!(entry(r#"{"name":""}"#).is_err());
443 assert!(entry(r#"{"name":42}"#).is_err());
444 }
445
446 #[test]
448 fn visibility_defaults_to_private() {
449 assert_eq!(
450 entry(r#"{"name":"h"}"#).unwrap().visibility,
451 Visibility::Private
452 );
453 }
454
455 #[test]
456 fn visibility_uses_the_same_strings_as_resource_permissions() {
457 let vis = |v: &str| entry(&format!(r#"{{"name":"h","visibility":"{v}"}}"#)).unwrap();
458 assert_eq!(vis("public").visibility, Visibility::Public);
459 assert_eq!(vis("authenticated").visibility, Visibility::Authenticated);
460 assert_eq!(vis("private").visibility, Visibility::Private);
461
462 let gated = vis("role:admin");
463 assert_eq!(gated.visibility, Visibility::RoleGated);
464 assert_eq!(gated.role.as_str(), "admin");
465 }
466
467 #[test]
470 fn an_unknown_visibility_is_rejected() {
471 let err = entry(r#"{"name":"h","visibility":"pubic"}"#).unwrap_err();
472 assert!(err.contains("unknown permission"), "{err}");
473 assert!(entry(r#"{"name":"h","visibility":"role:"}"#).is_err());
474 }
475
476 #[test]
479 fn permission_is_read_and_outranks_visibility() {
480 let member = entry(r#"{"name":"h","permission":"member"}"#).unwrap();
481 assert_eq!(member.access(), FunctionAccess::Member);
482 assert_eq!(member.visibility, Visibility::Authenticated);
485
486 let both = entry(r#"{"name":"h","visibility":"public","permission":"role:ops"}"#).unwrap();
487 assert_eq!(both.access(), FunctionAccess::Role("ops".into()));
488 assert_eq!(both.visibility, Visibility::RoleGated);
489 assert_eq!(both.role.as_str(), "ops");
490
491 assert_eq!(
493 entry(r#"{"name":"h"}"#).unwrap().access(),
494 FunctionAccess::Private
495 );
496 }
497
498 #[test]
501 fn the_admin_block_survives_as_an_object_or_a_string() {
502 let inline = entry(r#"{"name":"h","admin":{"label":"Do it","order":2}}"#).unwrap();
503 let parsed: Value = serde_json::from_str(inline.admin.as_str()).unwrap();
504 assert_eq!(parsed["label"], "Do it");
505 assert_eq!(parsed["order"], 2);
506
507 let preserialised = entry(r#"{"name":"h","admin":"{\"label\":\"Do it\"}"}"#).unwrap();
508 assert_eq!(preserialised.admin.as_str(), r#"{"label":"Do it"}"#);
509
510 assert!(entry(r#"{"name":"h"}"#).unwrap().admin.is_empty());
511 }
512
513 #[test]
514 fn methods_are_case_insensitive_and_validated() {
515 let m = |v: &str| entry(&format!(r#"{{"name":"h","method":"{v}"}}"#));
516 assert_eq!(m("get").unwrap().method, HttpMethod::Get);
517 assert_eq!(m("Put").unwrap().method, HttpMethod::Put);
518 assert_eq!(m("DELETE").unwrap().method, HttpMethod::Delete);
519
520 let err = m("PATCH").unwrap_err();
521 assert!(err.contains("unsupported method"), "{err}");
522 }
523
524 #[test]
527 fn schemas_accept_an_object_or_a_string() {
528 let inline = entry(r#"{"name":"h","input_schema":{"type":"object"}}"#).unwrap();
529 assert_eq!(
530 serde_json::from_str::<Value>(inline.input_schema.as_str()).unwrap(),
531 serde_json::json!({"type":"object"})
532 );
533
534 let preserialised =
535 entry(r#"{"name":"h","input_schema":"{\"type\":\"string\"}"}"#).unwrap();
536 assert_eq!(
537 serde_json::from_str::<Value>(preserialised.input_schema.as_str()).unwrap(),
538 serde_json::json!({"type":"string"})
539 );
540
541 assert!(entry(r#"{"name":"h"}"#).unwrap().input_schema.is_empty());
542 assert!(entry(r#"{"name":"h","input_schema":null}"#)
543 .unwrap()
544 .input_schema
545 .is_empty());
546 }
547
548 #[test]
551 fn a_library_that_is_not_c_abi_is_not_an_error() {
552 let dir = std::env::temp_dir().join(format!("apiplant-cabi-{}", std::process::id()));
553 std::fs::create_dir_all(&dir).unwrap();
554 let path = dir.join("libgarbage.so");
555 std::fs::write(&path, b"not an elf file").unwrap();
556
557 assert!(load(&path).is_err());
559
560 std::fs::remove_dir_all(&dir).unwrap();
561 }
562}