Skip to content

Commit 52fc8d0

Browse files
committed
(Breaking Changes) Re-implement tracing wrapper in a safer way, add smoke test, update tracing callback to use &[u8] instead of &str.
1 parent f1f09ce commit 52fc8d0

File tree

1 file changed

+75
-21
lines changed

1 file changed

+75
-21
lines changed

src/tracing.rs

+75-21
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use std::sync::atomic::{AtomicUsize, Ordering};
1+
use std::{ffi::CStr, sync::atomic::{AtomicPtr, Ordering}};
22

3-
use libc::c_char;
3+
use libc::{c_char, c_int};
44

5-
use crate::{panic, raw, util::Binding};
5+
use crate::{panic, raw, util::Binding, Error};
66

77
/// Available tracing levels. When tracing is set to a particular level,
88
/// callers will be provided tracing at the given level and all lower levels.
@@ -57,29 +57,83 @@ impl Binding for TraceLevel {
5757
}
5858
}
5959

60-
//TODO: pass raw &[u8] and leave conversion to consumer (breaking API)
6160
/// Callback type used to pass tracing events to the subscriber.
6261
/// see `trace_set` to register a subscriber.
63-
pub type TracingCb = fn(TraceLevel, &str);
64-
65-
static CALLBACK: AtomicUsize = AtomicUsize::new(0);
66-
67-
///
68-
pub fn trace_set(level: TraceLevel, cb: TracingCb) -> bool {
69-
CALLBACK.store(cb as usize, Ordering::SeqCst);
70-
71-
unsafe {
72-
raw::git_trace_set(level.raw(), Some(tracing_cb_c));
62+
pub type TracingCb = fn(TraceLevel, &[u8]);
63+
64+
/// Use an atomic pointer to store the global tracing subscriber function.
65+
static CALLBACK: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut());
66+
67+
/// Set the global subscriber called when libgit2 produces a tracing message.
68+
pub fn trace_set(level: TraceLevel, cb: TracingCb) -> Result<(), Error> {
69+
// Store the callback in the global atomic.
70+
CALLBACK.store(cb as *mut (), Ordering::SeqCst);
71+
72+
// git_trace_set returns 0 if there was no error.
73+
let return_code: c_int = unsafe {
74+
raw::git_trace_set(level.raw(), Some(tracing_cb_c))
75+
};
76+
77+
if return_code != 0 {
78+
// Unwrap here is fine since `Error::last_error` always returns `Some`.
79+
Err(Error::last_error(return_code).unwrap())
80+
} else {
81+
Ok(())
7382
}
74-
75-
return true;
7683
}
7784

85+
/// The tracing callback we pass to libgit2 (C ABI compatible).
7886
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());
87+
// Load the callback function pointer from the global atomic.
88+
let cb: *mut () = CALLBACK.load(Ordering::SeqCst);
89+
90+
// Transmute the callback pointer into the function pointer we know it to be.
91+
//
92+
// SAFETY: We only ever set the callback pointer with something cast from a TracingCb
93+
// so transmuting back to a TracingCb is safe. This is notably not an integer-to-pointer
94+
// transmute as described in the mem::transmute documentation and is in-line with the
95+
// example in that documentation for casing between *const () to fn pointers.
96+
let cb: TracingCb = unsafe { std::mem::transmute(cb) };
97+
98+
// If libgit2 passes us a message that is null, drop it and do not pass it to the callback.
99+
// This is to avoid ever exposing rust code to a null ref, which would be Undefined Behavior.
100+
if msg.is_null() {
101+
return;
102+
}
103+
104+
// Convert the message from a *const c_char to a &[u8] and pass it to the callback.
105+
//
106+
// SAFETY: We've just checked that the pointer is not null. The other safety requirements are left to
107+
// libgit2 to enforce -- namely that it gives us a valid, nul-terminated, C string, that that string exists
108+
// entirely in one allocation, that the string will not be mutated once passed to us, and that the nul-terminator is
109+
// within isize::MAX bytes from the given pointers data address.
110+
let msg: &CStr = unsafe { CStr::from_ptr(msg) };
111+
112+
// Convert from a CStr to &[u8] to pass to the rust code callback.
113+
let msg: &[u8] = CStr::to_bytes(msg);
114+
115+
// Do the remaining part of this function in a panic wrapper, to catch any panics it produces.
116+
panic::wrap(|| {
117+
// Convert the raw trace level into a type we can pass to the rust callback fn.
118+
//
119+
// SAFETY: Currently the implementation of this function (above) may panic, but is only marked as unsafe to match
120+
// the trait definition, thus we can consider this call safe.
121+
let level: TraceLevel = unsafe { Binding::from_raw(level) };
122+
123+
// Call the user-supplied callback (which may panic).
124+
(cb)(level, msg);
84125
});
85126
}
127+
128+
#[cfg(test)]
129+
mod tests {
130+
use super::TraceLevel;
131+
132+
// Test that using the above function to set a tracing callback doesn't panic.
133+
#[test]
134+
fn smoke() {
135+
super::trace_set(TraceLevel::Trace, |level, msg| {
136+
dbg!(level, msg);
137+
}).expect("libgit2 can set global trace callback");
138+
}
139+
}

0 commit comments

Comments
 (0)