Skip to content

Commit e42fb9c

Browse files
committed
chore(sidecar): reorg unsafe code in span ffi
Some of the safety comments were missing, some unsafe annotations didn't need to be there, etc. This commit makes a pass on the span API to clean up `unsafe` annotations and add missing Safety comments, leaving behavior unchanged.
1 parent 6a02f01 commit e42fb9c

File tree

2 files changed

+121
-86
lines changed

2 files changed

+121
-86
lines changed

datadog-sidecar-ffi/src/span.rs

Lines changed: 115 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ use std::ffi::{c_char, CString};
1212

1313
fn convert_char_slice_to_bytes_string(slice: CharSlice) -> BytesString {
1414
// TODO: Strip the invalid bytes in the tracer instead
15-
unsafe {
16-
match String::from_utf8_lossy(slice.as_bytes()) {
17-
Cow::Owned(s) => s.into(),
18-
Cow::Borrowed(_) => {
19-
BytesString::from_bytes_unchecked(Bytes::from_underlying(slice.as_bytes().to_vec()))
20-
}
21-
}
15+
match String::from_utf8_lossy(slice.as_bytes()) {
16+
Cow::Owned(s) => s.into(),
17+
Cow::Borrowed(_) => unsafe {
18+
// Safety: if `from_utf8_lossy` returns a borrowed `str`, the latter can't be anything
19+
// else than the original slice which is thus valid UTF8.
20+
BytesString::from_bytes_unchecked(Bytes::from_underlying(slice.as_bytes().to_vec()))
21+
},
2222
}
2323
}
2424

@@ -31,8 +31,10 @@ fn set_string_field(field: &mut BytesString, slice: CharSlice) {
3131
}
3232

3333
#[inline]
34-
fn get_string_field(field: &BytesString) -> CharSlice<'static> {
34+
fn get_string_field(field: &BytesString) -> CharSlice<'_> {
3535
let string = field.as_str();
36+
// Safety: `field` is a `ByteString`, which guarantees it is backed by valid UTF8 bytes. The
37+
// lifetime of the returned slice makes sure it borrows from `field`.
3638
unsafe { CharSlice::from_raw_parts(string.as_ptr().cast(), string.len()) }
3739
}
3840

@@ -57,33 +59,39 @@ fn exists_hashmap<V>(map: &HashMap<BytesString, V>, key: CharSlice) -> bool {
5759
map.contains_key(&bytes_str_key)
5860
}
5961

60-
#[allow(clippy::missing_safety_doc)]
61-
unsafe fn get_hashmap_keys<V>(
62-
map: &HashMap<BytesString, V>,
62+
/// The return value is an owned array of slices (`Box<[CharSlice<'a>]>`) that must be dropped
63+
/// explicitly.
64+
fn get_hashmap_keys<'a, V>(
65+
map: &'a HashMap<BytesString, V>,
6366
out_count: &mut usize,
64-
) -> *mut CharSlice<'static> {
67+
) -> *mut CharSlice<'a> {
6568
let mut keys: Vec<&str> = map.keys().map(|b| b.as_str()).collect();
6669
keys.sort_unstable();
6770

68-
let mut slices = Vec::with_capacity(keys.len());
69-
for key in keys {
70-
slices.push(CharSlice::from_raw_parts(key.as_ptr().cast(), key.len()));
71-
}
71+
let slices: Box<[CharSlice]> = keys
72+
.iter()
73+
.map(|key| {
74+
// Safety: `BytesString` enforces that the content is valid UTF8. We returne a slice
75+
// with lifetime `'a`, which guarantees that the underlying allocation will remain live
76+
// and immutable for the lifetime of the slice.
77+
unsafe { CharSlice::from_raw_parts(key.as_ptr().cast(), key.len()) }
78+
})
79+
.collect();
7280

