go-embedded-ruby documentation¶
A Ruby interpreter written in pure Go, with cgo disabled — a bytecode VM with the lexer, parser and compiler embedded in the binary. Embed it in a Go program, or ship it as a single static executable that cross-compiles wherever Go does, with no C toolchain and no libruby.
How complete is it? It passes 23,511 of ruby/spec's language/ + core/
examples — a large and growing subset of Ruby, and not a drop-in replacement for
CRuby. Conformance states
exactly what that counts and what it leaves out, and
Known limitations is worth reading before you
depend on it.
Embedding it in a Go program¶
The public API is one function:
import "github.com/go-embedded-ruby/ruby"
var out bytes.Buffer
err := ruby.Run(`puts "hello from Ruby"`, &out) // out: "hello from Ruby\n"
Everything else lives under internal/, so this is the whole embedding surface
today. Measured behaviour worth knowing: there is one writer, carrying both
streams ($stderr and warn land in out too); a Ruby exception comes back as a
Go error (ArgumentError: bad), though exit, exit! and abort stop the
program and return nil, so a caller cannot tell “finished” from “stopped”;
each call gets a fresh VM, so the Ruby heap is not shared between Runs and
there is no exported way to hold a VM open, call a Ruby method from Go, or pass
Go values in; but process state is shared with the host and with every
other Run — ENV, the working directory, open files and signal handlers — which
is also why require_relative resolves against the process working
directory, since Run has no script to anchor to.
Run is not a sandbox
Conformance is the goal and MRI is not sandboxed either, so neither is this.
Through that one function, Ruby source can spawn /bin/sh, read and write
the filesystem as the host's uid, open sockets, require any absolute path,
and change the host's ENV and working directory. Do not pass untrusted
Ruby to it. Two things are deliberately out of reach: there is no path from
Ruby to the host's Go state, and the host's standard descriptors are not
exposed, so a script cannot write outside the out you passed. The
repository README
carries the measurements.
go-embedded-ruby compiles Ruby source to bytecode and runs it on a stack VM in
the mruby/YARV lineage. Because the front-end (lexer + parser + compiler)
ships inside the binary, dynamic features like eval and runtime require
work without a separate toolchain. Ruby objects are ordinary Go heap objects, so
Go's garbage collector is reused rather than reimplemented.
Being pure Go buys three things at once: trivial cross-compilation, a single
static binary, and — most importantly — it is the first Ruby embeddable in a
Go program with no C toolchain (go-mruby needs cgo). The project
targets Ruby 4.0 semantics and is grown test-first against a differential
oracle — every snippet's output is checked against two independent reference
implementations, MRI (CRuby) 4.0.5 and JRuby
(scripts/oracle.sh
runs all three side by side) — with 100% coverage enforced in CI on the two
POSIX lanes (ubuntu, macOS; the full -race suite also runs on windows, where a
few POSIX-only paths make the gate unreachable). Per-PR architecture cover is the
native amd64 and arm64 lanes; the four exotic 64-bit targets (riscv64,
loong64, ppc64le, s390x) are validated nightly under QEMU and on scheduled real
hardware. See Conformance. Beyond synthetic tests, the bar is real-world Ruby: idioms and test suites
from reference applications such as Rails (ActiveSupport's pure-Ruby
core_ext) and OpenVox/Puppet drive the remaining work and double as
performance baselines against MRI and JRuby.
Supported today¶
Every feature below is differential-tested against MRI Ruby 4.0.5:
- Values: integers (
int64, with automatic Bignum promotion on int64 overflow and arbitrary-precision integer literals, radix literals0x/0o/ bare-0-octal/0b/0dwith underscores), floats (incl. scientific notation1.5e3/1e-3), strings (single- and double-quoted, with interpolation and heredocs), symbols (incl. operator-method symbols:+/:<</:[]=/:<=>), arrays, hashes, ranges (incl. beginless/endless),true/false/nil,self,Proc/lambda,Regexp/MatchData,Struct. - Operators: arithmetic (
+ - * / %, Ruby floor division,**), comparison/<=>,==/===, bitwise/shift (<< >> & | ^ ~, arbitrary precision),&&/||, ternary, ranges; correct negative-literal precedence (-2.abs == 2,-2**2 == -4). - Control flow:
if/elsif/else,unless,while/until,case/when, statement modifiers (incl. modifierrescue,expr rescue fallback),begin/rescue/ensure/else/retry,break/next,Kernel#loop, andFiber(cooperative coroutines on goroutines —Fiber.new/resume/Fiber.yield/alive?). - Concurrency:
Thread(new/join/value/status/current/main/list/pass, thread-locals, exception propagation on join),Mutex(lock/try_lock/synchronize/owned?) andQueue(blockingpush/pop/close), on an emulated GVL — one Ruby thread runs at a time, matching MRI's memory model (race-free under the Go race detector). - Pattern matching (
case/in): value, variable-binding, class/constant, array (incl. splat and nested), hash (deconstruct_keys,**rest/**nil), find ([*pre, x, *post]), pin (^x) and alternative (a | b) patterns; the=> namebinding suffix, guards (if/unless), the one-line formsexpr => patternandexpr in pattern, andNoMatchingPatternError. - Assignment: multiple assignment / destructuring (
a, b = 1, 2,x, *rest = …,*init, last = …) and compound assignment (+= -= *= /= %= <<= ||= &&=). - Methods: required / optional /
*splat/ keyword (a:,b: 2) /**rest/&blockparameters, setter defs (def name=), endless methods (def foo = expr), singleton method defs (def obj.foo/def Const.foo), recursion,return,super. - Blocks / Procs / lambdas:
{ }/do…endclosures,yield,block_given?,&blockcapture, block params with destructuring (|(a, b)|) and rest (|*rest|,|head, *rest|), numbered params (_1/_2) andit,Proc/lambda/stabby->(){},&procblock-pass andSymbol#to_proc(the&:symshorthand). - Classes & modules: inheritance,
@ivars,@@class variables(shared down the hierarchy),new/initialize, constants and constant assignment, class methods (def self.foo), modules +include/prepend(mixins, with ancestor-chainsuperthrough included/prepended modules and the singleton chain;Module#ancestors/include?),attr_accessor/reader/writer,Struct.new. - Metaprogramming: dynamic dispatch via mutable method tables,
method_missing,send/public_send,respond_to?,define_method,instance_eval/instance_exec,class_eval/module_eval/class_exec,instance_variable_get/set/defined?, stringeval— the embedded front-end compiling Ruby at runtime (against the caller'sself) —Binding(binding,Binding#eval,eval(str, binding),local_variable_get/set/defined?,local_variables,receiver— capturing a frame's locals so eval'd code reads and writes them), the class/module hooksinherited/included/method_added/extended,define_singleton_method/extend, and$global variables. - Runtime loading:
require/require_relativeload, compile and run a.rbfile once (relative + search-path resolution,LoadErroron miss) — the embedded front-end loading code at runtime. - Strings: mutable (reference semantics) with
<</concat/replace/prepend/ insert/[]=/slice!/the bang methods andfreeze/FrozenError; interpolation, heredocs (<</<<-/<<~),%w/%iliterals,%/format/sprintf, case/strip/split/each_char/lines/succ(next) and friends. - Regular expressions:
/re/imxliterals,Regexp/MatchData,=~/match/match?/scan/gsub/sub/split, and the match globals$~/$1..$N/$&/$`/$'— running on the standalone pure-Go go-ruby-regexp engine, so the build stays CGO=0. - Standard library leaves:
JSON(generate/dump/pretty_generate/parse+Object#to_json, key order preserved),Digest(MD5/SHA1/ SHA256/SHA512),Base64, andZlib(crc32/adler32 + Deflate/Inflate) — allrequire-able and pure-Go. Marshal(dump/load/restore+MAJOR_VERSION/MINOR_VERSION): Ruby's binary serialization, byte-for-byte identical to MRI across Integer/Bignum, Float, Symbol, String, Array and Hash (incl. defaults), with the symbol and object-link tables so shared objects and cycles round-trip. Runs on the standalone pure-Go go-ruby-marshal engine, so the build stays CGO=0.- File & Random:
File— path helpers andread/write/exist?/size/delete(Errno::ENOENTon a missing path);Random— a bit-exact reimplementation of MRI's seeded MT19937. - IO & Dir:
IOwith$stdout/$stderr/STDOUT/STDERR/$stdinas real objects,StringIO(require "stringio") with the full read/write surface, andKernel#warn—puts/print/proute through the current$stdoutso reassigning it to aStringIOcaptures output, as in MRI;Dir—entries/children/glob,exist?/empty?/pwd/home,mkdir/rmdir/chdir, raisingErrno::ENOENT/Errno::EEXIST. - Collections: Array / Hash / Range with
Enumerable(map/select/reduce/minmax/…) andComparable, both written once in embedded Ruby; Array bang methods (map!/sort!/select!/reject!/compact!/uniq!/reverse!), structural/combinatorial ops (transpose/product/combination/to_h), theHash[…]constructor, String ranges (("a".."e")viaString#succ);Range#step/Integer#step; an eagerEnumerator— every blockless iterator returns one, withnext/peek/rewind/with_index/to_aand fullEnumerablechaining — andEnumerator::Lazy(lazy) for deferred chains over finite or infinite sources. - Objects:
dup/clone/freeze/frozen?,equal?,object_id/__id__(MRI's deterministic immediate-value ids, stable per reference object),instance_variable_get/set. - Numeric & scientific stack:
Complex,Rational, theMathmodule, and a cgo-free scientific stack —NDArray(NumPy-style arrays), anFFTmodule (numpy.fft-style transforms),SpecialandStats(scipy.special's 67 functions and scipy.stats's 32 univariate distributions),IntegrateandOptimize(scipy.integrate's quadrature and scipy.optimize's root finders and minimisers),Image(scikit-image-style processing) — plus container/value typesSet,Bag(multiset),Time,DateandBigDecimal, each binding a pure-Go sibling library (go-ndarray / go-fft / go-numerics / go-statistics / go-images / go-composites). See Scientific stack. - Native capability modules: a set of
require-able modules, each backed by a canonical pure-Gogo-ruby-*library so the build stays CGO=0. They wrap real I/O / network / crypto engines, so throughput tracks the underlying driver rather than the interpreter:- Databases & KV stores —
mysql2(MySQL client),mongo+bson(MongoDB with ordered BSON docs),etcd/etcdv3(etcd v3 KV: get/put/txn/lease/watch/lock),bolt(embedded bbolt B+tree, files interoperable with reference bbolt). - Messaging —
nats(publish/subscribe/request),kafka(producer/consumer/admin, over franz-go). - RPC, serialization & columnar data —
grpc(server + client stub, one program can be both peers),google/protobuf(wire-compatible Protocol Buffers + well-known types),arrow(Apache Arrow arrays / record batches / tables),parquet(Arrow-backed reader / writer). - Web & security —
faraday(HTTP client),puma(Rack HTTP server under the emulated GVL),graphql(type system + query execution),saml/ruby-saml(SAML SSO,OneLogin::RubySaml),webauthn(WebAuthn / FIDO2),acme(ACME / Let's EncryptAcme::Client),rbnacl(NaCl / libsodium),age(age file encryption, interop with the referenceageCLI). - Documents & observability —
prawn(PDF generation, well-formed PDF 1.3+),bleve(full-text search),opentelemetry(distributed tracing over the OpenTelemetry Go SDK).
- Databases & KV stores —
- WebAssembly: the WASI target (
GOOS=wasip1 GOARCH=wasm) builds and runs — verified under wazero, and gated in CI. The browser target (GOOS=js GOARCH=wasm) builds too, and has its own lane (wasm (js, browser)) since ruby#684. On any wasm target the gems whose backends need real TCP sockets or OS facilities (grpc,nats,kafka,mysql2,mongo,arrow,parquet, and theredis-backedsidekiq/resque) are compiled out andrequireraises a cleanLoadError; the native build is unchanged. See WebAssembly.
Closed-world builds have landed (rbgo build --closed bakes the program in as
bytecode and drops the front-end; --target wasm cross-compiles it to the
browser). Still ahead (see the roadmap):
conformance and representation/perf tuning (Phase 8).
Running real-world Ruby: Puppet¶
Beyond parsing real-world Ruby, rbgo runs the real Puppet
puppet apply CLI end-to-end. require "puppet" fully boots the framework
(Puppet 8.11.0) on a pure-Go CGO=0 rbgo — its pure-Ruby gem dependencies
(semantic_puppet, concurrent-ruby, facter, …) load on the $LOAD_PATH — and
a manifest then travels the complete Puppet path: the genuine
Puppet::Util::CommandLine → Puppet::Application::Apply entry point (real
OptionParser), all types + providers loaded, the settings catalog applied to
disk, then the user catalog applied through the transaction / RAL. The real CLI
emits genuine Puppet output and exits 0 — apply -e 'notify { "hello": message
=> "hi from rbgo cli" }' prints Notice: hi from rbgo cli and Notice:
…/Notify[hello]/message: defined 'message' as 'hi from rbgo cli'.
This is the C-extension → pure-Go shim strategy validated end to end: a real
Ruby app ships as one static CGO=0 binary because its C-backed gem APIs are backed
by pure Go (Puppet's deps are pure Ruby, so they load as-is). Reaching the CLI
added a real OptionParser, File::Stat/FileTest + on-disk filesystem
operations, and deep Ruby fixes (class_eval lexical scope, return in
define_method, class-method super, …) on top of the earlier boot surface
(autoload, ERB, openssl with real crypto, net/http, StringScanner,
fileutils, …). Three resource types now converge end-to-end through the
transaction / RAL: notify, file (managing a real file on disk), and
exec — which runs its command via pure-Go process execution, honouring the
onlyif / unless / creates / path guards — and puppet apply exits
cleanly, the YAML run report / state round-tripping through the pure-Go Psych
emitter / loader. The honest frontier is the broader resource providers:
user / group are provider-ready, while package / service need a host
package manager / systemd and root. See
Conformance.
Repositories¶
| Repo | What it is |
|---|---|
ruby |
the interpreter — the bytecode VM, the compiler, and the rbgo CLI binary (it imports the front-end below) |
docs |
this documentation site (MkDocs Material, versioned with mike) |
go-embedded-ruby.github.io |
the organization landing page (Hugo) |
Ecosystem — the go-ruby-* family¶
rbgo is increasingly assembled from a family of standalone pure-Go (CGO=0), MRI-compatible libraries living in sibling orgs, each with 100% coverage and 6-arch CI, that the interpreter composes and binds as native modules. The design principle is a clean seam: a stdlib piece whose work is pure compute and needs no interpreter — a regexp matcher, an ERB compiler, a YAML emitter, a Marshal codec, an OptionParser argv engine — becomes a reusable standalone library, while only the thin interpreter-dependent glue stays in rbgo. rbgo still ships as a single CGO=0 static binary; every extracted piece is independently reusable, tested and 6-arch by any Go program.
The family is a growing set of standalone pure-Go modules — all CI-green, 100%
coverage and 6-arch. The go.mod currently binds 178 such modules across 176
orgs into rbgo as
native modules — including the front-end (go-ruby-parser),
the regexp engine (go-ruby-regexp), and
go-ruby-erb,
go-ruby-marshal,
go-ruby-yaml,
go-ruby-format,
go-ruby-optparse,
go-ruby-strscan.
The full bound family (alphabetical, all native modules in go.mod):
aasm, abbrev, acme, actioncable, actionmailer, actionpack, actionview, activejob, activemodel, activerecord, activestorage, activesupport, addressable, age, arrow, async, augeas, base64, bbolt, bcrypt, benchmark, bigdecimal, bleve, builder, bundler, cancancan, capistrano, capybara, cgi, chronic, cmath, commonmark, concurrent-ruby, confd, connection-pool, csv, date, deep-merge, devise, did-you-mean, digest, dotenv, dry-struct, dry-types, dry-validation, erb, erubi, etcd, excon, facter, factory-bot, faker, faraday, fast-gettext, find, format, friendly-id, getoptlong, grape, graphql, grpc, haml, hanami, hcl2, hiera, hocon, http, httparty, i18n, images, ipaddr, irb, jbuilder, jekyll, json, jwt, kafka, kaminari, kramdown, liquid, logger, mail, marshal, matrix, mime-types, minitest, money, mongodb, msgpack, multi-json, mustache, mysql, nats, net-ftp, net-http, net-imap, net-pop, net-sftp, net-smtp, nokogiri, oauth2, observer, oidc, omniauth, openbao, openstack, opentelemetry, opentype, optparse, ostruct, pagy, paper-trail, parquet, parser, pathname, pg, prawn, prettyprint, prime, protobuf, pstore, public-suffix, puma, pundit, puppet, puppet-resource-api, racc, rack, rails, railties, rake, ransack, rdoc, redis, regexp, reline, resolv, resque, rexml, roda, rolify, rouge, rqrcode, rspec, rss, rubocop, rubygems, saml, sass, scanf, securerandom, semantic-puppet, sequel, shellwords, shrine, sidekiq, simplecov, sinatra, slim, sodium, sqlite3, strscan, thor, timecop, toml, tsort, typhoeus, tzinfo, unicode-normalize, uri, vcr, warden, webauthn, webmock, webrick, widgets, xslt, yaml, zeitwerk, zlib, activeldap, ldap, fast-gettext-locale — each at github.com/go-ruby-<name>/<name>, except commonmark, kramdown, liquid, mustache, nokogiri, rouge and xslt, which are bound from github.com/go-<name>/<name>. (set is not bound: Set is implemented in-tree, over rbgo's own object.Hash.)
The scientific / container stack (go-ndarray, go-fft, go-numerics, go-statistics, go-images, go-composites) binds in the same way.
How it differs from goruby¶
There is an existing Go project also called goruby. go-embedded-ruby takes a deliberately different path:
- Bytecode VM, not an AST-walking interpreter. Source is compiled to a YARV -style instruction sequence (ISeq) and executed on a stack machine, rather than re-walking the syntax tree on every call.
- Single, tree-shaken static binary.
rbgo buildselects only the reached stdlib and links one self-contained executable. - Dynamic
eval/requirevia an embedded front-end. The full compiler pipeline is in the binary, so code can be parsed and compiled at runtime. - Ruby 4.0 as the target. Behaviour is checked against MRI rather than approximated.
Where to go next¶
- Architecture overview — the compilation pipeline and the packages that make it up.
- Roadmap (phases) — the nine phases from vertical slice to conformance and performance.
- Phase 0 — Vertical slice — what runs today and how it is tested.
Source lives at github.com/go-embedded-ruby/ruby.