Skip to main content

hopper_native/
entrypoint.rs

1//! Program entrypoint ownership for Hopper Native.
2//!
3//! This file is the only raw program-entry boundary owner in Hopper Native.
4//! Loader input parsing lives in [`crate::raw_input`], while the public macros
5//! below own the raw `entrypoint(input: *mut u8)` boundary and delegate into
6//! Hopper callbacks.
7
8use core::mem::MaybeUninit;
9
10use crate::account_view::AccountView;
11use crate::address::Address;
12
13/// Process the BPF entrypoint input.
14///
15/// This is the function called by the canonical Hopper Native entrypoint macro's
16/// generated entrypoint.
17///
18/// # Safety
19///
20/// `input` must be the raw pointer provided by the Solana runtime.
21#[inline(always)]
22pub unsafe fn process_entrypoint<const MAX: usize>(
23    input: *mut u8,
24    process_instruction: fn(&Address, &[AccountView], &[u8]) -> crate::ProgramResult,
25) -> u64 {
26    const UNINIT: MaybeUninit<AccountView> = MaybeUninit::uninit();
27    let mut accounts = [UNINIT; 254]; // MAX_TX_ACCOUNTS
28
29    let (program_id, count, instruction_data) =
30        // 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.
31        unsafe { crate::raw_input::deserialize_accounts::<254>(input, &mut accounts) };
32
33    // Respect MAX: only pass up to MAX accounts to the callback.
34    let effective_count = count.min(MAX);
35    // 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.
36    let account_slice = unsafe {
37        core::slice::from_raw_parts(accounts.as_ptr() as *const AccountView, effective_count)
38    };
39
40    match process_instruction(&program_id, account_slice, instruction_data) {
41        Ok(()) => crate::SUCCESS,
42        Err(error) => error.into(),
43    }
44}
45
46/// Declare the canonical Hopper Native program entrypoint.
47///
48/// Generates the `extern "C" fn entrypoint` that the Solana runtime calls.
49/// `program_entrypoint!` remains available as a backward-compatible alias.
50///
51/// # Usage
52///
53/// ```ignore
54/// use hopper_native::hopper_program_entrypoint;
55///
56/// hopper_program_entrypoint!(process_instruction);
57///
58/// pub fn process_instruction(
59///     program_id: &Address,
60///     accounts: &[AccountView],
61///     instruction_data: &[u8],
62/// ) -> ProgramResult {
63///     Ok(())
64/// }
65/// ```
66#[macro_export]
67macro_rules! hopper_program_entrypoint {
68    ( $process_instruction:expr ) => {
69        $crate::hopper_program_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
70    };
71    ( $process_instruction:expr, $maximum:expr ) => {
72        /// # Safety
73        ///
74        /// Called by the Solana runtime; `input` is a valid BPF input buffer.
75        #[no_mangle]
76        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
77            const UNINIT: core::mem::MaybeUninit<$crate::AccountView> =
78                core::mem::MaybeUninit::<$crate::AccountView>::uninit();
79            let mut accounts = [UNINIT; $maximum];
80
81            // 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.
82            let (program_id, count, instruction_data) = unsafe {
83                $crate::raw_input::deserialize_accounts::<$maximum>(input, &mut accounts)
84            };
85
86            match $process_instruction(
87                &program_id,
88                // 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.
89                unsafe { core::slice::from_raw_parts(accounts.as_ptr() as _, count) },
90                instruction_data,
91            ) {
92                Ok(()) => $crate::SUCCESS,
93                Err(error) => error.into(),
94            }
95        }
96    };
97}
98
99/// Backward-compatible alias for `hopper_program_entrypoint!`.
100#[macro_export]
101macro_rules! program_entrypoint {
102    ( $process_instruction:expr ) => {
103        $crate::hopper_program_entrypoint!($process_instruction);
104    };
105    ( $process_instruction:expr, $maximum:expr ) => {
106        $crate::hopper_program_entrypoint!($process_instruction, $maximum);
107    };
108}
109
110/// Declare a fast two-argument Hopper Native program entrypoint.
111///
112/// Uses the SVM's second entrypoint register, which provides a direct
113/// pointer to instruction data, eliminating the full account-scanning pass
114/// that the single-argument entrypoint requires. Saves ~30-40 CU per
115/// instruction invocation.
116///
117/// The SVM has provided the second argument since runtime ~1.17.
118///
119/// # Usage
120///
121/// ```ignore
122/// use hopper_native::hopper_fast_entrypoint;
123///
124/// hopper_fast_entrypoint!(process_instruction, 3);
125///
126/// pub fn process_instruction(
127///     program_id: &Address,
128///     accounts: &[AccountView],
129///     instruction_data: &[u8],
130/// ) -> ProgramResult {
131///     Ok(())
132/// }
133/// ```
134#[macro_export]
135macro_rules! hopper_fast_entrypoint {
136    ( $process_instruction:expr ) => {
137        $crate::hopper_fast_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
138    };
139    ( $process_instruction:expr, $maximum:expr ) => {
140        /// # Safety
141        ///
142        /// Called by the Solana runtime; `input` is a valid BPF input buffer
143        /// and `ix_data` points to the instruction data with its u64 length
144        /// stored at offset -8.
145        #[no_mangle]
146        pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data: *const u8) -> u64 {
147            const UNINIT: core::mem::MaybeUninit<$crate::AccountView> =
148                core::mem::MaybeUninit::<$crate::AccountView>::uninit();
149            let mut accounts = [UNINIT; $maximum];
150
151            // Instruction data length is the u64 immediately before the data pointer.
152            // 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.
153            let ix_len = unsafe { *(ix_data.sub(8) as *const u64) as usize };
154            let instruction_data: &'static [u8] =
155                unsafe { core::slice::from_raw_parts(ix_data, ix_len) };
156
157            // Program ID immediately follows instruction data in the SVM buffer.
158            let program_id =
159                // 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.
160                unsafe { core::ptr::read(ix_data.add(ix_len) as *const $crate::Address) };
161
162            let (program_id, count, instruction_data) = unsafe {
163                $crate::raw_input::deserialize_accounts_fast::<$maximum>(
164                    input,
165                    &mut accounts,
166                    instruction_data,
167                    program_id,
168                )
169            };
170
171            match $process_instruction(
172                &program_id,
173                // 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.
174                unsafe { core::slice::from_raw_parts(accounts.as_ptr() as _, count) },
175                instruction_data,
176            ) {
177                Ok(()) => $crate::SUCCESS,
178                Err(error) => error.into(),
179            }
180        }
181    };
182}
183
184/// Backward-compatible alias for `hopper_fast_entrypoint!`.
185#[macro_export]
186macro_rules! fast_entrypoint {
187    ( $process_instruction:expr ) => {
188        $crate::hopper_fast_entrypoint!($process_instruction);
189    };
190    ( $process_instruction:expr, $maximum:expr ) => {
191        $crate::hopper_fast_entrypoint!($process_instruction, $maximum);
192    };
193}
194
195/// Declare the canonical lazy program entrypoint that defers account parsing.
196#[macro_export]
197macro_rules! hopper_lazy_entrypoint {
198    ( $process:expr ) => {
199        /// # Safety
200        ///
201        /// Called by the Solana runtime; `input` is a valid BPF input buffer.
202        #[no_mangle]
203        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
204            // 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.
205            let mut ctx = unsafe { $crate::lazy::lazy_deserialize(input) };
206            match $process(&mut ctx) {
207                Ok(()) => $crate::SUCCESS,
208                Err(error) => error.into(),
209            }
210        }
211    };
212}
213
214/// Backward-compatible alias for `hopper_lazy_entrypoint!`.
215#[macro_export]
216macro_rules! lazy_entrypoint {
217    ( $process:expr ) => {
218        $crate::hopper_lazy_entrypoint!($process);
219    };
220}
221
222/// Set up a no-op global allocator that aborts on allocation.
223///
224/// Useful for `no_std` programs that must not allocate. Any attempt to
225/// allocate will immediately abort the program rather than returning a
226/// null pointer (which violates the `GlobalAlloc` contract).
227#[macro_export]
228macro_rules! no_allocator {
229    () => {
230        #[cfg(target_os = "solana")]
231        mod __hopper_allocator {
232            struct NoAlloc;
233
234            unsafe impl core::alloc::GlobalAlloc for NoAlloc {
235                unsafe fn alloc(&self, _layout: core::alloc::Layout) -> *mut u8 {
236                    // Abort: returning null_mut violates the GlobalAlloc
237                    // contract and causes UB. Abort is the correct response
238                    // for a no-alloc program.
239                    core::arch::asm!("mov r0, 1", "exit", options(noreturn));
240                }
241                unsafe fn dealloc(&self, _ptr: *mut u8, _layout: core::alloc::Layout) {}
242            }
243
244            #[global_allocator]
245            static ALLOCATOR: NoAlloc = NoAlloc;
246        }
247    };
248}
249
250/// Default no_std panic handler that aborts immediately.
251///
252/// On BPF, uses inline assembly to return error code 1 (aborts the
253/// program). This is cheaper than `spin_loop()` which would burn CU
254/// until the runtime kills the program.
255#[macro_export]
256macro_rules! nostd_panic_handler {
257    () => {
258        #[cfg(target_os = "solana")]
259        #[panic_handler]
260        fn panic(_info: &core::panic::PanicInfo) -> ! {
261            // Abort immediately, spin_loop() would burn CU indefinitely.
262            // 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.
263            unsafe { core::arch::asm!("mov r0, 1", "exit", options(noreturn)) };
264        }
265    };
266}