CLI tool
hlorenzi/customasm avatar
hlorenzi/customasm

customasm assembles for instruction sets you define yourself

💻 An assembler for custom, user-defined instruction sets! https://hlorenzi.github.io/customasm/web/

1,067 stars73 forksRustApache-2.0

At a glance

What is it?
customasm is an Apache-2.0 licensed Rust assembler that reads a #ruledef block describing an instruction set and compiles assembly source against it, which makes it a tool for testing a new virtual machine's bytecode or for writing programs for a processor you implemented in an FPGA. It ships a browser version, a VSCode extension and a NES example.
Who is it for?
customasm fits a developer who has just designed an instruction set, whether for a virtual machine, a bytecode format or an FPGA processor, and who needs to write and debug real assembly programs against it without writing a full assembler first. It does not fit a team whose target is a mainstream architecture, since an existing assembler already handles those, and it does not fit a project that needs a production code generator with a maintained toolchain.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 46 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

The rule definition is a pattern language, not an opcode table

The distinguishing feature of customasm is the #ruledef directive, and the example in the README shows the whole mechanism in about twenty lines.

asm
#ruledef
{
    load r1, {value: i8} => 0x11 @ value
    load r2, {value: i8} => 0x12 @ value
    load r3, {value: i8} => 0x13 @ value
    add  r1, r2          => 0x21
    sub  r3, {value: i8} => 0x33 @ value
    jnz  {address: u16}  => 0x40 @ address
    ret                  => 0x50
}

The left side of each rule is the shape of the instruction, with register names, an immediate placeholder or a typed value. The right side is the encoding, a literal byte followed by an @ that names where a placeholder is placed. So 0x11 @ value means the opcode 0x11 followed by the bytes of the value, and 0x40 @ address means the opcode followed by a two byte address.

The type annotation matters, {value: i8} for a signed byte and {address: u16} for an unsigned 16 bit value, because it fixes the size of the immediate, and therefore where the next instruction starts. That is the piece a hand written assembler usually gets wrong, and having the type in the rule means the layout is derived rather than remembered.

The rules are not a table but a set of patterns, and the assembly is matched against them. A register operand matches a register pattern, an immediate matches a typed value, and a label reference matches the address pattern.

The output shows what the assembler produces, with the address, the segment and the data in base 16, and the comments carrying the original source.

asm
 outp | addr | data (base 16)

  0:0 |    0 |          ; multiply3x4:
  0:0 |    0 | 11 00    ; load r1, 0
  2:0 |    2 | 12 03    ; load r2, 3
  4:0 |    4 | 13 04    ; load r3, 4
  6:0 |    6 |          ; .loop:
  6:0 |    6 | 21       ; add r1, r2
  7:0 |    7 | 33 01    ; sub r3, 1
  9:0 |    9 | 40 00 06 ; jnz .loop
  c:0 |    c | 50       ; ret

Note the jnz encoding, 40 00 06, which is the opcode followed by the address of the loop label in little endian, and the fact that the load with a zero immediate is two bytes while the add is one, so the assembler is computing instruction lengths from the rules rather than from a fixed table.

The use case is a machine that does not exist yet

The README gives the two motivating cases and they are both about machines that are not shipping products.

It can be useful, for example, if you're trying to test the bytecode of a new virtual machine, or if you're eager to write programs for that new microprocessor architecture you just implemented in an FPGA chip.

That framing explains the whole design. An assembler for a real architecture, x86 or ARM, has a specification, a reference manual and twenty years of edge cases to accommodate. An assembler for an instruction set you designed last week has a definition you wrote down, and the tool's job is to turn that definition into something that can assemble a program immediately.

The consequence is that the rule definition language is the interface. If it can express your encoding, the tool is useful, and if it cannot, nothing else matters. The examples in the repository show the range. There is examples/basic.asm, examples/gameboy_hello_world.asm which targets a real architecture, and examples/nes_colors.asm which the README links as an example project targeting the NES, plus examples/ibmpc1.bin which is a prebuilt binary rather than source.

