dynasm-rs: writing machine code inside a Rust macro
A dynasm-like tool for rust.
At a glance
- What is it?
- dynasm-rs is a dynamic assembler for Rust that lets you write the assembly to be generated inline, in nasm-like syntax, inside a macro, with the errors caught at compile time and the output expanded into a series of Vec pushes. It covers x86 and x64, aarch64 and RISC-V, and is a proc macro, so nothing outside the Rust toolchain is needed.
- Who is it for?
- Adopt dynasm-rs if you are writing a JIT, an interpreter or a code generator in Rust and want the assembly inline in the source rather than in a separate file assembled by another tool, because the macro catches most mistakes at compile time and the crate works on stable rustc from 1.77 onwards.
- Can I use it commercially?
- Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 92 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
An assembler that is a macro, not a tool
dynasm-rs describes itself as a dynamic assembler written in Rust for Rust, and the purpose it states is to ease the creation of programs that require run-time assembling.
The design decision that makes it different from a traditional assembler is in the feature list. It is fully integrated in the Rust toolchain, with no other tools necessary. The assembly is written inline in nasm-like syntax using a simple macro, and the errors are almost all diagnosed at compile time in a clear fashion.
Compare that with the project it is inspired by. The README says it is heavily inspired by Dynasm. dynasm-rs carries that approach into the Rust toolchain, so the assembly sits in the file it belongs to, and a mistyped register is a compiler error rather than a runtime surprise.
The implementation is a procedural macro rather than a build script, which is why the README carries a note about how the project used to work. It is compatible with stable rustc 1.77 and higher, and the crate is published on crates.io as dynasm, with the runtime part used as dynasmrt in the example. The documentation lives at the project's own site, with the language guide at censoredusername.github.io/dynasm-rs, and there is a release notes document in the repository.
The licence is MPL-2.0, and the README records the copyright as 2016 CensoredUsername, which is a useful date to keep in mind, because the project is older than its current form suggests.
The macro syntax, in the README's own example
The example in the README is a complete program, and its shape is worth reading closely.
The imports come first, and they tell you the two halves of the design:
use dynasmrt::{dynasm, DynasmApi, DynasmLabelApi};The macro is dynasm, and the traits DynasmApi and DynasmLabelApi are what give the assembler object its methods and its label handling. Then the assembler is created for a specific architecture:
let mut ops = dynasmrt::x64::Assembler::new().unwrap();The x64 module is selected by path rather than by a generic parameter, so the architecture is part of the type, and the Result is unwrapped at creation.
The first macro invocation emits a data blob and a label:
dynasm!(ops
; .arch x64
; ->hello:
; .bytes string.as_bytes()
);The .arch directive states the instruction set, the arrow introduces a label, and .bytes emits the raw bytes of a string. The second invocation then uses that label as an address:
; lea rcx, [->hello]which is the whole point of the label syntax. The label is a value that the macro resolves, so the address of the data is computed at compile time and can be used as a memory operand in the generated code.
The example then calls ops.offset() to record where the entry point is, calls ops.finalize() to get the finished buffer, and transmutes a pointer into that buffer into an extern "win64" function pointer. It is a Windows-specific example, calling a function that writes to stdout through the standard Windows calling convention, and it asserts the call returns true.
What the macro expands into
The feature list describes the expansion in one line: the assembly is optimized into a series of Vec.push and Vec.extend statements.
That is the whole trick, and it is worth being precise about it. The macro does not emit an object file, does not call an assembler and does not leave a binary blob in the repository. It expands, inside your function, into Rust statements that push the encoded instruction bytes into a vector. The runtime crate provides the assembler object and the encoding, and the macro provides the parsing, the label resolution and the checks.
The consequences are visible in the example. There is no separate build step to configure, no tool to install and no path to point at, which is what the fully integrated in the Rust toolchain claim means in practice. There is also no intermediate representation on disk, so a build cache that watches the source tree sees the assembly change because the source changed.
The label resolution is the part that is genuinely nicer than an external assembler. Because the labels live in the same macro invocation as the code that refers to them, the compiler can type them, and a reference to a label that was never defined is caught before the program runs. Combined with the almost all diagnosed at compile time claim, the practical difference is that the failure mode of a mistake is a build error rather than a segfault in a JIT.
The runtime half is what the assembler object and finalize belong to. The generated code ends up as a buffer, and a buffer can be executed, inspected, or handed to a JIT that is not Rust at all, which is why the crate is used by projects that embed a foreign virtual machine.
Three architectures, and the sponsor behind two of them
The architecture support section is specific about what is covered and what is not, which is the right way to write it.
For x86 and x64, the assembler supports the instruction sets in long and protected mode, with every AMD, Intel and VIA extension except for AVX-512. That exception is the one to note, because AVX-512 is the newest big extension on that family and a project targeting a modern server chip may need it.
For aarch64, the support goes up to ARMv8.4 except for the SVE instructions, and the README says the development of this assembler backend has been generously sponsored by Wasmer.
For RISC-V, both riscv32 and riscv64 are supported, with many extensions, and that development was sponsored by Wasmer as well.
The sponsorship detail matters more than it might look. A dynamic assembler is a large amount of work per architecture, because each instruction set has its own encodings, its own registers, its own addressing modes and its own set of corner cases, and each of those has to be encoded correctly or the generated code will be silently wrong. A company whose business is running untrusted code at speed has a direct interest in that being right, and funding two of the three backends is a reasonable explanation for why a Rust hobby project covers architectures that are otherwise niche.
The x86 backend predates the others, which is consistent with a project whose README records the copyright as 2016 and which grew outward as sponsorship appeared.
From a compiler plugin to a procedural macro
The last section of the README is a historical note, and it explains a lot about the project.
It lists the guaranteed to be working compiler versions, and it opens by saying that this project used to be a compiler plugin, so for old compilers here is a list of which version of dynasm was guaranteed to work with which compiler: v0.2.0 with rustc 1.27.0-nightly from April 2018, v0.2.1 with rustc 1.28.0-nightly from May 2018, and v0.2.3 with rustc 1.31.0-nightly from October 2018.
Then the punchline: as the project has since transitioned to be a proc macro, this is not relevant for modern versions of the compiler.
That transition is the single most important design change in the project's history, and it is the reason the current requirement is a plain stable rustc from 1.77 rather than a nightly with a specific feature gate. A compiler plugin had to be rebuilt whenever the compiler's internal API changed, which is why the list of guaranteed versions is a list of nightlies from 2018. A procedural macro is an ordinary crate, so the only thing that has to track the compiler is the language edition.
The repository still shows the seams. The top-level listing has a plugin/ directory beside runtime/ and testing/, and the Cargo.toml workspace members include plugin and runtime and testing, plus the documentation crates. So the plugin is still in the tree, even though the modern path does not go through it.
The workspace metadata also shows the project's own hygiene. The version is 5.1.0, the edition is 2021, the licence is MPL-2.0, and the manifest carries the MPL Exhibit A source code form notice with a comment that it applies to all source files within the workspace, which is the notice that makes the file-level copyleft work as intended across several crates.
The workspace layout, and what the examples show
The top-level listing is the map of the project: .github/ for the workflows, a .gitmodules file, a .travis.yml from the earlier CI era, Cargo.toml, LICENSE, README.md, build_docs.sh, and the doc/, plugin/, runtime/, testing/ and tools/ directories.
The workspace members in Cargo.toml are more informative than the directory names, because they show the whole development setup rather than just the shipped crates. Besides plugin, runtime and testing, there are three documentation crates: doc/insref, and doc/examples/bf-interpreter, doc/examples/bf-jit and doc/examples/hello-world.
Those three examples are the clearest statement of what the project is for. A Brainfuck interpreter and a Brainfuck JIT are the two things you write when you want to test a dynamic assembler, because a Brainfuck JIT is a small program with a lot of instruction selection in it, and hello-world is the smallest case that produces executable code. A library that ships those as compiled examples has made the effort to keep working examples that actually run.
The tools/ directory and build_docs.sh are the support machinery, and the .gitmodules file is there because the documentation or the tooling pulls in something from another repository.
On maintenance, the last push was on 2026-07-22, the repository is not archived, and the project publishes no GitHub releases, which is consistent with a crate that ships to crates.io instead. The version in the workspace is 5.1.0, and the version history is in the release notes document linked from the README rather than in the repository root.
Editorial conclusion
Adopt dynasm-rs if you are writing a JIT, an interpreter or a code generator in Rust and want the assembly inline in the source rather than in a separate file assembled by another tool, because the macro catches most mistakes at compile time and the crate works on stable rustc from 1.77 onwards. Do not adopt it if you cannot accept architecture-specific code in your tree or you are on a toolchain older than 1.77, and do not expect it to remove the hard part, since writing correct instruction sequences is still your responsibility. Before you commit, read the architecture support section and check the instruction set you need against the exclusions, since AVX-512 and SVE are not covered, and look at the examples under doc/examples, which are the quickest way to see what the macro actually expands to.
Frequently asked questions
What is dynasm-rs?
A dynamic assembler written in Rust for Rust. You write the assembly to be generated inline in nasm-like syntax inside a macro, the assembly is optimised into a series of Vec.push and Vec.extend statements, and almost all errors are diagnosed at compile time. It is compatible with stable rustc 1.77 and higher and is published on crates.io as dynasm.
Which architectures does dynasm-rs support?
x86 and x64 in long and protected mode, with every AMD, Intel and VIA extension except AVX-512; aarch64 up to ARMv8.4 except for the SVE instructions; and riscv32 and riscv64 with many extensions. The aarch64 and RISC-V backends were sponsored by Wasmer.
Do I need an external assembler to use dynasm-rs?
No. The feature list says it is fully integrated in the Rust toolchain with no other tools necessary, because the macro expands into Rust statements that push the encoded bytes into a vector rather than producing a separate object or blob. The runtime part, used as dynasmrt, supplies the assembler and the finalise step that returns the buffer.
How does dynasm-rs handle labels between instructions?
Labels are introduced with an arrow inside the macro, such as ->hello, and referred to with the same syntax, for example lea rcx, [->hello]. Because the label and the code that uses it live in the same macro invocation, the compiler can resolve and check them, which is what lets the project claim that errors are almost all diagnosed at compile time.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/censoredusername-dynasm-rs)Was this useful?