// STRYKE-GRPC — REFLECTION-BASED gRPC CLIENT FOR STRYKE // LIKE grpcurl, AS A PACKAGE

stryke package · cdylib libstryke_grpc.{dylib,so} · dlopen'd in-process on use Grpc · opt-in (kept out of stryke core)

Report GitHub Issues
// Color scheme

>_STRYKE-GRPC

Describe, call, decode. Generic, reflection-based gRPC client for stryke — list services, describe methods, call unary and streaming RPCs with JSON in/out. Like grpcurl but as a stryke package: it ships as a Rust cdylib that stryke dlopens in-process on first use Grpc.

Install

# from a release (no rustc on the consumer machine)
s pkg install -g github.com/MenkeTechnologies/stryke-grpc

# from a local checkout
cd ~/projects/stryke-grpc
cargo build --release            # produces target/release/libstryke_grpc.{dylib,so}
s pkg install -g .               # cdylib lands in ~/.stryke/store/grpc@<version>/

# one-liner
make install

The cdylib is dlopened in-process on first use Grpc — no helper-binary fork per call. A shared tokio runtime + tonic::Channel cache per endpoint + DescriptorPool cache per (endpoint, symbol) are held in OnceCell, so back-to-back calls reuse the same multiplexed HTTP/2 connection. Any stryke script that declares use Grpc resolves the package automatically.

Quick start: use Grpc

list servicesmy @services = Grpc::list target => "localhost:50051", plaintext => 1
describe a servicep to_json Grpc::describe "helloworld.Greeter", target => "localhost:50051", plaintext => 1
describe a methodp to_json Grpc::describe "helloworld.Greeter/SayHello", target => "localhost:50051", plaintext => 1
unary callmy $reply = Grpc::call "helloworld.Greeter/SayHello", { name => "stryke" }, target => "localhost:50051", plaintext => 1
auth headers / metadataGrpc::call "myapi.v1.Service/Auth", { token => "x" }, target => "api.example.com:443", headers => { "authorization" => "Bearer $ENV{JWT}" }
liveness probeGrpc::ping target => "localhost:50051", plaintext => 1

The full API lives in the README "API reference" section, and is mirrored below.

Connection options

Every public function accepts these connection keys in its %opts trailer. target is required; the rest are optional.

targethost:port (required) — also accepts http://... / https://...
plaintext1 to force HTTP/2 cleartext (no TLS)
authorityoverride the SNI hostname used during TLS validation
headershashref {k => v} or arrayref ["k:v", ...] — sent as gRPC metadata
timeout_sconnection timeout, seconds (default 30)

RPC calls (call and the three stream verbs) understand additional per-call keys: deadline_ms (per-call grpc-timeout), send_compression / accept_compression (gzip | zstd | deflate), max_recv_mb / max_send_mb (message-size caps), ca_cert (custom CA root, PEM), client_cert + client_key (mTLS identity, PEM), with_metadata => 1 (wrap the result as { response, metadata }), the JSON-shaping flags emit_defaults / proto_names / enum_numbers / stringify_64bit, and max_messages (cap a server / bidi stream drain).

API reference: RPC verbs

Eight functions open a channel. describe's $symbol is one of "pkg.Service" (service + method list), "pkg.Service/Method" (method + input fields), or "pkg.MessageType" (message + field list). call's $request is any stryke value the cdylib decodes against the method's resolved input MessageDescriptor.

Grpc::version()cdylib's CARGO_PKG_VERSION string
Grpc::ping(%opts)connectivity probe — 1 on success, 0 on failure
Grpc::list(%opts)@services[{ service => "pkg.Name" }, ...]
Grpc::describe($symbol, %opts)\%info — kind + fields for a service / method / message
Grpc::call($method, $request, %opts)unary — decoded \%response (or scalar / arrayref)
Grpc::server_stream($method, $request, %opts)\%{ messages, count } — response stream drained into an array
Grpc::client_stream($method, \@requests, %opts)\%response — arrayref of requests streamed, single reply
Grpc::bidi_stream($method, \@requests, %opts)\%{ messages, count } — arrayref in, replies drained out

Streaming. Bounded streams are modelled as JSON arrays, so they fit the blocking FFI with no callback bridge: server_stream drains the response stream into messages; client_stream sends an arrayref of requests and returns the single reply; bidi_stream sends an arrayref and drains the replies. max_messages caps a drain; with_metadata => 1 wraps the result with captured response metadata. client_stream and bidi_stream die if $requests is not an arrayref.

