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