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};