Expand description
This crate provides the asm_block!
macro for allowing composition
through Rust macro when writing inline assembly.
asm!
in Rust accepts a template string as input. While it automatically
add \n
between comma-separated strings, it relies solely on the assembler
macros to build composable assembly. While it is fine in most cases, it
becomes a problem when we want to use the same macro across functions.
§Motivation
Consider the following code using x86_64
assembly:
unsafe fn f() -> u64 {
let mut x = 20;
asm!(
".macro mad x, y",
" mul x, y",
" lea x, [x + y]",
".endm",
"mad {x}, 5",
x = inout(reg) x
);
x
}
If we want to reuse mad
in another function, we must copy the verbatim
of the macro and change its name. Otherwise we will encounter compilation
error due to name collision.
unsafe fn f() -> u64 {
let mut x = 20;
asm!(
".macro mad x, y",
" mul x, y",
" lea x, [x + y]",
".endm",
"mad {x}, 5",
x = inout(reg) x
);
x
}
unsafe fn g() -> u64 {
let mut x = 10;
asm!(
// Only compiles if we remove this macro definition
// or rename it to another name
".macro mad x, y",
" mul x, y",
" lea x, [x + y]",
".endm",
"mad {x}, 8",
x = inout(reg) x
);
x
}
The above code fails with
error: macro 'mad' is already defined
If we omit the definition of mad
in g()
, it will compile, but only
when g()
is emitted after f()
. It is unclear which function should house
the definition, so the only sane option is to house it in a global_asm!
code. But again, it is hard to guarantee that the definition is emitted
before the actual use.
It is natural to resort to Rust macro in this case, but due to the fact that
asm!
accepts a template string, substituting metavariables becomes
tedious.
macro_rules! mad {
($x: ident, $y: literal) => {
concat!(
"mul {", stringify!($x), "}, ", stringify!($y), "\n",
"lea {", stringify!($x), "}, [{", stringify!($x), "}+", stringify!($y), "]"
)
};
}
unsafe fn f() -> u64 {
let mut x = 20;
asm!(
mad!(x, 5),
x = inout(reg) x
);
x
}
This approach has some multiple drawbacks:
- The definition is very noisy, making it hard to read and comprehend. It is
much worse if the definition becomes longer, and much much worse if
rustfmt
attempts to format it. - It is easy to forget
,
and\n
when the definition becomes longer. mad!
can only accept a named register as the first argument and a literal as the second argument. We cannot callmad!(x, rbx)
ormad!([rax], rbp)
, which we would have been able to if we were using the assembler macro. Trying to fix this by changingident
andliteral
tott
is also problematic, sincestringify!({x})
becomes"{ x }"
, and it is an invalid placeholder.
This crate tries to address this by providing a macro that makes it easier to compose assembly code.
§Example
Instead of the code above, using asm_block!
, we are able to write the
following:
use asm_block::asm_block;
macro_rules! mad {
($x: tt, $y: tt) => {
asm_block! {
mul $x, $y;
lea $x, [$x + $y];
}
};
}
#[rustfmt::skip::macros(mad)]
unsafe fn f() -> u64 {
let mut x = 20;
asm!(
mad!({x}, 5),
x = inout(reg) x
);
x
}
Now we are able to make calls like mad!({x}, rbx)
, mad!([rax], rbp)
, and
mad!({x:e}, [rsp - 4])
.
§Limitations
- Due to the tokenization rule of Rust macro, strings enclosed by
'
are not supported. asm_block!
mostly consumes tokens one by one, so it is possible to run out of recursion limit if the assembly code is long. User needs#![recursion_limit = "<a_larger_value>"]
when encountering the error.rustfmt
will formatmad!({x}, 5)
intomad!({ x }, 5)
. While this won’t make any difference in the emitted assembly code, it is confusing to read when the user is expecting a format placeholder. User can use#[rustfmt::skip::macros(mad)]
to preventrustfmt
from formatting the interior ofmad!
calls.- Some assemblers use
;
as the comment starter, but we are using it as instruction delimeter, so assembly comments may not work properly. Users are strongly suggested to stick to Rust comments. tt
cannot capture multiple tokens, so to makemad!(dword ptr [rax], ebp)
possible, calling convention ofmad!
needs to be changed. For exampleButuse asm_block::asm_block; macro_rules! mad { ([{ $($x: tt)+ }], $y: tt) => { asm_block! { mul $($x)+, $y; lea $($x)+, [$($x)+ + $y]; } }; ($x: tt, $y: tt) => { mad!([{ $x }], $y) }; }
mad!
must be called withmad!([{ dword ptr [rax] }], ebp)
instead.- Currently we don’t have an escape hatch to manually inject assembly if the macro is not able to emit the correct assembly code.
§License
Dual licensed under the Apache 2.0 license and the MIT license.
Macros§
- asm_
block - Translate tokens to a string containing assembly.