Skip to content
Projects

alirezahematidev/ferrogo has 9 GitHub stars and 1 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-14 20:01 UTC. Last push 2026-11-11.

How these counts are measured

Measured star growth

in 24h
0
in 7 days
Collecting history
in 15 days
Collecting history
in 1 month
Collecting history

Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.

About

Run and call Rust from Go — sandboxed WASM by default, native when trusted.

Developer Tools

README.md

Original

FerroGo

Go Reference CI Go Report Card License: MIT

Compile and run Rust from Go: in a WebAssembly sandbox by default, or as native processes when you trust the code.

rt, _ := ferrogo.New()
defer rt.Close()

res, err := rt.Run(ctx, ferrogo.Code(`
    fn main() {
        println!("Hello from Rust!");
    }
`))
fmt.Print(res.Stdout) // Hello from Rust!

FerroGo drives your Rust toolchain for you. It generates a Cargo project, runs cargo with structured arguments and a scrubbed environment, parses diagnostics, caches artifacts by a SHA-256 fingerprint of every input, and executes the result with timeouts, output limits and (for WASM) memory and filesystem isolation.

Status: v0.1, pre-1.0. The API may still change before v1.0.

  • Platforms: developed and tested on macOS arm64. The CI matrix covers Linux (amd64 and arm64), macOS (arm64 and amd64) and Windows.
  • Open items: see the roadmap. The main one is an OS sandbox for native processes on Windows.

What you can do

Run Rust programs rt.Run(ctx, ferrogo.Code(...)): sandboxed WASM by default, or native processes
Compile once, call many times rt.Compile + mod.Call: WASM, a native library in-process, or native worker processes
Write Rust inside Go files /*ferrogo … */ + go generate: typed wrappers, WASM embedded, no toolchain needed at run time
Call Go from Rust ferrogo::import! + WithHostFunction
Use crates WithDependencies under a deny-by-default DependencyPolicy, or an existing Cargo Project

Why

Rust code is sometimes the right tool inside a Go service: user-supplied logic, a fast kernel, or an existing Rust snippet. Wiring that up by hand means temporary projects, compiler invocation, artifact management, process plumbing and sandboxing. FerroGo packages all of it behind one API, with secure defaults.

Requirements

  • Go ≥ 1.25.
  • Rust ≥ 1.82 (stable), installed with rustup. Only needed where code is compiled.
  • For the default WASM backend: rustup target add wasm32-wasip1.
  • For native backends: a C linker (xcode-select --install, build-essential, or the Visual Studio Build Tools).

Run ferrogo doctor to check everything.

Installation

go get github.com/alirezahematidev/ferrogo@latest                   # library
go install github.com/alirezahematidev/ferrogo/cmd/ferrogo@latest   # CLI

Quick start

ferrogo doctor                      # check toolchain, targets, backends
ferrogo run main.rs                 # WASM sandbox (default)
ferrogo run main.rs --backend native -- arg1 arg2
ferrogo build main.rs -o main.wasm  # compile only
ferrogo generate ./mypkg            # compile Rust embedded in Go files
ferrogo call lib.rs add 10 20       # call an exported function (args typed from the Rust signature)
ferrogo inspect lib.wasm            # list a module's exports
ferrogo doctor --fix                # install missing targets with rustup (explicit)
ferrogo run main.rs --json          # machine-readable result
ferrogo cache stats
ferrogo init                        # write ferrogo.toml

Backends