API reference: wire helpers

38 connection-free functions parse, build, and validate gRPC HTTP/2 primitives — no socket is opened. Every parse_* has a matching build_* inverse.

Grpc::status_code($name_or_code){ code, name }"NOT_FOUND"5 (codes from tonic)
Grpc::status_description($name_or_code){ code, name, description } — canonical one-line description
Grpc::status_codes()@{ {code, name} } — the full 17-code enum, numeric order
Grpc::parse_grpc_status($code){ code, name, valid } — out-of-range stays valid => 0 (no die)
Grpc::build_grpc_trailer($n_or_c, $message?){ code, name, grpc_status, grpc_message } — trailer pair, message percent-encoded
Grpc::http_status_for($n_or_c){ code, name, http_status } — gRPC → HTTP (grpc-gateway map)
Grpc::grpc_status_for_http($http){ http_status, code, name } — HTTP → gRPC (spec table, distinct)
Grpc::retriable_status($n_or_c){ code, name, retriable, transient } — gRPC retry design A6 set
Grpc::health_status($n_or_c){ status, name }grpc.health.v1.Health serving status
Grpc::encode_status_message($message)percent-encode for the grpc-message trailer (space NOT encoded)
Grpc::decode_status_message($encoded)inverse — %XX → byte, UTF-8 lossy
Grpc::parse_method($method){ full_service, package, service, method } from /pkg.Service/Method
Grpc::build_method(%opts)inverse — parts → /pkg.Service/Method
Grpc::split_full_method($method)permissive split — accepts slash OR dotted pkg.Service.Method
Grpc::parse_target($target)gRPC channel target URI — dns / unix / unix-abstract / ipv4 / ipv6
Grpc::build_target(%opts)inverse — {scheme, endpoint, authority, addresses} → target string
Grpc::parse_authority($authority){ authority, host, port } from HTTP/2 :authority (bracketed IPv6)
Grpc::build_authority(%opts)inverse — host[+port]host:port (IPv6 literal bracketed)
Grpc::is_binary_key($key)1 | "" — the -bin metadata suffix convention
Grpc::is_reserved_key($key){ key, reserved, reason } — pseudo / content-type / te / user-agent / grpc-
Grpc::is_pseudo_header($key){ key, pseudo, known } — leading : HTTP/2 pseudo-header
Grpc::valid_metadata_key($key){ key, valid, reason, binary } — Custom-Metadata grammar
Grpc::normalize_metadata_key($key){ key, normalized, changed, binary } — lowercase wire form
Grpc::valid_metadata_value($key, $value){ key, value, binary, valid, reason } — rule by key (-bin → base64)
Grpc::encode_bin_value($value)base64-encode bytes for a -bin key
Grpc::decode_bin_value($base64)inverse — padded / un-padded base64, UTF-8 lossy
Grpc::parse_content_type($ct){ content_type, valid, type, codec, default, reason } — rejects grpc-web
Grpc::build_content_type(%opts)inverse — {codec, default}application/grpc[+codec]
Grpc::parse_timeout($timeout){ value, unit, unit_name, nanos, seconds } — units H/M/S/m/u/n (case-sensitive)
Grpc::build_timeout($nanos)inverse — finest unit ≤ 8 digits, rounds up; 0 → "0n"
Grpc::parse_user_agent($ua){ user_agent, grpc_impl, grpc_version, custom }
Grpc::build_user_agent(%opts)inverse — impl[/version] + optional custom prefix
Grpc::compression_codecs()@codecsidentity, gzip, deflate, zstd
Grpc::valid_compression($enc){ encoding, valid, identity, supported } — identity always valid
Grpc::parse_accept_encoding($h){ header, encodings, unknown, valid } — order kept, unknowns flagged
Grpc::build_accept_encoding(\@codecs)inverse — validate + lowercase + de-dup; unsupported codec dies
Grpc::frame_message($payload, %opts){ frame, length, compressed } — LPM framing: flag + BE length + bytes (base64)
Grpc::unframe_message($frame)inverse — parse a base64 LPM frame, validate the length prefix

How reflection works

No .proto files live on the client — the descriptor graph is fetched at call time. On each list / describe / call, the cdylib (src/reflection.rs):

  1. Opens a bidi stream to grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo.
  2. Sends ListServices (for list) or FileContainingSymbol (for the requested symbol).
  3. Follows import chains via FileByFilename until the full FileDescriptorProto graph is loaded.
  4. Builds a prost-reflect DescriptorPool, looks up the method, decodes the JSON request into a DynamicMessage, encodes it, fires the call via tonic's low-level Grpc::unary + a passthrough BytesCodec, then decodes the response back to JSON.

