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    LogCallbackManager::global().set_level(config.log_level);
92
93    // Initialize logging with the configured level
94    rustbridge_logging::init_logging();
95
96    // Install panic hook to log panics via FFI callback
97    crate::panic_guard::install_panic_hook();
98
99    // Take ownership of the plugin
100    // SAFETY: caller guarantees plugin_ptr is from plugin_create
101    let plugin: Box<Box<dyn rustbridge_core::Plugin>> =
102        unsafe { Box::from_raw(plugin_ptr as *mut Box<dyn rustbridge_core::Plugin>) };
103
104    // Create the handle
105    let handle = match PluginHandle::new(*plugin, config) {
106        Ok(h) => h,
107        Err(e) => {
108            tracing::error!("Failed to create handle: {}", e);
109            return ptr::null_mut();
110        }
111    };
112
113    // Start the plugin
114    if let Err(e) = handle.start() {
115        tracing::error!("Failed to start plugin: {}", e);
116        return ptr::null_mut();
117    }
118
119    // Register and return handle
120    let id = PluginHandleManager::global().register(handle);
121
122    // Store ID in the handle
123    if let Some(h) = PluginHandleManager::global().get(id) {
124        h.set_id(id);
125    }
126
127    id as FfiPluginHandle
128}
129
130/// Make a synchronous call to the plugin
131///
132/// # Parameters
133/// - `handle`: Plugin handle from plugin_init
134/// - `type_tag`: Message type identifier (null-terminated C string)
135/// - `request`: Request payload bytes
136/// - `request_len`: Length of request payload
137///
138/// # Returns
139/// FfiBuffer containing the response (must be freed with plugin_free_buffer)
140///
141/// # Safety
142/// - `handle` must be a valid handle from plugin_init
143/// - `type_tag` must be a valid null-terminated C string
144/// - `request` must be valid for `request_len` bytes
145#[unsafe(no_mangle)]
146pub unsafe extern "C" fn plugin_call(
147    handle: FfiPluginHandle,
148    type_tag: *const std::ffi::c_char,
149    request: *const u8,
150    request_len: usize,
151) -> FfiBuffer {
152    // Wrap in panic handler
153    let handle_id = handle as u64;
154    match catch_panic(
155        handle_id,
156        AssertUnwindSafe(|| unsafe { plugin_call_impl(handle, type_tag, request, request_len) }),
157    ) {
158        Ok(result) => result,
159        Err(error_buffer) => error_buffer,
160    }
161}
162
163/// Internal implementation of plugin_call (wrapped by panic handler)
164unsafe fn plugin_call_impl(
165    handle: FfiPluginHandle,
166    type_tag: *const std::ffi::c_char,
167    request: *const u8,
168    request_len: usize,
169) -> FfiBuffer {
170    // Validate handle
171    let id = handle as u64;
172    let plugin_handle = match PluginHandleManager::global().get(id) {
173        Some(h) => h,
174        None => return FfiBuffer::error(1, "Invalid handle"),
175    };
176
177    // Parse type tag
178    let type_tag_str = if type_tag.is_null() {
179        return FfiBuffer::error(4, "Type tag is null");
180    } else {
181        // SAFETY: caller guarantees type_tag is a valid null-terminated C string
182        match unsafe { std::ffi::CStr::from_ptr(type_tag) }.to_str() {
183            Ok(s) => s,
184            Err(_) => return FfiBuffer::error(4, "Invalid type tag encoding"),
185        }
186    };
187
188    // Get request data
189    let request_data = if request.is_null() || request_len == 0 {
190        &[]
191    } else {
192        // SAFETY: caller guarantees request is valid for request_len bytes
193        unsafe { std::slice::from_raw_parts(request, request_len) }
194    };
195
196    // Make the call
197    match plugin_handle.call(type_tag_str, request_data) {
198        Ok(response_data) => {
199            // Wrap in response envelope
200            match ResponseEnvelope::success_raw(&response_data) {
201                Ok(envelope) => match envelope.to_bytes() {
202                    Ok(bytes) => FfiBuffer::from_vec(bytes),
203                    Err(e) => FfiBuffer::error(5, &format!("Serialization error: {}", e)),
204                },
205                Err(e) => FfiBuffer::error(5, &format!("Serialization error: {}", e)),
206            }
207        }
208        Err(e) => {
209            let envelope = ResponseEnvelope::from_error(&e);
210            match envelope.to_bytes() {
211                Ok(bytes) => {
212                    let mut buf = FfiBuffer::from_vec(bytes);
213                    buf.error_code = e.error_code();
214                    buf
215                }
216                Err(se) => FfiBuffer::error(e.error_code(), &format!("{}: {}", e, se)),
217            }
218        }
219    }
220}
221
222/// Free a buffer returned by plugin_call
223///
224/// # Safety
225/// - `buffer` must be a valid FfiBuffer from plugin_call
226/// - Must only be called once per buffer
227#[unsafe(no_mangle)]
228pub unsafe extern "C" fn plugin_free_buffer(buffer: *mut FfiBuffer) {
229    unsafe {
230        if !buffer.is_null() {
231            (*buffer).free();
232        }
233    }
234}
235
236/// Shutdown a plugin instance
237///
238/// # Parameters
239/// - `handle`: Plugin handle from plugin_init
240///
241/// # Returns
242/// true on success, false on failure
243///
244/// # Safety
245/// - `handle` must be a valid handle from plugin_init
246/// - After this call, the handle is no longer valid
247#[unsafe(no_mangle)]
248pub unsafe extern "C" fn plugin_shutdown(handle: FfiPluginHandle) -> bool {
249    // Wrap in panic handler
250    let handle_id = handle as u64;
251    catch_panic(handle_id, AssertUnwindSafe(|| plugin_shutdown_impl(handle))).unwrap_or_default() // Returns false on panic
252}
253
254/// Internal implementation of plugin_shutdown (wrapped by panic handler)
255fn plugin_shutdown_impl(handle: FfiPluginHandle) -> bool {
256    let id = handle as u64;
257
258    // Remove from manager
259    let plugin_handle = match PluginHandleManager::global().remove(id) {
260        Some(h) => h,
261        None => return false,
262    };
263
264    // Shutdown with default timeout
265    let result = match plugin_handle.shutdown(5000) {
266        Ok(()) => true,
267        Err(e) => {
268            tracing::error!("Shutdown error: {}", e);
269            false
270        }
271    };
272
273    // Clear binary handlers for this thread to avoid stale handlers on reload
274    clear_binary_handlers();
275
276    // Unregister plugin from callback manager (decrements ref count)
277    // The callback will only be cleared when the last plugin shuts down.
278    // This allows multiple plugins to coexist and share the same callback.
279    LogCallbackManager::global().unregister_plugin();
280
281    result
282}
283
284/// Set the log level for a plugin
285///
286/// # Parameters
287/// - `handle`: Plugin handle from plugin_init
288/// - `level`: Log level (0=Trace, 1=Debug, 2=Info, 3=Warn, 4=Error, 5=Off)
289///
290/// # Safety
291/// - `handle` must be a valid handle from plugin_init
292#[unsafe(no_mangle)]
293pub unsafe extern "C" fn plugin_set_log_level(handle: FfiPluginHandle, level: u8) {
294    let id = handle as u64;
295
296    if let Some(plugin_handle) = PluginHandleManager::global().get(id) {
297        plugin_handle.set_log_level(LogLevel::from_u8(level));
298    }
299}
300
301/// Get the current state of a plugin
302///
303/// # Parameters
304/// - `handle`: Plugin handle from plugin_init
305///
306/// # Returns
307/// State code (0=Installed, 1=Starting, 2=Active, 3=Stopping, 4=Stopped, 5=Failed)
308/// Returns 255 if handle is invalid
309///
310/// # Safety
311/// - `handle` must be a valid handle from plugin_init
312#[unsafe(no_mangle)]
313pub unsafe extern "C" fn plugin_get_state(handle: FfiPluginHandle) -> u8 {
314    let id = handle as u64;
315
316    match PluginHandleManager::global().get(id) {
317        Some(h) => match h.state() {
318            rustbridge_core::LifecycleState::Installed => 0,
319            rustbridge_core::LifecycleState::Starting => 1,
320            rustbridge_core::LifecycleState::Active => 2,
321            rustbridge_core::LifecycleState::Stopping => 3,
322            rustbridge_core::LifecycleState::Stopped => 4,
323            rustbridge_core::LifecycleState::Failed => 5,
324        },
325        None => 255,
326    }
327}
328
329/// Get the number of requests rejected due to concurrency limits
330///
331/// # Parameters
332/// - `handle`: Plugin handle from plugin_init
333///
334/// # Returns
335/// Number of rejected requests since plugin initialization. Returns 0 if handle is invalid.
336///
337/// # Safety
338/// - `handle` must be a valid handle from plugin_init
339#[unsafe(no_mangle)]
340pub unsafe extern "C" fn plugin_get_rejected_count(handle: FfiPluginHandle) -> u64 {
341    let id = handle as u64;
342    match PluginHandleManager::global().get(id) {
343        Some(h) => h.rejected_request_count(),
344        None => 0,
345    }
346}
347
348// ============================================================================
349// Binary Transport Functions
350// ============================================================================
351
352/// Handler function type for binary message processing
353///
354/// Plugins that want to support binary transport must register handlers
355/// using this signature. The handler receives the request struct as raw bytes
356/// and must return response bytes (which will be wrapped in RbResponse).
357pub type BinaryMessageHandler =
358    fn(handle: &PluginHandle, request: &[u8]) -> Result<Vec<u8>, rustbridge_core::PluginError>;
359
360/// Global registry for binary message handlers
361///
362/// This uses a thread-safe DashMap so handlers registered from the Tokio
363/// worker thread are visible when plugin_call_raw is called from the host
364/// language thread.
365static BINARY_HANDLERS: OnceCell<DashMap<u32, BinaryMessageHandler>> = OnceCell::new();
366
367/// Get the binary handlers registry, initializing if needed
368fn binary_handlers() -> &'static DashMap<u32, BinaryMessageHandler> {
369    BINARY_HANDLERS.get_or_init(DashMap::new)
370}
371
372/// Register a binary message handler
373///
374/// Call this during plugin initialization to register handlers for
375/// binary transport message types.
376pub fn register_binary_handler(message_id: u32, handler: BinaryMessageHandler) {
377    binary_handlers().insert(message_id, handler);
378}
379
380/// Clear all binary message handlers
381///
382/// This should be called during plugin shutdown to ensure handlers
383/// don't persist across plugin reload cycles.
384pub(crate) fn clear_binary_handlers() {
385    binary_handlers().clear();
386}
387
388/// Make a synchronous binary call to the plugin
389///
390/// # Parameters
391/// - `handle`: Plugin handle from plugin_init
392/// - `message_id`: Numeric message identifier
393/// - `request`: Pointer to request struct
394/// - `request_size`: Size of request struct (for validation)
395///
396/// # Returns
397/// RbResponse containing the binary response (must be freed with rb_response_free)
398///
399/// # Safety
400/// - `handle` must be a valid handle from plugin_init
401/// - `request` must be valid for `request_size` bytes
402/// - The request struct must match the expected type for `message_id`
403#[unsafe(no_mangle)]
404pub unsafe extern "C" fn plugin_call_raw(
405    handle: FfiPluginHandle,
406    message_id: u32,
407    request: *const c_void,
408    request_size: usize,
409) -> RbResponse {
410    // Wrap in panic handler
411    let handle_id = handle as u64;
412    match catch_panic(
413        handle_id,
414        AssertUnwindSafe(|| unsafe {
415            plugin_call_raw_impl(handle, message_id, request, request_size)
416        }),
417    ) {
418        Ok(result) => result,
419        Err(error_buffer) => {
420            // Convert FfiBuffer error to RbResponse error
421            let msg = if error_buffer.is_error() && !error_buffer.data.is_null() {
422                // SAFETY: error buffer contains a valid string
423                let slice =
424                    unsafe { std::slice::from_raw_parts(error_buffer.data, error_buffer.len) };
425                String::from_utf8_lossy(slice).into_owned()
426            } else {
427                "Internal error (panic)".to_string()
428            };
429            // Free the original error buffer
430            let mut buf = error_buffer;
431            // SAFETY: buf is a valid FfiBuffer from catch_panic
432            unsafe { buf.free() };
433            RbResponse::error(11, &msg)
434        }
435    }
436}
437
438/// Internal implementation of plugin_call_raw (wrapped by panic handler)
439unsafe fn plugin_call_raw_impl(
440    handle: FfiPluginHandle,
441    message_id: u32,
442    request: *const c_void,
443    request_size: usize,
444) -> RbResponse {
445    // Validate handle
446    let id = handle as u64;
447    let plugin_handle = match PluginHandleManager::global().get(id) {
448        Some(h) => h,
449        None => return RbResponse::error(1, "Invalid handle"),
450    };
451
452    // Check plugin state
453    if !plugin_handle.state().can_handle_requests() {
454        return RbResponse::error(1, "Plugin not in Active state");
455    }
456
457    // Get request data
458    let request_data = if request.is_null() || request_size == 0 {
459        &[]
460    } else {
461        // SAFETY: caller guarantees request is valid for request_size bytes
462        unsafe { std::slice::from_raw_parts(request as *const u8, request_size) }
463    };
464
465    // Look up handler
466    let handler = binary_handlers().get(&message_id).map(|r| *r);
467
468    match handler {
469        Some(h) => {
470            // Call the handler
471            match h(&plugin_handle, request_data) {
472                Ok(response_bytes) => {
473                    // Return raw bytes as response
474                    // The caller is responsible for interpreting the bytes as the correct struct
475                    let mut response = RbResponse::empty();
476                    let len = response_bytes.len();
477                    let capacity = response_bytes.capacity();
478                    let data = response_bytes.leak().as_mut_ptr();
479
480                    response.error_code = 0;
481                    response.len = len as u32;
482                    response.capacity = capacity as u32;
483                    response.data = data as *mut c_void;
484
485                    response
486                }
487                Err(e) => RbResponse::error(e.error_code(), &e.to_string()),
488            }
489        }
490        None => RbResponse::error(6, &format!("Unknown message ID: {}", message_id)),
491    }
492}
493
494/// Free an RbResponse returned by plugin_call_raw
495///
496/// # Safety
497/// - `response` must be a valid pointer to an RbResponse from plugin_call_raw
498/// - Must only be called once per response
499#[unsafe(no_mangle)]
500pub unsafe extern "C" fn rb_response_free(response: *mut RbResponse) {
501    unsafe {
502        if !response.is_null() {
503            (*response).free();
504        }
505    }
506}
507
508// ============================================================================
509// Async API (future implementation)
510// ============================================================================
511
512#[cfg(test)]
513#[path = "exports/exports_tests.rs"]
514mod exports_tests;
515
516#[cfg(test)]
517#[path = "exports/ffi_boundary_tests.rs"]
518mod ffi_boundary_tests;