Skip to main content

hopper_runtime/
borrow.rs

1//! Hopper-owned borrow guards for account data.
2//!
3//! `Ref` and `RefMut` are the safe, drop-guarded handles returned by every
4//! Hopper access path: `load()`, `segment_ref()`, `raw_ref()`, and the
5//! mutable variants. The representation is backend-sensitive so the hot
6//! path stays tight:
7//!
8//! - **Solana (on-chain)**. `{ ptr, state_ptr }`. Two pointer words, no
9//!   extra guards, no slice fat-pointer, no ZSTs. Drop decrements or
10//!   restores the single `borrow_state` byte on the `RuntimeAccount`
11//!   directly. This is Hopper Native's pointer-shaped hot path with
12//!   deterministic RAII release built into the guard.
13//!
14//! - **non-Solana host tests**.
15//!   `{ ptr, guard, token, _marker }`. Shared guards retain the native
16//!   guard; exclusive guards retain its release state without retaining a
17//!   parent mutable reference that moving the wrapper could retag. Both also
18//!   retain Hopper's cross-handle alias registry token until drop.
19//!
20//! Both reprs expose the same surface: `Deref`/`DerefMut` into `T`,
21//! `as_ptr` / `as_mut_ptr`, byte-slice narrowing (`slice`, `slice_from`),
22//! and byte-level pointer projection (`project`). Generated accessors use the
23//! same API on both targets while the target-specific representation remains
24//! internal.
25
26use core::marker::PhantomData;
27
28use crate::borrow_registry::BorrowToken;
29use crate::error::ProgramError;
30use crate::native_boundary::{BackendRef, BackendRefMut};
31
32// ══════════════════════════════════════════════════════════════════════
33//  Ref (shared borrow)
34// ══════════════════════════════════════════════════════════════════════
35
36/// Shared (immutable) borrow guard for account data.
37///
38/// Derefs to the borrowed data. On drop, the shared borrow is released
39///. on Solana by decrementing the single `RuntimeAccount.borrow_state`
40/// byte, on host targets by dropping the backend guard and the
41/// cross-handle alias token.
42#[cfg(target_os = "solana")]
43pub struct Ref<'a, T: ?Sized> {
44    ptr: *const T,
45    state: *mut u8,
46    _marker: PhantomData<&'a T>,
47}
48
49#[cfg(not(target_os = "solana"))]
50pub struct Ref<'a, T: ?Sized> {
51    ptr: *const T,
52    guard: BackendRef<'a, [u8]>,
53    token: BorrowToken,
54    _marker: PhantomData<&'a T>,
55}
56
57impl<'a> Ref<'a, [u8]> {
58    /// Wrap an active-backend byte borrow into a Hopper Ref.
59    ///
60    /// On Solana this extracts the shared-borrow state pointer from the
61    /// native guard without any further wrapping. the resulting `Ref`
62    /// is `{ ptr, state }` only.
63    #[inline(always)]
64    pub(crate) fn from_backend(inner: BackendRef<'a, [u8]>, token: BorrowToken) -> Self {
65        #[cfg(target_os = "solana")]
66        {
67            let _ = token; // ZST on Solana, dropped immediately.
68            let (bytes, state) = inner.into_raw_parts();
69            Self {
70                ptr: bytes as *const [u8],
71                state,
72                _marker: PhantomData,
73            }
74        }
75        #[cfg(not(target_os = "solana"))]
76        {
77            let ptr = (&*inner) as *const [u8];
78            Self {
79                ptr,
80                guard: inner,
81                token,
82                _marker: PhantomData,
83            }
84        }
85    }
86
87    /// Project a byte borrow into another typed view over the same
88    /// underlying bytes. The new guard owns the same release mechanics
89    ///. when the returned `Ref<U>` drops, the underlying account
90    /// borrow is released exactly as if the original byte borrow had
91    /// dropped.
92    ///
93    /// # Safety
94    ///
95    /// `ptr` must point inside the byte slice that this `Ref<[u8]>`
96    /// guards (offset bounds checked by the caller), the pointee must
97    /// be valid `U` for any bit pattern (`U: Pod`-style), and `ptr`
98    /// must satisfy `U`'s alignment requirements. Hopper's typed access
99    /// APIs enforce this by requiring alignment-1 wire/pod types. The
100    /// returned `Ref<U>` inherits the source guard's lifetime, so the
101    /// account stays read-borrowed for as long as the typed view lives.
102    #[inline(always)]
103    pub unsafe fn project<U: ?Sized>(self, ptr: *const U) -> Ref<'a, U> {
104        #[cfg(target_os = "solana")]
105        {
106            let state = self.state;
107            core::mem::forget(self);
108            Ref {
109                ptr,
110                state,
111                _marker: PhantomData,
112            }
113        }
114        #[cfg(not(target_os = "solana"))]
115        {
116            let Self { guard, token, .. } = self;
117            Ref {
118                ptr,
119                guard,
120                token,
121                _marker: PhantomData,
122            }
123        }
124    }
125
126    /// Narrow a shared byte-slice borrow to a tail starting at `offset`.
127    #[inline(always)]
128    pub fn slice_from(self, offset: usize) -> Ref<'a, [u8]> {
129        // SAFETY: `self.ptr` is a valid slice pointer projected from the
130        // currently-held shared borrow; the subslice inherits the same
131        // borrow lifetime.
132        let bytes = unsafe { &*self.ptr };
133        let new_ptr = &bytes[offset..] as *const [u8];
134        // SAFETY: `new_ptr` points into the bytes this guard already covers,
135        // and `project` moves the guard to it, so the borrow that protects
136        // the sub-slice is the one that was taken.
137        unsafe { self.project(new_ptr) }
138    }
139
140    /// Narrow a shared byte-slice borrow to a checked sub-slice.
141    #[inline(always)]
142    pub fn slice(self, offset: usize, len: usize) -> Result<Ref<'a, [u8]>, ProgramError> {
143        // SAFETY: see `slice_from`.
144        let bytes = unsafe { &*self.ptr };
145        let end = offset
146            .checked_add(len)
147            .ok_or(ProgramError::ArithmeticOverflow)?;
148        if end > bytes.len() {
149            return Err(ProgramError::AccountDataTooSmall);
150        }
151        let new_ptr = &bytes[offset..end] as *const [u8];
152        // SAFETY: `new_ptr` points into the bytes this guard already covers
153        // (the range was checked above), and `project` moves the guard to it.
154        Ok(unsafe { self.project(new_ptr) })
155    }
156
157    #[inline(always)]
158    pub fn as_bytes_ptr(&self) -> *const u8 {
159        let bytes: &[u8] = self;
160        bytes.as_ptr()
161    }
162}
163
164impl<T: ?Sized> Ref<'_, T> {
165    #[inline(always)]
166    pub fn as_ptr(&self) -> *const T {
167        self.ptr
168    }
169}
170
171impl<'a, T> Ref<'a, T> {
172    /// Construct a lean Ref from a direct segment pointer plus the
173    /// shared-borrow state pointer that manages the RAII release.
174    ///
175    /// This is the Solana-native segment path: skips every intermediate
176    /// wrapper and materializes the final `{ptr, state}` shape directly.
177    #[cfg(target_os = "solana")]
178    #[inline(always)]
179    pub(crate) fn from_segment(ptr: *const T, state: *mut u8) -> Self {
180        Self {
181            ptr,
182            state,
183            _marker: PhantomData,
184        }
185    }
186}
187
188impl<T: ?Sized> core::ops::Deref for Ref<'_, T> {
189    type Target = T;
190
191    #[inline(always)]
192    fn deref(&self) -> &T {
193        // SAFETY: `self.ptr` was projected from a live shared borrow. On
194        // Solana the borrow is kept alive by the `state` field's Drop
195        // impl; on host targets by the `guard` + `token` fields. Field
196        // drop order guarantees the pointee outlives the `&self` borrow.
197        // SAFETY: `ptr` was projected from a live borrow that this guard
198        // keeps alive until it is dropped.
199        unsafe { &*self.ptr }
200    }
201}
202
203#[cfg(target_os = "solana")]
204impl<T: ?Sized> Drop for Ref<'_, T> {
205    #[inline(always)]
206    fn drop(&mut self) {
207        if self.state.is_null() {
208            return;
209        }
210        // Mirror `hopper_native::borrow::Ref::drop`: decrement the
211        // shared count, restoring NOT_BORROWED on the last release.
212        // SAFETY: `state` is non-null (checked above) and points at the
213        // borrow-state byte of the account this guard borrowed; the account
214        // header outlives every guard of the invocation.
215        unsafe {
216            let current = *self.state;
217            if current == 1 {
218                *self.state = hopper_native::NOT_BORROWED;
219            } else {
220                *self.state = current - 1;
221            }
222        }
223    }
224}
225
226// ══════════════════════════════════════════════════════════════════════
227//  RefMut (exclusive borrow)
228// ══════════════════════════════════════════════════════════════════════
229
230/// Exclusive (mutable) borrow guard for account data.
231///
232/// See the [module docs](self) for the representation split. On Solana
233/// the guard is `{ptr, state}`; on host targets the full backend-guard
234/// stack is kept so test harnesses behave identically to real runtime.
235#[cfg(target_os = "solana")]
236pub struct RefMut<'a, T: ?Sized> {
237    ptr: *mut T,
238    state: *mut u8,
239    _marker: PhantomData<&'a mut T>,
240}
241
242#[cfg(not(target_os = "solana"))]
243pub struct RefMut<'a, T: ?Sized> {
244    ptr: *mut T,
245    guard: ExclusiveLease,
246    token: BorrowToken,
247    _marker: PhantomData<&'a mut T>,
248}
249
250/// Owns the native release obligation without storing a parent `&mut [u8]`.
251#[cfg(not(target_os = "solana"))]
252struct ExclusiveLease(*mut u8);
253
254#[cfg(not(target_os = "solana"))]
255impl Drop for ExclusiveLease {
256    fn drop(&mut self) {
257        if !self.0.is_null() {
258            // SAFETY: from_backend transfers the native guard's live lease here.
259            // Projections move this owner; exactly one final drop releases it.
260            unsafe {
261                *self.0 = hopper_native::NOT_BORROWED;
262            }
263        }
264    }
265}
266
267impl<'a> RefMut<'a, [u8]> {
268    /// Wrap an active-backend mutable byte borrow into a Hopper RefMut.
269    #[inline(always)]
270    pub(crate) fn from_backend(inner: BackendRefMut<'a, [u8]>, token: BorrowToken) -> Self {
271        #[cfg(target_os = "solana")]
272        {
273            let _ = token;
274            let (bytes, state) = inner.into_raw_parts();
275            Self {
276                ptr: bytes as *mut [u8],
277                state,
278                _marker: PhantomData,
279            }
280        }
281        #[cfg(not(target_os = "solana"))]
282        {
283            // Consume the parent before deriving our raw pointer. Retaining and
284            // moving the parent's &mut after a reborrow invalidates that pointer
285            // under Stacked Borrows, even if the lease flag is still correct.
286            let (bytes, state) = inner.into_raw_parts();
287            Self {
288                ptr: bytes as *mut [u8],
289                guard: ExclusiveLease(state),
290                token,
291                _marker: PhantomData,
292            }
293        }
294    }
295
296    /// Project a mutable byte borrow into another mutable view over the
297    /// same underlying bytes. The new guard owns the same release
298    /// mechanics. the exclusive borrow stays held until the returned
299    /// `RefMut<U>` drops.
300    ///
301    /// # Safety
302    ///
303    /// Same contract as [`Ref::project`]: `ptr` must point inside the
304    /// byte slice this guard owns, the pointee must be valid `U`
305    /// for any bit pattern (`U: Pod`-style), and `ptr` must satisfy
306    /// `U` alignment. The returned `RefMut<U>`
307    /// inherits the source guard's lifetime so the account stays
308    /// exclusively borrowed for as long as the typed view lives.
309    #[inline(always)]
310    pub unsafe fn project<U: ?Sized>(self, ptr: *mut U) -> RefMut<'a, U> {
311        #[cfg(target_os = "solana")]
312        {
313            let state = self.state;
314            core::mem::forget(self);
315            RefMut {
316                ptr,
317                state,
318                _marker: PhantomData,
319            }
320        }
321        #[cfg(not(target_os = "solana"))]
322        {
323            let Self { guard, token, .. } = self;
324            RefMut {
325                ptr,
326                guard,
327                token,
328                _marker: PhantomData,
329            }
330        }
331    }
332
333    /// Narrow an exclusive byte-slice borrow to a tail starting at `offset`.
334    #[inline(always)]
335    pub fn slice_from(self, offset: usize) -> RefMut<'a, [u8]> {
336        // SAFETY: `new_ptr` points into the bytes this guard already covers,
337        // and `project` moves the guard to it, so the borrow that protects
338        // the sub-slice is the one that was taken.
339        let bytes = unsafe { &mut *self.ptr };
340        let new_ptr = &mut bytes[offset..] as *mut [u8];
341        // SAFETY: `new_ptr` lies inside the bytes this guard covers.
342        unsafe { self.project(new_ptr) }
343    }
344
345    /// Narrow an exclusive byte-slice borrow to a checked sub-slice.
346    #[inline(always)]
347    pub fn slice(self, offset: usize, len: usize) -> Result<RefMut<'a, [u8]>, ProgramError> {
348        // SAFETY: `new_ptr` points into the bytes this guard already covers
349        // (the range was checked above), and `project` moves the guard to it.
350        let bytes = unsafe { &mut *self.ptr };
351        let end = offset
352            .checked_add(len)
353            .ok_or(ProgramError::ArithmeticOverflow)?;
354        if end > bytes.len() {
355            return Err(ProgramError::AccountDataTooSmall);
356        }
357        let new_ptr = &mut bytes[offset..end] as *mut [u8];
358        // SAFETY: `new_ptr` points into the bytes this guard already covers
359        // (the range was checked above), and `project` moves the guard to it.
360        Ok(unsafe { self.project(new_ptr) })
361    }
362
363    #[inline(always)]
364    pub fn as_bytes_mut_ptr(&mut self) -> *mut u8 {
365        let bytes: &mut [u8] = self;
366        bytes.as_mut_ptr()
367    }
368}
369
370impl<'a, T> RefMut<'a, T> {
371    /// Construct a lean RefMut from a direct segment pointer plus the
372    /// exclusive-borrow state pointer.
373    #[cfg(target_os = "solana")]
374    #[inline(always)]
375    pub(crate) fn from_segment(ptr: *mut T, state: *mut u8) -> Self {
376        Self {
377            ptr,
378            state,
379            _marker: PhantomData,
380        }
381    }
382}
383
384impl<T: ?Sized> RefMut<'_, T> {
385    #[inline(always)]
386    pub fn as_ptr(&self) -> *const T {
387        self.ptr
388    }
389
390    #[inline(always)]
391    pub fn as_mut_ptr(&mut self) -> *mut T {
392        self.ptr
393    }
394}
395
396impl<T: ?Sized> core::ops::Deref for RefMut<'_, T> {
397    type Target = T;
398
399    #[inline(always)]
400    fn deref(&self) -> &T {
401        // SAFETY: see `Ref::deref`.
402        unsafe { &*self.ptr }
403    }
404}
405
406impl<T: ?Sized> core::ops::DerefMut for RefMut<'_, T> {
407    #[inline(always)]
408    fn deref_mut(&mut self) -> &mut T {
409        // SAFETY: exclusive borrow guaranteed by the guard's lifetime.
410        unsafe { &mut *self.ptr }
411    }
412}
413
414#[cfg(target_os = "solana")]
415impl<T: ?Sized> Drop for RefMut<'_, T> {
416    #[inline(always)]
417    fn drop(&mut self) {
418        if self.state.is_null() {
419            return;
420        }
421        // Exclusive borrow. restore NOT_BORROWED.
422        // SAFETY: `state` is non-null (checked above) and points at the
423        // borrow-state byte of the account this guard borrowed; the account
424        // header outlives every guard of the invocation.
425        unsafe {
426            *self.state = hopper_native::NOT_BORROWED;
427        }
428    }
429}
430
431impl<T: ?Sized> core::fmt::Debug for Ref<'_, T> {
432    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
433        f.debug_struct("Ref")
434            .field("ptr", &self.ptr)
435            .finish_non_exhaustive()
436    }
437}
438
439impl<T: ?Sized> core::fmt::Debug for RefMut<'_, T> {
440    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
441        f.debug_struct("RefMut")
442            .field("ptr", &self.ptr)
443            .finish_non_exhaustive()
444    }
445}
446
447// ══════════════════════════════════════════════════════════════════════
448//  Size invariants
449// ══════════════════════════════════════════════════════════════════════
450//
451// These `const _: ()` blocks bake the flat-wrapper promise into the
452// build. If a future refactor adds another pointer or RAII field the
453// build fails here, loudly, rather than silently re-inflating the hot
454// path. On Solana a `Ref<u64>` must be exactly two pointer-words
455// (ptr + state); a `Ref<[u8]>` takes one extra word for the slice-ptr
456// length component.
457
458#[cfg(target_os = "solana")]
459const _: () = {
460    assert!(
461        core::mem::size_of::<Ref<'static, u64>>() == core::mem::size_of::<usize>() * 2,
462        "Ref<T: Sized> on Solana must be exactly (ptr, state) = 2 words",
463    );
464    assert!(
465        core::mem::size_of::<RefMut<'static, u64>>() == core::mem::size_of::<usize>() * 2,
466        "RefMut<T: Sized> on Solana must be exactly (ptr, state) = 2 words",
467    );
468};