7381
*out_count = slices.len();
74-
Box::into_raw(slices.into_boxed_slice()) as *mut CharSlice<'static>
82+
Box::into_raw(slices) as *mut CharSlice<'a>
7583
}
7684

77-
#[allow(clippy::missing_safety_doc)]
78-
unsafe fn new_vector_item<T: Default>(vec: &mut Vec<T>) -> &mut T {
85+
fn new_vector_item<T: Default>(vec: &mut Vec<T>) -> &mut T {
7986
vec.push(T::default());
80-
vec.last_mut().unwrap_unchecked()
87+
// Safety: we just pushed a value to the vector, so `last_mut()` returns `Some`
88+
unsafe { vec.last_mut().unwrap_unchecked() }
8189
}
8290

83-
#[allow(clippy::missing_safety_doc)]
84-
unsafe fn new_vector_push<T>(vec: &mut Vec<T>, el: T) -> &mut T {
91+
fn new_vector_push<T>(vec: &mut Vec<T>, el: T) -> &mut T {
8592
vec.push(el);
86-
vec.last_mut().unwrap_unchecked()
93+
// Safety: we just pushed a value to the vector, so `last_mut()` returns `Some`
94+
unsafe { vec.last_mut().unwrap_unchecked() }
8795
}
8896

8997
#[no_mangle]
@@ -130,20 +138,20 @@ pub extern "C" fn ddog_get_traces_size(traces: &TracesBytes) -> usize {
130138
traces.len()
131139
}
132140

133-
#[no_mangle]
134-
pub extern "C" fn ddog_get_trace(traces: &mut TracesBytes, index: usize) -> *mut TraceBytes {
135-
if index >= traces.len() {
136-
return std::ptr::null_mut();
137-
}
141+
// Note that per [RFC3391](https://rust-lang.github.io/rfcs/3391-result_ffi_guarantees.html), we
142+
// can return values as `Option<&/&mut>` that will have the same ABI `*const/*mut TraceBytes`, with
143+
// `None` corresponding to `null`.
138144

139-
unsafe { traces.get_unchecked_mut(index) as *mut TraceBytes }
145+
#[no_mangle]
146+
pub extern "C" fn ddog_get_trace(traces: &mut TracesBytes, index: usize) -> Option<&TraceBytes> {
147+
traces.get(index)
140148
}
141149

142150
// ------------------ TraceBytes ------------------
143151

144152
#[no_mangle]
145153
pub extern "C" fn ddog_traces_new_trace(traces: &mut TracesBytes) -> &mut TraceBytes {
146-
unsafe { new_vector_item(traces) }
154+
new_vector_item(traces)
147155
}
148156

149157
#[no_mangle]
@@ -152,19 +160,15 @@ pub extern "C" fn ddog_get_trace_size(trace: &TraceBytes) -> usize {
152160
}
153161

154162
#[no_mangle]
155-
pub extern "C" fn ddog_get_span(trace: &mut TraceBytes, index: usize) -> *mut SpanBytes {
156-
if index >= trace.len() {
157-
return std::ptr::null_mut();
158-
}
159-
160-
unsafe { trace.get_unchecked_mut(index) as *mut SpanBytes }
163+
pub extern "C" fn ddog_get_span(trace: &mut TraceBytes, index: usize) -> Option<&SpanBytes> {
164+
trace.get(index)
161165
}
162166

163167
// ------------------- SpanBytes -------------------
164168

165169
#[no_mangle]
166170
pub extern "C" fn ddog_trace_new_span(trace: &mut TraceBytes) -> &mut SpanBytes {
167-
unsafe { new_vector_item(trace) }
171+
new_vector_item(trace)
168172
}
169173

170174
#[no_mangle]
@@ -173,40 +177,46 @@ pub extern "C" fn ddog_trace_new_span_with_capacities(
173177
meta_size: usize,
174178
metrics_size: usize,
175179
) -> &mut SpanBytes {
176-
unsafe {
177-
new_vector_push(
178-
trace,
179-
SpanBytes {
180-
meta: HashMap::with_capacity(meta_size),
181-
metrics: HashMap::with_capacity(metrics_size),
182-
..SpanBytes::default()
183-
},
184-
)
185-
}
180+
new_vector_push(
181+
trace,
182+
SpanBytes {
183+
meta: HashMap::with_capacity(meta_size),
184+
metrics: HashMap::with_capacity(metrics_size),
185+
..SpanBytes::default()
186+
},
187+
)
186188
}
187189

190+
/// The returned slice is an owned allocation that must be properly freed using
191+
/// [`ddog_free_charslice`].
188192
#[no_mangle]
189193
pub extern "C" fn ddog_span_debug_log(span: &SpanBytes) -> CharSlice<'static> {
190-
unsafe {
191-
let debug_str = format!("{span:?}");
192-
let len = debug_str.len();
193-
let cstring = CString::new(debug_str).unwrap_or_default();
194+
let debug_str = format!("{span:?}");
195+
let len = debug_str.len();
196+
let cstring = CString::new(debug_str).unwrap_or_default();
194197

195-
CharSlice::from_raw_parts(cstring.into_raw().cast(), len)
196-
}
198+
// Safety: `CString` is an owned, valid UTF8 string.
199+
unsafe { CharSlice::from_raw_parts(cstring.into_raw().cast(), len) }
197200
}
198201

202+
/// Frees an owned [`CharSlice`]. Note that some functions of this API return borrowed slices that
203+
/// must NOT be freed. Only a few selected functions return slices that must be freed, and this is
204+
/// mentioned explicitly in their documentation.
205+
///
206+
/// # Safety
207+
///
208+
/// `slice` must be an owned char slice that has been returned by one of the functions of this API.
199209
#[no_mangle]
200-
pub extern "C" fn ddog_free_charslice(slice: CharSlice<'static>) {
210+
pub unsafe extern "C" fn ddog_free_charslice(slice: CharSlice<'static>) {
201211
let (ptr, len) = slice.as_raw_parts();
202212

203213
if len == 0 || ptr.is_null() {
204214
return;
205215
}
206216

217+
// Safety: the owned char slices returned by functions of this API are `Box`-allocated.
207218
unsafe {
208-
let owned_ptr = ptr as *mut c_char;
209-
let _ = Box::from_raw(owned_ptr);
219+
let _ = Box::from_raw(ptr as *mut c_char);
210220
}
211221
}
212222

@@ -216,7 +226,7 @@ pub extern "C" fn ddog_set_span_service(span: &mut SpanBytes, slice: CharSlice)
216226
}
217227

218228
#[no_mangle]
219-
pub extern "C" fn ddog_get_span_service(span: &mut SpanBytes) -> CharSlice<'static> {
229+
pub extern "C" fn ddog_get_span_service(span: &mut SpanBytes) -> CharSlice<'_> {
220230
get_string_field(&span.service)
221231
}
222232

@@ -226,7 +236,7 @@ pub extern "C" fn ddog_set_span_name(span: &mut SpanBytes, slice: CharSlice) {
226236
}
227237

228238
#[no_mangle]
229-
pub extern "C" fn ddog_get_span_name(span: &mut SpanBytes) -> CharSlice<'static> {
239+
pub extern "C" fn ddog_get_span_name(span: &mut SpanBytes) -> CharSlice<'_> {
230240
get_string_field(&span.name)
231241
}
232242

@@ -236,7 +246,7 @@ pub extern "C" fn ddog_set_span_resource(span: &mut SpanBytes, slice: CharSlice)
236246
}
237247

