Skip to main content

rustbridge_logging/
callback.rs

1//! FFI log callback management
2
3use once_cell::sync::OnceCell;
4use parking_lot::RwLock;
5use rustbridge_core::LogLevel;
6use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
7
8/// FFI callback function type for logging
9///
10/// # Parameters
11/// - `level`: Log level (0=Trace, 1=Debug, 2=Info, 3=Warn, 4=Error)
12/// - `target`: Log target (module path), null-terminated C string
13/// - `message`: Log message, pointer to UTF-8 bytes
14/// - `message_len`: Length of the message in bytes
15///
16/// # Safety
17/// The callback is invoked from Rust code. The `target` string is null-terminated.
18/// The `message` pointer is valid for `message_len` bytes during the callback.
19pub type LogCallback = extern "C" fn(
20    level: u8,
21    target: *const std::ffi::c_char,
22    message: *const u8,
23    message_len: usize,
24);
25
26/// Global log callback manager
27static CALLBACK_MANAGER: OnceCell<LogCallbackManager> = OnceCell::new();
28
29/// Manager for FFI log callbacks
30///
31/// Each plugin can register its own log callback. The callback is cleared
32/// when the plugin that registered it shuts down to prevent use-after-free
33/// (the callback function pointer is tied to the plugin's FFI arena lifetime).
34///
35/// Log level is shared globally and persists across plugin reload cycles.
36pub struct LogCallbackManager {
37    callback: RwLock<Option<LogCallback>>,
38    level: AtomicU8,
39    /// Number of active plugins using this callback manager
40    ref_count: AtomicUsize,
41    /// Whether the current callback was registered by a plugin (vs None)
42    has_callback: std::sync::atomic::AtomicBool,
43}
44
45impl LogCallbackManager {
46    /// Create a new callback manager
47    pub fn new() -> Self {
48        Self {
49            callback: RwLock::new(None),
50            level: AtomicU8::new(LogLevel::Info as u8),
51            ref_count: AtomicUsize::new(0),
52            has_callback: std::sync::atomic::AtomicBool::new(false),
53        }
54    }
55
56    /// Get the global callback manager instance
57    pub fn global() -> &'static LogCallbackManager {
58        CALLBACK_MANAGER.get_or_init(LogCallbackManager::new)
59    }
60
61    /// Set the log callback
62    pub fn set_callback(&self, callback: Option<LogCallback>) {
63        let mut guard = self.callback.write();
64        *guard = callback;
65    }
66
67    /// Register a plugin with the callback manager
68    ///
69    /// This increments the reference count and optionally sets the callback.
70    /// If a callback is provided, it replaces any existing callback.
71    ///
72    /// **Important**: The callback function pointer is tied to the plugin's
73    /// FFI arena lifetime. When the plugin shuts down, the callback becomes
74    /// invalid and must be cleared before the arena is closed.
75    ///
76    /// This should be called during plugin initialization.
77    pub fn register_plugin(&self, callback: Option<LogCallback>) {
78        // Increment reference count
79        self.ref_count.fetch_add(1, Ordering::SeqCst);
80
81        // Set callback if provided
82        if let Some(cb) = callback {
83            let mut guard = self.callback.write();
84            *guard = Some(cb);
85            self.has_callback
86                .store(true, std::sync::atomic::Ordering::SeqCst);
87        }
88    }
89
90    /// Unregister a plugin from the callback manager
91    ///
92    /// **Critical**: This ALWAYS clears the callback to prevent use-after-free.
93    /// The callback function pointer is tied to the plugin's FFI arena, which
94    /// will be closed immediately after this function returns. If we didn't
95    /// clear the callback, any subsequent logging would call an invalid pointer.
96    ///
97    /// This means that with multiple plugins, the last one to unregister will
98    /// disable logging for any remaining plugins until they re-register a callback.
99    /// This is a safety trade-off: we prioritize crash prevention over convenience.
100    ///
101    /// Note: The reload handle is NOT cleared because logging initialization
102    /// only happens once per process. The reload handle must persist across
103    /// plugin reload cycles.
104    ///
105    /// This should be called during plugin shutdown.
106    pub fn unregister_plugin(&self) {
107        // ALWAYS clear the callback first, before decrementing ref count.
108        // This prevents use-after-free when the plugin's arena is closed.
109        {
110            let mut guard = self.callback.write();
111            *guard = None;
112            self.has_callback
113                .store(false, std::sync::atomic::Ordering::SeqCst);
114        } // Write lock must be released BEFORE logging to avoid deadlock
115
116        let prev_count = self.ref_count.fetch_sub(1, Ordering::SeqCst);
117
118        if prev_count == 1 {
119            tracing::debug!("Last plugin unregistered, cleared log callback");
120        } else {
121            tracing::debug!(
122                "Plugin unregistered, cleared log callback (safety). {} plugins remaining",
123                prev_count - 1
124            );
125        }
126    }
127
128    /// Get the current reference count (number of active plugins)
129    pub fn plugin_count(&self) -> usize {
130        self.ref_count.load(Ordering::SeqCst)
131    }
132
133    /// Get the current log callback
134    pub fn get_callback(&self) -> Option<LogCallback> {
135        *self.callback.read()
136    }
137
138    /// Set the log level
139    pub fn set_level(&self, level: LogLevel) {
140        self.level.store(level as u8, Ordering::SeqCst);
141    }
142
143    /// Get the current log level
144    pub fn level(&self) -> LogLevel {
145        LogLevel::from_u8(self.level.load(Ordering::SeqCst))
146    }
147
148    /// Check if a log level is enabled
149    pub fn is_enabled(&self, level: LogLevel) -> bool {
150        level >= self.level()
151    }
152
153    /// Invoke the callback if set and level is enabled
154    ///
155    /// # Safety
156    /// The callback must be a valid function pointer that follows the
157    /// LogCallback signature contract.
158    pub fn log(&self, level: LogLevel, target: &str, message: &str) {
159        // Check if level is enabled
160        if !self.is_enabled(level) {
161            return;
162        }
163
164        // Get callback
165        let callback = match self.get_callback() {
166            Some(cb) => cb,
167            None => return,
168        };
169
170        // Prepare target as C string
171        let target_cstring = match std::ffi::CString::new(target) {
172            Ok(s) => s,
173            Err(_) => return, // Invalid target string
174        };
175
176        // Invoke callback
177        callback(
178            level as u8,
179            target_cstring.as_ptr(),
180            message.as_ptr(),
181            message.len(),
182        );
183    }
184}
185
186impl Default for LogCallbackManager {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192// Ensure LogCallbackManager is thread-safe
193unsafe impl Send for LogCallbackManager {}
194unsafe impl Sync for LogCallbackManager {}
195
196#[cfg(test)]
197#[path = "callback/callback_tests.rs"]
198mod callback_tests;