The NES example is the interesting one, because a NES program needs to live at a fixed address in a banked memory map, and the addressing is not a flat byte range. The presence of a segment in the output, the addr column showing values like 0:0 and 2:0, suggests the assembler supports a segmented address space, which is exactly what the NES and the Game Boy both need and what a flat assembler does not give you.

The second motivating case, an FPGA processor, has a different constraint. When the instruction set is yours, the encoding is yours too, so you would normally write the encoder by hand for each instruction. A pattern matching assembler takes that work and turns it into a declaration, which is a much better place for the specification to live.

One crate, a library, a binary and a WebAssembly build

The Cargo.toml is short and it tells you what the project produces.

The package is customasm at version 0.14.2, on Rust edition 2024, with a build script at src/build.rs, and it is published to crates.io with the Apache-2.0 licence.

The interesting part is the crate type declaration.

toml
[lib]
crate-type = ["lib", "cdylib"]
name = "customasm"
path = "src/lib.rs"

[[bin]]
name = "customasm"
path = "src/main.rs"

A cdylib alongside a normal lib is what makes the WebAssembly build possible, because a cdylib is what wasm-pack and similar tools consume. That is what the build_wasm.ps1 script at the repository root is for, and the README links to a browser version at hlorenzi.github.io/customasm/web/ with the invitation to try it right now in a web browser.

A browser build of an assembler is genuinely useful for a project like this, because the person most likely to want it is someone designing an instruction set, and they can paste a rule definition and a program into a page without installing anything. It also means the tool reaches people who are not Rust users, which is a fair description of the audience for an assembler for a new virtual machine.

The runtime dependencies are two, getopts for the command line options and num-bigint for arbitrary precision integers, which is a sensible choice for an assembler that has to handle wide immediates and address arithmetic without overflow. The dev dependency is sha2, which is for the test suite, presumably to hash output binaries so the tests can compare against known values. The tests directory is in the repository and cargo test is the command to run them.

The library target also means customasm can be embedded in another program, which matters if you are building a toolchain that includes it.

The web build and the Windows scripts are maintained by hand

The repository top level shows a project with several build targets and a few manual steps that are worth knowing about.

code
build_ghpages.ps1
build_wasm.ps1
mkpkg_win64.ps1
run_local_server.bat

Four PowerShell and batch scripts for the non-Rust parts of the build. build_wasm.ps1 builds the WebAssembly version, build_ghpages.ps1 builds and publishes the web site to GitHub Pages, mkpkg_win64.ps1 packages a Windows 64 bit binary, and run_local_server.bat serves the web version locally. There is a .gitignore.ghpages file for the Pages deployment and the Cargo.toml excludes web/ from the published package, so the site sources do not bloat the crate.

The reason these are PowerShell rather than a cargo xtask or a Makefile is probably that the author develops on Windows, and the Windows 64 bit packaging in particular is a step that a Unix CI runner would not do naturally.

The other thing in the top level listing is the std/ directory, which is the standard library for the assembler itself, meaning the shipped definitions of common things, the directives and possibly a standard rule set for a default architecture, shipped alongside the tool. The web/ directory holds the browser application, and examples/ holds the sample programs.

For a user, the install options are straightforward. You can install directly from crates.io by running cargo install customasm, and the customasm application should automatically become available in your command-line environment. Pre-built executables are on the releases page, and you can compile from source by cloning the repository and running cargo build.

There is also a VSCode syntax highlight extension on the marketplace, which is the kind of small thing that makes an assembler pleasant to use day to day.

Where customasm is the wrong tool

The first wrong case is a mainstream architecture. If you are writing x86, ARM or RISC-V assembly, you have assemblers that are decades old and handle every corner of the specification, and customasm would be a worse version of a solved problem. The tool is for architectures that do not have an assembler yet, and the example projects targeting the NES and the Game Boy are demonstrations of what it can do rather than a recommendation to replace a mature toolchain.

The second wrong case is a production code generator. If you are shipping an assembler as part of a product, a compiler backend or a toolchain, you need versioned releases, a stable rule definition syntax, a specification document, and a compatibility promise. The release history, v0.14.0 on 2026-04-12, v0.14.1 on 2026-04-13 and v0.14.2 on 2026-09-07, shows active development, but a pre-1.0 version with a rule language that is defined by a wiki is not a dependency you would build a shipped product on.

