|
1 |
| -use std::sync::atomic::{AtomicUsize, Ordering}; |
| 1 | +use std::{ |
| 2 | + ffi::CStr, |
| 3 | + sync::atomic::{AtomicPtr, Ordering}, |
| 4 | +}; |
2 | 5 |
|
3 |
| -use libc::c_char; |
| 6 | +use libc::{c_char, c_int}; |
4 | 7 |
|
5 |
| -use crate::{panic, raw, util::Binding}; |
| 8 | +use crate::{panic, raw, util::Binding, Error}; |
6 | 9 |
|
7 | 10 | /// Available tracing levels. When tracing is set to a particular level,
|
8 | 11 | /// callers will be provided tracing at the given level and all lower levels.
|
@@ -57,29 +60,82 @@ impl Binding for TraceLevel {
|
57 | 60 | }
|
58 | 61 | }
|
59 | 62 |
|
60 |
| -//TODO: pass raw &[u8] and leave conversion to consumer (breaking API) |
61 | 63 | /// Callback type used to pass tracing events to the subscriber.
|
62 | 64 | /// see `trace_set` to register a subscriber.
|
63 |
| -pub type TracingCb = fn(TraceLevel, &str); |
| 65 | +pub type TracingCb = fn(TraceLevel, &[u8]); |
64 | 66 |
|
65 |
| -static CALLBACK: AtomicUsize = AtomicUsize::new(0); |
| 67 | +/// Use an atomic pointer to store the global tracing subscriber function. |
| 68 | +static CALLBACK: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); |
66 | 69 |
|
67 |
| -/// |
68 |
| -pub fn trace_set(level: TraceLevel, cb: TracingCb) -> bool { |
69 |
| - CALLBACK.store(cb as usize, Ordering::SeqCst); |
| 70 | +/// Set the global subscriber called when libgit2 produces a tracing message. |
| 71 | +pub fn trace_set(level: TraceLevel, cb: TracingCb) -> Result<(), Error> { |
| 72 | + // Store the callback in the global atomic. |
| 73 | + CALLBACK.store(cb as *mut (), Ordering::SeqCst); |
70 | 74 |
|
71 |
| - unsafe { |
72 |
| - raw::git_trace_set(level.raw(), Some(tracing_cb_c)); |
73 |
| - } |
| 75 | + // git_trace_set returns 0 if there was no error. |
| 76 | + let return_code: c_int = unsafe { raw::git_trace_set(level.raw(), Some(tracing_cb_c)) }; |
74 | 77 |
|
75 |
| - return true; |
| 78 | + if return_code != 0 { |
| 79 | + // Unwrap here is fine since `Error::last_error` always returns `Some`. |
| 80 | + Err(Error::last_error(return_code).unwrap()) |
| 81 | + } else { |
| 82 | + Ok(()) |
| 83 | + } |
76 | 84 | }
|
77 | 85 |
|
| 86 | +/// The tracing callback we pass to libgit2 (C ABI compatible). |
78 | 87 | extern "C" fn tracing_cb_c(level: raw::git_trace_level_t, msg: *const c_char) {
|
79 |
| - let cb = CALLBACK.load(Ordering::SeqCst); |
80 |
| - panic::wrap(|| unsafe { |
81 |
| - let cb: TracingCb = std::mem::transmute(cb); |
82 |
| - let msg = std::ffi::CStr::from_ptr(msg).to_string_lossy(); |
83 |
| - cb(Binding::from_raw(level), msg.as_ref()); |
| 88 | + // Load the callback function pointer from the global atomic. |
| 89 | + let cb: *mut () = CALLBACK.load(Ordering::SeqCst); |
| 90 | + |
| 91 | + // Transmute the callback pointer into the function pointer we know it to be. |
| 92 | + // |
| 93 | + // SAFETY: We only ever set the callback pointer with something cast from a TracingCb |
| 94 | + // so transmuting back to a TracingCb is safe. This is notably not an integer-to-pointer |
| 95 | + // transmute as described in the mem::transmute documentation and is in-line with the |
| 96 | + // example in that documentation for casing between *const () to fn pointers. |
| 97 | + let cb: TracingCb = unsafe { std::mem::transmute(cb) }; |
| 98 | + |
| 99 | + // If libgit2 passes us a message that is null, drop it and do not pass it to the callback. |
| 100 | + // This is to avoid ever exposing rust code to a null ref, which would be Undefined Behavior. |
| 101 | + if msg.is_null() { |
| 102 | + return; |
| 103 | + } |
| 104 | + |
| 105 | + // Convert the message from a *const c_char to a &[u8] and pass it to the callback. |
| 106 | + // |
| 107 | + // SAFETY: We've just checked that the pointer is not null. The other safety requirements are left to |
| 108 | + // libgit2 to enforce -- namely that it gives us a valid, nul-terminated, C string, that that string exists |
| 109 | + // entirely in one allocation, that the string will not be mutated once passed to us, and that the nul-terminator is |
| 110 | + // within isize::MAX bytes from the given pointers data address. |
| 111 | + let msg: &CStr = unsafe { CStr::from_ptr(msg) }; |
| 112 | + |
| 113 | + // Convert from a CStr to &[u8] to pass to the rust code callback. |
| 114 | + let msg: &[u8] = CStr::to_bytes(msg); |
| 115 | + |
| 116 | + // Do the remaining part of this function in a panic wrapper, to catch any panics it produces. |
| 117 | + panic::wrap(|| { |
| 118 | + // Convert the raw trace level into a type we can pass to the rust callback fn. |
| 119 | + // |
| 120 | + // SAFETY: Currently the implementation of this function (above) may panic, but is only marked as unsafe to match |
| 121 | + // the trait definition, thus we can consider this call safe. |
| 122 | + let level: TraceLevel = unsafe { Binding::from_raw(level) }; |
| 123 | + |
| 124 | + // Call the user-supplied callback (which may panic). |
| 125 | + (cb)(level, msg); |
84 | 126 | });
|
85 | 127 | }
|
| 128 | + |
| 129 | +#[cfg(test)] |
| 130 | +mod tests { |
| 131 | + use super::TraceLevel; |
| 132 | + |
| 133 | + // Test that using the above function to set a tracing callback doesn't panic. |
| 134 | + #[test] |
| 135 | + fn smoke() { |
| 136 | + super::trace_set(TraceLevel::Trace, |level, msg| { |
| 137 | + dbg!(level, msg); |
| 138 | + }) |
| 139 | + .expect("libgit2 can set global trace callback"); |
| 140 | + } |
| 141 | +} |
0 commit comments