How to make your own Godbolt (for C++ and Rust)
Tags: tutorial, assembly, cpp, rust
Published: 2023-08-28 16:14:58

## Introduction
I hope you are aware of the existence of [Compiler Explorer](https://godbolt.org/), a.k.a. Godbolt. It is an incredible tool for inspecting how your code gets translated to assembly, just one layer above machine code.

So, for example, if you want to check if range-based loops in C++ incur some additional runtime cost, you can check the assembly output. But what if you want to run that on your machine? I mean, you can for sure use Compiler Explorer [locally](https://github.com/compiler-explorer/compiler-explorer), but let's do it the fun way.


## Generating Assembly

First, I will assume you have some GNU Make experience, if not, check [this](https://makefiletutorial.com/) resource.
Let's start by creating a `src` directory and some C++ file.

Then, edit your file to something interesting!
```cpp
// src/func.cpp

#include <array>

int sum_manual(const std::array<int, 10>& arr) {
    int sum = 0;
    for (std::size_t i = 0; i < arr.size(); ++i)
        sum += arr[i];

    return sum;
}

int sum_range(const std::array<int, 10>& arr) {
    int sum = 0;
    for (int num: arr)
        sum += num;

    return sum;
}
```
<!-- Why syntax highlighting does that weird thing? -->


Then we will generate the assembly output using gcc. For this purpose and because writing the whole compile command is error prone, we create a Makefile, and then edit it.


```Makefile
# Makefile
CC := g++
CFLAGS := -O2 -std=c++20 \
		  -march=x86-64 -masm=intel -fno-stack-protector \
		  -fno-dwarf2-cfi-asm -fno-asynchronous-unwind-tables 

all: out/func.asm

out/func.asm: src/func.cpp
	@ mkdir -p out				# Create directory
	${CC} -S ${CFLAGS} $< -o $@ # Compile to assembly
	cat $@ | c++filt | tee $@ >/dev/null	# Make it pretty

.PHONY: clean
clean:
	rm -rf out
```

The flags `-masm=intel -fno-stack-protector -fno-dwarf2-cfi-asm -fno-asynchronous-unwind-tables` are for getting a nice looking assembly. `c++filt` is for demangling C++ symbols.

And when running `make`, it generates a... 168 lines assembly, in my computer. This is because, in theory, you can run GAS (GNU Assembler) on the generated output and get a fully working program.

But **we don't want a fully working program**, we want a small readable assembly. So we need to filter away the things we don't care about.

## Filtering
So we could use `grep` and inverse filtering.
But basically, we need to:

- **Include** lines that start with `.L{number}`.
- **Include** lines that start with `.quad` or `.string` or `.ascii` or `.asciz`.
- **Remove** lines that start with `.L{something_else}`.
- **Remove** all lines after `:0` or `.Lframe1`.
- If the line starts with `.LFE` change it with a newline. That's because that indicates the end of the function.

And, after trying to make it in C++ and failing to optimize it, I made it in Rust. I'm not gonna explain the code a lot, you just need to know it is quite fast. And it reads and writes to `stdin`.

```rust
// rspper/src/main.rs

use std::io::{self, BufWriter, Write};

type File = Box<dyn Iterator<Item = String>>;

struct AssemblyFile {
    lines: File,
}

impl AssemblyFile {
    fn new(lines: File) -> Self {
        Self { lines }
    }

    fn add_function_separator(self) -> Self {
        let lines = self.lines.map(|l| {
            if l.starts_with(".LFE") {
                String::new()
            } else {
                l
            }
        });

        Self::new(Box::new(lines))
    }

    fn remove_last_section(self) -> Self {
        let lines = self
            .lines
            .take_while(|l| !l.starts_with("0:") && !l.starts_with(".Lframe1"));

        Self::new(Box::new(lines))
    }

    fn remove_directives(self) -> Self {
        let lines = self.lines.filter(|l| {
            !l.starts_with("\t.")
                || l.starts_with("\t.quad")
                || l.starts_with("\t.string")
                || l.starts_with("\t.ascii") | l.starts_with("\t.asciz")
        });

        Self::new(Box::new(lines))
    }

    fn remove_ltags(self) -> Self {
        let lines = self.lines.filter(|l| {
            !(l.starts_with(".LF")
                || l.starts_with(".Lfunc")
                || l.starts_with(".LCFI")
                || l.starts_with(".LEH")
                || l.starts_with(".LHOT")
                || l.starts_with(".LCOLD")
                || l.starts_with(".LLSDA")
                || l.contains("endbr64"))
        });

        Self::new(Box::new(lines))
    }
}

fn main() {
    // Get an interator to stdin
    let input_lines = io::stdin().lines().map_while(Result::ok);

    let input_file = AssemblyFile::new(Box::new(input_lines))
        .add_function_separator()
        .remove_last_section()
        .remove_directives()
        .remove_ltags();

    // Hot output loop
    let stdout = io::stdout().lock();
    let mut stdout = BufWriter::new(stdout);

    for li in input_file.lines {
        stdout.write(li.as_bytes()).expect("stdin broken!");
        stdout.write("\n".as_bytes()).expect("wut");
    }
}
```

And then I compile it and modify the Makefile.

```Makefile
# Makefile
# <...>

CHOPPER := ./rspper/target/release/rspper

# <...>

out/func.asm: src/func.cpp
	@ mkdir -p out				# Create directory
	${CC} -S ${CFLAGS} $< -o $@ # Compile to assembly
	cat $@ | c++filt | ${CHOPPER} | tee $@ >/dev/null	# Make it pretty
```

And **finally**, the generated good looking assembly output!
```asm
sum_manual(std::array<int, 10ul> const&):
	lea	rdx, 40[rdi]
	xor	eax, eax
.L2:
	add	eax, DWORD PTR [rdi]
	add	rdi, 4
	cmp	rdi, rdx
	jne	.L2
	ret

sum_range(std::array<int, 10ul> const&):
	lea	rdx, 40[rdi]
	xor	eax, eax
.L6:
	add	eax, DWORD PTR [rdi]
	add	rdi, 4
	cmp	rdi, rdx
	jne	.L6
	ret
```
As you can see, using range-based loops generates the exact same assembly!

## Getting Rusty
The process isn't that different. We just need some more compilation flags. Edit your Makefile to have this.

```Makefile
#<...>

all: out/func.asm out/rust.asm

out/rust.asm: src/lib.rs
	@ mkdir -p out
	rustc $< -O --crate-type=lib --emit=asm -C llvm-args=-x86-asm-syntax=intel -o $@
	cat $@ | c++filt | ${CHOPPER} | tee $@ >/dev/null

```
With this, we are basically doing the same as before. It is important to **mark functions as** `pub`, because if not they're optimized away by the compiler.

## Ending Notes
This has not been profoundly tested, and I may be missing to include/exclude some assembly lines. Also, this makes use of `c++filt` for C++ and Rust. There is [this](https://github.com/luser/rustfilt) project, `rustfilt`, which does the same but for Rust.

I hope you found this usefull, thank you for reading!