A pure-Go (no cgo), MRI-faithful reimplementation of the surface of Ruby's
grpc gem — the GRPC
object model a Ruby program uses to build servers and stubs — without any Ruby
runtime and without the gem's C extension (upstream grpc ships as a heavy C
extension around the gRPC-core C library).
It does not reimplement HTTP/2 or the gRPC wire protocol. It is a
Ruby-faithful API layer on top of
google.golang.org/grpc and
google.golang.org/protobuf,
the official pure-Go gRPC and protobuf runtimes. Every byte on the wire is
produced by the canonical Go gRPC stack, so a server built here interoperates
with any conformant gRPC peer and a stub built here can call any conformant gRPC
server — by construction.
It is the gRPC binding for
go-embedded-ruby, and it reuses
go-ruby-protobuf (the pure-Go
google-protobuf gem) for the message layer. It is a sibling of
go-ruby-oauth2,
go-ruby-regexp and
go-ruby-net-http.
The network is a host seam. The core server logic and client stub never touch a socket: both the listener and the dialer come from an injected
Transport.NetTransportis the production transport (real TCP);MemTransportis an in-process, bufconn-backed transport that carries a real HTTP/2 gRPC session over an in-memory pipe. So the whole stack is exercised end-to-end in tests without binding a port — mirroring the host seam the OIDC/OAuth2 bindings use for their HTTP round-trip.
Faithful port of the grpc gem's server and client surface:
GRPC::RpcServer→RpcServer—#add_http2_port→AddHTTP2Port,#handle→Handle,#run/#run_till_terminated→Run/RunTillTerminated,#stop→Stop.GRPC::ClientStub→ClientStub— all four cardinalities:#request_response→RequestResponse,#client_streamer→ClientStreamer,#server_streamer→ServerStreamer,#bidi_streamer→BidiStreamer; per-call deadlines and metadata (a Hash).GRPC::ActiveCall→ActiveCall—Send,Read,EachRemoteRead,Metadata,Deadlineover one call.GRPC::Core::StatusCodes→ theStatusCodeconstants (OK,InvalidArgument,DeadlineExceeded, …, all 17), with the gem's SCREAMING_SNAKE names.GRPC::BadStatus→*BadStatus(code,details, trailing metadata,to_status), andGRPC::Core::CallError→*CallError; full error mapping to and from the gRPC runtime.- Message-agnostic, exactly like the gem: each call carries a
Marshal/Unmarshalfunction (the marshal/unmarshal procs a generated*_services_pb.rbattaches to aRpcDesc). Messages from go-ruby-protobuf drop straight in via itsEncode/Decode.
CGO-free, 100% test coverage, -race clean, gofmt + go vet clean, and
green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le,
s390x — including the big-endian s390x).
go get github.com/go-ruby-grpc/grpcpackage main
import (
"fmt"
grpc "github.com/go-ruby-grpc/grpc"
)
func main() {
tr := grpc.NewMemTransport() // or grpc.NetTransport{} in production
// --- server: mirrors GRPC::RpcServer ---
srv := grpc.NewRpcServer(grpc.WithTransport(tr))
srv.AddHTTP2Port("localhost:50051", ":this_port_is_insecure")
srv.Handle(grpc.Service{
Name: "helloworld.Greeter",
Methods: []grpc.Method{{
Name: "SayHello",
Type: grpc.Unary,
RequestUnmarshal: func(b []byte) (any, error) { return string(b), nil },
ResponseMarshal: func(m any) ([]byte, error) { return []byte(m.(string)), nil },
UnaryHandler: func(req any, call *grpc.ActiveCall) (any, error) {
return "Hello " + req.(string), nil
},
}},
})
go srv.Run()
defer srv.Stop()
// --- client: mirrors GRPC::ClientStub ---
stub, _ := grpc.NewClientStub("localhost:50051", ":this_channel_is_insecure",
grpc.WithStubTransport(tr))
defer stub.Close()
resp, _ := stub.RequestResponse("/helloworld.Greeter/SayHello", "world", grpc.CallOptions{
Marshal: func(m any) ([]byte, error) { return []byte(m.(string)), nil },
Unmarshal: func(b []byte) (any, error) { return string(b), nil },
Metadata: grpc.Metadata{"x-trace": "abc"},
})
fmt.Println(resp) // Hello world
}ClientStreamer, ServerStreamer and BidiStreamer mirror the gem's streaming
helpers; a streaming handler uses ActiveCall.Read / EachRemoteRead to consume
requests and ActiveCall.Send to emit responses.
Real gem usage rarely builds a Service by hand: a .proto's service block is
compiled by grpc_tools_ruby_protoc into a *_services_pb.rb that declares a
GRPC::GenericService base class and its Stub. This package ports both halves.
-
GRPC::GenericService→GenericService—NewGenericService(name)thenRPC(...)per rpc (the gem'srpc :Name, In, Outmacro).BuildServicepairs the declarations with handlers to yield aServicetoHandle;StubClassderives the client stub (the gem'srpc_stub_class). The generated stub carries each rpc's marshal/unmarshal, so a caller supplies only the request:gs := grpc.NewGenericService("helloworld.Greeter"). RPC(grpc.RpcDesc{Name: "SayHello", Type: grpc.Unary, RequestMarshal: enc, RequestUnmarshal: dec, ResponseMarshal: enc, ResponseUnmarshal: dec}) svc, _ := gs.BuildService(grpc.Handlers{ "SayHello": func(req any, call *grpc.ActiveCall) (any, error) { return "Hello " + req.(string), nil }}) srv.Handle(svc) stub := gs.StubClass(clientStub) resp, _ := stub.RequestResponse("SayHello", "world", grpc.CallOptions{})
-
grpc_tools_ruby_protoc→GenerateRubyServices— given a.proto's service block (ServiceFile/ServiceGen/MethodGen), it emits the exact*_services_pb.rbsource, byte-for-byte as the gem's generator. All four cardinalities (stream(...)on the request and/or response), multiple services per file, dotted and underscored packages, nested and cross-package message types, and package-less files are reproduced. The generated Ruby loads unchanged and binds this runtime through go-embedded-ruby.src, _ := grpc.GenerateRubyServices(grpc.ServiceFile{ ProtoFile: "helloworld.proto", Package: "helloworld", Services: []grpc.ServiceGen{{Name: "Greeter", Methods: []grpc.MethodGen{ {Name: "SayHello", InputType: "helloworld.HelloRequest", OutputType: "helloworld.HelloReply"}}}}, })
The generator is checked against the real grpc_tools_ruby_protoc (the
grpc-tools gem) as a differential oracle: for each .proto, our output must
equal the binary's to the byte; the test skips only when the gem is not
installed, and inline goldens still pin the format in that case.
Residual (named, not silent): message-.proto parsing and message codegen
stay in go-ruby-protobuf (which
also builds descriptors at runtime rather than parsing .proto text); a Ruby
constant for a message imported from another package with a nested type is
approximated (the common same-package and flat cross-package cases are
byte-exact); and TLS/ChannelCredentials, xDS, channelz and health/reflection
services remain follow-ups.
_ = grpc.NewBadStatus(grpc.InvalidArgument, "bad argument", grpc.Metadata{})
// err.Error() == "3:bad argument"; err.Code.Name() == "INVALID_ARGUMENT"A handler returns a *BadStatus to fail a call with a specific code; the stub
raises the matching *BadStatus on the client side. Any other handler error maps
to UNKNOWN, exactly as the gem surfaces a bare exception.
| gem | this package |
|---|---|
GRPC::RpcServer.new |
NewRpcServer(...) |
#add_http2_port(addr, creds) |
(*RpcServer).AddHTTP2Port |
#handle(service) |
(*RpcServer).Handle |
#run / #run_till_terminated |
(*RpcServer).Run / RunTillTerminated |
#stop |
(*RpcServer).Stop |
GRPC::ClientStub.new(host, creds) |
NewClientStub(host, creds, ...) |
#request_response |
(*ClientStub).RequestResponse |
#client_streamer |
(*ClientStub).ClientStreamer |
#server_streamer |
(*ClientStub).ServerStreamer |
#bidi_streamer |
(*ClientStub).BidiStreamer |
GRPC::ActiveCall |
*ActiveCall |
GRPC::Core::StatusCodes::* |
the StatusCode constants |
GRPC::BadStatus |
*BadStatus |
GRPC::Core::CallError |
*CallError |
| metadata (a Hash) | Metadata (map[string]string) |
| generated marshal/unmarshal procs | Marshaler / Unmarshaler per call |
GRPC::GenericService |
*GenericService |
rpc :Name, In, Out |
(*GenericService).RPC |
Service.rpc_stub_class |
(*GenericService).StubClass → *GenericStub |
grpc_tools_ruby_protoc |
GenerateRubyServices |
The suite drives every RPC cardinality (unary + the three streaming shapes),
status-code and deadline propagation, metadata round-trips and error mapping
over the in-memory transport, verified against a real
google.golang.org/grpc server and client as the oracle: our stub calls a
plain grpc-go server and a plain grpc-go client calls our RpcServer, both over
bufconn, so protocol conformance is checked end-to-end — not asserted. A
go-ruby-protobuf message is round-tripped through the stack to confirm the
message-layer integration.
COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1 # 100.0%BSD-3-Clause — see LICENSE. Copyright the go-ruby-grpc/grpc authors.
Being pure Go (CGO=0), this library also compiles to WebAssembly — both
GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI).
CI builds both targets on every push, alongside the six 64-bit native/qemu arches.
GOOS=js GOARCH=wasm go build ./... # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./... # WASI (wasmtime, wasmer, wasmedge, …)