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: 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: ExclusiveLease,
240 token: BorrowToken,
241 _marker: PhantomData<&'a mut T>,
242}
243
244/// Owns the native release obligation without storing a parent `&mut [u8]`.
245#[cfg(not(target_os = "solana"))]
246struct ExclusiveLease(*mut u8);
247
248#[cfg(not(target_os = "solana"))]
249impl Drop for ExclusiveLease {
250 fn drop(&mut self) {
251 if !self.0.is_null() {
252 // SAFETY: from_backend transfers the native guard's live lease here.
253 // Projections move this owner; exactly one final drop releases it.
254 unsafe {
255 *self.0 = hopper_native::NOT_BORROWED;
256 }
257 }
258 }
259}
260
261impl<'a> RefMut<'a, [u8]> {
262 /// Wrap an active-backend mutable byte borrow into a Hopper RefMut.
263 #[inline(always)]
264 pub(crate) fn from_backend(inner: BackendRefMut<'a, [u8]>, token: BorrowToken) -> Self {
265 #[cfg(target_os = "solana")]
266 {
267 let _ = token;
268 let (bytes, state) = inner.into_raw_parts();
269 Self {
270 ptr: bytes as *mut [u8],
271 state,
272 _marker: PhantomData,
273 }
274 }
275 #[cfg(not(target_os = "solana"))]
276 {
277 // Consume the parent before deriving our raw pointer. Retaining and
278 // moving the parent's &mut after a reborrow invalidates that pointer
279 // under Stacked Borrows, even if the lease flag is still correct.
280 let (bytes, state) = inner.into_raw_parts();
281 Self {
282 ptr: bytes as *mut [u8],
283 guard: ExclusiveLease(state),
284 token,
285 _marker: PhantomData,
286 }
287 }
288 }
289
290 /// Project a mutable byte borrow into another mutable view over the
291 /// same underlying bytes. The new guard owns the same release
292 /// mechanics. the exclusive borrow stays held until the returned
293 /// `RefMut<U>` drops.
294 ///
295 /// # Safety
296 ///
297 /// Same contract as [`Ref::project`]: `ptr` must point inside the
298 /// byte slice this guard owns, the pointee must be valid `U`
299 /// for any bit pattern (`U: Pod`-style), and `ptr` must satisfy
300 /// `U` alignment. The returned `RefMut<U>`
301 /// inherits the source guard's lifetime so the account stays
302 /// exclusively borrowed for as long as the typed view lives.
303 #[inline(always)]
304 pub unsafe fn project<U: ?Sized>(self, ptr: *mut U) -> RefMut<'a, U> {
305 #[cfg(target_os = "solana")]
306 {
307 let state = self.state;
308 core::mem::forget(self);
309 RefMut {
310 ptr,
311 state,
312 _marker: PhantomData,
313 }
314 }
315 #[cfg(not(target_os = "solana"))]
316 {
317 let Self { guard, token, .. } = self;
318 RefMut {
319 ptr,
320 guard,
321 token,
322 _marker: PhantomData,
323 }
324 }
325 }
326
327 /// Narrow an exclusive byte-slice borrow to a tail starting at `offset`.
328 #[inline(always)]
329 pub fn slice_from(self, offset: usize) -> RefMut<'a, [u8]> {
330 // 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.
331 let bytes = unsafe { &mut *self.ptr };
332 let new_ptr = &mut bytes[offset..] as *mut [u8];
333 unsafe { self.project(new_ptr) }
334 }
335
336 /// Narrow an exclusive byte-slice borrow to a checked sub-slice.
337 #[inline(always)]
338 pub fn slice(self, offset: usize, len: usize) -> Result<RefMut<'a, [u8]>, ProgramError> {
339 // 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.
340 let bytes = unsafe { &mut *self.ptr };
341 let end = offset
342 .checked_add(len)
343 .ok_or(ProgramError::ArithmeticOverflow)?;
344 if end > bytes.len() {
345 return Err(ProgramError::AccountDataTooSmall);
346 }
347 let new_ptr = &mut bytes[offset..end] as *mut [u8];
348 // 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.
349 Ok(unsafe { self.project(new_ptr) })
350 }
351
352 #[inline(always)]
353 pub fn as_bytes_mut_ptr(&mut self) -> *mut u8 {
354 let bytes: &mut [u8] = self;
355 bytes.as_mut_ptr()
356 }
357}
358
359impl<'a, T> RefMut<'a, T> {
360 /// Construct a lean RefMut from a direct segment pointer plus the
361 /// exclusive-borrow state pointer.
362 #[cfg(target_os = "solana")]
363 #[inline(always)]
364 pub(crate) fn from_segment(ptr: *mut T, state: *mut u8) -> Self {
365 Self {
366 ptr,
367 state,
368 _marker: PhantomData,
369 }
370 }
371}
372
373impl<T: ?Sized> RefMut<'_, T> {
374 #[inline(always)]
375 pub fn as_ptr(&self) -> *const T {
376 self.ptr
377 }
378
379 #[inline(always)]
380 pub fn as_mut_ptr(&mut self) -> *mut T {
381 self.ptr
382 }
383}
384
385impl<T: ?Sized> core::ops::Deref for RefMut<'_, T> {
386 type Target = T;
387
388 #[inline(always)]
389 fn deref(&self) -> &T {
390 // SAFETY: see `Ref::deref`.
391 unsafe { &*self.ptr }
392 }
393}
394
395impl<T: ?Sized> core::ops::DerefMut for RefMut<'_, T> {
396 #[inline(always)]
397 fn deref_mut(&mut self) -> &mut T {
398 // SAFETY: exclusive borrow guaranteed by the guard's lifetime.
399 unsafe { &mut *self.ptr }
400 }
401}
402
403#[cfg(target_os = "solana")]
404impl<T: ?Sized> Drop for RefMut<'_, T> {
405 #[inline(always)]
406 fn drop(&mut self) {
407 if self.state.is_null() {
408 return;
409 }
410 // Exclusive borrow. restore NOT_BORROWED.
411 // 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.
412 unsafe {
413 *self.state = hopper_native::NOT_BORROWED;
414 }
415 }
416}
417
418impl<T: ?Sized> core::fmt::Debug for Ref<'_, T> {
419 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
420 f.debug_struct("Ref")
421 .field("ptr", &self.ptr)
422 .finish_non_exhaustive()
423 }
424}
425
426impl<T: ?Sized> core::fmt::Debug for RefMut<'_, T> {
427 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
428 f.debug_struct("RefMut")
429 .field("ptr", &self.ptr)
430 .finish_non_exhaustive()
431 }
432}
433
434// ══════════════════════════════════════════════════════════════════════
435// Size invariants
436// ══════════════════════════════════════════════════════════════════════
437//
438// These `const _: ()` blocks bake the flat-wrapper promise into the
439// build. If a future refactor adds another pointer or RAII field the
440// build fails here, loudly, rather than silently re-inflating the hot
441// path. On Solana a `Ref<u64>` must be exactly two pointer-words
442// (ptr + state); a `Ref<[u8]>` takes one extra word for the slice-ptr
443// length component.
444
445#[cfg(target_os = "solana")]
446const _: () = {
447 assert!(
448 core::mem::size_of::<Ref<'static, u64>>() == core::mem::size_of::<usize>() * 2,
449 "Ref<T: Sized> on Solana must be exactly (ptr, state) = 2 words",
450 );
451 assert!(
452 core::mem::size_of::<RefMut<'static, u64>>() == core::mem::size_of::<usize>() * 2,
453 "RefMut<T: Sized> on Solana must be exactly (ptr, state) = 2 words",
454 );
455};