33
44use crate :: ProfileError ;
55use allocator_api2:: alloc:: { AllocError , Allocator , Global , Layout } ;
6+ use libdd_profiling:: profiles:: FallibleStringWriter ;
67use std:: borrow:: Cow ;
78use std:: ffi:: { c_char, CStr , CString } ;
89use std:: fmt:: Display ;
910use std:: mem:: ManuallyDrop ;
1011use 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 ) ]
3638pub 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.
9189unsafe impl Sync for ProfileStatus { }
@@ -111,7 +109,7 @@ impl From<anyhow::Error> for ProfileStatus {
111109impl 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 {
120118impl 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-
165127impl 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