gomonty is an experimental standalone repository for the Go bindings to Monty. The Go package keeps the copied binding API and package name monty, while the Rust FFI crate is split out so it can build against upstream Monty through pinned Cargo git dependencies.
Documentation: https://pkg.go.dev/github.com/ewhauser/gomonty
- Experimental.
- Go module path:
github.com/ewhauser/gomonty - Go bindings are cgo-free and use
puregowith bundled shared libraries - Rust FFI crate:
crates/monty-go-ffi - Upstream Monty source: pinned in the root
Cargo.toml - Native shared libraries: checked into
internal/ffi/lib/<target> - Generated header: checked into
internal/ffi/include/monty_go_ffi.h - Alpine/musl builds use a separate
muslGo build tag and musl-specific shared libraries
Tagged source trees must already contain the native shared libraries required by the runtime loader. GitHub release assets are optional convenience copies, not the source of truth for Go module consumers.
*.go,vfs/,internal/ffi/: copied Go bindings adapted to the root module layoutgo/README.md: consumer-facing Go API notes and examples carried over from the source repoexamples/: standalone example module for local consumption examplescrates/monty-go-ffi/: copied Rust C ABI cratescripts/build-go-ffi.sh <target-triple>: builds one target shared library intointernal/ffi/lib/...
The Go package is cgo-free. It uses purego to load a bundled shared library for the current target from internal/ffi/lib/<target>.
On first use, the loader extracts the embedded shared library to os.UserCacheDir() with an os.TempDir() fallback, then opens it with the platform loader.
Default Linux builds target the GNU/glibc shared libraries. Alpine and other musl-based Linux builds must opt into the musl family with the musl Go build tag.
The verify workflow runs CGO_ENABLED=0 Go tests on native Linux, macOS, and Windows runners. Musl shared libraries are build-verified rather than executed in CI.
To build or refresh the shared library for the current host:
scripts/build-go-ffi.sh aarch64-apple-darwin
CGO_ENABLED=0 go test ./...Requirements:
- Go 1.25+
- Rust toolchain
- Python available on
PATH, orPYO3_PYTHONset explicitly cbindgenonly when regeneratinginternal/ffi/include/monty_go_ffi.h
For repeat builds where the checked-in header does not need to change, set MONTY_GO_FFI_SKIP_HEADER=1.
For Alpine or another musl-based Linux environment:
scripts/build-go-ffi.sh x86_64-unknown-linux-musl
go test -tags musl ./...For normal consumers, the intended path is to depend on a tagged version of this repo whose source tree already contains the native shared library for the consumer's target platform.
Add the module:
go get github.com/ewhauser/gomonty@latestOr in go.mod:
require github.com/ewhauser/gomonty vX.Y.ZThen import and use it:
package main
import (
"context"
"fmt"
"log"
monty "github.com/ewhauser/gomonty"
)
func main() {
runner, err := monty.New("40 + 2", monty.CompileOptions{
ScriptName: "example.py",
})
if err != nil {
log.Fatal(err)
}
value, err := runner.Run(context.Background(), monty.RunOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Println(value.Raw())
}The same example lives in examples/cmd/example. To run it from this repo checkout:
cd examples
CGO_ENABLED=0 go run ./cmd/exampleIf you are consuming a branch, local checkout, or unreleased commit instead of a prepared tag, you may need to build or refresh the shared library for your platform first:
scripts/build-go-ffi.sh aarch64-apple-darwinFor Alpine or musl-based Linux consumers, also add the musl build tag when
building or testing your application:
go build -tags musl ./...The Go benchmark suite mirrors the current upstream Monty benchmark cases so
the two projects exercise the same scripts and expected outputs. The shared
kitchen-sink workload is copied into testdata/bench_kitchen_sink.py.
With a host shared library built, run the local Go-only benchmarks with:
CGO_ENABLED=0 go test -run '^$' -bench BenchmarkMonty -benchmemThis covers the parse-once/repeated-run benchmark cases plus
BenchmarkMontyEndToEnd for parse-and-run in the loop.
There are also Go-specific benchmark suites for wrapper overhead:
CGO_ENABLED=0 go test -run '^$' -bench BenchmarkMontyCallbacks -benchmem
CGO_ENABLED=0 go test -run '^$' -bench BenchmarkMontyDecompose -benchmemThese add:
- callback-heavy runs with repeated external function and OS handler calls
- low-level decomposition benchmarks for compile-only, dump/load, start-to-first-progress, name lookup, call resume, and pending resume paths
To capture CPU and allocation profiles for the representative hot paths, run:
scripts/profile-benchmarks.shBy default the script writes profiles and pprof -top summaries to /tmp/gomonty-bench-profiles for:
BenchmarkMontyEndToEndBenchmarkMonty/list_append_intBenchmarkMontyCallbacks/external_loop
To compare gomonty against a local upstream Monty checkout on the same host,
run:
python3 scripts/compare-benchmarks.py --upstream ../montyThe comparison script:
- runs the Go benchmark suite and aggregates the median
ns/opacross three runs - runs the upstream Criterion
__montybenchmarks - sets
PYO3_PYTHONfor the upstream run if the upstream checkout still expects a local.venv/bin/python3 - prints a Markdown table suitable for pasting back into this README
Current sample comparison from 2026-03-24 on darwin/arm64 (Apple M3 Max),
measured from gomonty dddae9616d8b-dirty against upstream Monty
982709bd52b1-dirty:
| Case | gomonty | raw monty | Ratio |
|---|---|---|---|
add_two |
2.916 us |
721 ns |
4.04x |
list_append |
3.204 us |
853 ns |
3.76x |
loop_mod_13 |
42.157 us |
37.906 us |
1.11x |
kitchen_sink |
7.942 us |
4.035 us |
1.97x |
func_call_kwargs |
3.501 us |
1.045 us |
3.35x |
list_append_str |
14.200 ms |
14.557 ms |
0.98x |
list_append_int |
4.855 ms |
4.976 ms |
0.98x |
fib |
20.547 ms |
21.204 ms |
0.97x |
list_comp |
32.750 us |
29.786 us |
1.10x |
dict_comp |
78.033 us |
69.671 us |
1.12x |
empty_tuples |
2.664 ms |
2.794 ms |
0.95x |
pair_tuples |
8.917 ms |
9.111 ms |
0.98x |
end_to_end |
5.240 us |
1.891 us |
2.77x |
These numbers are host-specific. They compare the same benchmark scripts, but
the Go side uses testing.B while upstream uses Criterion.
The repo also includes Go fuzz targets for:
FuzzValueJSON: pure-Go value wire-format decoding and normalizationFuzzCompileAndRun: arbitrary source strings compiled and executed with tight resource limitsFuzzLoadRunner: arbitrary bytes fed throughLoadRunner, including valid dumped-runner seeds
Run a short fuzzing pass with:
CGO_ENABLED=0 go test -run '^$' -fuzz FuzzValueJSON -fuzztime=10s .
CGO_ENABLED=0 go test -run '^$' -fuzz FuzzCompileAndRun -fuzztime=10s .
CGO_ENABLED=0 go test -run '^$' -fuzz FuzzLoadRunner -fuzztime=10s .The native runner fuzzers require a supported host shared library and run with
CGO_ENABLED=0. FuzzValueJSON remains pure Go.
The default build uses pinned git dependencies on https://github.com/pydantic/monty.git. For local development against a sibling checkout, you can temporarily override them with a Cargo patch:
[patch."https://github.com/pydantic/monty.git"]
monty = { path = "../monty/crates/monty" }
monty_type_checking = { path = "../monty/crates/monty-type-checking" }See RELEASING.md for bumping the upstream pin and for the protected-branch release flow: make release opens the release-prep PR, then make publish-release VERSION=vX.Y.Z tags merged main, creates the GitHub release, and warms the Go module proxy.