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 matches Pinocchio's pointer shape while adding the
12//!   deterministic RAII release that Pinocchio pushes onto the caller.
13//!
14//! - **non-Solana (host tests, legacy-pinocchio-compat, solana-program)**.
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`). Whoever reads a
23//! generated accessor like `ctx.vault_balance_mut()` cannot tell which
24//! repr is in use. and on Solana the compiler collapses every hop to
25//! `ptr + offset -> cast`, exactly the shape the finish-line audit
26//! demanded.
27
28use core::marker::PhantomData;
29
30use crate::borrow_registry::BorrowToken;
31use crate::compat::{BackendRef, BackendRefMut};
32use crate::error::ProgramError;
33
34// ══════════════════════════════════════════════════════════════════════
35//  Ref (shared borrow)
36// ══════════════════════════════════════════════════════════════════════
37
38/// Shared (immutable) borrow guard for account data.
39///
40/// Derefs to the borrowed data. On drop, the shared borrow is released
41///. on Solana by decrementing the single `RuntimeAccount.borrow_state`
42/// byte, on host targets by dropping the backend guard and the
43/// cross-handle alias token.
44#[cfg(target_os = "solana")]
45pub struct Ref<'a, T: ?Sized> {
46    ptr: *const T,
47    state: *mut u8,
48    _marker: PhantomData<&'a T>,
49}
50
51#[cfg(not(target_os = "solana"))]
52pub struct Ref<'a, T: ?Sized> {
53    ptr: *const T,
54    guard: BackendRef<'a, [u8]>,
55    token: BorrowToken,
56    _marker: PhantomData<&'a T>,
57}
58
59impl<'a> Ref<'a, [u8]> {
60    /// Wrap an active-backend byte borrow into a Hopper Ref.
61    ///
62    /// On Solana this extracts the shared-borrow state pointer from the
63    /// native guard without any further wrapping. the resulting `Ref`
64    /// is `{ ptr, state }` only.
65    #[inline(always)]
66    pub(crate) fn from_backend(inner: BackendRef<'a, [u8]>, token: BorrowToken) -> Self {
67        #[cfg(target_os = "solana")]
68        {
69            let _ = token; // ZST on Solana, dropped immediately.
70            let (bytes, state) = inner.into_raw_parts();
71            Self {
72                ptr: bytes as *const [u8],
73                state,
74                _marker: PhantomData,
75            }
76        }
77        #[cfg(not(target_os = "solana"))]
78        {
79            let ptr = (&*inner) as *const [u8];
80            Self {
81                ptr,
82                guard: inner,
83                token,
84                _marker: PhantomData,
85            }
86        }
87    }
88
89    /// Project a byte borrow into another typed view over the same
90    /// underlying bytes. The new guard owns the same release mechanics
91    ///. when the returned `Ref<U>` drops, the underlying account
92    /// borrow is released exactly as if the original byte borrow had
93    /// dropped.
94    ///
95    /// # Safety
96    ///
97    /// `ptr` must point inside the byte slice that this `Ref<[u8]>`
98    /// guards (offset bounds checked by the caller), the pointee must
99    /// be valid `U` for any bit pattern (`U: Pod`-style), and no
100    /// alignment beyond the source slice's may be assumed for `U`. The
101    /// returned `Ref<U>` inherits the source guard's lifetime, so the
102    /// account stays read-borrowed for as long as the typed view lives.
103    #[inline(always)]
104    pub unsafe fn project<U: ?Sized>(self, ptr: *const U) -> Ref<'a, U> {
105        #[cfg(target_os = "solana")]
106        {
107            let state = self.state;
108            core::mem::forget(self);
109            Ref {
110                ptr,
111                state,
112                _marker: PhantomData,
113            }
114        }
115        #[cfg(not(target_os = "solana"))]
116        {
117            let Self { guard, token, .. } = self;
118            Ref {
119                ptr,
120                guard,
121                token,
122                _marker: PhantomData,
123            }
124        }
125    }
126
127    /// Narrow a shared byte-slice borrow to a tail starting at `offset`.
128    #[inline(always)]
129    pub fn slice_from(self, offset: usize) -> Ref<'a, [u8]> {
130        // SAFETY: `self.ptr` is a valid slice pointer projected from the
131        // currently-held shared borrow; the subslice inherits the same
132        // borrow lifetime.
133        let bytes = unsafe { &*self.ptr };
134        let new_ptr = &bytes[offset..] as *const [u8];
135        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
136        unsafe { self.project(new_ptr) }
137    }
138
139    /// Narrow a shared byte-slice borrow to a checked sub-slice.
140    #[inline(always)]
141    pub fn slice(self, offset: usize, len: usize) -> Result<Ref<'a, [u8]>, ProgramError> {
142        // SAFETY: see `slice_from`.
143        let bytes = unsafe { &*self.ptr };
144        let end = offset
145            .checked_add(len)
146            .ok_or(ProgramError::ArithmeticOverflow)?;
147        if end > bytes.len() {
148            return Err(ProgramError::AccountDataTooSmall);
149        }
150        let new_ptr = &bytes[offset..end] as *const [u8];
151        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
152        Ok(unsafe { self.project(new_ptr) })
153    }
154
155    #[inline(always)]
156    pub fn as_bytes_ptr(&self) -> *const u8 {
157        let bytes: &[u8] = self;
158        bytes.as_ptr()
159    }
160}
161
162impl<T: ?Sized> Ref<'_, T> {
163    #[inline(always)]
164    pub fn as_ptr(&self) -> *const T {
165        self.ptr
166    }
167}
168
169impl<'a, T> Ref<'a, T> {
170    /// Construct a lean Ref from a direct segment pointer plus the
171    /// shared-borrow state pointer that manages the RAII release.
172    ///
173    /// This is the Solana-native segment path: skips every intermediate
174    /// wrapper and materializes the final `{ptr, state}` shape directly.
175    #[cfg(target_os = "solana")]
176    #[inline(always)]
177    pub(crate) fn from_segment(ptr: *const T, state: *mut u8) -> Self {
178        Self {
179            ptr,
180            state,
181            _marker: PhantomData,
182        }
183    }
184}
185
186impl<T: ?Sized> core::ops::Deref for Ref<'_, T> {
187    type Target = T;
188
189    #[inline(always)]
190    fn deref(&self) -> &T {
191        // SAFETY: `self.ptr` was projected from a live shared borrow. On
192        // Solana the borrow is kept alive by the `state` field's Drop
193        // impl; on host targets by the `guard` + `token` fields. Field
194        // drop order guarantees the pointee outlives the `&self` borrow.
195        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
196        unsafe { &*self.ptr }
197    }
198}
199
200#[cfg(target_os = "solana")]
201impl<T: ?Sized> Drop for Ref<'_, T> {
202    #[inline(always)]
203    fn drop(&mut self) {
204        // Mirror `hopper_native::borrow::Ref::drop`: decrement the
205        // shared count, restoring NOT_BORROWED on the last release.
206        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
207        unsafe {
208            let current = *self.state;
209            if current == 1 {
210                *self.state = hopper_native::NOT_BORROWED;
211            } else {
212                *self.state = current - 1;
213            }
214        }
215    }
216}
217
218// ══════════════════════════════════════════════════════════════════════
219//  RefMut (exclusive borrow)
220// ══════════════════════════════════════════════════════════════════════
221
222/// Exclusive (mutable) borrow guard for account data.
223///
224/// See the [module docs](self) for the representation split. On Solana
225/// the guard is `{ptr, state}`; on host targets the full backend-guard
226/// stack is kept so test harnesses behave identically to real runtime.
227#[cfg(target_os = "solana")]
228pub struct RefMut<'a, T: ?Sized> {
229    ptr: *mut T,
230    state: *mut u8,
231    _marker: PhantomData<&'a mut T>,
232}
233
234#[cfg(not(target_os = "solana"))]
235pub struct RefMut<'a, T: ?Sized> {
236    ptr: *mut T,
237    guard: BackendRefMut<'a, [u8]>,
238    token: BorrowToken,
239    _marker: PhantomData<&'a mut T>,
240}
241
242impl<'a> RefMut<'a, [u8]> {
243    /// Wrap an active-backend mutable byte borrow into a Hopper RefMut.
244    #[inline(always)]
245    pub(crate) fn from_backend(inner: BackendRefMut<'a, [u8]>, token: BorrowToken) -> Self {
246        #[cfg(target_os = "solana")]
247        {
248            let _ = token;
249            let (bytes, state) = inner.into_raw_parts();
250            Self {
251                ptr: bytes as *mut [u8],
252                state,
253                _marker: PhantomData,
254            }
255        }
256        #[cfg(not(target_os = "solana"))]
257        {
258            let ptr = (&*inner as *const [u8]).cast_mut();
259            Self {
260                ptr,
261                guard: inner,
262                token,
263                _marker: PhantomData,
264            }
265        }
266    }
267
268    /// Project a mutable byte borrow into another mutable view over the
269    /// same underlying bytes. The new guard owns the same release
270    /// mechanics. the exclusive borrow stays held until the returned
271    /// `RefMut<U>` drops.
272    ///
273    /// # Safety
274    ///
275    /// Same contract as [`Ref::project`]: `ptr` must point inside the
276    /// byte slice this guard owns, and the pointee must be valid `U`
277    /// for any bit pattern (`U: Pod`-style). The returned `RefMut<U>`
278    /// inherits the source guard's lifetime so the account stays
279    /// exclusively borrowed for as long as the typed view lives.
280    #[inline(always)]
281    pub unsafe fn project<U: ?Sized>(self, ptr: *mut U) -> RefMut<'a, U> {
282        #[cfg(target_os = "solana")]
283        {
284            let state = self.state;
285            core::mem::forget(self);
286            RefMut {
287                ptr,
288                state,
289                _marker: PhantomData,
290            }
291        }
292        #[cfg(not(target_os = "solana"))]
293        {
294            let Self { guard, token, .. } = self;
295            RefMut {
296                ptr,
297                guard,
298                token,
299                _marker: PhantomData,
300            }
301        }
302    }
303
304    /// Narrow an exclusive byte-slice borrow to a tail starting at `offset`.
305    #[inline(always)]
306    pub fn slice_from(self, offset: usize) -> RefMut<'a, [u8]> {
307        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
308        let bytes = unsafe { &mut *self.ptr };
309        let new_ptr = &mut bytes[offset..] as *mut [u8];
310        unsafe { self.project(new_ptr) }
311    }
312
313    /// Narrow an exclusive byte-slice borrow to a checked sub-slice.
314    #[inline(always)]
315    pub fn slice(self, offset: usize, len: usize) -> Result<RefMut<'a, [u8]>, ProgramError> {
316        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
317        let bytes = unsafe { &mut *self.ptr };
318        let end = offset
319            .checked_add(len)
320            .ok_or(ProgramError::ArithmeticOverflow)?;
321        if end > bytes.len() {
322            return Err(ProgramError::AccountDataTooSmall);
323        }
324        let new_ptr = &mut bytes[offset..end] as *mut [u8];
325        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
326        Ok(unsafe { self.project(new_ptr) })
327    }
328
329    #[inline(always)]
330    pub fn as_bytes_mut_ptr(&mut self) -> *mut u8 {
331        let bytes: &mut [u8] = self;
332        bytes.as_mut_ptr()
333    }
334}
335
336impl<'a, T> RefMut<'a, T> {
337    /// Construct a lean RefMut from a direct segment pointer plus the
338    /// exclusive-borrow state pointer.
339    #[cfg(target_os = "solana")]
340    #[inline(always)]
341    pub(crate) fn from_segment(ptr: *mut T, state: *mut u8) -> Self {
342        Self {
343            ptr,
344            state,
345            _marker: PhantomData,
346        }
347    }
348}
349
350impl<T: ?Sized> RefMut<'_, T> {
351    #[inline(always)]
352    pub fn as_ptr(&self) -> *const T {
353        self.ptr
354    }
355
356    #[inline(always)]
357    pub fn as_mut_ptr(&mut self) -> *mut T {
358        self.ptr
359    }
360}
361
362impl<T: ?Sized> core::ops::Deref for RefMut<'_, T> {
363    type Target = T;
364
365    #[inline(always)]
366    fn deref(&self) -> &T {
367        // SAFETY: see `Ref::deref`.
368        unsafe { &*self.ptr }
369    }
370}
371
372impl<T: ?Sized> core::ops::DerefMut for RefMut<'_, T> {
373    #[inline(always)]
374    fn deref_mut(&mut self) -> &mut T {
375        // SAFETY: exclusive borrow guaranteed by the guard's lifetime.
376        unsafe { &mut *self.ptr }
377    }
378}
379
380#[cfg(target_os = "solana")]
381impl<T: ?Sized> Drop for RefMut<'_, T> {
382    #[inline(always)]
383    fn drop(&mut self) {
384        // Exclusive borrow. restore NOT_BORROWED.
385        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
386        unsafe {
387            *self.state = hopper_native::NOT_BORROWED;
388        }
389    }
390}
391
392impl<T: ?Sized> core::fmt::Debug for Ref<'_, T> {
393    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
394        f.debug_struct("Ref")
395            .field("ptr", &self.ptr)
396            .finish_non_exhaustive()
397    }
398}
399
400impl<T: ?Sized> core::fmt::Debug for RefMut<'_, T> {
401    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
402        f.debug_struct("RefMut")
403            .field("ptr", &self.ptr)
404            .finish_non_exhaustive()
405    }
406}
407
408// ══════════════════════════════════════════════════════════════════════
409//  Size invariants
410// ══════════════════════════════════════════════════════════════════════
411//
412// These `const _: ()` blocks bake the flat-wrapper promise into the
413// build. If a future refactor adds another pointer or RAII field the
414// build fails here, loudly, rather than silently re-inflating the hot
415// path. On Solana a `Ref<u64>` must be exactly two pointer-words
416// (ptr + state); a `Ref<[u8]>` takes one extra word for the slice-ptr
417// length component.
418
419#[cfg(target_os = "solana")]
420const _: () = {
421    assert!(
422        core::mem::size_of::<Ref<'static, u64>>() == core::mem::size_of::<usize>() * 2,
423        "Ref<T: Sized> on Solana must be exactly (ptr, state) = 2 words",
424    );
425    assert!(
426        core::mem::size_of::<RefMut<'static, u64>>() == core::mem::size_of::<usize>() * 2,
427        "RefMut<T: Sized> on Solana must be exactly (ptr, state) = 2 words",
428    );
429};