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 }`. Richer because host tests rely on
16//!   the active backend's borrow machinery (RefCell, etc.) plus Hopper's
17//!   own cross-handle alias registry (`BorrowToken`). Both are real RAII
18//!   and must live until the runtime guard drops.
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: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
135        unsafe { self.project(new_ptr) }
136    }
137
138    /// Narrow a shared byte-slice borrow to a checked sub-slice.
139    #[inline(always)]
140    pub fn slice(self, offset: usize, len: usize) -> Result<Ref<'a, [u8]>, ProgramError> {
141        // SAFETY: see `slice_from`.
142        let bytes = unsafe { &*self.ptr };
143        let end = offset
144            .checked_add(len)
145            .ok_or(ProgramError::ArithmeticOverflow)?;
146        if end > bytes.len() {
147            return Err(ProgramError::AccountDataTooSmall);
148        }
149        let new_ptr = &bytes[offset..end] as *const [u8];
150        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
151        Ok(unsafe { self.project(new_ptr) })
152    }
153
154    #[inline(always)]
155    pub fn as_bytes_ptr(&self) -> *const u8 {
156        let bytes: &[u8] = self;
157        bytes.as_ptr()
158    }
159}
160
161impl<T: ?Sized> Ref<'_, T> {
162    #[inline(always)]
163    pub fn as_ptr(&self) -> *const T {
164        self.ptr
165    }
166}
167
168impl<'a, T> Ref<'a, T> {
169    /// Construct a lean Ref from a direct segment pointer plus the
170    /// shared-borrow state pointer that manages the RAII release.
171    ///
172    /// This is the Solana-native segment path: skips every intermediate
173    /// wrapper and materializes the final `{ptr, state}` shape directly.
174    #[cfg(target_os = "solana")]
175    #[inline(always)]
176    pub(crate) fn from_segment(ptr: *const T, state: *mut u8) -> Self {
177        Self {
178            ptr,
179            state,
180            _marker: PhantomData,
181        }
182    }
183}
184
185impl<T: ?Sized> core::ops::Deref for Ref<'_, T> {
186    type Target = T;
187
188    #[inline(always)]
189    fn deref(&self) -> &T {
190        // SAFETY: `self.ptr` was projected from a live shared borrow. On
191        // Solana the borrow is kept alive by the `state` field's Drop
192        // impl; on host targets by the `guard` + `token` fields. Field
193        // drop order guarantees the pointee outlives the `&self` borrow.
194        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
195        unsafe { &*self.ptr }
196    }
197}
198
199#[cfg(target_os = "solana")]
200impl<T: ?Sized> Drop for Ref<'_, T> {
201    #[inline(always)]
202    fn drop(&mut self) {
203        if self.state.is_null() {
204            return;
205        }
206        // Mirror `hopper_native::borrow::Ref::drop`: decrement the
207        // shared count, restoring NOT_BORROWED on the last release.
208        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
209        unsafe {
210            let current = *self.state;
211            if current == 1 {
212                *self.state = hopper_native::NOT_BORROWED;
213            } else {
214                *self.state = current - 1;
215            }
216        }
217    }
218}
219
220// ══════════════════════════════════════════════════════════════════════
221//  RefMut (exclusive borrow)
222// ══════════════════════════════════════════════════════════════════════
223
224/// Exclusive (mutable) borrow guard for account data.
225///
226/// See the [module docs](self) for the representation split. On Solana
227/// the guard is `{ptr, state}`; on host targets the full backend-guard
228/// stack is kept so test harnesses behave identically to real runtime.
229#[cfg(target_os = "solana")]
230pub struct RefMut<'a, T: ?Sized> {
231    ptr: *mut T,
232    state: *mut u8,
233    _marker: PhantomData<&'a mut T>,
234}
235
236#[cfg(not(target_os = "solana"))]
237pub struct RefMut<'a, T: ?Sized> {
238    ptr: *mut T,
239    guard: BackendRefMut<'a, [u8]>,
240    token: BorrowToken,
241    _marker: PhantomData<&'a mut T>,
242}
243
244impl<'a> RefMut<'a, [u8]> {
245    /// Wrap an active-backend mutable byte borrow into a Hopper RefMut.
246    #[inline(always)]
247    pub(crate) fn from_backend(inner: BackendRefMut<'a, [u8]>, token: BorrowToken) -> Self {
248        #[cfg(target_os = "solana")]
249        {
250            let _ = token;
251            let (bytes, state) = inner.into_raw_parts();
252            Self {
253                ptr: bytes as *mut [u8],
254                state,
255                _marker: PhantomData,
256            }
257        }
258        #[cfg(not(target_os = "solana"))]
259        {
260            // Take the write pointer from a *mutable* reborrow: deriving it
261            // from `&*inner` (a shared reborrow) and casting would give the
262            // pointer shared provenance, making later writes through it UB
263            // under the aliasing model (Miri flags it). The `mut` rebinding
264            // is host-only so the Solana lane (which consumes `inner` by
265            // value) stays warning-free.
266            let mut inner = inner;
267            let ptr = (&mut *inner) as *mut [u8];
268            Self {
269                ptr,
270                guard: inner,
271                token,
272                _marker: PhantomData,
273            }
274        }
275    }
276
277    /// Project a mutable byte borrow into another mutable view over the
278    /// same underlying bytes. The new guard owns the same release
279    /// mechanics. the exclusive borrow stays held until the returned
280    /// `RefMut<U>` drops.
281    ///
282    /// # Safety
283    ///
284    /// Same contract as [`Ref::project`]: `ptr` must point inside the
285    /// byte slice this guard owns, the pointee must be valid `U`
286    /// for any bit pattern (`U: Pod`-style), and `ptr` must satisfy
287    /// `U` alignment. The returned `RefMut<U>`
288    /// inherits the source guard's lifetime so the account stays
289    /// exclusively borrowed for as long as the typed view lives.
290    #[inline(always)]
291    pub unsafe fn project<U: ?Sized>(self, ptr: *mut U) -> RefMut<'a, U> {
292        #[cfg(target_os = "solana")]
293        {
294            let state = self.state;
295            core::mem::forget(self);
296            RefMut {
297                ptr,
298                state,
299                _marker: PhantomData,
300            }
301        }
302        #[cfg(not(target_os = "solana"))]
303        {
304            let Self { guard, token, .. } = self;
305            RefMut {
306                ptr,
307                guard,
308                token,
309                _marker: PhantomData,
310            }
311        }
312    }
313
314    /// Narrow an exclusive byte-slice borrow to a tail starting at `offset`.
315    #[inline(always)]
316    pub fn slice_from(self, offset: usize) -> RefMut<'a, [u8]> {
317        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
318        let bytes = unsafe { &mut *self.ptr };
319        let new_ptr = &mut bytes[offset..] as *mut [u8];
320        unsafe { self.project(new_ptr) }
321    }
322
323    /// Narrow an exclusive byte-slice borrow to a checked sub-slice.
324    #[inline(always)]
325    pub fn slice(self, offset: usize, len: usize) -> Result<RefMut<'a, [u8]>, ProgramError> {
326        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
327        let bytes = unsafe { &mut *self.ptr };
328        let end = offset
329            .checked_add(len)
330            .ok_or(ProgramError::ArithmeticOverflow)?;
331        if end > bytes.len() {
332            return Err(ProgramError::AccountDataTooSmall);
333        }
334        let new_ptr = &mut bytes[offset..end] as *mut [u8];
335        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
336        Ok(unsafe { self.project(new_ptr) })
337    }
338
339    #[inline(always)]
340    pub fn as_bytes_mut_ptr(&mut self) -> *mut u8 {
341        let bytes: &mut [u8] = self;
342        bytes.as_mut_ptr()
343    }
344}
345
346impl<'a, T> RefMut<'a, T> {
347    /// Construct a lean RefMut from a direct segment pointer plus the
348    /// exclusive-borrow state pointer.
349    #[cfg(target_os = "solana")]
350    #[inline(always)]
351    pub(crate) fn from_segment(ptr: *mut T, state: *mut u8) -> Self {
352        Self {
353            ptr,
354            state,
355            _marker: PhantomData,
356        }
357    }
358}
359
360impl<T: ?Sized> RefMut<'_, T> {
361    #[inline(always)]
362    pub fn as_ptr(&self) -> *const T {
363        self.ptr
364    }
365
366    #[inline(always)]
367    pub fn as_mut_ptr(&mut self) -> *mut T {
368        self.ptr
369    }
370}
371
372impl<T: ?Sized> core::ops::Deref for RefMut<'_, T> {
373    type Target = T;
374
375    #[inline(always)]
376    fn deref(&self) -> &T {
377        // SAFETY: see `Ref::deref`.
378        unsafe { &*self.ptr }
379    }
380}
381
382impl<T: ?Sized> core::ops::DerefMut for RefMut<'_, T> {
383    #[inline(always)]
384    fn deref_mut(&mut self) -> &mut T {
385        // SAFETY: exclusive borrow guaranteed by the guard's lifetime.
386        unsafe { &mut *self.ptr }
387    }
388}
389
390#[cfg(target_os = "solana")]
391impl<T: ?Sized> Drop for RefMut<'_, T> {
392    #[inline(always)]
393    fn drop(&mut self) {
394        if self.state.is_null() {
395            return;
396        }
397        // Exclusive borrow. restore NOT_BORROWED.
398        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
399        unsafe {
400            *self.state = hopper_native::NOT_BORROWED;
401        }
402    }
403}
404
405impl<T: ?Sized> core::fmt::Debug for Ref<'_, T> {
406    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
407        f.debug_struct("Ref")
408            .field("ptr", &self.ptr)
409            .finish_non_exhaustive()
410    }
411}
412
413impl<T: ?Sized> core::fmt::Debug for RefMut<'_, T> {
414    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
415        f.debug_struct("RefMut")
416            .field("ptr", &self.ptr)
417            .finish_non_exhaustive()
418    }
419}
420
421// ══════════════════════════════════════════════════════════════════════
422//  Size invariants
423// ══════════════════════════════════════════════════════════════════════
424//
425// These `const _: ()` blocks bake the flat-wrapper promise into the
426// build. If a future refactor adds another pointer or RAII field the
427// build fails here, loudly, rather than silently re-inflating the hot
428// path. On Solana a `Ref<u64>` must be exactly two pointer-words
429// (ptr + state); a `Ref<[u8]>` takes one extra word for the slice-ptr
430// length component.
431
432#[cfg(target_os = "solana")]
433const _: () = {
434    assert!(
435        core::mem::size_of::<Ref<'static, u64>>() == core::mem::size_of::<usize>() * 2,
436        "Ref<T: Sized> on Solana must be exactly (ptr, state) = 2 words",
437    );
438    assert!(
439        core::mem::size_of::<RefMut<'static, u64>>() == core::mem::size_of::<usize>() * 2,
440        "RefMut<T: Sized> on Solana must be exactly (ptr, state) = 2 words",
441    );
442};