Skip to content

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.

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

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,
}
// 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 of anchor_lang
  • Uses solana_program::pubkey::Pubkey for public keys
  • No #[account] macro (manual account handling)
  • Full control over serialization behavior
// 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'),
]);

Terminal window
# Generate Rust + TypeScript (default)
lumos generate schema.lumos
# Generate only Rust
lumos generate schema.lumos --lang rust
# Generate with explicit native target (coming soon)
lumos generate schema.lumos --target native
use borsh::BorshDeserialize;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program_error::ProgramError,
pubkey::Pubkey,
};
// Import generated types
mod 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(())
}
}
}

For native programs, you need to manually calculate account sizes. LUMOS helps with the check-size command:

Terminal window
lumos check-size schema.lumos

Output:

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 bytes

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
}
}

FeatureNativeAnchor
Attribute#[solana]#[solana] #[account]
Importsborsh, solana_programanchor_lang
Account validationManualAutomatic via macros
Space calculationManual#[account] handles it
Binary sizeSmallerLarger (Anchor runtime)
Learning curveSteeperGentler
FlexibilityMaximumOpinionated
  • Performance critical: Minimal overhead and dependencies
  • Existing codebase: Integrating with non-Anchor programs
  • Learning: Understanding Solana internals
  • Custom requirements: Non-standard account layouts
  • Rapid development: Less boilerplate
  • Safety: Built-in account validation
  • Ecosystem: IDL generation, client libraries
  • Team projects: Standardized patterns

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"

If migrating from Anchor to native:

  1. Remove #[account] from schema definitions
  2. Regenerate code: lumos generate schema.lumos
  3. Update program entrypoint to use solana_program
  4. Handle account validation manually
  5. Calculate and allocate account space explicitly
// Before (Anchor)
#[solana]
#[account]
struct PlayerData { ... }
// After (Native)
#[solana]
struct PlayerData { ... }

// Check account ownership
if account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Check account is writable when needed
if !account.is_writable {
return Err(ProgramError::InvalidAccountData);
}

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;
}
let data = PlayerData::try_from_slice(&account.data.borrow())
.map_err(|e| {
msg!("Failed to deserialize: {:?}", e);
ProgramError::InvalidAccountData
})?;