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;