rustbridge_ffi/exports.rs
1//! C ABI exported functions
2//!
3//! These functions are the FFI entry points called by host languages.
4
5use crate::binary_types::RbResponse;
6use crate::buffer::FfiBuffer;
7use crate::handle::{PluginHandle, PluginHandleManager};
8use crate::panic_guard::catch_panic;
9use dashmap::DashMap;
10use once_cell::sync::OnceCell;
11use rustbridge_core::{LogLevel, PluginConfig};
12use rustbridge_logging::{LogCallback, LogCallbackManager};
13use rustbridge_transport::ResponseEnvelope;
14use std::ffi::c_void;
15use std::panic::AssertUnwindSafe;
16use std::ptr;
17
18/// Opaque handle type for FFI
19pub type FfiPluginHandle = *mut c_void;
20
21/// Initialize a plugin instance
22///
23/// # Parameters
24/// - `plugin_ptr`: Pointer to the plugin instance (from plugin_create)
25/// - `config_json`: JSON configuration bytes (can be null for defaults)
26/// - `config_len`: Length of config_json
27/// - `log_callback`: Optional log callback function
28///
29/// # Returns
30/// Handle to the initialized plugin, or null on failure
31///
32/// # Safety
33/// - `plugin_ptr` must be a valid pointer from `plugin_create`
34/// - `config_json` must be valid for `config_len` bytes if not null
35/// - The log callback must remain valid for the lifetime of the plugin
36#[unsafe(no_mangle)]
37pub unsafe extern "C" fn plugin_init(
38 plugin_ptr: *mut c_void,
39 config_json: *const u8,
40 config_len: usize,
41 log_callback: Option<LogCallback>,
42) -> FfiPluginHandle {
43 // Wrap in panic handler (handle_id = 0 since no handle exists yet)
44 match catch_panic(
45 0,
46 AssertUnwindSafe(|| unsafe {
47 plugin_init_impl(plugin_ptr, config_json, config_len, log_callback)
48 }),
49 ) {
50 Ok(handle) => handle,
51 Err(_error_buffer) => {
52 // For plugin_init, return null on panic instead of FfiBuffer
53 // since we're returning a handle, not a buffer
54 ptr::null_mut()
55 }
56 }
57}
58
59/// Internal implementation of plugin_init (wrapped by panic handler)
60unsafe fn plugin_init_impl(
61 plugin_ptr: *mut c_void,
62 config_json: *const u8,
63 config_len: usize,
64 log_callback: Option<LogCallback>,
65) -> FfiPluginHandle {
66 // Validate plugin pointer
67 if plugin_ptr.is_null() {
68 return ptr::null_mut();
69 }
70
71 // Parse configuration FIRST (before initializing logging)
72 let config = if config_json.is_null() || config_len == 0 {
73 PluginConfig::default()
74 } else {
75 // SAFETY: caller guarantees config_json is valid for config_len bytes
76 let config_slice = unsafe { std::slice::from_raw_parts(config_json, config_len) };
77 match PluginConfig::from_json(config_slice) {
78 Ok(c) => c,
79 Err(e) => {
80 // Can't use tracing yet since logging isn't initialized
81 eprintln!("Failed to parse config: {}", e);
82 return ptr::null_mut();
83 }
84 }
85 };
86
87 // Register plugin with callback manager (increments ref count)
88 LogCallbackManager::global().register_plugin(log_callback);
89
90 // Set the log level from config BEFORE initializing logging
91 let log_level = match config.log_level.to_lowercase().as_str() {
92 "trace" => LogLevel::Trace,
93 "debug" => LogLevel::Debug,
94 "info" => LogLevel::Info,
95 "warn" => LogLevel::Warn,
96 "error" => LogLevel::Error,
97 "off" => LogLevel::Off,
98 _ => LogLevel::Info, // Default to Info for unknown values
99 };
100 LogCallbackManager::global().set_level(log_level);
101
102 // Initialize logging with the configured level
103 rustbridge_logging::init_logging();
104
105 // Install panic hook to log panics via FFI callback
106 crate::panic_guard::install_panic_hook();
107
108 // Take ownership of the plugin
109 // SAFETY: caller guarantees plugin_ptr is from plugin_create
110 let plugin: Box<Box<dyn rustbridge_core::Plugin>> =
111 unsafe { Box::from_raw(plugin_ptr as *mut Box<dyn rustbridge_core::Plugin>) };
112
113 // Create the handle
114 let handle = match PluginHandle::new(*plugin, config) {
115 Ok(h) => h,
116 Err(e) => {
117 tracing::error!("Failed to create handle: {}", e);
118 return ptr::null_mut();
119 }
120 };
121
122 // Start the plugin
123 if let Err(e) = handle.start() {
124 tracing::error!("Failed to start plugin: {}", e);
125 return ptr::null_mut();
126 }
127
128 // Register and return handle
129 let id = PluginHandleManager::global().register(handle);
130
131 // Store ID in the handle
132 if let Some(h) = PluginHandleManager::global().get(id) {
133 h.set_id(id);
134 }
135
136 id as FfiPluginHandle
137}
138
139/// Make a synchronous call to the plugin
140///
141/// # Parameters
142/// - `handle`: Plugin handle from plugin_init
143/// - `type_tag`: Message type identifier (null-terminated C string)
144/// - `request`: Request payload bytes
145/// - `request_len`: Length of request payload
146///
147/// # Returns
148/// FfiBuffer containing the response (must be freed with plugin_free_buffer)
149///
150/// # Safety
151/// - `handle` must be a valid handle from plugin_init
152/// - `type_tag` must be a valid null-terminated C string
153/// - `request` must be valid for `request_len` bytes
154#[unsafe(no_mangle)]
155pub unsafe extern "C" fn plugin_call(
156 handle: FfiPluginHandle,
157 type_tag: *const std::ffi::c_char,
158 request: *const u8,
159 request_len: usize,
160) -> FfiBuffer {
161 // Wrap in panic handler
162 let handle_id = handle as u64;
163 match catch_panic(
164 handle_id,
165 AssertUnwindSafe(|| unsafe { plugin_call_impl(handle, type_tag, request, request_len) }),
166 ) {
167 Ok(result) => result,
168 Err(error_buffer) => error_buffer,
169 }
170}
171
172/// Internal implementation of plugin_call (wrapped by panic handler)
173unsafe fn plugin_call_impl(
174 handle: FfiPluginHandle,
175 type_tag: *const std::ffi::c_char,
176 request: *const u8,
177 request_len: usize,
178) -> FfiBuffer {
179 // Validate handle
180 let id = handle as u64;
181 let plugin_handle = match PluginHandleManager::global().get(id) {
182 Some(h) => h,
183 None => return FfiBuffer::error(1, "Invalid handle"),
184 };
185
186 // Parse type tag
187 let type_tag_str = if type_tag.is_null() {
188 return FfiBuffer::error(4, "Type tag is null");
189 } else {
190 // SAFETY: caller guarantees type_tag is a valid null-terminated C string
191 match unsafe { std::ffi::CStr::from_ptr(type_tag) }.to_str() {
192 Ok(s) => s,
193 Err(_) => return FfiBuffer::error(4, "Invalid type tag encoding"),
194 }
195 };
196
197 // Get request data
198 let request_data = if request.is_null() || request_len == 0 {
199 &[]
200 } else {
201 // SAFETY: caller guarantees request is valid for request_len bytes
202 unsafe { std::slice::from_raw_parts(request, request_len) }
203 };
204
205 // Make the call
206 match plugin_handle.call(type_tag_str, request_data) {
207 Ok(response_data) => {
208 // Wrap in response envelope
209 match ResponseEnvelope::success_raw(&response_data) {
210 Ok(envelope) => match envelope.to_bytes() {
211 Ok(bytes) => FfiBuffer::from_vec(bytes),
212 Err(e) => FfiBuffer::error(5, &format!("Serialization error: {}", e)),
213 },
214 Err(e) => FfiBuffer::error(5, &format!("Serialization error: {}", e)),
215 }
216 }
217 Err(e) => {
218 let envelope = ResponseEnvelope::from_error(&e);
219 match envelope.to_bytes() {
220 Ok(bytes) => {
221 let mut buf = FfiBuffer::from_vec(bytes);
222 buf.error_code = e.error_code();
223 buf
224 }
225 Err(se) => FfiBuffer::error(e.error_code(), &format!("{}: {}", e, se)),
226 }
227 }
228 }
229}
230
231/// Free a buffer returned by plugin_call
232///
233/// # Safety
234/// - `buffer` must be a valid FfiBuffer from plugin_call
235/// - Must only be called once per buffer
236#[unsafe(no_mangle)]
237pub unsafe extern "C" fn plugin_free_buffer(buffer: *mut FfiBuffer) {
238 unsafe {
239 if !buffer.is_null() {
240 (*buffer).free();
241 }
242 }
243}
244
245/// Shutdown a plugin instance
246///
247/// # Parameters
248/// - `handle`: Plugin handle from plugin_init
249///
250/// # Returns
251/// true on success, false on failure
252///
253/// # Safety
254/// - `handle` must be a valid handle from plugin_init
255/// - After this call, the handle is no longer valid
256#[unsafe(no_mangle)]
257pub unsafe extern "C" fn plugin_shutdown(handle: FfiPluginHandle) -> bool {
258 // Wrap in panic handler
259 let handle_id = handle as u64;
260 catch_panic(handle_id, AssertUnwindSafe(|| plugin_shutdown_impl(handle))).unwrap_or_default() // Returns false on panic
261}
262
263/// Internal implementation of plugin_shutdown (wrapped by panic handler)
264fn plugin_shutdown_impl(handle: FfiPluginHandle) -> bool {
265 let id = handle as u64;
266
267 // Remove from manager
268 let plugin_handle = match PluginHandleManager::global().remove(id) {
269 Some(h) => h,
270 None => return false,
271 };
272
273 // Shutdown with default timeout
274 let result = match plugin_handle.shutdown(5000) {
275 Ok(()) => true,
276 Err(e) => {
277 tracing::error!("Shutdown error: {}", e);
278 false
279 }
280 };
281
282 // Clear binary handlers for this thread to avoid stale handlers on reload
283 clear_binary_handlers();
284
285 // Unregister plugin from callback manager (decrements ref count)
286 // The callback will only be cleared when the last plugin shuts down.
287 // This allows multiple plugins to coexist and share the same callback.
288 LogCallbackManager::global().unregister_plugin();
289
290 result
291}
292
293/// Set the log level for a plugin
294///
295/// # Parameters
296/// - `handle`: Plugin handle from plugin_init
297/// - `level`: Log level (0=Trace, 1=Debug, 2=Info, 3=Warn, 4=Error, 5=Off)
298///
299/// # Safety
300/// - `handle` must be a valid handle from plugin_init
301#[unsafe(no_mangle)]
302pub unsafe extern "C" fn plugin_set_log_level(handle: FfiPluginHandle, level: u8) {
303 let id = handle as u64;
304
305 if let Some(plugin_handle) = PluginHandleManager::global().get(id) {
306 plugin_handle.set_log_level(LogLevel::from_u8(level));
307 }
308}
309
310/// Get the current state of a plugin
311///
312/// # Parameters
313/// - `handle`: Plugin handle from plugin_init
314///
315/// # Returns
316/// State code (0=Installed, 1=Starting, 2=Active, 3=Stopping, 4=Stopped, 5=Failed)
317/// Returns 255 if handle is invalid
318///
319/// # Safety
320/// - `handle` must be a valid handle from plugin_init
321#[unsafe(no_mangle)]
322pub unsafe extern "C" fn plugin_get_state(handle: FfiPluginHandle) -> u8 {
323 let id = handle as u64;
324
325 match PluginHandleManager::global().get(id) {
326 Some(h) => match h.state() {
327 rustbridge_core::LifecycleState::Installed => 0,
328 rustbridge_core::LifecycleState::Starting => 1,
329 rustbridge_core::LifecycleState::Active => 2,
330 rustbridge_core::LifecycleState::Stopping => 3,
331 rustbridge_core::LifecycleState::Stopped => 4,
332 rustbridge_core::LifecycleState::Failed => 5,
333 },
334 None => 255,
335 }
336}
337
338/// Get the number of requests rejected due to concurrency limits
339///
340/// # Parameters
341/// - `handle`: Plugin handle from plugin_init
342///
343/// # Returns
344/// Number of rejected requests since plugin initialization. Returns 0 if handle is invalid.
345///
346/// # Safety
347/// - `handle` must be a valid handle from plugin_init
348#[unsafe(no_mangle)]
349pub unsafe extern "C" fn plugin_get_rejected_count(handle: FfiPluginHandle) -> u64 {
350 let id = handle as u64;
351 match PluginHandleManager::global().get(id) {
352 Some(h) => h.rejected_request_count(),
353 None => 0,
354 }
355}
356
357// ============================================================================
358// Binary Transport Functions
359// ============================================================================
360
361/// Handler function type for binary message processing
362///
363/// Plugins that want to support binary transport must register handlers
364/// using this signature. The handler receives the request struct as raw bytes
365/// and must return response bytes (which will be wrapped in RbResponse).
366pub type BinaryMessageHandler =
367 fn(handle: &PluginHandle, request: &[u8]) -> Result<Vec<u8>, rustbridge_core::PluginError>;
368
369/// Global registry for binary message handlers
370///
371/// This uses a thread-safe DashMap so handlers registered from the Tokio
372/// worker thread are visible when plugin_call_raw is called from the host
373/// language thread.
374static BINARY_HANDLERS: OnceCell<DashMap<u32, BinaryMessageHandler>> = OnceCell::new();
375
376/// Get the binary handlers registry, initializing if needed
377fn binary_handlers() -> &'static DashMap<u32, BinaryMessageHandler> {
378 BINARY_HANDLERS.get_or_init(DashMap::new)
379}
380
381/// Register a binary message handler
382///
383/// Call this during plugin initialization to register handlers for
384/// binary transport message types.
385pub fn register_binary_handler(message_id: u32, handler: BinaryMessageHandler) {
386 binary_handlers().insert(message_id, handler);
387}
388
389/// Clear all binary message handlers
390///
391/// This should be called during plugin shutdown to ensure handlers
392/// don't persist across plugin reload cycles.
393pub(crate) fn clear_binary_handlers() {
394 binary_handlers().clear();
395}
396
397/// Make a synchronous binary call to the plugin
398///
399/// # Parameters
400/// - `handle`: Plugin handle from plugin_init
401/// - `message_id`: Numeric message identifier
402/// - `request`: Pointer to request struct
403/// - `request_size`: Size of request struct (for validation)
404///
405/// # Returns
406/// RbResponse containing the binary response (must be freed with rb_response_free)
407///
408/// # Safety
409/// - `handle` must be a valid handle from plugin_init
410/// - `request` must be valid for `request_size` bytes
411/// - The request struct must match the expected type for `message_id`
412#[unsafe(no_mangle)]
413pub unsafe extern "C" fn plugin_call_raw(
414 handle: FfiPluginHandle,
415 message_id: u32,
416 request: *const c_void,
417 request_size: usize,
418) -> RbResponse {
419 // Wrap in panic handler
420 let handle_id = handle as u64;
421 match catch_panic(
422 handle_id,
423 AssertUnwindSafe(|| unsafe {
424 plugin_call_raw_impl(handle, message_id, request, request_size)
425 }),
426 ) {
427 Ok(result) => result,
428 Err(error_buffer) => {
429 // Convert FfiBuffer error to RbResponse error
430 let msg = if error_buffer.is_error() && !error_buffer.data.is_null() {
431 // SAFETY: error buffer contains a valid string
432 let slice =
433 unsafe { std::slice::from_raw_parts(error_buffer.data, error_buffer.len) };
434 String::from_utf8_lossy(slice).into_owned()
435 } else {
436 "Internal error (panic)".to_string()
437 };
438 // Free the original error buffer
439 let mut buf = error_buffer;
440 // SAFETY: buf is a valid FfiBuffer from catch_panic
441 unsafe { buf.free() };
442 RbResponse::error(11, &msg)
443 }
444 }
445}
446
447/// Internal implementation of plugin_call_raw (wrapped by panic handler)
448unsafe fn plugin_call_raw_impl(
449 handle: FfiPluginHandle,
450 message_id: u32,
451 request: *const c_void,
452 request_size: usize,
453) -> RbResponse {
454 // Validate handle
455 let id = handle as u64;
456 let plugin_handle = match PluginHandleManager::global().get(id) {
457 Some(h) => h,
458 None => return RbResponse::error(1, "Invalid handle"),
459 };
460
461 // Check plugin state
462 if !plugin_handle.state().can_handle_requests() {
463 return RbResponse::error(1, "Plugin not in Active state");
464 }
465
466 // Get request data
467 let request_data = if request.is_null() || request_size == 0 {
468 &[]
469 } else {
470 // SAFETY: caller guarantees request is valid for request_size bytes
471 unsafe { std::slice::from_raw_parts(request as *const u8, request_size) }
472 };
473
474 // Look up handler
475 let handler = binary_handlers().get(&message_id).map(|r| *r);
476
477 match handler {
478 Some(h) => {
479 // Call the handler
480 match h(&plugin_handle, request_data) {
481 Ok(response_bytes) => {
482 // Return raw bytes as response
483 // The caller is responsible for interpreting the bytes as the correct struct
484 let mut response = RbResponse::empty();
485 let len = response_bytes.len();
486 let capacity = response_bytes.capacity();
487 let data = response_bytes.leak().as_mut_ptr();
488
489 response.error_code = 0;
490 response.len = len as u32;
491 response.capacity = capacity as u32;
492 response.data = data as *mut c_void;
493
494 response
495 }
496 Err(e) => RbResponse::error(e.error_code(), &e.to_string()),
497 }
498 }
499 None => RbResponse::error(6, &format!("Unknown message ID: {}", message_id)),
500 }
501}
502
503/// Free an RbResponse returned by plugin_call_raw
504///
505/// # Safety
506/// - `response` must be a valid pointer to an RbResponse from plugin_call_raw
507/// - Must only be called once per response
508#[unsafe(no_mangle)]
509pub unsafe extern "C" fn rb_response_free(response: *mut RbResponse) {
510 unsafe {
511 if !response.is_null() {
512 (*response).free();
513 }
514 }
515}
516
517// ============================================================================
518// Async API (future implementation)
519// ============================================================================
520
521// Type definitions for async API (for future implementation)
522
523/// Completion callback for async requests
524pub type CompletionCallbackFn = extern "C" fn(
525 context: *mut c_void,
526 request_id: u64,
527 data: *const u8,
528 len: usize,
529 error_code: u32,
530);
531
532/// Make an async call to the plugin (placeholder for future implementation)
533///
534/// # Safety
535/// - `handle` must be a valid handle from `plugin_init`, or null
536/// - `type_tag` must be a valid null-terminated C string, or null
537/// - `request` must be valid for `request_len` bytes, or null if `request_len` is 0
538/// - `callback` will be invoked when the request completes
539/// - `context` is passed through to the callback
540///
541/// # Returns
542/// Request ID that can be used with plugin_cancel_async, or 0 if not implemented
543#[unsafe(no_mangle)]
544pub unsafe extern "C" fn plugin_call_async(
545 _handle: FfiPluginHandle,
546 _type_tag: *const std::ffi::c_char,
547 _request: *const u8,
548 _request_len: usize,
549 _callback: CompletionCallbackFn,
550 _context: *mut c_void,
551) -> u64 {
552 // TODO: Implement async call support
553 0 // Return 0 to indicate not implemented
554}
555
556/// Cancel an async request (placeholder for future implementation)
557///
558/// # Safety
559/// - `handle` must be a valid handle from `plugin_init`, or null
560/// - `request_id` must be a valid request ID from `plugin_call_async`
561///
562/// # Returns
563/// `true` if cancellation was successful, `false` otherwise
564#[unsafe(no_mangle)]
565pub unsafe extern "C" fn plugin_cancel_async(_handle: FfiPluginHandle, _request_id: u64) -> bool {
566 // TODO: Implement async cancellation
567 false
568}
569
570#[cfg(test)]
571#[path = "exports/exports_tests.rs"]
572mod exports_tests;
573
574#[cfg(test)]
575#[path = "exports/ffi_boundary_tests.rs"]
576mod ffi_boundary_tests;