Native Solana Programs
LUMOS supports generating code for native Solana programs that don’t use the Anchor framework. This generates pure Borsh serialization code with solana_program imports.
When to Use Native Mode
Section titled “When to Use Native Mode”Use native Solana program generation when:
- Building low-level programs without Anchor overhead
- Working with existing non-Anchor codebases
- Needing maximum control over program structure
- Optimizing for minimal dependencies and binary size
Schema Syntax
Section titled “Schema Syntax”For native programs, use #[solana] without #[account]:
// Native Solana schema - no #[account] attribute#[solana]struct PlayerData { wallet: PublicKey, level: u16, experience: u64, username: String,}
#[solana]enum GameInstruction { Initialize { max_players: u32 }, UpdateScore { player: PublicKey, score: u64 }, EndGame,}Generated Code
Section titled “Generated Code”Rust Output
Section titled “Rust Output”// Auto-generated by LUMOS// DO NOT EDIT - Changes will be overwritten
use borsh::{BorshSerialize, BorshDeserialize};use solana_program::pubkey::Pubkey;
#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]pub struct PlayerData { pub wallet: Pubkey, pub level: u16, pub experience: u64, pub username: String,}
#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]pub enum GameInstruction { Initialize { max_players: u32 }, UpdateScore { player: Pubkey, score: u64 }, EndGame,}Key differences from Anchor mode:
- Uses
borsh::{BorshSerialize, BorshDeserialize}instead ofanchor_lang - Uses
solana_program::pubkey::Pubkeyfor public keys - No
#[account]macro (manual account handling) - Full control over serialization behavior
TypeScript Output
Section titled “TypeScript Output”// Auto-generated by LUMOS// DO NOT EDIT - Changes will be overwritten
import * as borsh from '@coral-xyz/borsh';import { PublicKey } from '@solana/web3.js';
export interface PlayerData { wallet: PublicKey; level: number; experience: bigint; username: string;}
export const PlayerDataSchema = borsh.struct([ borsh.publicKey('wallet'), borsh.u16('level'), borsh.u64('experience'), borsh.str('username'),]);Generate Native Code
Section titled “Generate Native Code”# Generate Rust + TypeScript (default)lumos generate schema.lumos
# Generate only Rustlumos generate schema.lumos --lang rust
# Generate with explicit native target (coming soon)lumos generate schema.lumos --target nativeUsing Generated Code in Solana Program
Section titled “Using Generated Code in Solana Program”use borsh::BorshDeserialize;use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey,};
// Import generated typesmod generated;use generated::{PlayerData, GameInstruction};
pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8],) -> ProgramResult { // Deserialize instruction using generated schema let instruction = GameInstruction::try_from_slice(instruction_data) .map_err(|_| ProgramError::InvalidInstructionData)?;
match instruction { GameInstruction::Initialize { max_players } => { // Handle initialization Ok(()) } GameInstruction::UpdateScore { player, score } => { // Update player score Ok(()) } GameInstruction::EndGame => { // End the game Ok(()) } }}Account Size Calculation
Section titled “Account Size Calculation”For native programs, you need to manually calculate account sizes. LUMOS helps with the check-size command:
lumos check-size schema.lumosOutput:
Account Size Analysis=====================
PlayerData: wallet (Pubkey): 32 bytes level (u16): 2 bytes experience (u64): 8 bytes username (String): 4 bytes (length) + N bytes (content) -------------------------------- Minimum size: 46 bytes + username length
Recommendation: Allocate space for maximum expected username length.For username max 32 chars: 46 + 32 = 78 bytesManual Size Constants
Section titled “Manual Size Constants”Add size constants to your generated code:
impl PlayerData { /// Base size without dynamic fields pub const BASE_SIZE: usize = 32 + 2 + 8 + 4; // 46 bytes
/// Calculate total size with username pub fn size_with_username(username_len: usize) -> usize { Self::BASE_SIZE + username_len }}Comparison: Native vs Anchor
Section titled “Comparison: Native vs Anchor”| Feature | Native | Anchor |
|---|---|---|
| Attribute | #[solana] | #[solana] #[account] |
| Imports | borsh, solana_program | anchor_lang |
| Account validation | Manual | Automatic via macros |
| Space calculation | Manual | #[account] handles it |
| Binary size | Smaller | Larger (Anchor runtime) |
| Learning curve | Steeper | Gentler |
| Flexibility | Maximum | Opinionated |
When to Choose Native
Section titled “When to Choose Native”- Performance critical: Minimal overhead and dependencies
- Existing codebase: Integrating with non-Anchor programs
- Learning: Understanding Solana internals
- Custom requirements: Non-standard account layouts
When to Choose Anchor
Section titled “When to Choose Anchor”- Rapid development: Less boilerplate
- Safety: Built-in account validation
- Ecosystem: IDL generation, client libraries
- Team projects: Standardized patterns
Dependencies
Section titled “Dependencies”Add these to your Cargo.toml for native Solana programs:
[dependencies]borsh = "1.5"solana-program = "2.0"
[dev-dependencies]solana-program-test = "2.0"solana-sdk = "2.0"Migration from Anchor
Section titled “Migration from Anchor”If migrating from Anchor to native:
- Remove
#[account]from schema definitions - Regenerate code:
lumos generate schema.lumos - Update program entrypoint to use
solana_program - Handle account validation manually
- Calculate and allocate account space explicitly
// Before (Anchor)#[solana]#[account]struct PlayerData { ... }
// After (Native)#[solana]struct PlayerData { ... }Best Practices
Section titled “Best Practices”1. Validate Accounts Manually
Section titled “1. Validate Accounts Manually”// Check account ownershipif account.owner != program_id { return Err(ProgramError::IncorrectProgramId);}
// Check account is writable when neededif !account.is_writable { return Err(ProgramError::InvalidAccountData);}2. Use Discriminators
Section titled “2. Use Discriminators”Add a discriminator byte to distinguish account types:
#[solana]struct PlayerData { discriminator: u8, // Always first, value = 1 wallet: PublicKey, // ...}
impl PlayerData { pub const DISCRIMINATOR: u8 = 1;}3. Handle Serialization Errors
Section titled “3. Handle Serialization Errors”let data = PlayerData::try_from_slice(&account.data.borrow()) .map_err(|e| { msg!("Failed to deserialize: {:?}", e); ProgramError::InvalidAccountData })?;See Also
Section titled “See Also”- Anchor Integration - Using LUMOS with Anchor
- Borsh Internals - Deep dive into serialization
- Type System - Complete type reference