Skip to content

Commit 1fb86c4

Browse files
author
RECTOR
committed
feat(cli): Add --target flag for native Solana support (#57)
- Add --target flag with options: auto, native, anchor - Native mode: force pure Borsh output, no Anchor dependencies - Warn when schema has #[account] but target is native - Warn when target is anchor but no #[account] found - Strip #[account] attributes when target=native
1 parent 8cdf0d8 commit 1fb86c4

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
---
2+
title: Native Solana Programs
3+
description: Generate pure Borsh code for native Solana programs without Anchor
4+
---
5+
6+
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.
7+
8+
## When to Use Native Mode
9+
10+
Use native Solana program generation when:
11+
12+
- Building low-level programs without Anchor overhead
13+
- Working with existing non-Anchor codebases
14+
- Needing maximum control over program structure
15+
- Optimizing for minimal dependencies and binary size
16+
17+
## Schema Syntax
18+
19+
For native programs, use `#[solana]` **without** `#[account]`:
20+
21+
```rust
22+
// Native Solana schema - no #[account] attribute
23+
#[solana]
24+
struct PlayerData {
25+
wallet: PublicKey,
26+
level: u16,
27+
experience: u64,
28+
username: String,
29+
}
30+
31+
#[solana]
32+
enum GameInstruction {
33+
Initialize { max_players: u32 },
34+
UpdateScore { player: PublicKey, score: u64 },
35+
EndGame,
36+
}
37+
```
38+
39+
## Generated Code
40+
41+
### Rust Output
42+
43+
```rust
44+
// Auto-generated by LUMOS
45+
// DO NOT EDIT - Changes will be overwritten
46+
47+
use borsh::{BorshSerialize, BorshDeserialize};
48+
use solana_program::pubkey::Pubkey;
49+
50+
#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
51+
pub struct PlayerData {
52+
pub wallet: Pubkey,
53+
pub level: u16,
54+
pub experience: u64,
55+
pub username: String,
56+
}
57+
58+
#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
59+
pub enum GameInstruction {
60+
Initialize { max_players: u32 },
61+
UpdateScore { player: Pubkey, score: u64 },
62+
EndGame,
63+
}
64+
```
65+
66+
Key differences from Anchor mode:
67+
- Uses `borsh::{BorshSerialize, BorshDeserialize}` instead of `anchor_lang`
68+
- Uses `solana_program::pubkey::Pubkey` for public keys
69+
- No `#[account]` macro (manual account handling)
70+
- Full control over serialization behavior
71+
72+
### TypeScript Output
73+
74+
```typescript
75+
// Auto-generated by LUMOS
76+
// DO NOT EDIT - Changes will be overwritten
77+
78+
import * as borsh from '@coral-xyz/borsh';
79+
import { PublicKey } from '@solana/web3.js';
80+
81+
export interface PlayerData {
82+
wallet: PublicKey;
83+
level: number;
84+
experience: bigint;
85+
username: string;
86+
}
87+
88+
export const PlayerDataSchema = borsh.struct([
89+
borsh.publicKey('wallet'),
90+
borsh.u16('level'),
91+
borsh.u64('experience'),
92+
borsh.str('username'),
93+
]);
94+
```
95+
96+
---
97+
98+
## Usage
99+
100+
### Generate Native Code
101+
102+
```bash
103+
# Generate Rust + TypeScript (default)
104+
lumos generate schema.lumos
105+
106+
# Generate only Rust
107+
lumos generate schema.lumos --lang rust
108+
109+
# Generate with explicit native target (coming soon)
110+
lumos generate schema.lumos --target native
111+
```
112+
113+
### Using Generated Code in Solana Program
114+
115+
```rust
116+
use borsh::BorshDeserialize;
117+
use solana_program::{
118+
account_info::AccountInfo,
119+
entrypoint::ProgramResult,
120+
program_error::ProgramError,
121+
pubkey::Pubkey,
122+
};
123+
124+
// Import generated types
125+
mod generated;
126+
use generated::{PlayerData, GameInstruction};
127+
128+
pub fn process_instruction(
129+
program_id: &Pubkey,
130+
accounts: &[AccountInfo],
131+
instruction_data: &[u8],
132+
) -> ProgramResult {
133+
// Deserialize instruction using generated schema
134+
let instruction = GameInstruction::try_from_slice(instruction_data)
135+
.map_err(|_| ProgramError::InvalidInstructionData)?;
136+
137+
match instruction {
138+
GameInstruction::Initialize { max_players } => {
139+
// Handle initialization
140+
Ok(())
141+
}
142+
GameInstruction::UpdateScore { player, score } => {
143+
// Update player score
144+
Ok(())
145+
}
146+
GameInstruction::EndGame => {
147+
// End the game
148+
Ok(())
149+
}
150+
}
151+
}
152+
```
153+
154+
---
155+
156+
## Account Size Calculation
157+
158+
For native programs, you need to manually calculate account sizes. LUMOS helps with the `check-size` command:
159+
160+
```bash
161+
lumos check-size schema.lumos
162+
```
163+
164+
Output:
165+
```
166+
Account Size Analysis
167+
=====================
168+
169+
PlayerData:
170+
wallet (Pubkey): 32 bytes
171+
level (u16): 2 bytes
172+
experience (u64): 8 bytes
173+
username (String): 4 bytes (length) + N bytes (content)
174+
--------------------------------
175+
Minimum size: 46 bytes + username length
176+
177+
Recommendation: Allocate space for maximum expected username length.
178+
For username max 32 chars: 46 + 32 = 78 bytes
179+
```
180+
181+
### Manual Size Constants
182+
183+
Add size constants to your generated code:
184+
185+
```rust
186+
impl PlayerData {
187+
/// Base size without dynamic fields
188+
pub const BASE_SIZE: usize = 32 + 2 + 8 + 4; // 46 bytes
189+
190+
/// Calculate total size with username
191+
pub fn size_with_username(username_len: usize) -> usize {
192+
Self::BASE_SIZE + username_len
193+
}
194+
}
195+
```
196+
197+
---
198+
199+
## Comparison: Native vs Anchor
200+
201+
| Feature | Native | Anchor |
202+
|---------|--------|--------|
203+
| Attribute | `#[solana]` | `#[solana] #[account]` |
204+
| Imports | `borsh`, `solana_program` | `anchor_lang` |
205+
| Account validation | Manual | Automatic via macros |
206+
| Space calculation | Manual | `#[account]` handles it |
207+
| Binary size | Smaller | Larger (Anchor runtime) |
208+
| Learning curve | Steeper | Gentler |
209+
| Flexibility | Maximum | Opinionated |
210+
211+
### When to Choose Native
212+
213+
- **Performance critical**: Minimal overhead and dependencies
214+
- **Existing codebase**: Integrating with non-Anchor programs
215+
- **Learning**: Understanding Solana internals
216+
- **Custom requirements**: Non-standard account layouts
217+
218+
### When to Choose Anchor
219+
220+
- **Rapid development**: Less boilerplate
221+
- **Safety**: Built-in account validation
222+
- **Ecosystem**: IDL generation, client libraries
223+
- **Team projects**: Standardized patterns
224+
225+
---
226+
227+
## Dependencies
228+
229+
Add these to your `Cargo.toml` for native Solana programs:
230+
231+
```toml
232+
[dependencies]
233+
borsh = "1.5"
234+
solana-program = "2.0"
235+
236+
[dev-dependencies]
237+
solana-program-test = "2.0"
238+
solana-sdk = "2.0"
239+
```
240+
241+
---
242+
243+
## Migration from Anchor
244+
245+
If migrating from Anchor to native:
246+
247+
1. Remove `#[account]` from schema definitions
248+
2. Regenerate code: `lumos generate schema.lumos`
249+
3. Update program entrypoint to use `solana_program`
250+
4. Handle account validation manually
251+
5. Calculate and allocate account space explicitly
252+
253+
```rust
254+
// Before (Anchor)
255+
#[solana]
256+
#[account]
257+
struct PlayerData { ... }
258+
259+
// After (Native)
260+
#[solana]
261+
struct PlayerData { ... }
262+
```
263+
264+
---
265+
266+
## Best Practices
267+
268+
### 1. Validate Accounts Manually
269+
270+
```rust
271+
// Check account ownership
272+
if account.owner != program_id {
273+
return Err(ProgramError::IncorrectProgramId);
274+
}
275+
276+
// Check account is writable when needed
277+
if !account.is_writable {
278+
return Err(ProgramError::InvalidAccountData);
279+
}
280+
```
281+
282+
### 2. Use Discriminators
283+
284+
Add a discriminator byte to distinguish account types:
285+
286+
```rust
287+
#[solana]
288+
struct PlayerData {
289+
discriminator: u8, // Always first, value = 1
290+
wallet: PublicKey,
291+
// ...
292+
}
293+
294+
impl PlayerData {
295+
pub const DISCRIMINATOR: u8 = 1;
296+
}
297+
```
298+
299+
### 3. Handle Serialization Errors
300+
301+
```rust
302+
let data = PlayerData::try_from_slice(&account.data.borrow())
303+
.map_err(|e| {
304+
msg!("Failed to deserialize: {:?}", e);
305+
ProgramError::InvalidAccountData
306+
})?;
307+
```
308+
309+
---
310+
311+
## See Also
312+
313+
- [Anchor Integration](/frameworks/anchor) - Using LUMOS with Anchor
314+
- [Borsh Internals](/guides/borsh-internals) - Deep dive into serialization
315+
- [Type System](/api/types) - Complete type reference

0 commit comments

Comments
 (0)