Skip to main content

arrow_buffer/
bytes.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! This module contains an implementation of a contiguous immutable memory region that knows
19//! how to de-allocate itself, [`Bytes`].
20//! Note that this is a low-level functionality of this crate.
21
22use core::slice;
23use std::ptr::NonNull;
24use std::{fmt::Debug, fmt::Formatter};
25
26use crate::alloc::Deallocation;
27use crate::buffer::dangling_ptr;
28#[cfg(feature = "pool")]
29use crate::{MemoryPool, TrackedReservation};
30
31/// A continuous, fixed-size, immutable memory region that knows how to de-allocate itself.
32///
33/// Note that this structure is an internal implementation detail of the
34/// arrow-rs crate. While it has the same name and similar API as
35/// [`bytes::Bytes`] it is not limited to rust's global allocator nor u8
36/// alignment. It is possible to create a `Bytes` from `bytes::Bytes` using the
37/// `From` implementation.
38///
39/// In the most common case, this buffer is allocated using [`alloc`](std::alloc::alloc)
40/// with an alignment of [`ALIGNMENT`](crate::alloc::ALIGNMENT)
41///
42/// When the region is allocated by a different allocator, [Deallocation::Custom], this calls the
43/// custom deallocator to deallocate the region when it is no longer needed.
44///
45pub(crate) struct Bytes {
46    /// The raw pointer to be beginning of the region
47    ptr: NonNull<u8>,
48
49    /// The number of bytes visible to this region. This is always smaller than its capacity (when available).
50    len: usize,
51
52    /// how to deallocate this region
53    deallocation: Deallocation,
54
55    /// Memory reservation for tracking memory usage
56    #[cfg(feature = "pool")]
57    pub(super) reservation: TrackedReservation,
58}
59
60impl Bytes {
61    /// Takes ownership of an allocated memory region,
62    ///
63    /// # Arguments
64    ///
65    /// * `ptr` - Pointer to raw parts
66    /// * `len` - Length of raw parts in **bytes**
67    /// * `deallocation` - Type of allocation
68    ///
69    /// # Safety
70    ///
71    /// This function is unsafe as there is no guarantee that the given pointer is valid for `len`
72    /// bytes. If the `ptr` and `capacity` come from a `Buffer`, then this is guaranteed.
73    #[inline]
74    pub(crate) unsafe fn new(ptr: NonNull<u8>, len: usize, deallocation: Deallocation) -> Bytes {
75        Bytes {
76            ptr,
77            len,
78            deallocation,
79            #[cfg(feature = "pool")]
80            reservation: TrackedReservation::default(),
81        }
82    }
83
84    fn as_slice(&self) -> &[u8] {
85        self
86    }
87
88    #[inline]
89    pub(crate) fn len(&self) -> usize {
90        self.len
91    }
92
93    #[inline]
94    pub(crate) fn ptr(&self) -> NonNull<u8> {
95        self.ptr
96    }
97
98    pub(crate) fn capacity(&self) -> usize {
99        match self.deallocation {
100            Deallocation::Standard(layout) => layout.size(),
101            // we only know the size of the custom allocation
102            // its underlying capacity might be larger
103            Deallocation::Custom(_, size) => size,
104        }
105    }
106
107    /// Register this [`Bytes`] with the provided [`MemoryPool`], replacing any prior reservation.
108    #[cfg(feature = "pool")]
109    pub(crate) fn claim(&self, pool: &dyn MemoryPool) {
110        self.reservation.claim(pool, self.capacity());
111    }
112
113    /// Resize the memory reservation of this buffer
114    ///
115    /// This is a no-op if this buffer doesn't have a reservation.
116    #[cfg(feature = "pool")]
117    fn resize_reservation(&self, new_size: usize) {
118        self.reservation.resize(new_size);
119    }
120
121    /// Try to reallocate the underlying memory region to a new size (smaller or larger).
122    ///
123    /// Only works for bytes allocated with the standard allocator.
124    /// Returns `Err` if the memory was allocated with a custom allocator,
125    /// or the call to `realloc` failed, for whatever reason.
126    /// In case of `Err`, the [`Bytes`] will remain as it was (i.e. have the old size).
127    ///
128    /// `on_reallocated` is called after [`Bytes`] has updated its internal
129    /// pointer, but before resizing the memory reservation, which may call user
130    /// code.
131    pub(crate) fn try_realloc(
132        &mut self,
133        new_len: usize,
134        on_reallocated: impl FnOnce(NonNull<u8>),
135    ) -> Result<(), ()> {
136        if let Deallocation::Standard(old_layout) = self.deallocation {
137            if old_layout.size() == new_len {
138                return Ok(()); // Nothing to do
139            }
140
141            if let Ok(new_layout) = std::alloc::Layout::from_size_align(new_len, old_layout.align())
142            {
143                let old_ptr = self.ptr.as_ptr();
144
145                let new_ptr = match new_layout.size() {
146                    0 => {
147                        // SAFETY: Verified that old_layout.size != new_len (0)
148                        unsafe { std::alloc::dealloc(self.ptr.as_ptr(), old_layout) };
149                        Some(dangling_ptr())
150                    }
151                    // SAFETY: the call to `realloc` is safe if all the following hold (from https://doc.rust-lang.org/stable/std/alloc/trait.GlobalAlloc.html#method.realloc):
152                    // * `old_ptr` must be currently allocated via this allocator (guaranteed by the invariant/contract of `Bytes`)
153                    // * `old_layout` must be the same layout that was used to allocate that block of memory (same)
154                    // * `new_len` must be greater than zero
155                    // * `new_len`, when rounded up to the nearest multiple of `layout.align()`, must not overflow `isize` (guaranteed by the success of `Layout::from_size_align`)
156                    _ => NonNull::new(unsafe { std::alloc::realloc(old_ptr, old_layout, new_len) }),
157                };
158
159                if let Some(ptr) = new_ptr {
160                    self.ptr = ptr;
161                    self.len = new_len;
162                    self.deallocation = Deallocation::Standard(new_layout);
163                    on_reallocated(ptr);
164
165                    #[cfg(feature = "pool")]
166                    {
167                        // Resize reservation
168                        self.resize_reservation(new_len);
169                    }
170
171                    return Ok(());
172                }
173            }
174        }
175
176        Err(())
177    }
178
179    #[inline]
180    pub(crate) fn deallocation(&self) -> &Deallocation {
181        &self.deallocation
182    }
183}
184
185// Deallocation is Send + Sync, repeating the bound here makes that refactoring safe
186// The only field that is not automatically Send+Sync then is the NonNull ptr
187unsafe impl Send for Bytes where Deallocation: Send {}
188unsafe impl Sync for Bytes where Deallocation: Sync {}
189
190impl Drop for Bytes {
191    #[inline]
192    fn drop(&mut self) {
193        match &self.deallocation {
194            Deallocation::Standard(layout) => match layout.size() {
195                0 => {} // Nothing to do
196                _ => unsafe { std::alloc::dealloc(self.ptr.as_ptr(), *layout) },
197            },
198            // The automatic drop implementation will free the memory once the reference count reaches zero
199            Deallocation::Custom(_allocation, _size) => (),
200        }
201    }
202}
203
204impl std::ops::Deref for Bytes {
205    type Target = [u8];
206
207    fn deref(&self) -> &[u8] {
208        unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
209    }
210}
211
212impl PartialEq for Bytes {
213    fn eq(&self, other: &Bytes) -> bool {
214        self.as_slice() == other.as_slice()
215    }
216}
217
218impl Debug for Bytes {
219    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
220        write!(f, "Bytes {{ ptr: {:?}, len: {}, data: ", self.ptr, self.len)?;
221
222        f.debug_list().entries(self.iter()).finish()?;
223
224        write!(f, " }}")
225    }
226}
227
228impl From<bytes::Bytes> for Bytes {
229    fn from(value: bytes::Bytes) -> Self {
230        let len = value.len();
231        Self {
232            len,
233            // `bytes::Bytes` is shared and immutable, so the buffer is never written
234            // through this pointer; the cast only changes constness.
235            ptr: NonNull::new(value.as_ptr().cast_mut()).unwrap(),
236            deallocation: Deallocation::Custom(std::sync::Arc::new(value), len),
237            #[cfg(feature = "pool")]
238            reservation: TrackedReservation::default(),
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_from_bytes() {
249        let message = b"hello arrow";
250
251        // we can create a Bytes from bytes::Bytes (created from slices)
252        let c_bytes: bytes::Bytes = message.as_ref().into();
253        let a_bytes: Bytes = c_bytes.into();
254        assert_eq!(a_bytes.as_slice(), message);
255
256        // we can create a Bytes from bytes::Bytes (created from Vec)
257        let c_bytes: bytes::Bytes = bytes::Bytes::from(message.to_vec());
258        let a_bytes: Bytes = c_bytes.into();
259        assert_eq!(a_bytes.as_slice(), message);
260    }
261
262    #[cfg(feature = "pool")]
263    mod pool_tests {
264        use super::*;
265
266        use crate::pool::TrackingMemoryPool;
267
268        #[test]
269        fn test_bytes_with_pool() {
270            // Create a standard allocation
271            let buffer = unsafe {
272                let layout =
273                    std::alloc::Layout::from_size_align(1024, crate::alloc::ALIGNMENT).unwrap();
274                let ptr = std::alloc::alloc(layout);
275                assert!(!ptr.is_null());
276
277                Bytes::new(
278                    NonNull::new(ptr).unwrap(),
279                    1024,
280                    Deallocation::Standard(layout),
281                )
282            };
283
284            // Create a memory pool
285            let pool = TrackingMemoryPool::default();
286            assert_eq!(pool.used(), 0);
287
288            // Reserve memory and assign to buffer. Claim twice.
289            buffer.claim(&pool);
290            assert_eq!(pool.used(), 1024);
291            buffer.claim(&pool);
292            assert_eq!(pool.used(), 1024);
293
294            // Memory should be released when buffer is dropped
295            drop(buffer);
296            assert_eq!(pool.used(), 0);
297        }
298
299        #[test]
300        fn test_bytes_drop_releases_pool() {
301            let pool = TrackingMemoryPool::default();
302
303            {
304                // Create a buffer with pool
305                let _buffer = unsafe {
306                    let layout =
307                        std::alloc::Layout::from_size_align(1024, crate::alloc::ALIGNMENT).unwrap();
308                    let ptr = std::alloc::alloc(layout);
309                    assert!(!ptr.is_null());
310
311                    let bytes = Bytes::new(
312                        NonNull::new(ptr).unwrap(),
313                        1024,
314                        Deallocation::Standard(layout),
315                    );
316
317                    bytes.claim(&pool);
318                    bytes
319                };
320
321                assert_eq!(pool.used(), 1024);
322            }
323
324            // Buffer has been dropped, memory should be released
325            assert_eq!(pool.used(), 0);
326        }
327    }
328}