Skip to main content

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// =============================================================================
522// Async API (Reserved for Future Implementation)
523// =============================================================================
524//
525// These functions are reserved for a future async API that will allow
526// non-blocking plugin calls with completion callbacks. They are exported
527// to maintain ABI stability but currently return stub values.
528//
529// Planned features:
530// - Non-blocking request submission with callback-based completion
531// - Request cancellation support
532// - Integration with host language async runtimes (tokio, async-std, etc.)
533
534/// Completion callback for async requests.
535///
536/// When implemented, this callback will be invoked when an async request completes.
537/// The callback receives the original context pointer, request ID, response data,
538/// and an error code (0 for success).
539pub type CompletionCallbackFn = extern "C" fn(
540    context: *mut c_void,
541    request_id: u64,
542    data: *const u8,
543    len: usize,
544    error_code: u32,
545);
546
547/// Initiate an asynchronous plugin call.
548///
549/// # Status
550///
551/// **Not yet implemented.** This function is reserved for future async API support.
552/// Currently returns 0 to indicate the operation is not available.
553///
554/// # Planned Behavior
555///
556/// When implemented, this function will:
557/// 1. Accept a request and completion callback
558/// 2. Submit the request for async processing
559/// 3. Return a non-zero request ID for tracking/cancellation
560/// 4. Invoke the callback with the response when complete
561///
562/// # Safety
563///
564/// When implemented, the following invariants must hold:
565/// - `handle` must be a valid handle from `plugin_init`, or null
566/// - `type_tag` must be a valid null-terminated C string, or null
567/// - `request` must be valid for `request_len` bytes, or null if `request_len` is 0
568/// - `callback` must remain valid until invoked or the request is cancelled
569/// - `context` is passed through to the callback unchanged
570///
571/// # Returns
572///
573/// Request ID that can be used with `plugin_cancel_async`, or 0 if not implemented.
574#[unsafe(no_mangle)]
575pub unsafe extern "C" fn plugin_call_async(
576    _handle: FfiPluginHandle,
577    _type_tag: *const std::ffi::c_char,
578    _request: *const u8,
579    _request_len: usize,
580    _callback: CompletionCallbackFn,
581    _context: *mut c_void,
582) -> u64 {
583    // Reserved for future implementation
584    0
585}
586
587/// Cancel a pending async request.
588///
589/// # Status
590///
591/// **Not yet implemented.** This function is reserved for future async API support.
592/// Currently returns `false` to indicate the operation is not available.
593///
594/// # Planned Behavior
595///
596/// When implemented, this function will:
597/// 1. Attempt to cancel the pending request identified by `request_id`
598/// 2. Return `true` if cancellation succeeded, `false` if the request
599///    already completed or was not found
600/// 3. The completion callback will NOT be invoked for cancelled requests
601///
602/// # Safety
603///
604/// When implemented, the following invariants must hold:
605/// - `handle` must be a valid handle from `plugin_init`, or null
606/// - `request_id` must be a valid request ID from `plugin_call_async`
607///
608/// # Returns
609///
610/// `true` if cancellation was successful, `false` otherwise.
611#[unsafe(no_mangle)]
612pub unsafe extern "C" fn plugin_cancel_async(_handle: FfiPluginHandle, _request_id: u64) -> bool {
613    // Reserved for future implementation
614    false
615}
616
617#[cfg(test)]
618#[path = "exports/exports_tests.rs"]
619mod exports_tests;
620
621#[cfg(test)]
622#[path = "exports/ffi_boundary_tests.rs"]
623mod ffi_boundary_tests;