Skip to content

Commit 0189a13

Browse files
authored
feat(ENG-245): Add frame macros for stack layout and signer seeds (#33)
# Changes 1. Add `#[frame]` attribute macro: applies `#[repr(C, align(8))]` and asserts the struct fits within one SBPF stack frame (4096 bytes); registers field metadata and doc comment in proc-macro shared state 2. Add `signer_seeds!` function-like macro: defines a `#[repr(C)]` struct where every field is `SolSignerSeed`; registers field names in shared state for auto-discovery by `constant_group!` 3. Extend `constant_group!` with `#[frame(Type)]` attribute for frame-relative offsets (`offset_of - size_of`, with 8-byte alignment assertion) and `signer_seeds!(field)` syntax that auto-expands `_ADDR_OFF`/`_LEN_OFF` constants per seed plus an `N_SEEDS` count 4. Add `shared_state` module for cross-proc-macro communication via process-global `LazyLock<Mutex<HashMap>>` — frame field→type mappings, signer seed field lists, and frame doc comments 5. Decompose `constant_group` expand and parse modules into subdirectories (`expand/{mod,offset,immediate,signer_seeds}`, `parse/{mod,offset,signer_seeds}`) 6. Define `RegisterMarketFrame` and `PdaSignerSeeds` in `interface::market` using the new macros, with a frame-aware `constant_group!` that injects `.equ` directives into `market/register.s` 7. Document `#[frame]`, `signer_seeds!`, and frame-relative `constant_group!` mode in the build-scaffolding docs page
1 parent ae534c6 commit 0189a13

File tree

17 files changed

+710
-64
lines changed

17 files changed

+710
-64
lines changed

docs/src/development/build-scaffolding.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,36 @@ The [`macros`] crate provides several [proc macros]:
3434

3535
<Include rs="macros::lib" collapsed/>
3636

37-
### `constant_group!`
37+
### `constant_group!` {#constant_group}
3838

3939
Defines a group of named assembly constants with an injection target. The
4040
`#[inject("file")]` attribute specifies which assembly file receives the
4141
constants. An optional `#[prefix("...")]` attribute prepends a prefix to all
4242
generated constant names. An optional `///` doc comment on the group itself
4343
adds a header comment and separator lines around the group in the output
44-
assembly file. Each constant is assigned a value using one of two custom
44+
assembly file. Each constant is assigned a value using one of three custom
4545
syntax forms (parsed within the proc macro, not standalone macros):
4646

4747
- `offset!(expr)`: an `i16` memory offset, the generated name is suffixed with
4848
`_OFF`
4949
- `immediate!(expr)`: an `i32` immediate value
50+
- `signer_seeds!(field)`: expands a [`signer_seeds!`](#signer_seeds) field into
51+
`_ADDR_OFF` and `_LEN_OFF` constants per seed, plus an `N_SEEDS` count
52+
(requires `#[frame(Type)]`, see below)
5053

5154
<Include rs="interface::memory#constant_group_example" collapsible/>
5255

56+
#### Frame-relative offsets
57+
58+
When annotated with `#[frame(Type)]`, the group enters frame-relative mode.
59+
In this mode, `offset!(field)` computes a negative offset from the frame
60+
pointer (`offset_of - size_of`) and asserts 8-byte alignment
61+
(`BPF_ALIGN_OF_U128`). The group's doc comment defaults to the frame struct's
62+
doc comment if not explicitly provided. The `signer_seeds!(field)` form is
63+
only available in frame-relative mode.
64+
65+
<Include rs="interface::market#register_market_stack" collapsible/>
66+
5367
Each group generates:
5468

5569
- A Rust module with public constants (with compile-time range checks)
@@ -101,6 +115,27 @@ The count is accessible in Rust as `RegisterMarketAccounts::LEN`.
101115

102116
<Include rs="interface::market#register_market_accounts" collapsible/>
103117

118+
### `#[frame]`
119+
120+
Attribute macro for stack frame structs. Applies `#[repr(C, align(8))]`
121+
(aligned to `BPF_ALIGN_OF_U128`) and asserts at compile time that the struct
122+
fits within one SBPF stack frame (4096 bytes). Also registers field-to-type
123+
mappings and the struct's doc comment in proc-macro shared state so that
124+
[`constant_group!`](#constant_group) can auto-discover frame fields and
125+
derive its header comment.
126+
127+
<Include rs="interface::market#frame_example" collapsible/>
128+
129+
### `signer_seeds!` {#signer_seeds}
130+
131+
Function-like macro that defines a `#[repr(C)]` struct where every field is
132+
typed as `SolSignerSeed`. Field names are registered in proc-macro shared
133+
state so that `signer_seeds!(field)` inside a
134+
[`constant_group!`](#constant_group) can auto-discover all seed fields by
135+
looking up the parent field's type on the frame struct.
136+
137+
<Include rs="interface::market#signer_seeds_example" collapsible/>
138+
104139
## Interface
105140

106141
The [`interface`] crate uses the macros to declare all program constants. The

interface/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,5 @@ pub const INJECTION_GROUPS: &[&dropset_build::ConstantGroup] = &[
5252
&market::register_market_accounts::GROUP,
5353
&memory::data::GROUP,
5454
&memory::input_buffer::GROUP,
55+
&market::register_market_frame::GROUP,
5556
];

interface/src/market.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use crate::cpi_bindings::SolInstruction;
1+
use crate::cpi_bindings::SolSignerSeed;
22
use crate::memory::StackNode;
33
use crate::order::Order;
44
use crate::seat::Seat;
5-
use dropset_macros::{instruction_accounts, instruction_data};
5+
use dropset_macros::{constant_group, frame, instruction_accounts, instruction_data, signer_seeds};
6+
use pinocchio::Address;
67

78
// region: market_header
89
#[repr(C, packed)]
@@ -45,8 +46,45 @@ pub enum RegisterMarketAccounts {
4546
// endregion: register_market_accounts
4647

4748
// region: register_market_stack
48-
#[repr(C)]
49-
pub struct RegisterMarketStack {
50-
pub instruction: SolInstruction,
49+
50+
// region: frame_example
51+
#[frame]
52+
/// Stack frame for REGISTER-MARKET.
53+
pub struct RegisterMarketFrame {
54+
/// For CreateAccount CPI.
55+
pub pda_seeds: PdaSignerSeeds,
56+
/// From `sol_try_find_program_address`.
57+
pub pda: Address,
58+
/// From `sol_try_find_program_address`.
59+
pub bump: u8,
60+
}
61+
// endregion: frame_example
62+
63+
// region: signer_seeds_example
64+
signer_seeds! {
65+
pub struct PdaSignerSeeds {
66+
/// Base mint seed.
67+
base,
68+
/// Quote mint seed.
69+
quote,
70+
/// Bump seed from `sol_try_find_program_address`.
71+
bump,
72+
}
5173
}
74+
// endregion: signer_seeds_example
75+
76+
constant_group! {
77+
#[prefix("RM")]
78+
#[inject("market/register")]
79+
#[frame(RegisterMarketFrame)]
80+
register_market_frame {
81+
/// PDA signer seeds.
82+
PDA_SEEDS = signer_seeds!(pda_seeds),
83+
/// PDA address.
84+
PDA = offset!(pda),
85+
/// Bump seed.
86+
BUMP = offset!(bump),
87+
}
88+
}
89+
5290
// endregion: register_market_stack

macros/src/attrs.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ pub fn extract_attr_string(attrs: &[syn::Attribute], name: &str) -> Option<Strin
3232
None
3333
}
3434

35+
/// Extract a type path from an attribute like `#[name(Type)]`.
36+
pub fn extract_attr_path(attrs: &[syn::Attribute], name: &str) -> Option<syn::Path> {
37+
for attr in attrs {
38+
if attr.path().is_ident(name)
39+
&& let Meta::List(list) = &attr.meta
40+
{
41+
let path: syn::Path = syn::parse2(list.tokens.clone()).ok()?;
42+
return Some(path);
43+
}
44+
}
45+
None
46+
}
47+
3548
/// Extract `#[inject("target")]` from attributes.
3649
pub fn extract_inject_target(attrs: &[syn::Attribute]) -> Option<String> {
3750
extract_attr_string(attrs, "inject")
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use quote::quote;
2+
use syn::Ident;
3+
4+
use crate::codegen;
5+
6+
/// Expand `immediate!(expr)` into a usize constant with i32 range check.
7+
pub fn expand_immediate(
8+
base_name: &Ident,
9+
asm_name: &str,
10+
doc: &str,
11+
expr: &syn::Expr,
12+
) -> (proc_macro2::TokenStream, Ident) {
13+
let rust_name = base_name.clone();
14+
let meta_ident = codegen::meta_ident(asm_name, base_name.span());
15+
16+
let meta = codegen::immediate_meta(&meta_ident, asm_name, doc, quote! { #rust_name as i32 });
17+
18+
let def = quote! {
19+
#[doc = #doc]
20+
pub const #rust_name: usize = {
21+
use super::*;
22+
const VALUE: usize = #expr;
23+
const _: () = assert!(
24+
VALUE <= i32::MAX as usize,
25+
"immediate must fit in i32",
26+
);
27+
VALUE
28+
};
29+
30+
#meta
31+
};
32+
33+
(def, meta_ident)
34+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
mod immediate;
2+
mod offset;
3+
mod signer_seeds;
4+
5+
use super::ConstantKind;
6+
use super::parse::ConstantGroupInput;
7+
use crate::codegen;
8+
9+
/// Expand a parsed `ConstantGroupInput` into a module with constants and a GROUP.
10+
pub fn expand(input: &ConstantGroupInput) -> proc_macro2::TokenStream {
11+
let mut const_defs = Vec::new();
12+
let mut meta_idents = Vec::new();
13+
14+
for c in &input.constants {
15+
let doc = &c.doc;
16+
let base_name = &c.name;
17+
18+
let asm_name = match &input.prefix {
19+
Some(p) => format!("{}_{}", p, base_name),
20+
None => base_name.to_string(),
21+
};
22+
23+
match &c.kind {
24+
ConstantKind::Offset { negate, expr } => {
25+
let (def, meta) = offset::expand_offset(base_name, &asm_name, doc, *negate, expr);
26+
const_defs.push(def);
27+
meta_idents.push(meta);
28+
}
29+
ConstantKind::FrameOffset { fields } => {
30+
let frame_ty = input
31+
.frame_type
32+
.as_ref()
33+
.expect("frame_type must be set for FrameOffset");
34+
let (def, meta) =
35+
offset::expand_frame_offset(base_name, &asm_name, doc, frame_ty, fields);
36+
const_defs.push(def);
37+
meta_idents.push(meta);
38+
}
39+
ConstantKind::SignerSeeds {
40+
parent_field,
41+
seeds,
42+
} => {
43+
let frame_ty = input
44+
.frame_type
45+
.as_ref()
46+
.expect("frame_type must be set for SignerSeeds");
47+
signer_seeds::expand_signer_seeds(
48+
&asm_name,
49+
frame_ty,
50+
parent_field,
51+
seeds,
52+
&mut const_defs,
53+
&mut meta_idents,
54+
);
55+
}
56+
ConstantKind::Immediate { expr } => {
57+
let (def, meta) = immediate::expand_immediate(base_name, &asm_name, doc, expr);
58+
const_defs.push(def);
59+
meta_idents.push(meta);
60+
}
61+
};
62+
}
63+
64+
codegen::group_module(
65+
&input.mod_name,
66+
&input.target,
67+
&input.doc,
68+
&const_defs,
69+
&meta_idents,
70+
)
71+
}
Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,8 @@
11
use quote::quote;
22
use syn::Ident;
33

4-
use super::{ConstantKind, parse::ConstantGroupInput};
54
use crate::codegen;
65

7-
/// Expand a parsed `ConstantGroupInput` into a module with constants and a GROUP.
8-
pub fn expand(input: &ConstantGroupInput) -> proc_macro2::TokenStream {
9-
let mut const_defs = Vec::new();
10-
let mut meta_idents = Vec::new();
11-
12-
for c in &input.constants {
13-
let doc = &c.doc;
14-
let base_name = &c.name;
15-
16-
let asm_name = match &input.prefix {
17-
Some(p) => format!("{}_{}", p, base_name),
18-
None => base_name.to_string(),
19-
};
20-
21-
let (def, meta_ident) = match &c.kind {
22-
ConstantKind::Offset { negate, expr } => {
23-
expand_offset(base_name, &asm_name, doc, *negate, expr)
24-
}
25-
ConstantKind::Immediate { expr } => expand_immediate(base_name, &asm_name, doc, expr),
26-
};
27-
28-
const_defs.push(def);
29-
meta_idents.push(meta_ident);
30-
}
31-
32-
codegen::group_module(
33-
&input.mod_name,
34-
&input.target,
35-
&input.doc,
36-
&const_defs,
37-
&meta_idents,
38-
)
39-
}
40-
416
/// Try to decompose a field-access chain like `Foo.bar.baz` into `(Foo, [bar, baz])`.
427
fn try_decompose_field_chain(expr: &syn::Expr) -> Option<(syn::Path, Vec<&syn::Member>)> {
438
let mut fields = Vec::new();
@@ -57,7 +22,8 @@ fn try_decompose_field_chain(expr: &syn::Expr) -> Option<(syn::Path, Vec<&syn::M
5722
}
5823
}
5924

60-
fn expand_offset(
25+
/// Expand `offset!(expr)` or `offset!(-expr)` into an i16 offset constant.
26+
pub fn expand_offset(
6127
base_name: &Ident,
6228
asm_name: &str,
6329
doc: &str,
@@ -100,31 +66,52 @@ fn expand_offset(
10066
(def, meta_ident)
10167
}
10268

103-
fn expand_immediate(
104-
base_name: &Ident,
69+
/// Emit a single frame-relative offset constant with i16 range and alignment
70+
/// assertions. Used by both `expand_frame_offset` and `expand_signer_seeds`.
71+
pub fn emit_frame_offset_const(
72+
rust_name: &Ident,
10573
asm_name: &str,
10674
doc: &str,
107-
expr: &syn::Expr,
75+
frame_ty: &syn::Path,
76+
field_chain: proc_macro2::TokenStream,
10877
) -> (proc_macro2::TokenStream, Ident) {
109-
let rust_name = base_name.clone();
110-
let meta_ident = codegen::meta_ident(asm_name, base_name.span());
111-
112-
let meta = codegen::immediate_meta(&meta_ident, asm_name, doc, quote! { #rust_name as i32 });
78+
let meta_ident = codegen::meta_ident(asm_name, rust_name.span());
79+
let meta = codegen::offset_meta(&meta_ident, asm_name, doc, rust_name);
11380

11481
let def = quote! {
11582
#[doc = #doc]
116-
pub const #rust_name: usize = {
83+
pub const #rust_name: i16 = {
11784
use super::*;
118-
const VALUE: usize = #expr;
85+
const VALUE: i64 =
86+
core::mem::offset_of!(#frame_ty, #field_chain) as i64
87+
- core::mem::size_of::<#frame_ty>() as i64;
88+
const _: () = assert!(
89+
VALUE >= i16::MIN as i64 && VALUE <= i16::MAX as i64,
90+
"frame offset must fit in i16",
91+
);
11992
const _: () = assert!(
120-
VALUE <= i32::MAX as usize,
121-
"immediate must fit in i32",
93+
VALUE % 8 == 0,
94+
"frame offset must be aligned to BPF_ALIGN_OF_U128",
12295
);
123-
VALUE
96+
VALUE as i16
12497
};
12598

12699
#meta
127100
};
128101

129102
(def, meta_ident)
130103
}
104+
105+
/// Expand `offset!(field)` inside a `#[frame(Type)]` group.
106+
pub fn expand_frame_offset(
107+
base_name: &Ident,
108+
asm_name: &str,
109+
doc: &str,
110+
frame_ty: &syn::Path,
111+
fields: &[syn::Member],
112+
) -> (proc_macro2::TokenStream, Ident) {
113+
let rust_name = Ident::new(&format!("{}_OFF", base_name), base_name.span());
114+
let asm_name = format!("{}_OFF", asm_name);
115+
let field_chain = quote! { #(#fields).* };
116+
emit_frame_offset_const(&rust_name, &asm_name, doc, frame_ty, field_chain)
117+
}

0 commit comments

Comments
 (0)