The third wrong case is an architecture whose encoding does not fit the pattern model. The rules express an instruction as a sequence of literals and placeholders, which suits a fixed layout instruction set. A VLIW or a variable length instruction where the encoding depends on the values of the operands, a compressed encoding where one instruction has several forms, or an instruction set with a descriptor byte that selects among variants, is harder to express. The examples all use simple, regular encodings.

The fourth wrong case is a machine with more than 64 bits of addressable state, since the addressing model is built around segments and offsets rather than an unbounded address space.

How it differs from a conventional assembler

The comparison worth making is with nasm, gas or an LLVM assembler, and the difference in approach is that the instruction set is data rather than code.

A conventional assembler has the architecture compiled into it. The encoding of each mnemonic is a function in the source, the operand types are known, the relocation rules are known, and the directives are known. That is why it is fast and why it is specific. It also means adding an instruction means recompiling the assembler.

customasm has none of that compiled in. The instruction set arrives as the #ruledef block in the same file as the program, which means the assembler is a general function over rule sets, and the same binary handles a new architecture the moment you describe it. The rules are matched against the source, and the encoding falls out of the match.

The second difference is where the specification lives. For a conventional architecture the specification is a document written by the vendor, and the assembler is an implementation of it that may lag or differ. Here the specification is the rule block, and the rule block is in the same file as the program, so the encoding and the code cannot disagree.

The third difference is the toolchain position. A conventional assembler sits at the end of a large ecosystem with debuggers, linkers, disassemblers and profilers. customasm is a single binary with a library target, a browser build and a syntax highlighting extension. It is the right size for the job it is meant to do, which is a personal tool for a machine you are designing, and it would be the wrong foundation for a production toolchain.

The fourth difference is output. A conventional assembler produces an object file with symbols and relocations. The output shown in the README is a listing with address, segment and data, which is what you want when you are looking at a byte encoding, and less what you want when you are linking a program.

Editorial conclusion

customasm fits a developer who has just designed an instruction set, whether for a virtual machine, a bytecode format or an FPGA processor, and who needs to write and debug real assembly programs against it without writing a full assembler first. It does not fit a team whose target is a mainstream architecture, since an existing assembler already handles those, and it does not fit a project that needs a production code generator with a maintained toolchain. Before adopting it, check the rule definition language against your instruction set, since the encoding is expressed as patterns and literals rather than as a table, and note that the current release is v0.14.2 from 2026-09-07 with the last push on the same day, on Rust edition 2024.

Frequently asked questions

How do I install customasm?

Install it from crates.io with cargo install customasm, after which the customasm application becomes available on your command line. You can also download pre-built executables from the releases section, or clone the repository and run cargo build, with cargo test available for the test suite.

What is a #ruledef block in customasm?

It is the directive that defines your instruction set. Each rule pairs the shape of an instruction on the left, with register names, typed placeholders such as {value: i8} or {address: u16}, and an encoding on the right, a literal byte followed by an @ naming where the placeholder goes. The assembler matches the source against these rules to produce the binary output.

Can I use customasm in a browser?

Yes, there is a WebAssembly build at hlorenzi.github.io/customasm/web/ that the README invites you to try in your browser. The Cargo.toml declares the library with crate-type lib and cdylib, which is what makes the wasm build possible, and build_wasm.ps1 at the repository root drives it.

Does customasm have example projects?

The examples directory holds basic.asm, gameboy_hello_world.asm and nes_colors.asm, and the README points at the NES one as an example project targeting the NES. There is also an ibmpc1.bin binary in the examples.

What license is customasm released under?

Apache-2.0, declared in the Cargo.toml with a LICENSE.txt file in the repository. The current release is v0.14.2 from 2026-09-07, the crate is on Rust edition 2024, and its only runtime dependencies are getopts and num-bigint.

Official sources

  1. hlorenzi/customasm on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/hlorenzi-customasm.svg)](https://hysenlabs.com/projects/hlorenzi-customasm)

Was this useful?