238248
#[no_mangle]
239-
pub extern "C" fn ddog_get_span_resource(span: &mut SpanBytes) -> CharSlice<'static> {
249+
pub extern "C" fn ddog_get_span_resource(span: &mut SpanBytes) -> CharSlice<'_> {
240250
get_string_field(&span.resource)
241251
}
242252

@@ -246,7 +256,7 @@ pub extern "C" fn ddog_set_span_type(span: &mut SpanBytes, slice: CharSlice) {
246256
}
247257

248258
#[no_mangle]
249-
pub extern "C" fn ddog_get_span_type(span: &mut SpanBytes) -> CharSlice<'static> {
259+
pub extern "C" fn ddog_get_span_type(span: &mut SpanBytes) -> CharSlice<'_> {
250260
get_string_field(&span.r#type)
251261
}
252262

@@ -325,9 +335,15 @@ pub extern "C" fn ddog_del_span_meta(span: &mut SpanBytes, key: CharSlice) {
325335
}
326336

327337
#[no_mangle]
328-
pub extern "C" fn ddog_get_span_meta(span: &mut SpanBytes, key: CharSlice) -> CharSlice<'static> {
338+
pub extern "C" fn ddog_get_span_meta<'a>(
339+
span: &'a mut SpanBytes,
340+
key: CharSlice<'_>,
341+
) -> CharSlice<'a> {
329342
let bytes_str_key = convert_char_slice_to_bytes_string(key);
330343
match span.meta.get(&bytes_str_key) {
344+
// Safety: value is a `ByteString`, which guarantees it is valid UTF8. We return a slice
345+
// that borrows from `span`, making sure it remains alive and immutable for the lifetime of
346+
// the slice.
331347
Some(value) => unsafe {
332348
CharSlice::from_raw_parts(value.as_str().as_ptr().cast(), value.as_str().len())
333349
},
@@ -340,12 +356,14 @@ pub extern "C" fn ddog_has_span_meta(span: &mut SpanBytes, key: CharSlice) -> bo
340356
exists_hashmap(&span.meta, key)
341357
}
342358

359+
/// The return value is an owned array of slices (`Box<[CharSlice]>`) that must be freed explicitly
360+
/// through [`ddog_span_free_keys_ptr`].
343361
#[no_mangle]
344-
pub extern "C" fn ddog_span_meta_get_keys(
345-
span: &mut SpanBytes,
362+
pub extern "C" fn ddog_span_meta_get_keys<'a>(
363+
span: &'a mut SpanBytes,
346364
out_count: &mut usize,
347-
) -> *mut CharSlice<'static> {
348-
unsafe { get_hashmap_keys(&span.meta, out_count) }
365+
) -> *mut CharSlice<'a> {
366+
get_hashmap_keys(&span.meta, out_count)
349367
}
350368

351369
#[no_mangle]
@@ -380,11 +398,11 @@ pub extern "C" fn ddog_has_span_metrics(span: &mut SpanBytes, key: CharSlice) ->
380398
}
381399

382400
#[no_mangle]
383-
pub extern "C" fn ddog_span_metrics_get_keys(
384-
span: &mut SpanBytes,
401+
pub extern "C" fn ddog_span_metrics_get_keys<'a>(
402+
span: &'a mut SpanBytes,
385403
out_count: &mut usize,
386-
) -> *mut CharSlice<'static> {
387-
unsafe { get_hashmap_keys(&span.metrics, out_count) }
404+
) -> *mut CharSlice<'a> {
405+
get_hashmap_keys(&span.metrics, out_count)
388406
}
389407

390408
#[no_mangle]
@@ -402,12 +420,15 @@ pub extern "C" fn ddog_del_span_meta_struct(span: &mut SpanBytes, key: CharSlice
402420
}
403421

