Skip to content

Commit 5027515

Browse files
refactor: harden and simplify ProfileStatus more (#1419)
refactor: harden and simplify ProfileStatus more Co-authored-by: gyuheon.oh <[email protected]>
1 parent 5974f2e commit 5027515

File tree

2 files changed

+82
-188
lines changed

2 files changed

+82
-188
lines changed

libdd-profiling-ffi/src/profile_error.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,11 +354,9 @@ mod tests {
354354
let status = ProfileStatus::from(err);
355355

356356
assert!(!status.err.is_null()); // Should be an error
357-
let cstr: &CStr = (&status).try_into().unwrap();
357+
let result: Result<(), Cow<'static, CStr>> = status.into();
358+
let cstr = result.unwrap_err();
358359
assert!(cstr.to_str().unwrap().contains("memory allocator"));
359-
360-
// Clean up
361-
let _: Result<(), Cow<'static, CStr>> = status.into();
362360
}
363361

364362
#[test]

libdd-profiling-ffi/src/profile_status.rs

Lines changed: 80 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33

44
use crate::ProfileError;
55
use allocator_api2::alloc::{AllocError, Allocator, Global, Layout};
6+
use libdd_profiling::profiles::FallibleStringWriter;
67
use std::borrow::Cow;
78
use std::ffi::{c_char, CStr, CString};
89
use std::fmt::Display;
910
use std::mem::ManuallyDrop;
1011
use std::ptr::{null, NonNull};
1112

12-
// ProfileStatus uses `err` being null to encode OK, so we only need
13-
// one bit in flags to distinguish between STATIC and ALLOCATED errors.
14-
const FLAG_STATIC: usize = 0;
15-
const FLAG_ALLOCATED: usize = 1;
13+
/// ProfileStatus uses `err` being null to encode OK, so we only need
14+
/// one bit in flags to distinguish between STATIC and ALLOCATED errors.
15+
const IS_ALLOCATED_MASK: usize = 1;
1616

1717
/// Represents the result of an operation that either succeeds with no value, or fails with an
1818
/// error message. This is like `Result<(), Cow<'static, CStr>` except its representation is
@@ -25,37 +25,35 @@ const FLAG_ALLOCATED: usize = 1;
2525
/// A `ProfileStatus` owns its error message data. When a `ProfileStatus` with an error is
2626
/// created, it takes ownership of the error string (either as a static reference or heap
2727
/// allocation). The caller is responsible for eventually calling [`ddog_prof_Status_drop`] to
28-
/// free any heap-allocated memory. This is safe to call on OK as well; it does nothing.
28+
/// free any heap-allocated memory. This is safe to call on OK as well.
2929
///
3030
/// # FFI Safety
3131
///
32-
/// This type is `#[repr(C)]` and safe to pass across FFI boundaries. The C side must treat
33-
/// this as an opaque struct and use the provided FFI functions to inspect and drop it.
32+
/// This type is `#[repr(C)]` and safe to pass across FFI boundaries. The C side must treat the
33+
/// `.flags` as opaque and use API functions; the `.err` field is guaranteed to be null when the
34+
/// `ProfileStatus` is OK, and on Err it will be non-null pointer to a UTF8 encoded string which
35+
/// has a null terminator.
3436
#[repr(C)]
3537
#[derive(Debug)]
3638
pub struct ProfileStatus {
3739
/// Bitflags indicating the storage type of the error message.
3840
/// This is only meaningful when `err` is non-null. When `err` is
39-
/// null (indicating OK), this field is ignored (but typically 0).
40-
/// - `FLAG_STATIC` (0): Error message points to static data that must not be freed.
41-
/// - `FLAG_ALLOCATED` (1): Error message is heap-allocated and owned by this `ProfileStatus`.
42-
/// Must be freed via [`ddog_prof_Status_drop`].
41+
/// null (indicating OK), this field SHOULD be zero. Currently, only one
42+
/// bit is used `IS_ALLOCATED_MASK`, which determines whether the error
43+
/// message is owned or statically borrowed.
44+
/// In the future, we may store error codes in here as well.
4345
pub flags: libc::size_t,
4446

4547
/// Pointer to a null-terminated UTF-8 error message string.
46-
/// - If null: indicates OK (success). Check this from C to determine if the operation
47-
/// succeeded.
48-
/// - If non-null and `flags == FLAG_STATIC`: points to static data with lifetime `'static`.
49-
/// Must not be freed.
50-
/// - If non-null and `flags == FLAG_ALLOCATED`: points to heap-allocated data owned by this
51-
/// `ProfileStatus`. Must be freed by [`ddog_prof_Status_drop`].
48+
/// - If null this indicates OK (success). This is an FFI guarantee.
49+
/// - If non-null and allocated bit is clear: points to static data with `'static` lifetime.
50+
/// - If non-null and allocated bit is set: points to owned heap-allocated data.
5251
///
5352
/// # Safety Invariant
5453
///
55-
/// When non-null, `err` must point to a valid, null-terminated C
56-
/// string in UTF-8 encoding. The pointer remains valid for the
57-
/// lifetime of this `ProfileStatus` or until [`ddog_prof_Status_drop`]
58-
/// is called.
54+
/// When non-null, `err` must point to a valid, null-terminated C string in UTF-8 encoding.
55+
/// The pointer remains valid for the lifetime of this `ProfileStatus` or until
56+
/// [`ddog_prof_Status_drop`] is called.
5957
pub err: *const c_char,
6058
}
6159

@@ -71,9 +69,9 @@ impl Default for ProfileStatus {
7169
// SAFETY: ProfileStatus is Send because:
7270
// 1. The `flags` field is a usize, which is Send.
7371
// 2. The `err` pointer is either:
74-
// - Null (OK status), which is trivially Send
75-
// - Points to static data (FLAG_STATIC), which is 'static and therefore Send
76-
// - Points to heap-allocated data (FLAG_ALLOCATED) that this ProfileStatus owns exclusively.
72+
// - Null (OK status), which is trivially Send.
73+
// - Points to static data (allocated bit clear), which is 'static and therefore Send.
74+
// - Points to heap-allocated data (allocated bit set) that this ProfileStatus owns exclusively.
7775
// When sent to another thread, the ownership of the allocation transfers with it, and the drop
7876
// implementation ensures proper cleanup on the receiving thread.
7977
// This is semantically equivalent to `Result<(), Cow<'static, CStr>>`, which is Send.
@@ -82,10 +80,10 @@ unsafe impl Send for ProfileStatus {}
8280
// SAFETY: ProfileStatus is Sync because:
8381
// 1. All fields are immutable from a shared reference (&ProfileStatus).
8482
// 2. The `err` pointer points to immutable data:
85-
// - Null (OK status): trivially Sync
86-
// - Static CStr (FLAG_STATIC): &'static CStr is Sync
87-
// - Heap-allocated CStr (FLAG_ALLOCATED): The CStr is never mutated after creation, so multiple
88-
// threads can safely read it concurrently.
83+
// - Null (OK status): trivially Sync.
84+
// - Static CStr (allocated bit clear): &'static CStr is Sync.
85+
// - Heap-allocated CStr (allocated bit set): The CStr is never mutated after creation, so
86+
// multiple threads can safely read it concurrently.
8987
// 3. There are no interior mutability patterns (no Cell, RefCell, etc.).
9088
// Multiple threads holding &ProfileStatus can safely read the same error message.
9189
unsafe impl Sync for ProfileStatus {}
@@ -111,7 +109,7 @@ impl From<anyhow::Error> for ProfileStatus {
111109
impl From<&'static CStr> for ProfileStatus {
112110
fn from(value: &'static CStr) -> Self {
113111
Self {
114-
flags: FLAG_STATIC,
112+
flags: 0,
115113
err: value.as_ptr(),
116114
}
117115
}
@@ -120,58 +118,23 @@ impl From<&'static CStr> for ProfileStatus {
120118
impl From<CString> for ProfileStatus {
121119
fn from(cstring: CString) -> Self {
122120
Self {
123-
flags: FLAG_ALLOCATED,
121+
flags: IS_ALLOCATED_MASK,
124122
err: cstring.into_raw(),
125123
}
126124
}
127125
}
128126

129-
#[derive(thiserror::Error, Debug)]
130-
pub enum TryFromProfileStatusError {
131-
#[error("failed to convert profile status because the pointer was null")]
132-
Null,
133-
#[error(
134-
"failed to convert profile status because the flags were incorrect for the conversion: `{0}`"
135-
)]
136-
IncorrectFlags(usize),
137-
}
138-
139-
impl TryFrom<ProfileStatus> for CString {
140-
type Error = TryFromProfileStatusError;
141-
142-
fn try_from(status: ProfileStatus) -> Result<Self, Self::Error> {
143-
if status.err.is_null() {
144-
Err(TryFromProfileStatusError::Null)
145-
} else if status.flags == FLAG_ALLOCATED {
146-
Ok(unsafe { CString::from_raw(status.err.cast_mut()) })
147-
} else {
148-
Err(TryFromProfileStatusError::IncorrectFlags(status.flags))
149-
}
150-
}
151-
}
152-
153-
impl TryFrom<&ProfileStatus> for &CStr {
154-
type Error = TryFromProfileStatusError;
155-
156-
fn try_from(status: &ProfileStatus) -> Result<Self, Self::Error> {
157-
if status.err.is_null() {
158-
Err(TryFromProfileStatusError::Null)
159-
} else {
160-
Ok(unsafe { CStr::from_ptr(status.err.cast_mut()) })
161-
}
162-
}
163-
}
164-
165127
impl From<ProfileStatus> for Result<(), Cow<'static, CStr>> {
166128
fn from(status: ProfileStatus) -> Self {
129+
let flags = status.flags;
167130
if status.err.is_null() {
168-
Ok(())
169-
} else if status.flags == FLAG_ALLOCATED {
131+
status.verify_flags()
132+
} else if flags == IS_ALLOCATED_MASK {
170133
Err(Cow::Owned(unsafe {
171134
CString::from_raw(status.err.cast_mut())
172135
}))
173136
} else {
174-
// STATIC error (flags == FLAG_STATIC)
137+
// Static error (allocated bit clear)
175138
Err(Cow::Borrowed(unsafe { CStr::from_ptr(status.err) }))
176139
}
177140
}
@@ -248,6 +211,53 @@ impl ProfileStatus {
248211
err: null(),
249212
};
250213

214+
/// Verifies that the flags make sense for the state of the object. With
215+
/// debug assertions on, this will panic, and in production, it will return
216+
/// an error instead.
217+
fn verify_flags(&self) -> Result<(), Cow<'static, CStr>> {
218+
let flags = self.flags;
219+
if self.err.is_null() {
220+
if flags == 0 {
221+
Ok(())
222+
} else {
223+
// This is a programming error, so in debug builds we panic,
224+
// but in non-debug builds, we return an error message.
225+
debug_assert_eq!(
226+
flags, 0,
227+
"expected empty flag bits for ProfileStatus with no error, saw {flags:#x}"
228+
);
229+
use core::fmt::Write;
230+
let mut writer = FallibleStringWriter::new();
231+
// Include the null terminator here for the sake of allocating
232+
// the correct amount of memory, but it will be removed below.
233+
Err(
234+
if write!(
235+
writer,
236+
"expected empty flag bits for ProfileStatus with no error, saw {flags:#x}\0"
237+
)
238+
.is_err()
239+
{
240+
// We couldn't allocate or format for some reason, so just
241+
// use a static string with less information.
242+
Cow::Borrowed(c"expected empty flag bits for ProfileStatus with no error")
243+
} else {
244+
let string = String::from(writer);
245+
let mut bytes = string.into_bytes();
246+
// Remove the null terminator because from_vec_unchecked
247+
// expects no nulls at all.
248+
bytes.pop();
249+
// SAFETY: the error message is ASCII, the only dynamic
250+
// bit (ha) is the hexadecimal repr of the flags.
251+
let err = unsafe { CString::from_vec_unchecked(bytes) };
252+
Cow::Owned(err)
253+
},
254+
)
255+
}
256+
} else {
257+
Ok(())
258+
}
259+
}
260+
251261
pub fn from_ffi_safe_error_message<E: libdd_common::error::FfiSafeErrorMessage>(
252262
err: E,
253263
) -> Self {
@@ -300,60 +310,6 @@ mod tests {
300310
assert!(result.is_ok());
301311
}
302312

303-
#[test]
304-
fn test_static_error() {
305-
let msg = c"test error message";
306-
let status = ProfileStatus::from(msg);
307-
308-
assert_eq!(status.flags, FLAG_STATIC);
309-
assert_eq!(status.err, msg.as_ptr());
310-
311-
// Convert to CStr
312-
let cstr: &CStr = (&status).try_into().unwrap();
313-
assert_eq!(cstr, msg);
314-
315-
// Convert to Result
316-
let result: Result<(), Cow<'static, CStr>> = status.into();
317-
assert!(result.is_err());
318-
match result {
319-
Err(Cow::Borrowed(borrowed)) => assert_eq!(borrowed, msg),
320-
_ => panic!("Expected Cow::Borrowed"),
321-
}
322-
}
323-
324-
#[test]
325-
fn test_allocated_error() {
326-
let msg = CString::new("allocated error").unwrap();
327-
let msg_clone = msg.clone();
328-
let status = ProfileStatus::from(msg);
329-
330-
assert_eq!(status.flags, FLAG_ALLOCATED);
331-
assert!(!status.err.is_null());
332-
333-
// Convert to CStr
334-
let cstr: &CStr = (&status).try_into().unwrap();
335-
assert_eq!(cstr, msg_clone.as_c_str());
336-
337-
// Convert to CString
338-
let recovered = CString::try_from(status).unwrap();
339-
assert_eq!(recovered, msg_clone);
340-
}
341-
342-
#[test]
343-
fn test_from_anyhow_error() {
344-
let err = anyhow::anyhow!("something went wrong");
345-
let status = ProfileStatus::from(err);
346-
347-
assert!(status.flags != 0);
348-
assert!(!status.err.is_null());
349-
350-
let cstr: &CStr = (&status).try_into().unwrap();
351-
assert_eq!(cstr.to_str().unwrap(), "something went wrong");
352-
353-
// Clean up
354-
let _result: Result<(), Cow<'static, CStr>> = status.into();
355-
}
356-
357313
#[test]
358314
fn test_from_result_ok() {
359315
let result: Result<(), anyhow::Error> = Ok(());
@@ -363,44 +319,6 @@ mod tests {
363319
assert!(status.err.is_null());
364320
}
365321

366-
#[test]
367-
fn test_from_result_err() {
368-
let result: Result<(), anyhow::Error> = Err(anyhow::anyhow!("error from result"));
369-
let status = ProfileStatus::from(result);
370-
371-
assert!(status.flags != 0);
372-
assert!(!status.err.is_null());
373-
374-
let cstr: &CStr = (&status).try_into().unwrap();
375-
assert_eq!(cstr.to_str().unwrap(), "error from result");
376-
377-
// Clean up
378-
let _result: Result<(), Cow<'static, CStr>> = status.into();
379-
}
380-
381-
#[test]
382-
fn test_from_error_with_display() {
383-
#[derive(Debug)]
384-
struct CustomError(&'static str);
385-
386-
impl std::fmt::Display for CustomError {
387-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388-
write!(f, "custom: {}", self.0)
389-
}
390-
}
391-
392-
let status = ProfileStatus::from_error(CustomError("test"));
393-
394-
assert_eq!(status.flags, FLAG_ALLOCATED);
395-
assert!(!status.err.is_null());
396-
397-
let cstr: &CStr = (&status).try_into().unwrap();
398-
assert_eq!(cstr.to_str().unwrap(), "custom: test");
399-
400-
// Clean up
401-
let _result: Result<(), Cow<'static, CStr>> = status.into();
402-
}
403-
404322
#[test]
405323
fn test_ffi_drop_null() {
406324
// Should not crash
@@ -436,7 +354,7 @@ mod tests {
436354
let msg = CString::new("allocated message").unwrap();
437355
let mut status = ProfileStatus::from(msg);
438356

439-
assert_eq!(status.flags, FLAG_ALLOCATED);
357+
assert_eq!(status.flags, IS_ALLOCATED_MASK);
440358
let err_ptr = status.err;
441359
assert!(!err_ptr.is_null());
442360

@@ -448,28 +366,6 @@ mod tests {
448366
// The allocated memory should have been freed (can't really test this without valgrind)
449367
}
450368

451-
#[test]
452-
fn test_try_from_cstr_on_ok_fails() {
453-
let status = ProfileStatus::OK;
454-
let result: Result<&CStr, TryFromProfileStatusError> = (&status).try_into();
455-
assert!(result.is_err());
456-
assert!(matches!(
457-
result.unwrap_err(),
458-
TryFromProfileStatusError::Null
459-
));
460-
}
461-
462-
#[test]
463-
fn test_try_from_cstring_on_static_fails() {
464-
let status = ProfileStatus::from(c"static");
465-
let result = CString::try_from(status);
466-
assert!(result.is_err());
467-
assert!(matches!(
468-
result.unwrap_err(),
469-
TryFromProfileStatusError::IncorrectFlags(FLAG_STATIC)
470-
));
471-
}
472-
473369
#[test]
474370
fn test_send_sync() {
475371
// Just check that ProfileStatus implements Send and Sync

0 commit comments

Comments
 (0)