Skip to main content

samp_sdk/omp/
containers.rs

1//! The generic containers the open.mp headers pass across the ABI: `Pair`,
2//! `Span`, `HybridString`, and the `robin_hood` sets the pools and entities
3//! hand out.
4//!
5//! Each is laid out as the C++ type is, from clang's record layout for both
6//! ABIs; the tests pin the numbers.
7
8use std::marker::PhantomData;
9
10use super::vtable::VirtualReturn;
11
12/// `Pair<A, B>` — `std::pair`, two fields in order. The same on both ABIs:
13/// `std::pair` of trivially copyable members travels by value like a struct.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15#[repr(C)]
16pub struct Pair<A, B> {
17    pub first: A,
18    pub second: B,
19}
20
21impl<A, B> VirtualReturn for Pair<A, B> {
22    type Raw = Self;
23    fn from_raw(raw: Self) -> Self {
24        raw
25    }
26}
27
28/// `Span<T>` — `nonstd::span`, a pointer and a count, by value. The server
29/// reads or fills `size` elements at `data`, so both must describe memory the
30/// caller owns for the call.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[repr(C)]
33pub struct Span<T> {
34    pub data: *mut T,
35    pub size: usize,
36}
37
38impl<T> Span<T> {
39    /// A span over `items`, for one call.
40    #[must_use]
41    pub fn of(items: &mut [T]) -> Self {
42        Self {
43            data: items.as_mut_ptr(),
44            size: items.len(),
45        }
46    }
47}
48
49/// `HybridStringDynamicStorage` — where a [`HybridString`] too long for its
50/// inline buffer keeps its bytes, and how the owner frees them.
51#[derive(Debug, Clone, Copy)]
52#[repr(C)]
53pub struct HybridStringDynamicStorage {
54    pub ptr: *mut u8,
55    pub free: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
56}
57
58/// The storage of a [`HybridString`]: inline, or a pointer elsewhere.
59#[derive(Clone, Copy)]
60#[repr(C)]
61pub union HybridStringStorage<const N: usize> {
62    pub dynamic: HybridStringDynamicStorage,
63    pub inline: [u8; N],
64}
65
66/// `HybridString<N>` — open.mp's ABI-stable string: `N` bytes inline
67/// (terminator included), or a heap pointer past that. `len_dynamic` holds the
68/// length shifted left by one, and whether the string is on the heap in bit 0.
69///
70/// Not `Copy`: a heap string belongs to whoever built it and is freed through
71/// the pointer it carries. Structs holding one travel by reference only.
72#[repr(C)]
73pub struct HybridString<const N: usize> {
74    pub len_dynamic: usize,
75    pub storage: HybridStringStorage<N>,
76}
77
78impl<const N: usize> HybridString<N> {
79    /// An inline copy of `text`, or `None` when it does not fit in `N - 1`
80    /// bytes. A string built here never owns heap memory, so it needs no drop.
81    #[must_use]
82    pub fn new(text: &str) -> Option<Self> {
83        let bytes = text.as_bytes();
84        if bytes.len() >= N {
85            return None;
86        }
87        let mut inline = [0u8; N];
88        inline[..bytes.len()].copy_from_slice(bytes);
89        Some(Self {
90            len_dynamic: bytes.len() << 1,
91            storage: HybridStringStorage { inline },
92        })
93    }
94
95    /// Whether the bytes live on the heap.
96    #[must_use]
97    pub fn is_dynamic(&self) -> bool {
98        self.len_dynamic & 1 != 0
99    }
100
101    /// The length in bytes, terminator excluded.
102    #[must_use]
103    pub fn len(&self) -> usize {
104        self.len_dynamic >> 1
105    }
106
107    /// Whether the string is empty.
108    #[must_use]
109    pub fn is_empty(&self) -> bool {
110        self.len() == 0
111    }
112
113    /// The bytes, wherever they live.
114    ///
115    /// # Safety
116    /// A heap string's pointer must still be live — true of one the server
117    /// hands over by reference, for as long as the object that holds it.
118    #[must_use]
119    pub unsafe fn as_bytes(&self) -> &[u8] {
120        let len = self.len();
121        if self.is_dynamic() {
122            let ptr = unsafe { self.storage.dynamic.ptr };
123            if ptr.is_null() {
124                return &[];
125            }
126            unsafe { std::slice::from_raw_parts(ptr, len) }
127        } else {
128            let inline = unsafe { &self.storage.inline };
129            &inline[..len.min(N)]
130        }
131    }
132
133    /// The text, with invalid UTF-8 replaced.
134    ///
135    /// # Safety
136    /// As for [`as_bytes`](Self::as_bytes).
137    #[must_use]
138    pub unsafe fn to_string_lossy(&self) -> String {
139        String::from_utf8_lossy(unsafe { self.as_bytes() }).into_owned()
140    }
141}
142
143impl<const N: usize> Default for HybridString<N> {
144    fn default() -> Self {
145        Self {
146            len_dynamic: 0,
147            storage: HybridStringStorage { inline: [0; N] },
148        }
149    }
150}
151
152/// A `robin_hood` flat set of pointers — `FlatPtrHashSet<T>` and
153/// `FlatHashSet<T*>` in the headers. Held by pointer only; read it with
154/// [`flat_set_entries`].
155#[repr(C)]
156pub struct FlatSet<T> {
157    _opaque: [u8; 0],
158    _item: PhantomData<*mut T>,
159}
160
161/// Field offsets inside the set, from clang's layout of
162/// `robin_hood::detail::Table<true, 80, T*, ...>` for each ABI. MSVC aligns the
163/// `uint64_t` multiplier to 8, which moves every field after it.
164#[cfg(not(target_env = "msvc"))]
165mod set_layout {
166    pub const KEY_VALS: usize = 8;
167    pub const INFO: usize = 12;
168    pub const NUM_ELEMENTS: usize = 16;
169    pub const MASK: usize = 20;
170}
171
172#[cfg(target_env = "msvc")]
173mod set_layout {
174    pub const KEY_VALS: usize = 16;
175    pub const INFO: usize = 20;
176    pub const NUM_ELEMENTS: usize = 24;
177    pub const MASK: usize = 28;
178}
179
180/// Buckets a `robin_hood` table allocates for `mask + 1` slots:
181/// `calcNumElementsWithBuffer` — the slots, plus an overflow area of 80% of
182/// them, capped at 255. The info array is that long (and a sentinel more), so
183/// a walk bounded by it never reads outside the allocation.
184fn buckets(mask: usize) -> Option<usize> {
185    let slots = mask.checked_add(1)?;
186    let allowed = if slots <= usize::MAX / 100 {
187        slots * 80 / 100
188    } else {
189        (slots / 100) * 80
190    };
191    slots.checked_add(allowed.min(0xff))
192}
193
194/// Every pointer the set holds, in table order.
195///
196/// Like the extension-map walk in [`super::extensions`], this reads the
197/// internals of a vendored hash table, not an ABI — so it fails closed. The
198/// walk stops once it has found as many entries as the table says it holds,
199/// never goes past the table's own bound, and an empty `Vec` comes back if the
200/// numbers disagree.
201///
202/// # Safety
203/// `set` must be null or a set the server handed out, still alive.
204#[must_use]
205pub unsafe fn flat_set_entries<T>(set: *const FlatSet<T>) -> Vec<*mut T> {
206    if set.is_null() {
207        return Vec::new();
208    }
209    let base = set.cast::<u8>();
210    let read_usize = |offset: usize| unsafe { base.add(offset).cast::<usize>().read_unaligned() };
211    let read_ptr = |offset: usize| unsafe { base.add(offset).cast::<*const u8>().read_unaligned() };
212
213    let count = read_usize(set_layout::NUM_ELEMENTS);
214    let mask = read_usize(set_layout::MASK);
215    let key_vals = read_ptr(set_layout::KEY_VALS).cast::<*mut T>();
216    let info = read_ptr(set_layout::INFO);
217    if count == 0 || key_vals.is_null() || info.is_null() {
218        return Vec::new();
219    }
220    let Some(bound) = buckets(mask) else {
221        return Vec::new();
222    };
223    if count > bound {
224        return Vec::new();
225    }
226
227    let mut found = Vec::with_capacity(count);
228    for i in 0..bound {
229        if found.len() == count {
230            break;
231        }
232        if unsafe { info.add(i).read() } != 0 {
233            found.push(unsafe { key_vals.add(i).read() });
234        }
235    }
236    if found.len() == count {
237        found
238    } else {
239        Vec::new()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    // The layouts clang gives are i686 ones: pointers are four bytes there.
248    #[cfg(target_arch = "x86")]
249    #[test]
250    fn layouts_match_clang() {
251        use std::mem::{offset_of, size_of};
252        assert_eq!(size_of::<Span<u8>>(), 8);
253        assert_eq!(offset_of!(Span<u8>, size), 4);
254        assert_eq!(size_of::<Pair<i32, i32>>(), 8);
255        assert_eq!(size_of::<Pair<bool, crate::omp::StringView>>(), 12);
256        assert_eq!(offset_of!(Pair<bool, crate::omp::StringView>, second), 4);
257        assert_eq!(size_of::<HybridString<16>>(), 20);
258        assert_eq!(size_of::<HybridString<46>>(), 52);
259        assert_eq!(offset_of!(HybridString<16>, storage), 4);
260    }
261
262    #[test]
263    fn hybrid_string_round_trips_inline() {
264        let s = HybridString::<16>::new("hello").unwrap();
265        assert!(!s.is_dynamic());
266        assert_eq!(s.len(), 5);
267        assert_eq!(unsafe { s.to_string_lossy() }, "hello");
268        // Fifteen bytes and the terminator fill it; sixteen do not fit.
269        assert!(HybridString::<16>::new("123456789012345").is_some());
270        assert!(HybridString::<16>::new("1234567890123456").is_none());
271    }
272
273    #[test]
274    fn hybrid_string_reads_a_heap_copy() {
275        let mut bytes = *b"far too long for sixteen";
276        let s = HybridString::<16> {
277            len_dynamic: (bytes.len() << 1) | 1,
278            storage: HybridStringStorage {
279                dynamic: HybridStringDynamicStorage {
280                    ptr: bytes.as_mut_ptr(),
281                    free: None,
282                },
283            },
284        };
285        assert_eq!(unsafe { s.to_string_lossy() }, "far too long for sixteen");
286    }
287
288    /// A table laid out as `robin_hood` does, with three entries spread over
289    /// eight buckets.
290    /// Pointer-typed words, so that moving it keeps the pointers' provenance.
291    #[repr(C, align(8))]
292    struct FakeSet([*const u8; 16]);
293
294    fn fake(count: usize, mask: usize, keys: &[*mut i32], info: &[u8]) -> FakeSet {
295        let mut raw = FakeSet([std::ptr::null(); 16]);
296        let base = raw.0.as_mut_ptr().cast::<u8>();
297        // Pointers go in as pointers, so they keep their provenance.
298        unsafe {
299            base.add(set_layout::KEY_VALS)
300                .cast::<*const *mut i32>()
301                .write_unaligned(keys.as_ptr());
302            base.add(set_layout::INFO)
303                .cast::<*const u8>()
304                .write_unaligned(info.as_ptr());
305            base.add(set_layout::NUM_ELEMENTS)
306                .cast::<usize>()
307                .write_unaligned(count);
308            base.add(set_layout::MASK)
309                .cast::<usize>()
310                .write_unaligned(mask);
311        }
312        raw
313    }
314
315    // The field offsets are the i686 table's.
316    #[cfg(target_arch = "x86")]
317    #[test]
318    fn walks_the_occupied_buckets() {
319        let (mut a, mut b, mut c) = (1, 2, 3);
320        let n = std::ptr::null_mut();
321        let keys = [n, &raw mut a, n, n, &raw mut b, n, &raw mut c, n, n];
322        let info = [0u8, 1, 0, 0, 1, 0, 2, 0, 1];
323        let raw = fake(3, 7, &keys, &info);
324        let got = unsafe { flat_set_entries((&raw const raw).cast::<FlatSet<i32>>()) };
325        assert_eq!(got, vec![&raw mut a, &raw mut b, &raw mut c]);
326    }
327
328    #[test]
329    fn fails_closed_when_the_count_disagrees() {
330        let mut a = 1;
331        let keys = [&raw mut a, std::ptr::null_mut()];
332        let info = [1u8, 0];
333        // Claims five entries in a one-slot table: more than it can hold, so
334        // nothing is read at all.
335        let raw = fake(5, 0, &keys, &info);
336        let got = unsafe { flat_set_entries((&raw const raw).cast::<FlatSet<i32>>()) };
337        assert!(got.is_empty());
338    }
339
340    #[test]
341    fn bucket_count_follows_robin_hood() {
342        assert_eq!(buckets(0), Some(1));
343        assert_eq!(buckets(7), Some(14));
344        assert_eq!(buckets(1023), Some(1024 + 255));
345    }
346
347    #[test]
348    fn a_null_set_is_empty() {
349        assert!(unsafe { flat_set_entries(std::ptr::null::<FlatSet<i32>>()) }.is_empty());
350    }
351}