Skip to main content

asm_rs_macros/
lib.rs

1//! Compile-time assembly proc-macros for [`asm-rs`](https://crates.io/crates/asm-rs).
2//!
3//! Provides the [`asm_bytes!`] macro that assembles source text at compile time,
4//! producing a `&'static [u8]` constant with zero runtime overhead.
5//!
6//! # Usage
7//!
8//! ```rust,ignore
9//! use asm_rs_macros::asm_bytes;
10//!
11//! // x86-64 shellcode assembled at compile time
12//! const SHELLCODE: &[u8] = asm_bytes!(x86_64, "mov rax, 1\nret");
13//!
14//! // AArch64
15//! const A64_CODE: &[u8] = asm_bytes!(aarch64, "add x0, x1, x2\nret");
16//!
17//! // ARM32
18//! const ARM_CODE: &[u8] = asm_bytes!(arm, "add r0, r1, r2\nbx lr");
19//!
20//! // RISC-V 64-bit
21//! const RV_CODE: &[u8] = asm_bytes!(rv64, "add a0, a1, a2\nret");
22//! ```
23
24use proc_macro::TokenStream;
25
26/// Assemble source text at compile time, producing a `&'static [u8]` byte slice.
27///
28/// # Syntax
29///
30/// ```rust,ignore
31/// asm_bytes!(ARCH, "assembly source")
32/// ```
33///
34/// where `ARCH` is one of: `x86`, `x86_64`, `arm`, `thumb`, `aarch64`, `rv32`, `rv64`.
35///
36/// # Examples
37///
38/// ```rust,ignore
39/// use asm_rs_macros::asm_bytes;
40///
41/// // Single instruction
42/// const NOP: &[u8] = asm_bytes!(x86_64, "nop");
43/// assert_eq!(NOP, &[0x90]);
44///
45/// // Multi-instruction with labels
46/// const CODE: &[u8] = asm_bytes!(x86_64, "
47///     start:
48///         xor eax, eax
49///         inc eax
50///         ret
51/// ");
52///
53/// // With base address
54/// const BASED: &[u8] = asm_bytes!(x86_64, 0x400000, "mov rax, 1\nret");
55/// ```
56///
57/// # Compile-time errors
58///
59/// If the assembly source contains errors, the macro emits a compile-time error
60/// with the full `AsmError` diagnostic message.
61#[proc_macro]
62pub fn asm_bytes(input: TokenStream) -> TokenStream {
63    match asm_bytes_impl(input) {
64        Ok(ts) => ts,
65        Err(err) => err.into_compile_error(),
66    }
67}
68
69/// Assemble source text at compile time, producing a fixed-size array `[u8; N]`.
70///
71/// Unlike [`asm_bytes!`] which returns `&'static [u8]`, this macro returns a
72/// `[u8; N]` value that can be used where a fixed-size array is needed.
73///
74/// # Syntax
75///
76/// ```rust,ignore
77/// asm_array!(ARCH, "assembly source")
78/// ```
79///
80/// # Examples
81///
82/// ```rust,ignore
83/// use asm_rs_macros::asm_array;
84///
85/// const NOP: [u8; 1] = asm_array!(x86_64, "nop");
86/// const RET: [u8; 1] = asm_array!(x86_64, "ret");
87/// ```
88#[proc_macro]
89pub fn asm_array(input: TokenStream) -> TokenStream {
90    match asm_array_impl(input) {
91        Ok(ts) => ts,
92        Err(err) => err.into_compile_error(),
93    }
94}
95
96// ─── Implementation ─────────────────────────────────────────────────────────
97
98struct MacroInput {
99    arch: asm_rs::Arch,
100    base_addr: u64,
101    source: String,
102    /// Span of the source literal for error reporting.
103    source_span: proc_macro::Span,
104}
105
106fn parse_input(input: TokenStream) -> Result<MacroInput, syn_free::Error> {
107    let mut tokens = input.into_iter().peekable();
108
109    // 1. Parse architecture identifier
110    let arch_tt = tokens.next().ok_or_else(|| {
111        syn_free::Error::new(
112            "expected architecture identifier (x86_64, aarch64, arm, thumb, rv32, rv64)",
113        )
114    })?;
115    let arch = parse_arch(&arch_tt)?;
116
117    // 2. Expect comma
118    expect_comma(&mut tokens)?;
119
120    // 3. Optional base address (integer literal followed by comma)
121    let base_addr;
122    let source;
123    let source_span;
124
125    if let Some(tt) = tokens.peek() {
126        if is_integer_literal(tt) {
127            let tt = tokens.next().unwrap();
128            base_addr = parse_integer_literal(&tt)?;
129            expect_comma(&mut tokens)?;
130            let (src, span) = parse_string_literal(&mut tokens)?;
131            source = src;
132            source_span = span;
133        } else {
134            base_addr = 0;
135            let (src, span) = parse_string_literal(&mut tokens)?;
136            source = src;
137            source_span = span;
138        }
139    } else {
140        return Err(syn_free::Error::new("expected assembly source string"));
141    }
142
143    // Ensure no trailing tokens
144    if tokens.next().is_some() {
145        return Err(syn_free::Error::new(
146            "unexpected extra tokens after source string",
147        ));
148    }
149
150    Ok(MacroInput {
151        arch,
152        base_addr,
153        source,
154        source_span,
155    })
156}
157
158fn asm_bytes_impl(input: TokenStream) -> Result<TokenStream, syn_free::Error> {
159    let mi = parse_input(input)?;
160    let bytes = do_assemble(&mi)?;
161    Ok(bytes_to_slice_expr(&bytes))
162}
163
164fn asm_array_impl(input: TokenStream) -> Result<TokenStream, syn_free::Error> {
165    let mi = parse_input(input)?;
166    let bytes = do_assemble(&mi)?;
167    Ok(bytes_to_array_expr(&bytes))
168}
169
170fn do_assemble(mi: &MacroInput) -> Result<Vec<u8>, syn_free::Error> {
171    let result = if mi.base_addr != 0 {
172        asm_rs::assemble_at(&mi.source, mi.arch, mi.base_addr)
173    } else {
174        asm_rs::assemble(&mi.source, mi.arch)
175    };
176
177    result.map_err(|e| syn_free::Error::with_span(mi.source_span, &format!("assembly error: {e}")))
178}
179
180fn parse_arch(tt: &proc_macro::TokenTree) -> Result<asm_rs::Arch, syn_free::Error> {
181    let ident = match tt {
182        proc_macro::TokenTree::Ident(id) => id.to_string(),
183        _ => {
184            return Err(syn_free::Error::new(
185                "expected architecture identifier (x86, x86_64, aarch64, arm, thumb, rv32, rv64)",
186            ));
187        }
188    };
189    match ident.as_str() {
190        "x86" => Ok(asm_rs::Arch::X86),
191        "x86_64" => Ok(asm_rs::Arch::X86_64),
192        "arm" => Ok(asm_rs::Arch::Arm),
193        "thumb" => Ok(asm_rs::Arch::Thumb),
194        "aarch64" => Ok(asm_rs::Arch::Aarch64),
195        "rv32" => Ok(asm_rs::Arch::Rv32),
196        "rv64" => Ok(asm_rs::Arch::Rv64),
197        _ => Err(syn_free::Error::with_span(
198            tt.span(),
199            &format!(
200                "unknown architecture `{ident}`, expected: x86, x86_64, arm, thumb, aarch64, rv32, rv64"
201            ),
202        )),
203    }
204}
205
206fn expect_comma(
207    tokens: &mut std::iter::Peekable<proc_macro::token_stream::IntoIter>,
208) -> Result<(), syn_free::Error> {
209    match tokens.next() {
210        Some(proc_macro::TokenTree::Punct(p)) if p.as_char() == ',' => Ok(()),
211        Some(other) => Err(syn_free::Error::with_span(other.span(), "expected `,`")),
212        None => Err(syn_free::Error::new("expected `,`")),
213    }
214}
215
216fn is_integer_literal(tt: &proc_macro::TokenTree) -> bool {
217    matches!(tt, proc_macro::TokenTree::Literal(lit) if {
218        let s = lit.to_string();
219        s.starts_with(|c: char| c.is_ascii_digit())
220            && !s.starts_with('"')
221            && !s.starts_with('\'')
222    })
223}
224
225fn parse_integer_literal(tt: &proc_macro::TokenTree) -> Result<u64, syn_free::Error> {
226    let proc_macro::TokenTree::Literal(lit) = tt else {
227        return Err(syn_free::Error::with_span(
228            tt.span(),
229            "expected integer literal",
230        ));
231    };
232    let s = lit.to_string();
233    let val = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
234        u64::from_str_radix(hex, 16)
235    } else {
236        s.parse::<u64>()
237    };
238    val.map_err(|_| syn_free::Error::with_span(tt.span(), "invalid integer literal"))
239}
240
241fn parse_string_literal(
242    tokens: &mut std::iter::Peekable<proc_macro::token_stream::IntoIter>,
243) -> Result<(String, proc_macro::Span), syn_free::Error> {
244    let tt = tokens
245        .next()
246        .ok_or_else(|| syn_free::Error::new("expected string literal"))?;
247    let proc_macro::TokenTree::Literal(lit) = &tt else {
248        return Err(syn_free::Error::with_span(
249            tt.span(),
250            "expected string literal",
251        ));
252    };
253    let raw = lit.to_string();
254    // Strip quotes — handle both `"..."` and `r"..."` / `r#"..."#`
255    let content = if raw.starts_with("r#\"") {
256        raw.strip_prefix("r#\"")
257            .and_then(|s| s.strip_suffix("\"#"))
258            .ok_or_else(|| syn_free::Error::with_span(tt.span(), "malformed raw string"))?
259    } else if raw.starts_with("r\"") {
260        raw.strip_prefix("r\"")
261            .and_then(|s| s.strip_suffix('"'))
262            .ok_or_else(|| syn_free::Error::with_span(tt.span(), "malformed raw string"))?
263    } else if raw.starts_with('"') {
264        // Regular string — need to unescape
265        let inner = raw
266            .strip_prefix('"')
267            .and_then(|s| s.strip_suffix('"'))
268            .ok_or_else(|| syn_free::Error::with_span(tt.span(), "malformed string literal"))?;
269        return Ok((unescape_string(inner), tt.span()));
270    } else {
271        return Err(syn_free::Error::with_span(
272            tt.span(),
273            "expected string literal",
274        ));
275    };
276    Ok((content.to_string(), tt.span()))
277}
278
279fn unescape_string(s: &str) -> String {
280    let mut out = String::with_capacity(s.len());
281    let mut chars = s.chars();
282    while let Some(c) = chars.next() {
283        if c == '\\' {
284            match chars.next() {
285                Some('n') => out.push('\n'),
286                Some('r') => out.push('\r'),
287                Some('t') => out.push('\t'),
288                Some('\\') => out.push('\\'),
289                Some('"') => out.push('"'),
290                Some('0') => out.push('\0'),
291                Some(other) => {
292                    out.push('\\');
293                    out.push(other);
294                }
295                None => out.push('\\'),
296            }
297        } else {
298            out.push(c);
299        }
300    }
301    out
302}
303
304fn bytes_to_slice_expr(bytes: &[u8]) -> TokenStream {
305    let byte_strs: Vec<String> = bytes.iter().map(|b| format!("{b:#04X}u8")).collect();
306    let inner = byte_strs.join(", ");
307    let code = format!("{{ const BYTES: &[u8] = &[{inner}]; BYTES }}");
308    code.parse().expect("generated code should parse")
309}
310
311fn bytes_to_array_expr(bytes: &[u8]) -> TokenStream {
312    let len = bytes.len();
313    let byte_strs: Vec<String> = bytes.iter().map(|b| format!("{b:#04X}u8")).collect();
314    let inner = byte_strs.join(", ");
315    let code = format!("{{ const BYTES: [u8; {len}] = [{inner}]; BYTES }}");
316    code.parse().expect("generated code should parse")
317}
318
319// ─── Minimal syn-free error type ─────────────────────────────────────────────
320// We avoid the `syn` dependency entirely for fast compile times — the macro
321// input is simple enough to parse manually from `proc_macro::TokenStream`.
322
323mod syn_free {
324    use proc_macro::{Span, TokenStream};
325
326    pub struct Error {
327        message: String,
328        span: Option<Span>,
329    }
330
331    impl Error {
332        pub fn new(msg: &str) -> Self {
333            Self {
334                message: msg.to_string(),
335                span: None,
336            }
337        }
338
339        pub fn with_span(span: Span, msg: &str) -> Self {
340            Self {
341                message: msg.to_string(),
342                span: Some(span),
343            }
344        }
345
346        pub fn into_compile_error(self) -> TokenStream {
347            let msg = self.message.replace('"', "\\\"");
348            let code = format!("compile_error!(\"{msg}\")");
349            // Try to set the span for better diagnostics
350            if let Some(span) = self.span {
351                let ts: TokenStream = code.parse().unwrap();
352                ts.into_iter()
353                    .map(|mut tt| {
354                        tt.set_span(span);
355                        tt
356                    })
357                    .collect()
358            } else {
359                code.parse().unwrap()
360            }
361        }
362    }
363}