Download crates/bex-runtime/src/ffi.rs from krystv/pluginengine01: direct link, hf CLI and curl.
- Browser
- Download file 24.1 kB
-
https://huggingface.co/krystv/pluginengine01/resolve/main/crates/bex-runtime/src/ffi.rs
- Command line
-
hf download hf://krystv/pluginengine01/crates/bex-runtime/src/ffi.rs
-
curl -L -o ffi.rs https://huggingface.co/krystv/pluginengine01/resolve/main/crates/bex-runtime/src/ffi.rs
24.1 kB
| //! Pure C ABI for the Bex WASM Plugin Engine. | |
| //! | |
| //! This module exports `extern "C"` functions that match the declarations in | |
| //! `bex_engine.h`. The architecture is callback-driven: | |
| //! | |
| //! 1. C++ calls `bex_submit_search(engine, plugin_id, query, callback, user_data)` | |
| //! 2. Rust spawns a Tokio task that does the work | |
| //! 3. On completion, Rust invokes `callback(user_data, request_id, success, payload, len)` | |
| //! from the Tokio background thread | |
| //! 4. C++ receives the result and can parse/copy it before the callback returns | |
| //! | |
| //! There is NO event queue, NO polling, NO cxx dependency. | |
| //! This is a clean, high-performance Pure C ABI boundary. | |
| 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; | |
| // ββ Opaque Engine Handle ββββββββββββββββββββββββββββββββββββββββββββββ | |
| /// The internal representation behind the opaque `BexEngine*` pointer. | |
| pub struct BexEngineInner { | |
| runtime: BexRuntime, | |
| next_request_id: AtomicU64, | |
| /// Stores the last error message from a sync operation. | |
| last_error: parking_lot::Mutex<Option<CString>>, | |
| } | |
| // ββ Callback type matching bex_engine.h βββββββββββββββββββββββββββββββ | |
| type ResultCallback = unsafe extern "C" fn( | |
| user_data: *mut c_void, | |
| request_id: u64, | |
| success: bool, | |
| payload: *const u8, | |
| payload_len: usize, | |
| ); | |
| // ββ FFI-visible structs βββββββββββββββββββββββββββββββββββββββββββββββ | |
| /// FFI-visible BexPluginInfo struct β must match the C header exactly. | |
| 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, | |
| } | |
| /// FFI-visible BexPluginInfoList β must match the C header exactly. | |
| pub struct BexPluginInfoList { | |
| pub items: *mut BexPluginInfo, | |
| pub count: usize, | |
| } | |
| // ββ Helper functions ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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", | |
| } | |
| } | |
| /// Helper to convert a C string pointer to a Rust String. | |
| /// Returns None if the pointer is null or invalid UTF-8. | |
| 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()) | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // FFI-exported functions β must match bex_engine.h exactly | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ Lifecycle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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() | |
| } | |
| } | |
| } | |
| 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(); | |
| } | |
| // ββ Plugin Management (synchronous) ββββββββββββββββββββββββββββββββββ | |
| 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), | |
| } | |
| } | |
| 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), | |
| } | |
| } | |
| 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, | |
| }; | |
| } | |
| // Allocate array of BexPluginInfo | |
| 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 } | |
| } | |
| 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 | |
| } | |
| } | |
| } | |
| 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), | |
| } | |
| } | |
| 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), | |
| } | |
| } | |
| 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); | |
| } | |
| 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); } | |
| } | |
| // ββ API Key / Secret Management (synchronous) ββββββββββββββββββββββββ | |
| 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), | |
| } | |
| } | |
| 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), | |
| } | |
| } | |
| 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), | |
| } | |
| } | |
| 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(), | |
| } | |
| } | |
| pub unsafe extern "C" fn bex_string_free(s: *mut c_char) { | |
| if !s.is_null() { | |
| let _ = CString::from_raw(s); | |
| } | |
| } | |
| // ββ Async Operations βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // | |
| // Each submit function: | |
| // 1. Converts all C strings to owned Rust Strings BEFORE creating the closure | |
| // 2. Generates a request_id | |
| // 3. Spawns a Tokio task that executes the work and invokes the callback | |
| // | |
| // The closure captures only owned types (String, Arc<Engine>, etc.) β | |
| // no raw pointers, making it Send-safe. | |
| 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()) | |
| }) | |
| } | |
| 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()) | |
| }) | |
| } | |
| 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()) | |
| }) | |
| } | |
| 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()) | |
| }) | |
| } | |
| 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()) | |
| }) | |
| } | |
| // ββ Cancellation βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |
| } | |
| // ββ Engine Stats βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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(), | |
| } | |
| } | |
| // ββ Last Error βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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(), | |
| } | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Internal async submit helper | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /// Common implementation for all async submit functions. | |
| /// | |
| /// - `engine`: raw pointer to BexEngineInner | |
| /// - `pid`: owned String (plugin_id, already converted from C) | |
| /// - `callback`: C function pointer | |
| /// - `user_data`: opaque pointer from C++ | |
| /// - `work`: closure that takes `&bex_core::Engine` + `&str` (plugin_id), | |
| /// does the work, and returns `Result<Vec<u8>, BexError>` | |
| /// | |
| /// All C strings must be converted to Rust Strings BEFORE calling this, | |
| /// so the closure only captures Send types. | |
| 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); | |
| // Clone the engine (Arc-based, cheap) for the spawned task | |
| let engine_clone = inner.runtime.clone_engine(); | |
| // Store callback and user_data as usize for Send-safety across threads. | |
| // The callback is a function pointer (inherently Send), and user_data | |
| // is an opaque pointer that C++ guarantees remains valid until callback | |
| // is invoked. | |
| let callback_addr = callback as usize; | |
| let user_data_addr = user_data as usize; | |
| // Get cancellation token | |
| let cancel_token = tokio_util::sync::CancellationToken::new(); | |
| inner.runtime.insert_cancellation(request_id, cancel_token.clone()); | |
| // Spawn on the BexRuntime's internal Tokio runtime | |
| let rt_handle = inner.runtime.tokio_handle(); | |
| rt_handle.spawn(async move { | |
| // Check cancellation before starting | |
| 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; | |
| } | |
| // Execute the work using spawn_blocking since bex-core Engine | |
| // internally uses its own Tokio runtime for HTTP/WASM operations. | |
| 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 | |
| } | |
| /// Invoke the C callback with a byte payload. | |
| 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()); | |
| } | |