Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-ruby-grpc/grpc

grpc — go-ruby-grpc

Docs License Go Coverage

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. NetTransport is the production transport (real TCP); MemTransport is 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.

Features

Faithful port of the grpc gem's server and client surface:

  • GRPC::RpcServerRpcServer#add_http2_portAddHTTP2Port, #handleHandle, #run / #run_till_terminatedRun / RunTillTerminated, #stopStop.
  • GRPC::ClientStubClientStub — all four cardinalities: #request_responseRequestResponse, #client_streamerClientStreamer, #server_streamerServerStreamer, #bidi_streamerBidiStreamer; per-call deadlines and metadata (a Hash).
  • GRPC::ActiveCallActiveCallSend, Read, EachRemoteRead, Metadata, Deadline over one call.
  • GRPC::Core::StatusCodes → the StatusCode constants (OK, InvalidArgument, DeadlineExceeded, …, all 17), with the gem's SCREAMING_SNAKE names.
  • GRPC::BadStatus*BadStatus (code, details, trailing metadata, to_status), and GRPC::Core::CallError*CallError; full error mapping to and from the gRPC runtime.
  • Message-agnostic, exactly like the gem: each call carries a Marshal / Unmarshal function (the marshal/unmarshal procs a generated *_services_pb.rb attaches to a RpcDesc). Messages from go-ruby-protobuf drop straight in via its Encode / 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).

Install

go get github.com/go-ruby-grpc/grpc

Usage

package 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
}

Streaming

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.

Generated services & codegen

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::GenericServiceGenericServiceNewGenericService(name) then RPC(...) per rpc (the gem's rpc :Name, In, Out macro). BuildService pairs the declarations with handlers to yield a Service to Handle; StubClass derives the client stub (the gem's rpc_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_protocGenerateRubyServices — given a .proto's service block (ServiceFile / ServiceGen / MethodGen), it emits the exact *_services_pb.rb source, 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.

Status codes & errors

_ = 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.

Mapping to the gem

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

Tests & coverage

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%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-grpc/grpc authors.

WebAssembly

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, …)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages