Skip to content

Commit 91c0d49

Browse files
feat(tinybytes): use a custom ref counted cell for Bytes with dynamic dispatch (#1061)
# What does this PR do? Allow to replace the refcounted type used in tinybytes, by using a custom dispatch table for clone a drop
1 parent 2901823 commit 91c0d49

File tree

2 files changed

+157
-5
lines changed

2 files changed

+157
-5
lines changed

tinybytes/src/lib.rs

Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
use std::{
1111
borrow, cmp, fmt, hash,
1212
ops::{self, RangeBounds},
13-
sync::Arc,
13+
ptr::NonNull,
14+
sync::atomic::AtomicUsize,
1415
};
1516

1617
#[cfg(feature = "serde")]
@@ -22,7 +23,7 @@ pub struct Bytes {
2223
slice: &'static [u8],
2324
// The `bytes`` field is used to ensure that the underlying bytes are freed when there are no
2425
// more references to the `Bytes` object. For static buffers the field is `None`.
25-
bytes: Option<Arc<dyn UnderlyingBytes>>,
26+
bytes: Option<RefCountedCell>,
2627
}
2728

2829
/// The underlying bytes that the `Bytes` object references.
@@ -34,6 +35,27 @@ unsafe impl Send for Bytes {}
3435
unsafe impl Sync for Bytes {}
3536

3637
impl Bytes {
38+
#[inline]
39+
/// Creates a new `Bytes` from the given slice data and the refcount
40+
///
41+
/// # Safety
42+
///
43+
/// * the pointer should be valid for the given length
44+
/// * the pointer should be valid for reads as long as the refcount or any of it's clone is not
45+
/// dropped
46+
pub const unsafe fn from_raw_refcount(
47+
ptr: NonNull<u8>,
48+
len: usize,
49+
refcount: RefCountedCell,
50+
) -> Self {
51+
// SAFETY: The caller must ensure that the pointer is valid and that the length is correct.
52+
let slice = unsafe { std::slice::from_raw_parts(ptr.as_ptr(), len) };
53+
Self {
54+
slice,
55+
bytes: Some(refcount),
56+
}
57+
}
58+
3759
/// Creates empty `Bytes`.
3860
#[inline]
3961
pub const fn empty() -> Self {
@@ -186,9 +208,16 @@ impl Bytes {
186208
// private
187209

188210
fn from_underlying(value: impl UnderlyingBytes) -> Self {
189-
Self {
190-
slice: unsafe { std::mem::transmute::<&'_ [u8], &'static [u8]>(value.as_ref()) },
191-
bytes: Some(Arc::new(value)),
211+
unsafe {
212+
// SAFETY:
213+
// * the pointer associated with a slice is non null and valid for the length of the
214+
// slice
215+
// * it stays valid as long as value is not dopped
216+
let (ptr, len) = {
217+
let s = value.as_ref();
218+
(NonNull::new_unchecked(s.as_ptr().cast_mut()), s.len())
219+
};
220+
Self::from_raw_refcount(ptr, len, make_refcounted(value))
192221
}
193222
}
194223

@@ -291,6 +320,118 @@ impl Serialize for Bytes {
291320
}
292321
}
293322

323+
pub struct RefCountedCell {
324+
data: NonNull<()>,
325+
vtable: &'static RefCountedCellVTable,
326+
}
327+
328+
unsafe impl Send for RefCountedCell {}
329+
unsafe impl Sync for RefCountedCell {}
330+
331+
impl RefCountedCell {
332+
#[inline]
333+
/// Creates a new `RefCountedCell` from the given data and vtable.
334+
///
335+
/// The data pointer can be used to store arbitrary data, that won't be dropped until the last
336+
/// clone to the `RefCountedCell` is dropped.
337+
/// The vtable customizes the behavior of a Waker which gets created from a RawWaker. For each
338+
/// operation on the Waker, the associated function in the vtable of the underlying RawWaker
339+
/// will be called.
340+
///
341+
/// # Safety
342+
///
343+
/// * The value pointed to by `data` must be 'static + Send + Sync
344+
pub const unsafe fn from_raw(data: NonNull<()>, vtable: &'static RefCountedCellVTable) -> Self {
345+
RefCountedCell { data, vtable }
346+
}
347+
}
348+
349+
impl Clone for RefCountedCell {
350+
fn clone(&self) -> Self {
351+
unsafe { (self.vtable.clone)(self.data) }
352+
}
353+
}
354+
355+
impl Drop for RefCountedCell {
356+
fn drop(&mut self) {
357+
unsafe { (self.vtable.drop)(self.data) }
358+
}
359+
}
360+
361+
pub struct RefCountedCellVTable {
362+
pub clone: unsafe fn(NonNull<()>) -> RefCountedCell,
363+
pub drop: unsafe fn(NonNull<()>),
364+
}
365+
366+
/// Creates a refcounted cell.
367+
///
368+
/// The data passed to this cell will only be dopped when the last
369+
/// clone of the cell is dropped.
370+
fn make_refcounted<T: Send + Sync + 'static>(data: T) -> RefCountedCell {
371+
/// A custom Arc implementation that contains only the strong count
372+
///
373+
/// This struct is not exposed to the outside of this functions and is
374+
/// only interacted with through the `RefCountedCell` API.
375+
struct CustomArc<T> {
376+
rc: AtomicUsize,
377+
#[allow(unused)]
378+
data: T,
379+
}
380+
381+
unsafe fn custom_arc_clone<T>(data: NonNull<()>) -> RefCountedCell {
382+
let custom_arc = data.cast::<CustomArc<T>>().as_ref();
383+
custom_arc
384+
.rc
385+
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
386+
RefCountedCell::from_raw(
387+
data,
388+
&RefCountedCellVTable {
389+
clone: custom_arc_clone::<T>,
390+
drop: custom_arc_drop::<T>,
391+
},
392+
)
393+
}
394+
395+
unsafe fn custom_arc_drop<T>(data: NonNull<()>) {
396+
let custom_arc = data.cast::<CustomArc<T>>().as_ref();
397+
if custom_arc
398+
.rc
399+
.fetch_sub(1, std::sync::atomic::Ordering::Release)
400+
!= 1
401+
{
402+
return;
403+
}
404+
405+
// Run drop + free memory on the data manually rather than casting back to a box
406+
// because otherwise miri complains
407+
408+
// See standard library documentation for std::sync::Arc to see why this is needed.
409+
// https://github.com/rust-lang/rust/blob/2a5da7acd4c3eae638aa1c46f3a537940e60a0e4/library/alloc/src/sync.rs#L2647-L2675
410+
std::sync::atomic::fence(std::sync::atomic::Ordering::Acquire);
411+
{
412+
let custom_arc = data.cast::<CustomArc<T>>().as_mut();
413+
std::ptr::drop_in_place(custom_arc);
414+
}
415+
416+
std::alloc::dealloc(
417+
data.as_ptr() as *mut u8,
418+
std::alloc::Layout::new::<CustomArc<T>>(),
419+
);
420+
}
421+
422+
let rc = Box::leak(Box::new(CustomArc {
423+
rc: AtomicUsize::new(1),
424+
data,
425+
})) as *mut _ as *const ();
426+
RefCountedCell {
427+
data: unsafe { NonNull::new_unchecked(rc as *mut ()) },
428+
vtable: &RefCountedCellVTable {
429+
clone: custom_arc_clone::<T>,
430+
drop: custom_arc_drop::<T>,
431+
},
432+
}
433+
}
434+
294435
#[cfg(feature = "bytes_string")]
295436
mod bytes_string;
296437
#[cfg(feature = "bytes_string")]

tinybytes/src/test.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use core::str;
55
use std::sync::atomic::{self, AtomicUsize};
6+
use std::sync::Arc;
67

78
use super::*;
89
use once_cell::sync::OnceCell;
@@ -21,6 +22,16 @@ fn hello_slice(range: impl RangeBounds<usize>) -> Bytes {
2122
hello().slice(range)
2223
}
2324

25+
#[test]
26+
fn test_make_refcounted() {
27+
let data = vec![1, 2, 3];
28+
let refcounted = make_refcounted(data);
29+
let refcounted_clone = refcounted.clone();
30+
31+
drop(refcounted);
32+
drop(refcounted_clone);
33+
}
34+
2435
#[allow(clippy::reversed_empty_ranges)]
2536
#[test_case(0..0, ""; "0 to 0 is empty")]
2637
#[test_case(.., "hello"; "full range is hello")]

0 commit comments

Comments
 (0)