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