Skip to content

asmgen — Quick start

This walks through the examples/add program shipped in the asmgen repo: a func add(a, b int64) int64 whose body is generated arm64 assembly.

arm64 required to run

The generated assembly is built under //go:build arm64 and exercised by a runtime test. Generation runs anywhere, but building and testing the example requires an arm64 host (Apple Silicon, an arm64 Linux box, or a GitHub ubuntu-24.04-arm runner). The library packages themselves are architecture-independent.

Install

go get github.com/go-asmgen/asmgen@latest

1. Declare the function with no body

// add.go
package add

//go:generate go run gen.go

// add returns a + b. Implemented in add_arm64.s, generated by go-asmgen.
func add(a, b int64) int64

2. Write the generator

//go:build ignore

// gen.go — run with: go run gen.go
package main

import (
    "fmt"
    "os"

    "github.com/go-asmgen/asmgen/arm64"
    "github.com/go-asmgen/asmgen/emit"
)

func main() {
    // func add(a, b int64) int64
    sig := arm64.Layout(
        []string{"a", "b"}, []arm64.Type{arm64.Int64, arm64.Int64},
        []string{"ret"}, []arm64.Type{arm64.Int64},
    )

    b := arm64.NewFunc("add", sig, 0)
    b.LoadArg("a", "R0").
        LoadArg("b", "R1").
        Raw("ADD R1, R0, R2").
        StoreRet("R2", "ret").
        Ret()

    f := emit.NewFile("arm64")
    f.Add(b.Func())

    if err := os.WriteFile("add_arm64.s", []byte(f.String()), 0o644); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Println("wrote add_arm64.s")
}

3. Generate, vet, build, test

go generate ./...          # runs gen.go -> writes add_arm64.s
GOARCH=arm64 go vet ./...  # asmdecl cross-checks .s offsets vs add.go decl
GOARCH=arm64 go build ./...
go test ./...              # on an arm64 host (or under qemu/Rosetta)

The generated add_arm64.s:

// Code generated by go-asmgen. DO NOT EDIT.

//go:build arm64

#include "textflag.h"

TEXT ·add(SB), NOSPLIT, $0-24
    MOVD a+0(FP), R0
    MOVD b+8(FP), R1
    ADD R1, R0, R2
    MOVD R2, ret+16(FP)
    RET

How correctness is checked

In priority order — the same checks the asmgen CI runs:

  1. go vet asmdecl passes. The cheapest, strongest check: it verifies every name+offset(FP) in the .s matches the Go declaration. Wrong offsets are caught before runtime.
  2. Generated assembly is committed and up to date. CI regenerates and fails on any diff, so the checked-in .s always matches the generator.
  3. Runtime test on real arm64 — the function is called and its result checked, on a native arm64 runner.