>_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 services | my @services = Grpc::list target => "localhost:50051", plaintext => 1 |
| describe a service | p to_json Grpc::describe "helloworld.Greeter", target => "localhost:50051", plaintext => 1 |
| describe a method | p to_json Grpc::describe "helloworld.Greeter/SayHello", target => "localhost:50051", plaintext => 1 |
| unary call | my $reply = Grpc::call "helloworld.Greeter/SayHello", { name => "stryke" }, target => "localhost:50051", plaintext => 1 |
| auth headers / metadata | Grpc::call "myapi.v1.Service/Auth", { token => "x" }, target => "api.example.com:443", headers => { "authorization" => "Bearer $ENV{JWT}" } |
| liveness probe | Grpc::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.
target | host:port (required) — also accepts http://... / https://... |
plaintext | 1 to force HTTP/2 cleartext (no TLS) |
authority | override the SNI hostname used during TLS validation |
headers | hashref {k => v} or arrayref ["k:v", ...] — sent as gRPC metadata |
timeout_s | connection 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() | @codecs — identity, 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):
- Opens a bidi stream to
grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo. - Sends
ListServices(forlist) orFileContainingSymbol(for the requested symbol). - Follows import chains via
FileByFilenameuntil the fullFileDescriptorProtograph is loaded. - Builds a
prost-reflectDescriptorPool, looks up the method, decodes the JSON request into aDynamicMessage, encodes it, fires the call via tonic's low-levelGrpc::unary+ a passthroughBytesCodec, 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 root | shipped |
ASCII + binary (-bin) metadata; per-call deadline (grpc-timeout) | shipped |
| gzip / zstd / deflate compression; message-size caps; response-metadata capture | shipped |
| connection-free wire helpers (38 functions) | shipped |
--proto FILE fallback when reflection is off | open |
| unbounded / callback-style streaming for infinite streams | open |
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:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-clickhouse — ClickHouse
- stryke-demo — live demos for every connector
- stryke-docker — Docker daemon API
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mcpd — MCP servers without a runtime
- stryke-mongo — MongoDB
- stryke-mssql — Microsoft SQL Server
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-polars — Polars + ndarray + linalg + FFT
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-scylla — ScyllaDB / Cassandra
- stryke-search — Elasticsearch / OpenSearch
- stryke-selenium — browser automation (WebDriver)
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt
- stryke-zmq — ZeroMQ messaging