The reflection client is generated at build time by build.rs (tonic-prost-build compiling proto/reflection.proto, client-only). If a server hides reflection in production, enable it on a dev / canary box — one builder line for tonic, similar for grpc-go / grpc-java.

Examples

Runnable scripts ship under examples/; each takes the target as @ARGV.

# list services via reflection
s examples/list_services.stk localhost:50051 --plaintext

# show a service's methods + input fields
s examples/describe_service.stk localhost:50051 helloworld.Greeter

# unary call with a JSON payload
s examples/call_unary.stk localhost:50051 helloworld.Greeter/SayHello '{"name":"stryke"}'

# minimum-viable tour (eval-guarded — runs even with no server)
GRPC_HOST=localhost:50051 s examples/discover.stk

# enumerate every service + method
GRPC_HOST=localhost:50051 s examples/reflect.stk

Scope

The package is intentionally narrower than tonic itself — "useful from a shell pipeline", not "complete API coverage".

list / describe / unary call (JSON in/out)shipped
server-, client-, and bidi-streaming (bounded → JSON arrays)shipped
TLS + native-roots; mTLS (client cert) + custom CA rootshipped
ASCII + binary (-bin) metadata; per-call deadline (grpc-timeout)shipped
gzip / zstd / deflate compression; message-size caps; response-metadata captureshipped
connection-free wire helpers (38 functions)shipped
--proto FILE fallback when reflection is offopen
unbounded / callback-style streaming for infinite streamsopen

Tests

The compile + surface tests run without a server; the live round-trip is opt-in via env vars.

cargo test                                          # compiles, no live calls
STRYKE_GRPC_TEST_TARGET=localhost:50051 \
STRYKE_GRPC_TEST_PLAINTEXT=1 \
s test t/                                           # live round-trip

Opt-in env vars: STRYKE_GRPC_TEST_TARGET (host:port, required for the live suite), STRYKE_GRPC_TEST_PLAINTEXT (truthy → --plaintext), STRYKE_GRPC_TEST_METHOD (pkg.Service/Method for the optional call test), STRYKE_GRPC_TEST_DATA (JSON payload, default {}). CI brings up a tonic-based reflection-enabled echo server and exercises list / describe / call against it.

Why a package, not a builtin

Reflection-driven means no .proto files needed on the client side. Targets that expose the gRPC reflection service work out of the box. Every gRPC client pulls in tonic + prost + tokio + hyper + rustls + a file-descriptor reflection stack (~30+ transitive crates) — useful when you need it, off the daily-driver path otherwise.

The stryke side is a thin .stk wrapper that calls grpc__* FFI symbols on the cdylib; the heavy code lives in libstryke_grpc.{dylib,so}, dlopened in-process on first use Grpc. Core stryke is never linked against this package's deps.

FFI layer

Each Grpc::* wrapper builds a JSON args dict and calls a sibling grpc__* symbol resolved out of libstryke_grpc.{dylib,so}. The cdylib is dlopened in-process on first use Grpc. It exports 46 grpc__* symbols — 8 RPC verbs (grpc__pkg_version, grpc__ping, grpc__list, grpc__describe, grpc__call, grpc__server_stream, grpc__client_stream, grpc__bidi_stream) plus 38 connection-free wire helpers (grpc__status_code, grpc__status_codes, grpc__parse_method, grpc__parse_target, grpc__valid_metadata_key, grpc__frame_message, …); the authoritative list is [ffi].exports in stryke.toml. A shared tokio runtime + tonic::Channel cache per endpoint + DescriptorPool cache per (endpoint, symbol) are held in OnceCell — no fork-per-call. Responses are JSON; errors come back as {"error": "<msg>"} and the wrapper dies with Grpc::<op>: <reason>.

Layout

stryke-grpc/
├── Cargo.toml             # cdylib crate manifest (crate-type = ["cdylib"], publish = false)
├── src/
│   └── lib.rs            # cdylib — grpc__* extern "C" exports
├── lib/                   # stryke .stk wrapper(s) — `use Grpc`
├── stryke.toml            # stryke package manifest ([ffi] exports table)
├── t/                     # zunit-style tests
├── examples/              # runnable .stk examples
├── Makefile               # `make install` builds + installs
└── docs/                  # this site (GitHub Pages)

Sibling packages

Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo: