hagane compiles Go source to readable, portable C11.
You write normal Go, run hagane build, and the tool loads your packages via go/packages, builds SSA form, walks the SSA, and emits C11 that GCC or Clang can compile into a native binary.
The emitted C uses a small runtime header (hagane_rt.h) for strings, slices, and wrapping arithmetic.
No CGO, no LLVM, no intermediate IR files.
Every Go-to-C call through CGO crosses a runtime boundary: the goroutine stack is switched to a system thread, arguments are marshalled, and the scheduler is notified on return. On a modern machine that costs roughly 100-200 ns per call.
| Approach | Call overhead | Notes |
|---|---|---|
| CGO | ~100-200 ns | Stack switch + marshalling per call |
| hagane | ~0 ns | No boundary; Go compiles to the same C translation unit |
| Pure Go | ~0 ns | No C at all |
At low call rates CGO is fine. When you need tight loops that cross a C library boundary, or you want to ship a binary with no Go runtime attached, CGO overhead becomes the ceiling on your throughput.
A typical CGO wrapper for a single C function:
// cgo approach
/*
#include <stdio.h>
*/
import "C"
import "unsafe"
func printMessage(s string) {
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
C.printf(cs)
}The same thing with hagane:
// hagane approach
//go:extern int printf(const char *fmt, ...);
func printMessage(s string) {
printf(s)
}No import "C", no unsafe, no manual memory management for the crossing.
Go install
go install github.com/tamnd/hagane/cmd/hagane@latestHomebrew (macOS / Linux)
brew install tamnd/tap/haganeScoop (Windows)
scoop bucket add tamnd https://github.com/tamnd/scoop-bucket
scoop install haganeapt (Debian / Ubuntu)
curl -fsSL https://pkg.tamnd.com/apt/key.gpg | sudo tee /etc/apt/keyrings/tamnd.gpg >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/tamnd.gpg] https://pkg.tamnd.com/apt stable main" \
| sudo tee /etc/apt/sources.list.d/tamnd.list
sudo apt update && sudo apt install haganednf (Fedora / RHEL)
sudo dnf config-manager --add-repo https://pkg.tamnd.com/rpm/tamnd.repo
sudo dnf install haganeOr download a prebuilt binary from Releases.
# 1. write a Go program
cat > hello.go <<'EOF'
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
EOF
# 2. build it
hagane build .
# 3. run the native binary
./helloOutput:
Hello, World!
Transpile a Go package to C and compile to a binary.
hagane build ./myprogram
hagane build -o out ./myprogram
hagane build -cc clang ./myprogramPrint the emitted C to stdout. Useful for auditing the translation or feeding the C into another build system.
hagane emit ./myprogramExample output (trimmed):
#include "hagane_rt.h"
void main_main(void) {
hagane_string s = hagane_str_lit("Hello, World!\n");
fmt_Println(s);
}
int main(int argc, char **argv) {
hagane_runtime_init(argc, argv);
main_main();
return 0;
}Transpile, compile, and run in one step.
hagane run ./myprogram
hagane run ./myprogram -- --flag valueType-check only. No C is emitted, no compilation happens. Exits non-zero if the package has type errors.
hagane check ./myprogramDeclare external C functions with a //go:extern directive and call them from Go as normal functions.
hagane emits a C prototype and wires the call through directly.
package main
//go:extern int printf(const char *fmt, ...);
//go:extern void exit(int status);
func main() {
printf("pid: %d\n", getpid())
exit(0)
}You can also pull in a header:
//go:include <stdio.h>
//go:include <stdlib.h>hagane emits the corresponding #include at the top of the C file.
- Load Go packages with
go/packages(full type information). - Build SSA form with
golang.org/x/tools/go/ssa. - Walk the SSA and emit C11 into a single
.cfile per package. - Invoke GCC or Clang to compile and link.
The emitted C is meant to be readable.
Variable names are preserved where possible, control flow maps to C if/for/goto, and struct fields keep their Go names.
The runtime header (hagane_rt.h) provides hagane_string, hagane_slice, and a handful of arithmetic helpers.
There is no garbage collector in the emitted binary; memory layout is explicit.
M0 complete (current release): single-package programs with integers, strings, slices, structs, functions, and basic control flow work end to end.
Roadmap:
- M1: multiple packages, init functions, global variables
- M2: interfaces and type assertions
- M3: goroutines and channels (via a small C coroutine library)
- M4: maps
- M5: standard library stubs (fmt, os, io, bufio, strings, strconv)
- M6: generics
See issues and docs for the full roadmap.
Bug reports and test cases are the most useful contributions right now. If you find a Go program that hagane translates incorrectly, open an issue with a minimal reproducer.
MIT. See LICENSE.