| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use std::ffi::{CStr, CString}; |
| use std::os::raw::{c_char, c_void}; |
| use std::path::PathBuf; |
| use std::sync::atomic::{AtomicU64, Ordering}; |
|
|
|
|
| use bex_core::EngineConfig; |
| use bex_types::BexError; |
|
|
| use crate::runtime::BexRuntime; |
|
|
| |
|
|
| |
| pub struct BexEngineInner { |
| runtime: BexRuntime, |
| next_request_id: AtomicU64, |
| |
| last_error: parking_lot::Mutex<Option<CString>>, |
| } |
|
|
| |
|
|
| type ResultCallback = unsafe extern "C" fn( |
| user_data: *mut c_void, |
| request_id: u64, |
| success: bool, |
| payload: *const u8, |
| payload_len: usize, |
| ); |
|
|
| |
|
|
| |
| #[repr(C)] |
| pub struct BexPluginInfo { |
| pub id: *mut c_char, |
| pub name: *mut c_char, |
| pub version: *mut c_char, |
| pub capabilities: u32, |
| pub enabled: bool, |
| pub description: *mut c_char, |
| pub author: *mut c_char, |
| pub homepage: *mut c_char, |
| } |
|
|
| |
| #[repr(C)] |
| pub struct BexPluginInfoList { |
| pub items: *mut BexPluginInfo, |
| pub count: usize, |
| } |
|
|
| |
|
|
| fn set_last_error(inner: &BexEngineInner, msg: &str) { |
| if let Ok(c) = CString::new(msg) { |
| *inner.last_error.lock() = Some(c); |
| } |
| } |
|
|
| fn clear_last_error(inner: &BexEngineInner) { |
| *inner.last_error.lock() = None; |
| } |
|
|
| fn error_to_code(inner: &BexEngineInner, e: &BexError) -> i32 { |
| let msg = e.to_string(); |
| set_last_error(inner, &msg); |
| match e { |
| BexError::PluginNotFound(_) => 2, |
| BexError::PluginDisabled(_) => 3, |
| BexError::NotReady => 4, |
| BexError::Storage(_) => 5, |
| BexError::Internal(_) => 6, |
| _ => -1, |
| } |
| } |
|
|
| fn str_to_cstring(s: &str) -> *mut c_char { |
| CString::new(s) |
| .map(|c| c.into_raw()) |
| .unwrap_or(std::ptr::null_mut()) |
| } |
|
|
| fn plugin_info_to_ffi(info: &bex_types::plugin_info::PluginInfo) -> BexPluginInfo { |
| BexPluginInfo { |
| id: str_to_cstring(&info.id), |
| name: str_to_cstring(&info.name), |
| version: str_to_cstring(&info.version), |
| capabilities: info.capabilities, |
| enabled: info.enabled, |
| description: str_to_cstring(""), |
| author: str_to_cstring(""), |
| homepage: str_to_cstring(""), |
| } |
| } |
|
|
| fn error_code_short(err: &BexError) -> &'static str { |
| match err { |
| BexError::AbiMismatch { .. } => "ABI_MISMATCH", |
| BexError::ManifestInvalid(_) => "INVALID_MANIFEST", |
| BexError::HashMismatch { .. } => "HASH_MISMATCH", |
| BexError::PluginNotFound(_) => "NOT_FOUND", |
| BexError::PluginDisabled(_) => "DISABLED", |
| BexError::Unsupported(_) => "UNSUPPORTED", |
| BexError::NetworkBlocked(_) => "NETWORK_BLOCKED", |
| BexError::Timeout { .. } => "TIMEOUT", |
| BexError::FuelExhausted => "FUEL_EXHAUSTED", |
| BexError::Cancelled => "CANCELLED", |
| BexError::PluginFault(_) => "PLUGIN_FAULT", |
| BexError::PluginError(_) => "PLUGIN_ERROR", |
| BexError::Network(_) => "NETWORK", |
| BexError::Storage(_) => "STORAGE", |
| BexError::NotReady => "NOT_READY", |
| BexError::Internal(_) => "INTERNAL", |
| _ => "UNKNOWN", |
| } |
| } |
|
|
| |
| |
| unsafe fn cstr_to_string(ptr: *const c_char) -> Option<String> { |
| if ptr.is_null() { |
| return None; |
| } |
| CStr::from_ptr(ptr).to_str().ok().map(|s| s.to_string()) |
| } |
|
|
| |
| |
| |
|
|
| |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_new(data_dir: *const c_char) -> *mut BexEngineInner { |
| if data_dir.is_null() { |
| return std::ptr::null_mut(); |
| } |
|
|
| let data_dir_str = match cstr_to_string(data_dir) { |
| Some(s) => s, |
| None => return std::ptr::null_mut(), |
| }; |
|
|
| let config = EngineConfig { |
| data_dir: PathBuf::from(data_dir_str), |
| ..Default::default() |
| }; |
|
|
| match BexRuntime::new(config) { |
| Ok(runtime) => { |
| let inner = Box::new(BexEngineInner { |
| runtime, |
| next_request_id: AtomicU64::new(1), |
| last_error: parking_lot::Mutex::new(None), |
| }); |
| Box::into_raw(inner) |
| } |
| Err(e) => { |
| tracing::error!("Failed to create BexEngine: {}", e); |
| std::ptr::null_mut() |
| } |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_free(engine: *mut BexEngineInner) { |
| if engine.is_null() { |
| return; |
| } |
| let inner = Box::from_raw(engine); |
| inner.runtime.shutdown(); |
| } |
|
|
| |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_install( |
| engine: *mut BexEngineInner, |
| path: *const c_char, |
| ) -> i32 { |
| if engine.is_null() || path.is_null() { |
| return -1; |
| } |
| let inner = &*engine; |
| clear_last_error(inner); |
|
|
| let path_str = match cstr_to_string(path) { |
| Some(s) => s, |
| None => { |
| set_last_error(inner, "Invalid UTF-8 in path"); |
| return -1; |
| } |
| }; |
|
|
| match inner.runtime.install_plugin(std::path::Path::new(&path_str)) { |
| Ok(_) => 0, |
| Err(e) => error_to_code(inner, &e), |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_uninstall( |
| engine: *mut BexEngineInner, |
| id: *const c_char, |
| ) -> i32 { |
| if engine.is_null() || id.is_null() { |
| return -1; |
| } |
| let inner = &*engine; |
| clear_last_error(inner); |
|
|
| let id_str = match cstr_to_string(id) { |
| Some(s) => s, |
| None => { |
| set_last_error(inner, "Invalid UTF-8 in id"); |
| return -1; |
| } |
| }; |
|
|
| match inner.runtime.uninstall_plugin(&id_str) { |
| Ok(_) => 0, |
| Err(e) => error_to_code(inner, &e), |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_list_plugins( |
| engine: *mut BexEngineInner, |
| ) -> BexPluginInfoList { |
| if engine.is_null() { |
| return BexPluginInfoList { |
| items: std::ptr::null_mut(), |
| count: 0, |
| }; |
| } |
| let inner = &*engine; |
| let plugins = inner.runtime.list_plugins(); |
| let count = plugins.len(); |
|
|
| if count == 0 { |
| return BexPluginInfoList { |
| items: std::ptr::null_mut(), |
| count: 0, |
| }; |
| } |
|
|
| |
| let layout = std::alloc::Layout::array::<BexPluginInfo>(count).unwrap(); |
| let items_ptr = std::alloc::alloc(layout) as *mut BexPluginInfo; |
| if items_ptr.is_null() { |
| return BexPluginInfoList { |
| items: std::ptr::null_mut(), |
| count: 0, |
| }; |
| } |
|
|
| for (i, info) in plugins.iter().enumerate() { |
| let ffi_info = plugin_info_to_ffi(info); |
| std::ptr::write(items_ptr.add(i), ffi_info); |
| } |
|
|
| BexPluginInfoList { items: items_ptr, count } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_plugin_info( |
| engine: *mut BexEngineInner, |
| id: *const c_char, |
| out: *mut BexPluginInfo, |
| ) -> i32 { |
| if engine.is_null() || id.is_null() || out.is_null() { |
| return -1; |
| } |
| let inner = &*engine; |
| clear_last_error(inner); |
|
|
| let id_str = match cstr_to_string(id) { |
| Some(s) => s, |
| None => { |
| set_last_error(inner, "Invalid UTF-8 in id"); |
| return -1; |
| } |
| }; |
|
|
| match inner.runtime.get_plugin_info(&id_str) { |
| Some(info) => { |
| std::ptr::write(out, plugin_info_to_ffi(&info)); |
| 0 |
| } |
| None => { |
| set_last_error(inner, &format!("Plugin not found: {}", id_str)); |
| 2 |
| } |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_enable( |
| engine: *mut BexEngineInner, |
| id: *const c_char, |
| ) -> i32 { |
| if engine.is_null() || id.is_null() { |
| return -1; |
| } |
| let inner = &*engine; |
| clear_last_error(inner); |
|
|
| let id_str = match cstr_to_string(id) { |
| Some(s) => s, |
| None => return -1, |
| }; |
|
|
| match inner.runtime.enable_plugin(&id_str) { |
| Ok(_) => 0, |
| Err(e) => error_to_code(inner, &e), |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_disable( |
| engine: *mut BexEngineInner, |
| id: *const c_char, |
| ) -> i32 { |
| if engine.is_null() || id.is_null() { |
| return -1; |
| } |
| let inner = &*engine; |
| clear_last_error(inner); |
|
|
| let id_str = match cstr_to_string(id) { |
| Some(s) => s, |
| None => return -1, |
| }; |
|
|
| match inner.runtime.disable_plugin(&id_str) { |
| Ok(_) => 0, |
| Err(e) => error_to_code(inner, &e), |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_plugin_info_list_free(list: BexPluginInfoList) { |
| if list.items.is_null() || list.count == 0 { |
| return; |
| } |
|
|
| for i in 0..list.count { |
| let info = &*list.items.add(i); |
| if !info.id.is_null() { let _ = CString::from_raw(info.id); } |
| if !info.name.is_null() { let _ = CString::from_raw(info.name); } |
| if !info.version.is_null() { let _ = CString::from_raw(info.version); } |
| if !info.description.is_null() { let _ = CString::from_raw(info.description); } |
| if !info.author.is_null() { let _ = CString::from_raw(info.author); } |
| if !info.homepage.is_null() { let _ = CString::from_raw(info.homepage); } |
| } |
|
|
| let layout = std::alloc::Layout::array::<BexPluginInfo>(list.count).unwrap(); |
| std::alloc::dealloc(list.items as *mut u8, layout); |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_plugin_info_free(info: BexPluginInfo) { |
| if !info.id.is_null() { let _ = CString::from_raw(info.id); } |
| if !info.name.is_null() { let _ = CString::from_raw(info.name); } |
| if !info.version.is_null() { let _ = CString::from_raw(info.version); } |
| if !info.description.is_null() { let _ = CString::from_raw(info.description); } |
| if !info.author.is_null() { let _ = CString::from_raw(info.author); } |
| if !info.homepage.is_null() { let _ = CString::from_raw(info.homepage); } |
| } |
|
|
| |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_secret_set( |
| engine: *mut BexEngineInner, |
| plugin_id: *const c_char, |
| key: *const c_char, |
| value: *const c_char, |
| ) -> i32 { |
| if engine.is_null() || plugin_id.is_null() || key.is_null() || value.is_null() { |
| return -1; |
| } |
| let inner = &*engine; |
| clear_last_error(inner); |
|
|
| let pid = match cstr_to_string(plugin_id) { Some(s) => s, None => return -1 }; |
| let k = match cstr_to_string(key) { Some(s) => s, None => return -1 }; |
| let v = match cstr_to_string(value) { Some(s) => s, None => return -1 }; |
|
|
| match inner.runtime.secret_set(&pid, &k, &v) { |
| Ok(_) => 0, |
| Err(e) => error_to_code(inner, &e), |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_secret_get( |
| engine: *mut BexEngineInner, |
| plugin_id: *const c_char, |
| key: *const c_char, |
| out_buf: *mut c_char, |
| out_buf_len: *mut usize, |
| ) -> i32 { |
| if engine.is_null() || plugin_id.is_null() || key.is_null() |
| || out_buf.is_null() || out_buf_len.is_null() |
| { |
| return -1; |
| } |
| let inner = &*engine; |
| clear_last_error(inner); |
|
|
| let pid = match cstr_to_string(plugin_id) { Some(s) => s, None => return -1 }; |
| let k = match cstr_to_string(key) { Some(s) => s, None => return -1 }; |
|
|
| match inner.runtime.secret_get(&pid, &k) { |
| Ok(Some(val)) => { |
| let buf_size = *out_buf_len; |
| let val_bytes = val.as_bytes(); |
| let copy_len = val_bytes.len().min(buf_size - 1); |
| if copy_len < val_bytes.len() { |
| set_last_error(inner, "Output buffer too small"); |
| *out_buf_len = val_bytes.len() + 1; |
| return -2; |
| } |
| std::ptr::copy_nonoverlapping(val_bytes.as_ptr(), out_buf as *mut u8, copy_len); |
| *out_buf.add(copy_len) = 0; |
| *out_buf_len = copy_len; |
| 0 |
| } |
| Ok(None) => { |
| set_last_error(inner, &format!("Secret '{}' not found for plugin '{}'", k, pid)); |
| 1 |
| } |
| Err(e) => error_to_code(inner, &e), |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_secret_delete( |
| engine: *mut BexEngineInner, |
| plugin_id: *const c_char, |
| key: *const c_char, |
| ) -> i32 { |
| if engine.is_null() || plugin_id.is_null() || key.is_null() { |
| return -1; |
| } |
| let inner = &*engine; |
| clear_last_error(inner); |
|
|
| let pid = match cstr_to_string(plugin_id) { Some(s) => s, None => return -1 }; |
| let k = match cstr_to_string(key) { Some(s) => s, None => return -1 }; |
|
|
| match inner.runtime.secret_remove(&pid, &k) { |
| Ok(true) => 0, |
| Ok(false) => 1, |
| Err(e) => error_to_code(inner, &e), |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_secret_keys( |
| engine: *mut BexEngineInner, |
| plugin_id: *const c_char, |
| ) -> *mut c_char { |
| if engine.is_null() || plugin_id.is_null() { |
| return std::ptr::null_mut(); |
| } |
| let inner = &*engine; |
|
|
| let pid = match cstr_to_string(plugin_id) { |
| Some(s) => s, |
| None => return std::ptr::null_mut(), |
| }; |
|
|
| match inner.runtime.secret_keys(&pid) { |
| Ok(keys) => { |
| let joined = keys.join(","); |
| str_to_cstring(&joined) |
| } |
| Err(_) => std::ptr::null_mut(), |
| } |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_string_free(s: *mut c_char) { |
| if !s.is_null() { |
| let _ = CString::from_raw(s); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_submit_search( |
| engine: *mut BexEngineInner, |
| plugin_id: *const c_char, |
| query: *const c_char, |
| callback: ResultCallback, |
| user_data: *mut c_void, |
| ) -> u64 { |
| let pid = match cstr_to_string(plugin_id) { Some(s) => s, None => return 0 }; |
| let query_str = match cstr_to_string(query) { Some(s) => s, None => return 0 }; |
|
|
| submit_async(engine, pid, callback, user_data, move |engine, pid| { |
| engine.call_search_json(pid, &query_str) |
| .map(|s| s.into_bytes()) |
| }) |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_submit_home( |
| engine: *mut BexEngineInner, |
| plugin_id: *const c_char, |
| callback: ResultCallback, |
| user_data: *mut c_void, |
| ) -> u64 { |
| let pid = match cstr_to_string(plugin_id) { Some(s) => s, None => return 0 }; |
|
|
| submit_async(engine, pid, callback, user_data, move |engine, pid| { |
| engine.call_get_home_json(pid) |
| .map(|s| s.into_bytes()) |
| }) |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_submit_info( |
| engine: *mut BexEngineInner, |
| plugin_id: *const c_char, |
| media_id: *const c_char, |
| callback: ResultCallback, |
| user_data: *mut c_void, |
| ) -> u64 { |
| let pid = match cstr_to_string(plugin_id) { Some(s) => s, None => return 0 }; |
| let mid = match cstr_to_string(media_id) { Some(s) => s, None => return 0 }; |
|
|
| submit_async(engine, pid, callback, user_data, move |engine, pid| { |
| engine.call_get_info_json(pid, &mid) |
| .map(|s| s.into_bytes()) |
| }) |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_submit_servers( |
| engine: *mut BexEngineInner, |
| plugin_id: *const c_char, |
| id: *const c_char, |
| callback: ResultCallback, |
| user_data: *mut c_void, |
| ) -> u64 { |
| let pid = match cstr_to_string(plugin_id) { Some(s) => s, None => return 0 }; |
| let id_str = match cstr_to_string(id) { Some(s) => s, None => return 0 }; |
|
|
| submit_async(engine, pid, callback, user_data, move |engine, pid| { |
| engine.call_get_servers_json(pid, &id_str) |
| .map(|s| s.into_bytes()) |
| }) |
| } |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_submit_stream( |
| engine: *mut BexEngineInner, |
| plugin_id: *const c_char, |
| server_json: *const c_char, |
| callback: ResultCallback, |
| user_data: *mut c_void, |
| ) -> u64 { |
| let pid = match cstr_to_string(plugin_id) { Some(s) => s, None => return 0 }; |
| let server_str = match cstr_to_string(server_json) { Some(s) => s, None => return 0 }; |
|
|
| submit_async(engine, pid, callback, user_data, move |engine, pid| { |
| engine.call_resolve_stream_json(pid, &server_str) |
| .map(|s| s.into_bytes()) |
| }) |
| } |
|
|
| |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_cancel_request( |
| engine: *mut BexEngineInner, |
| request_id: u64, |
| ) -> bool { |
| if engine.is_null() { |
| return false; |
| } |
| let inner = &*engine; |
| inner.runtime.cancel_request(request_id) |
| } |
|
|
| |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_stats( |
| engine: *mut BexEngineInner, |
| ) -> *mut c_char { |
| if engine.is_null() { |
| return std::ptr::null_mut(); |
| } |
| let inner = &*engine; |
| let stats = inner.runtime.stats(); |
| match serde_json::to_string(&stats) { |
| Ok(json) => str_to_cstring(&json), |
| Err(_) => std::ptr::null_mut(), |
| } |
| } |
|
|
| |
|
|
| #[no_mangle] |
| pub unsafe extern "C" fn bex_engine_last_error( |
| engine: *mut BexEngineInner, |
| ) -> *mut c_char { |
| if engine.is_null() { |
| return std::ptr::null_mut(); |
| } |
| let inner = &*engine; |
| match inner.last_error.lock().as_ref() { |
| Some(cstr) => { |
| let bytes = cstr.as_bytes(); |
| let dup = CString::from_vec_unchecked(bytes.to_vec()); |
| dup.into_raw() |
| } |
| None => std::ptr::null_mut(), |
| } |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| unsafe fn submit_async<F>( |
| engine: *mut BexEngineInner, |
| pid: String, |
| callback: ResultCallback, |
| user_data: *mut c_void, |
| work: F, |
| ) -> u64 |
| where |
| F: FnOnce(&bex_core::Engine, &str) -> Result<Vec<u8>, BexError> + Send + 'static, |
| { |
| if engine.is_null() { |
| return 0; |
| } |
|
|
| let inner = &*engine; |
| let request_id = inner.next_request_id.fetch_add(1, Ordering::Relaxed); |
|
|
| |
| let engine_clone = inner.runtime.clone_engine(); |
|
|
| |
| |
| |
| |
| let callback_addr = callback as usize; |
| let user_data_addr = user_data as usize; |
|
|
| |
| let cancel_token = tokio_util::sync::CancellationToken::new(); |
| inner.runtime.insert_cancellation(request_id, cancel_token.clone()); |
|
|
| |
| let rt_handle = inner.runtime.tokio_handle(); |
|
|
| rt_handle.spawn(async move { |
| |
| if cancel_token.is_cancelled() { |
| invoke_callback(callback_addr, user_data_addr, request_id, false, |
| format!("CANCELLED: Request {} was cancelled", request_id).as_bytes()); |
| return; |
| } |
|
|
| |
| |
| let result = tokio::task::spawn_blocking(move || { |
| work(&engine_clone, &pid) |
| }).await; |
|
|
| match result { |
| Ok(Ok(payload)) => { |
| invoke_callback(callback_addr, user_data_addr, request_id, true, &payload); |
| } |
| Ok(Err(e)) => { |
| let err_msg = format!("{}: {}", error_code_short(&e), e); |
| invoke_callback(callback_addr, user_data_addr, request_id, false, err_msg.as_bytes()); |
| } |
| Err(_) => { |
| invoke_callback(callback_addr, user_data_addr, request_id, false, |
| b"INTERNAL: Worker thread panicked"); |
| } |
| } |
| }); |
|
|
| request_id |
| } |
|
|
| |
| unsafe fn invoke_callback( |
| callback_addr: usize, |
| user_data_addr: usize, |
| request_id: u64, |
| success: bool, |
| payload: &[u8], |
| ) { |
| let cb: ResultCallback = std::mem::transmute(callback_addr); |
| let ud = user_data_addr as *mut c_void; |
| cb(ud, request_id, success, payload.as_ptr(), payload.len()); |
| } |
|
|