Skip to main content

varnish_sys/vcl/
ws.rs

1//! Store data in a task-centric store to share with the C layers
2//!
3//! The workspace is a memory allocator with a simple API that allows Varnish to store data that
4//! needs only to live for the lifetime of a task (handling a client or backend request for example).
5//! At the end of the task, the workspace is wiped, simplifying memory management.
6//!
7//! Rust handles its own memory, but some data must be shared/returned to the C caller, and the
8//! workspace is usually the easiest store available.
9//!
10//! **Note:** unless you know what you are doing, you should probably just use the automatic type
11//! conversion provided by [`crate::vcl::convert`], or store things in
12//! [`crate::vcl::vpriv::VPriv`].
13
14use std::any::type_name;
15use std::ffi::{c_char, c_void, CStr};
16use std::fmt::Debug;
17use std::marker::PhantomData;
18use std::mem::{align_of, size_of, transmute, MaybeUninit};
19use std::num::NonZeroUsize;
20use std::ptr;
21use std::slice::from_raw_parts_mut;
22
23use memchr::memchr;
24
25use crate::ffi::{txt, vrt_blob, WS_Allocated, VCL_BLOB, VCL_STRING};
26pub use crate::vcl::ws_str_buffer::WsBlobBuffer;
27pub use crate::vcl::ws_str_buffer::{WsBuffer, WsStrBuffer, WsTempBuffer};
28use crate::vcl::{VclError, VclResult};
29use crate::{ffi, validate_ws};
30
31#[cfg(not(test))]
32impl ffi::ws {
33    pub(crate) unsafe fn alloc(&mut self, size: u32) -> *mut c_void {
34        assert!(size > 0);
35        ffi::WS_Alloc(self, size)
36    }
37    pub(crate) unsafe fn reserve_all(&mut self) -> u32 {
38        ffi::WS_ReserveAll(self)
39    }
40    pub(crate) unsafe fn release(&mut self, len: u32) {
41        ffi::WS_Release(self, len);
42    }
43}
44
45#[cfg(test)]
46impl ffi::ws {
47    const ALIGN: usize = align_of::<*const c_void>();
48    pub(crate) unsafe fn alloc(&mut self, size: u32) -> *mut c_void {
49        // `WS_Alloc` is a private part of `varnishd`, not the Varnish library,
50        // so it is only available if the output is a `cdylib`.
51        // When testing, VMOD is a lib or a bin,
52        // so we have to fake our own allocator.
53        let ws = validate_ws(self);
54        assert!(size > 0);
55        let aligned_sz = (size as usize).div_ceil(Self::ALIGN) * Self::ALIGN;
56        if ws.e.offset_from(ws.f) < aligned_sz as isize {
57            ptr::null_mut()
58        } else {
59            let p = ws.f.cast::<c_void>();
60            ws.f = ws.f.add(aligned_sz);
61            assert!(p.is_aligned());
62            p
63        }
64    }
65
66    #[allow(clippy::unused_self)]
67    pub(crate) unsafe fn reserve_all(&mut self) -> u32 {
68        let ws = validate_ws(self);
69        assert!(ws.r.is_null());
70        ws.r = ws.e;
71        ws.e.offset_from(ws.f)
72            .try_into()
73            .expect("workspace free space must fit in u32")
74    }
75
76    #[allow(clippy::unused_self)]
77    pub(crate) unsafe fn release(&mut self, size: u32) {
78        let ws = validate_ws(self);
79        assert!(
80            isize::try_from(size).expect("workspace size must fit in isize")
81                <= ws.e.offset_from(ws.f)
82        );
83        assert!(
84            isize::try_from(size).expect("workspace size must fit in isize")
85                <= ws.r.offset_from(ws.f)
86        );
87        assert!(!ws.r.is_null());
88        let aligned_sz = usize::try_from(size)
89            .expect("workspace size must fit in usize")
90            .div_ceil(Self::ALIGN)
91            * Self::ALIGN;
92        ws.f = ws.f.add(aligned_sz);
93        assert!(ws.f.is_aligned());
94        ws.r = ptr::null_mut::<c_char>();
95    }
96}
97
98/// A workspace object
99///
100/// Used to allocate memory in an efficient manner, data will live there until the end of the
101/// transaction and the workspace is wiped, so there's no need to free the objects living in it.
102///
103/// The workspace is usually a few tens of kilobytes large, don't be greedy. If you need more
104/// space, consider storing your data in a `#[shared_per_task]` or `#[shared_per_vcl]` objects.
105#[derive(Debug)]
106pub struct Workspace<'ctx> {
107    /// Raw pointer to the C struct
108    pub raw: *mut ffi::ws,
109    _phantom: PhantomData<&'ctx ()>,
110}
111
112impl<'ctx> Workspace<'ctx> {
113    /// Wrap a raw pointer into an object we can use.
114    pub(crate) fn from_ptr(raw: *mut ffi::ws) -> Self {
115        assert!(!raw.is_null(), "raw pointer was null");
116        Self {
117            raw,
118            _phantom: PhantomData,
119        }
120    }
121
122    /// Allocate a buffer of a given size.
123    ///
124    /// # Safety
125    /// Allocated memory is not initialized.
126    pub unsafe fn alloc(&mut self, size: NonZeroUsize) -> *mut c_void {
127        validate_ws(self.raw).alloc(size.get() as u32)
128    }
129
130    /// Check if a pointer is part of the current workspace
131    pub fn contains(&self, data: &[u8]) -> bool {
132        unsafe { WS_Allocated(self.raw, data.as_ptr().cast(), data.len() as isize) == 1 }
133    }
134
135    /// Allocate `[u8; size]` array on Workspace.
136    /// Returns a reference to uninitialized buffer, or an out of memory error.
137    pub fn allocate(
138        &mut self,
139        size: NonZeroUsize,
140    ) -> Result<&'ctx mut [MaybeUninit<u8>], VclError> {
141        let ptr = unsafe { self.alloc(size) };
142        if ptr.is_null() {
143            Err(VclError::WsOutOfMemory(size))
144        } else {
145            Ok(unsafe { from_raw_parts_mut(ptr.cast(), size.get()) })
146        }
147    }
148
149    /// Allocate `[u8; size]` array on Workspace, and zero it.
150    pub fn allocate_zeroed(&mut self, size: NonZeroUsize) -> Result<&'ctx mut [u8], VclError> {
151        let buf = self.allocate(size)?;
152        unsafe {
153            buf.as_mut_ptr().write_bytes(0, buf.len());
154            Ok(slice_assume_init_mut(buf))
155        }
156    }
157
158    /// Allocate memory on Workspace, and move a value into it.
159    /// The value will be dropped in case of out of memory error.
160    pub(crate) fn copy_value<T>(&mut self, value: T) -> Result<&'ctx mut T, VclError> {
161        let size = NonZeroUsize::new(size_of::<T>())
162            .unwrap_or_else(|| panic!("Type {} has sizeof=0", type_name::<T>()));
163
164        let val = unsafe { self.alloc(size).cast::<T>().as_mut() };
165        let val = val.ok_or(VclError::WsOutOfMemory(size))?;
166        *val = value;
167        Ok(val)
168    }
169
170    /// Copy any `AsRef<[u8]>` into the workspace
171    fn copy_bytes(&mut self, src: impl AsRef<[u8]>) -> Result<&'ctx [u8], VclError> {
172        // Re-implement unstable `maybe_uninit_write_slice` and `maybe_uninit_slice`
173        // See https://github.com/rust-lang/rust/issues/79995
174        // See https://github.com/rust-lang/rust/issues/63569
175        let src = src.as_ref();
176        let Some(len) = NonZeroUsize::new(src.len()) else {
177            Err(VclError::CStr(c"Unable to allocate 0 bytes in a Workspace"))?
178        };
179        let dest = self.allocate(len)?;
180        dest.copy_from_slice(maybe_uninit(src));
181        Ok(unsafe { slice_assume_init_mut(dest) })
182    }
183
184    /// Copy any `AsRef<[u8]>` into a new [`VCL_BLOB`] stored in the workspace
185    pub fn copy_blob(&mut self, value: impl AsRef<[u8]>) -> Result<VCL_BLOB, VclError> {
186        let buf = self.copy_bytes(value)?;
187        let blob = self.copy_value(vrt_blob {
188            magic: ffi::VRT_BLOB_MAGIC,
189            blob: ptr::from_ref(buf).cast::<c_void>(),
190            len: buf.len(),
191            ..Default::default()
192        })?;
193        Ok(VCL_BLOB(ptr::from_ref(blob)))
194    }
195
196    /// Copy any `AsRef<CStr>` into a new [`txt`] stored in the workspace
197    pub fn copy_txt(&mut self, value: impl AsRef<CStr>) -> Result<txt, VclError> {
198        let dest = self.copy_bytes(value.as_ref().to_bytes_with_nul())?;
199        Ok(bytes_with_nul_to_txt(dest))
200    }
201
202    /// Copy any `AsRef<CStr>` into a new [`VCL_STRING`] stored in the workspace
203    pub fn copy_cstr(&mut self, value: impl AsRef<CStr>) -> Result<VCL_STRING, VclError> {
204        Ok(VCL_STRING(self.copy_txt(value)?.b))
205    }
206
207    /// Same as [`Workspace::copy_blob`], copying bytes into Workspace, but treats bytes
208    /// as a string with an optional NULL character at the end.  A `NULL` is added if it is missing.
209    /// Returns an error if `src` contain NULL characters in a non-last position.
210    pub fn copy_bytes_with_null(&mut self, src: impl AsRef<[u8]>) -> Result<txt, VclError> {
211        let src = src.as_ref();
212        match memchr(0, src) {
213            Some(pos) if pos + 1 == src.len() => {
214                // Safe because there is only one NULL at the end of the buffer.
215                self.copy_txt(unsafe { CStr::from_bytes_with_nul_unchecked(src) })
216            }
217            Some(_) => Err(VclError::CStr(c"NULL byte found in the source string")),
218            None => {
219                // NUL byte not found, add one at the end
220                // Similar to copy_bytes above
221                let len = src.len();
222                let dest = self.allocate(unsafe { NonZeroUsize::new_unchecked(len + 1) })?;
223                dest[..len].copy_from_slice(maybe_uninit(src));
224                dest[len].write(b'\0');
225                let dest = unsafe { slice_assume_init_mut(dest) };
226                Ok(bytes_with_nul_to_txt(dest))
227            }
228        }
229    }
230
231    /// Allocate workspace free memory as a string buffer until [`WsStrBuffer::finish()`]
232    /// is called, resulting in an unsafe [`VCL_STRING`] that can be returned to Varnish.
233    /// Note that it is possible for the returned buf size to be zero, which
234    /// would result in a zero-length nul-terminated [`VCL_STRING`] if finished.
235    pub fn vcl_string_builder(&mut self) -> VclResult<WsStrBuffer<'ctx>> {
236        unsafe { WsStrBuffer::new(validate_ws(self.raw)) }
237    }
238
239    /// Allocate workspace free memory as a byte buffer until [`WsBlobBuffer::finish()`]
240    /// is called, resulting in an unsafe [`VCL_BLOB`] that can be returned to Varnish.
241    pub fn vcl_blob_builder(&mut self) -> VclResult<WsBlobBuffer<'ctx>> {
242        unsafe { WsBlobBuffer::new(validate_ws(self.raw)) }
243    }
244
245    /// Allocate workspace free memory as a temporary vector-like buffer
246    /// until [`WsTempBuffer::finish()`] is called.  The buffer is not intended
247    /// to be returned to Varnish, but may be shared among context users.
248    /// The buffer is returned as a `&'ws [T]` to allow mutable access,
249    /// while tying the lifetime to the workspace.
250    pub fn slice_builder<T: Copy>(&mut self) -> VclResult<WsTempBuffer<'ctx, T>> {
251        unsafe { WsTempBuffer::new(validate_ws(self.raw)) }
252    }
253}
254
255/// Internal helper to convert a `&[u8]` to a `&[MaybeUninit<u8>]`
256fn maybe_uninit(value: &[u8]) -> &[MaybeUninit<u8>] {
257    // SAFETY: &[T] and &[MaybeUninit<T>] have the same layout
258    // This was copied from MaybeUninit::copy_from_slice, ignoring clippy lints
259    unsafe {
260        #[expect(clippy::transmute_ptr_to_ptr)]
261        transmute(value)
262    }
263}
264
265/// Internal helper to convert a `&mut [MaybeUninit<u8>]` to a `&[u8]`, assuming all elements are initialized
266unsafe fn slice_assume_init_mut(value: &mut [MaybeUninit<u8>]) -> &mut [u8] {
267    // SAFETY: Valid elements have just been copied into `this` so it is initialized
268    // This was copied from MaybeUninit::slice_assume_init_mut, ignoring clippy lints
269    &mut *(ptr::from_mut::<[MaybeUninit<u8>]>(value) as *mut [u8])
270}
271
272/// Helper to convert a byte slice with a null terminator to a `txt` struct.
273fn bytes_with_nul_to_txt(buf: &[u8]) -> txt {
274    txt::from_cstr(unsafe { CStr::from_bytes_with_nul_unchecked(buf) })
275}
276
277/// A struct holding both a native workspace struct and the space it points to.
278///
279/// As the name implies, this struct mainly exist to facilitate testing and should probably not be
280/// used elsewhere.
281#[derive(Debug)]
282pub struct TestWS {
283    c_ws: ffi::ws,
284    #[expect(dead_code)]
285    space: Vec<c_char>,
286}
287
288impl TestWS {
289    /// Instantiate a `C` ws struct and the required space of size `sz`.
290    pub fn new(sz: usize) -> Self {
291        let al = align_of::<*const c_void>();
292        let aligned_sz = (sz / al) * al;
293        let mut space: Vec<c_char> = vec![0; sz];
294        let s = space.as_mut_ptr();
295        assert!(s.is_aligned());
296        assert!(unsafe { s.add(aligned_sz).is_aligned() });
297        Self {
298            c_ws: ffi::ws {
299                magic: ffi::WS_MAGIC,
300                id: ['t' as c_char, 's' as c_char, 't' as c_char, '\0' as c_char],
301                s,
302                f: s,
303                r: ptr::null_mut(),
304                e: unsafe { s.add(aligned_sz) },
305            },
306            space,
307        }
308    }
309
310    /// Return a pointer to the underlying C ws struct. As usual, the caller needs to ensure that
311    /// self doesn't outlive the returned pointer.
312    pub fn as_ptr(&mut self) -> *mut ffi::ws {
313        ptr::from_mut::<ffi::ws>(&mut self.c_ws)
314    }
315
316    /// build a `Workspace`
317    pub fn workspace(&mut self) -> Workspace<'_> {
318        Workspace::from_ptr(self.as_ptr())
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use std::num::NonZero;
325
326    use super::*;
327
328    #[test]
329    fn ws_test_alloc() {
330        let mut test_ws = TestWS::new(160);
331        let mut ws = test_ws.workspace();
332        for _ in 0..10 {
333            unsafe {
334                assert!(!ws
335                    .alloc(NonZero::new(16).expect("16 is non-zero"))
336                    .is_null());
337            }
338        }
339        unsafe {
340            assert!(ws.alloc(NonZero::new(1).expect("1 is non-zero")).is_null());
341        }
342    }
343}