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