WASM (default) Native process Native library
Status ✅ ✅ ✅ (Linux, macOS, NetBSD, Windows)
Run programs / call functions ✅ / ✅ ✅ / ✅ (worker processes) — / ✅
Runs wasm32-wasip1 in wazero host executable as a child process .so/.dylib/.dll loaded in-process
Sandbox yes: no FS/network/env unless granted no (your user's privileges) no (your process)
Allowed for untrusted code yes no: needs WithTrust(Trusted) no: needs WithTrust(Trusted)
Timeouts interrupt running code yes yes (process-group kill) no
Memory limit yes no (returns ErrUnsupported) no
Threads, sockets no yes yes

BackendAuto always means WASM. FerroGo never silently falls back to a native backend. ferrogo.CapabilitiesOf(b) reports capabilities at run time, and rt.Inspect(ctx) reports what is usable on this machine. Details: docs/backends.md.

Runtime compilation

res, err := rt.Run(ctx, ferrogo.File("tool.rs"),
    ferrogo.WithStdin(strings.NewReader(input)),
    ferrogo.WithArgs("--fast"),
    ferrogo.WithEnv(map[string]string{"LEVEL": "3"}),
    ferrogo.WithTimeout(5*time.Second),
)
var ce *ferrogo.CompileError
switch {
case errors.As(err, &ce):
    for _, d := range ce.Errors() {
        fmt.Println(d.Location(), d.Message) // tool.rs:3:18 mismatched types
    }
case errors.Is(err, ferrogo.ErrTimeout):
    // res holds the partial output
}
  • Run returns a *Result (stdout, stderr, exit code, timing, artifact info) whenever the program started, even alongside an error.
  • Non-zero exits are *ExitError. Rust panics are *PanicError, with the file and line mapped to your names.
  • Build compiles without running.

Compile once, call many times

mod, err := rt.Compile(ctx, ferrogo.Code(`
    ferrogo::export! {
        fn add(a: i32, b: i32) -> i32 { a + b }
        fn greet(name: String) -> Result<String, String> {
            if name.is_empty() { return Err("empty name".into()); }
            Ok(format!("Hello, {name}!"))
        }
    }
`))
defer mod.Close()

sum, err := mod.Call(ctx, "add", ferrogo.Int32(10), ferrogo.Int32(20)) // i32(30)
_, err = mod.Call(ctx, "greet", ferrogo.String(""))                    // *ferrogo.CallError{"empty name"}
  • ferrogo::export! is injected into every library crate.
  • Functions take and return:
    • bool, i32/i64/u32/u64, f32/f64
    • String, Vec<u8>
    • ferrogo::Json (structs via encoding/json)
    • Result<T, E>
  • Panics become *PanicError on every backend and never crash your process.
  • Plain #[no_mangle] extern "C" scalar functions are callable on WASM (signature read from the module) and, with a declared Signature, on native libraries (CallRaw).
  • On the native-process backend, calls run in worker processes: they can be timed out, sandboxed and memory-limited, and crashes are contained.
  • One envelope ABI, identical on WASM and native libraries: docs/abi.md.
  • LoadModule(path) and LoadWASM(bytes) load prebuilt artifacts, for example from go:embed, with no toolchain at run time. Example: examples/functions.

Inline Rust

//go:generate go run github.com/alirezahematidev/ferrogo/cmd/ferrogo generate

/*ferrogo
pub fn fib(n: u32) -> u64 {
    let (mut a, mut b) = (0u64, 1u64);
    for _ in 0..n { (a, b) = (b, a + b); }
    a
}
*/

func main() {
    n, err := Fib(ctx, 90) // generated, typed wrapper
}
  • ferrogo generate compiles the block to WASM, embeds it with go:embed, and writes typed wrappers.
  • Programs using the package need no Rust toolchain.
  • Errors point at your .go lines, and generate -check keeps CI honest.
  • Details: docs/inline-rust.md. Example: examples/inline.

Calling Go from Rust (host functions)

rt, _ := ferrogo.New(ferrogo.WithHostFunction("lookup_user",
    func(ctx context.Context, args []ferrogo.Value) (ferrogo.Value, error) {
        id, _ := args[0].AsUint64()
        return ferrogo.String(users[id]), nil
    }))
mod, _ := rt.Compile(ctx, ferrogo.Code(`
    ferrogo::import! { fn lookup_user(id: u64) -> String; }
    ferrogo::export! {
        fn greet(id: u64) -> Result<String, String> { Ok(format!("hi {}", lookup_user(id)?)) }
    }
`))

WASM code can call exactly the functions you register and nothing else. Go errors and panics reach Rust as Err.

Dependencies and Cargo projects

rt, _ := ferrogo.New(ferrogo.WithDependencyPolicy(ferrogo.DependencyPolicy{
    AllowRegistry: true, // crates.io; AllowGit / AllowPath are separate
    Network:       false, // default: --offline, using crates already in ~/.cargo
}))
res, err := rt.Run(ctx, ferrogo.Code(`
    use serde_json::json;
    fn main() { println!("{}", json!({"language": "Rust", "fast": true})); }
`), ferrogo.WithDependencies(ferrogo.Dependency{
    Name: "serde_json", Version: "1", Features: []string{"preserve_order"},
}))
  • Denied by default. Dependencies run build scripts and proc-macros on your machine, so nothing is used unless a DependencyPolicy allows it:

    • registry, git and path dependencies are allowed separately;
    • crate allowlists are supported;
    • network access is a separate opt-in.
  • Reproducible. The resolved Cargo.lock is stored per dependency set and is part of the cache key.

  • Fast. Compiled dependencies are shared: a second snippet using serde_json builds in about 0.1 s instead of 1.7 s.

  • Cargo projects. ferrogo.Project("./tool") (CLI: ferrogo run ./tool) builds an existing project in place, with:

    • multi-file crates
    • its own Cargo.lock
    • custom profiles
    • incremental compilation

    Projects require WithTrust(Trusted) because their build scripts run on your machine. Their declared dependencies are checked against the policy.

Caching

Artifacts are stored in os.UserCacheDir()/ferrogo under a SHA-256 key over:

  • source
  • generated Cargo.toml
  • full rustc -vV output and cargo -V
  • target, backend and profile
  • edition and flags
  • explicit build environment
  • the contents of Cargo config files
  • the ABI helper version

Writes are atomic, entries are verified on read, and least-recently-used entries are evicted beyond 2 GiB. Machine code compiled from WASM is cached too. Use WithCacheDir, WithCacheMaxBytes and WithCache(false), or the CLI's ferrogo cache stats|clean.

Security

Treat Rust source as untrusted; FerroGo does by default.

  • The compiler's environment is scrubbed, so env!() cannot read your secrets.
  • No shell is ever used.
  • WASM guests get nothing they are not granted.
  • Every limit is finite by default:
    • execution timeout: 30s
    • compile timeout: 5m
    • output: 4 MiB
    • WASM memory: 256 MiB

Native processes can additionally run under the OS sandbox with WithSandbox (sandbox-exec on macOS, bubblewrap on Linux), with memory limits on Linux and Windows.

include_str! can read any file the compiler can read. Enable WithCompilerSandbox(true) to run the toolchain itself under the OS sandbox. It blocks reading your home directory at compile time. Otherwise, compile untrusted code in a container or VM. See docs/security.md.

Performance

Measured on Apple M-series, Go 1.25.6, Rust 1.94.1, wazero 1.12:

WASM Native process
Cold compile (trivial program) ~106 ms ~129 ms
Warm run, cached artifact 0.75 ms 2.0 ms
fib(30) incl. startup 26 ms 3.5 ms
Module.Call of add(i32, i32) 1.0 µs (raw C export: 0.55 µs) 0.7 µs (native library)

The WASM compute gap comes from interruption checks, which cost 8.7× on this call-heavy code. Those checks are what make timeouts work on untrusted code. Details and methodology: docs/performance.md. Reproduce with go test -run '^$' -bench . ..

Platform support

Library + CLI WASM backend Native process Native library
macOS arm64 tested tested tested tested
Linux amd64 / arm64 CI configured CI configured CI configured CI configured
macOS amd64, Windows amd64 CI configured CI configured CI configured (Windows: no process-tree kill yet) CI configured
FreeBSD, other builds builds builds not supported

"Tested" means tested locally. "CI configured" means the GitHub Actions matrix covers it but has not run yet. Unit tests (go test -short ./...) need no Rust toolchain.

Limitations

  • No machine-code JIT: code is compiled ahead of time by rustc, so the first build of a snippet takes 0.1–1s.
  • File sources are single files. Use a Project source for multi-file crates.
  • Dependencies are built offline unless DependencyPolicy.Network allows downloads, and their build scripts run on the build machine. Use WithCompilerSandbox to confine them.
  • Native processes have no memory limit on macOS, and no OS sandbox on Windows.
  • Host functions (Rust → Go) and deterministic mode work on WASM only.
  • CallRaw on native libraries is unavailable on Windows.
  • WASM (wasip1) has no threads and no sockets.
  • On macOS, a newly built native binary takes about 300 ms to start the first time (OS assessment).

Documentation

Development

make test         # unit + integration tests (integration tests skip without rustc)
make race         # with the race detector
make lint         # gofmt, go vet for linux/darwin/windows, staticcheck
make examples     # build and run every example

Contributing

See CONTRIBUTING.md. Report vulnerabilities privately, as described in SECURITY.md.

License

MIT