404422
#[no_mangle]
405-
pub extern "C" fn ddog_get_span_meta_struct(
406-
span: &mut SpanBytes,
407-
key: CharSlice,
408-
) -> CharSlice<'static> {
423+
pub extern "C" fn ddog_get_span_meta_struct<'a>(
424+
span: &'a mut SpanBytes,
425+
key: CharSlice<'_>,
426+
) -> CharSlice<'a> {
409427
let bytes_str_key = convert_char_slice_to_bytes_string(key);
410428
match span.meta_struct.get(&bytes_str_key) {
429+
// Safety: value is a `ByteString`, which guarantees it is valid UTF8. We return a slice
430+
// that borrows from `span`, ensuring it remains alive and unchanged for the lifetime of
431+
// the slice.
411432
Some(value) => unsafe { CharSlice::from_raw_parts(value.as_ptr().cast(), value.len()) },
412433
None => CharSlice::empty(),
413434
}
@@ -418,29 +439,39 @@ pub extern "C" fn ddog_has_span_meta_struct(span: &mut SpanBytes, key: CharSlice
418439
exists_hashmap(&span.meta_struct, key)
419440
}
420441

442+
/// The return value is an array of slices (`Box<[CharSlice]>`) that must be freed explicitly
443+
/// through [`ddog_span_free_keys_ptr`].
421444
#[no_mangle]
422-
pub extern "C" fn ddog_span_meta_struct_get_keys(
423-
span: &mut SpanBytes,
445+
pub extern "C" fn ddog_span_meta_struct_get_keys<'a>(
446+
span: &'a mut SpanBytes,
424447
out_count: &mut usize,
425-
) -> *mut CharSlice<'static> {
426-
unsafe { get_hashmap_keys(&span.meta_struct, out_count) }
448+
) -> *mut CharSlice<'a> {
449+
get_hashmap_keys(&span.meta_struct, out_count)
427450
}
428451

452+
/// # Safety
453+
///
454+
/// `keys_ptr` must have been returned by one of the `ddog_xxx_get_keys()` functions, and must not
455+
/// have been already freed.
429456
#[no_mangle]
430-
#[allow(clippy::missing_safety_doc)]
431-
pub unsafe extern "C" fn ddog_span_free_keys_ptr(keys_ptr: *mut CharSlice<'static>, count: usize) {
457+
pub unsafe extern "C" fn ddog_span_free_keys_ptr(keys_ptr: *mut CharSlice<'_>, count: usize) {
432458
if keys_ptr.is_null() || count == 0 {
433459
return;
434460
}
435461

436-
Vec::from_raw_parts(keys_ptr, count, count);
462+
// Safety: all `xxx_get_keys()` functions return from `get_hashmap_keys()`, which returns a
463+
// `Box<[T]>`. It is an official guarantee of `Vec` that this can be freely converted to and
464+
// from `Box<[T]>` when `len == capacity`.
465+
unsafe {
466+
Vec::from_raw_parts(keys_ptr, count, count);
467+
}
437468
}
438469

439470
// ------------------- SpanLinkBytes -------------------
440471

441472
#[no_mangle]
442473
pub extern "C" fn ddog_span_new_link(span: &mut SpanBytes) -> &mut SpanLinkBytes {
443-
unsafe { new_vector_item(&mut span.span_links) }
474+
new_vector_item(&mut span.span_links)
444475
}
445476

446477
#[no_mangle]
@@ -485,7 +516,7 @@ pub extern "C" fn ddog_add_link_attributes(
485516

486517
#[no_mangle]
487518
pub extern "C" fn ddog_span_new_event(span: &mut SpanBytes) -> &mut SpanEventBytes {
488-
unsafe { new_vector_item(&mut span.span_events) }
519+
new_vector_item(&mut span.span_events)
489520
}
490521

491522
#[no_mangle]
@@ -540,6 +571,8 @@ pub extern "C" fn ddog_add_event_attributes_float(
540571

541572
// ------------------- Export Functions -------------------
542573

574+
/// The returned slice is an owned allocation that must be properly freed using
575+
/// [`ddog_free_charslice`].
543576
#[no_mangle]
544577
pub extern "C" fn ddog_serialize_trace_into_charslice(
545578
trace: &mut TraceBytes,

0 commit comments

Comments
 (0)