Helix RPC — Developer Guide¶
Helix RPC is a high-performance, multi-protocol RPC framework with unified code generation for Go and Rust. Define your service once in Protobuf or Thrift and Helix generates zero-reflection stubs that serve gRPC, Thrift Binary/Compact, HTTP/JSON REST, and bidirectional streaming — all on a single TCP port.
Zero-Configuration Business Logic Focus: Helix RPC is architected so developers spend zero time configuring network stacks, security parameters, database connection limits, Docker files, or Terraform scripts. Scaffolding a project with
helix-gen inityields a fully pre-configured, production-grade microservice. Your only task is writing the core business logic handler.
Table of Contents¶
- Architecture Overview
- Project Structure
- Quick Start
- Single-Port Protocol Sniffing
- Service Registration
- REST Transcoding
- Bidirectional Streaming
- Interceptors & Middleware
- Metadata Propagation
- Error Mapping
- Client Pooling & Load Balancing
- Service Discovery
- Shared-Memory (SHM) Transport
- Production Features
- Performance Tuning
Architecture Overview¶
┌─────────────────────────────────────────────────────────────────┐
│ Single TCP Port │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Allocation-Free Protocol Sniffer │ │
│ │ (Peeks first bytes: gRPC H2 preface / Thrift / HTTP) │ │
│ └────────┬──────────────┬──────────────┬────────────────────┘ │
│ │ │ │ │
│ ┌─────▼─────┐ ┌────▼────┐ ┌──────▼──────┐ │
│ │ gRPC/H2 │ │ Thrift │ │ HTTP/1.1 │ │
│ │ Handler │ │ Handler │ │ REST Handler│ │
│ └─────┬─────┘ └────┬────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌─────▼──────────────▼──────────────▼─────┐ │
│ │ Method Registry (zero-reflection) │ │
│ │ Interceptor Chain │ │
│ │ Metadata Context │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Key Design Principles¶
| Principle | Description |
|---|---|
| Zero-Reflection | Generated stubs use static type registration — no reflect package, no runtime schema lookup |
| Single-Port | One TCP listener sniffs the wire protocol and routes to the correct handler |
| Dual-Runtime | Identical semantics in Go (stdlib net/http + h2c) and Rust (tokio + hyper) |
| Architecture-Neutral | No x86 intrinsics; runs at full speed on ARM64 / Apple Silicon |
Project Structure¶
helix-rpc/
├── Makefile # Top-level operational shortcuts
├── test.sh # Main test suite script runner
├── compiler/ # Helix IDL compiler
│ ├── ast/ # Unified AST (Protobuf + Thrift → common IR)
│ ├── parser/ # .proto and .thrift parsers
│ └── codegen/ # Code generators (Go, Rust, Python, Node)
├── runtimes/ # Shared core runtime libraries
│ ├── go/ # Go runtime library
│ ├── rust/ # Rust runtime library
│ ├── python/ # Python runtime library
│ └── node/ # Node.js/TypeScript runtime library
├── tests/ # Matrix E2E integration test suites
│ ├── go-go/ # Go-to-Go E2E tests
│ ├── rust-rust/ # Rust-to-Rust E2E tests
│ ├── go-python-ai/ # Go-to-Python AI E2E tests
│ ├── node-node/ # Node.js E2E tests
│ └── schema/ # Shared .proto / .thrift definitions
├── integrations/ # Sidecar proxy extensions
│ └── envoy-filter/ # Envoy proxy-wasm filter in Rust
└── examples/ # Quickstarts and architectural examples
Quick Start¶
Go Server¶
package main
import (
"context"
"log"
runtime "github.com/helix-rpc/helix/runtime-go"
generated "github.com/helix-rpc/helix/generated"
)
func main() {
server := runtime.NewServer("0.0.0.0:9000")
// Register a unary RPC method
server.RegisterMethod("/myapp.UserService/GetUser", runtime.MethodInfo{
Decoder: func(dec func(interface{}) error) (interface{}, error) {
req := &generated.UserRequest{}
return req, dec(req)
},
Handler: func(ctx context.Context, req interface{}) (interface{}, error) {
r := req.(*generated.UserRequest)
return &generated.UserResponse{Name: "Hello, " + r.Name}, nil
},
})
log.Fatal(server.Start())
}
Rust Server¶
use std::sync::Arc;
use helix_rt::{HttpServiceHandler, handle_http_connection};
struct MyService;
#[async_trait::async_trait]
impl HttpServiceHandler for MyService {
async fn handle_request(&self, path: &str, body: Vec<u8>, is_json: bool)
-> Result<(Vec<u8>, String), String>
{
match path {
"/myapp.UserService/GetUser" => {
// Decode, process, encode...
Ok((response_bytes, content_type))
}
_ => Err(format!("unknown path: {}", path)),
}
}
}
Single-Port Protocol Sniffing¶
Helix uses an allocation-free sniffer that peeks at the first bytes of each TCP connection to determine the wire protocol:
| Byte Pattern | Protocol |
|---|---|
PRI * HTTP/2.0 (HTTP/2 preface) |
gRPC over HTTP/2 |
0x82 (Compact) or 0x80 0x01 (Binary) |
Thrift |
GET, POST, PUT, DELETE, PATCH |
HTTP/1.1 REST |
Go¶
// The sniffer is built into the Server. Just call Start():
server := runtime.NewServer(":9000")
server.Start()
// All protocols are served on port 9000
Rust¶
let protocol = helix_rt::sniff_protocol(&stream).await?;
match protocol {
Protocol::Grpc => { /* HTTP/2 handler */ }
Protocol::ThriftCompact | Protocol::ThriftBinary => { /* Thrift handler */ }
Protocol::Http => { /* REST handler */ }
_ => { /* unsupported */ }
}
Service Registration¶
Go — Static Method Registration¶
server.RegisterMethod("/pkg.Service/Method", runtime.MethodInfo{
Decoder: func(dec func(interface{}) error) (interface{}, error) {
req := &pb.MyRequest{}
return req, dec(req)
},
Handler: func(ctx context.Context, req interface{}) (interface{}, error) {
r := req.(*pb.MyRequest)
return handler.Process(ctx, r)
},
// Optional: bind REST path parameters
Binder: func(req interface{}, params map[string]string) error {
r := req.(*pb.MyRequest)
r.Id = params["id"]
return nil
},
})
REST Transcoding¶
Map HTTP verbs and URL paths to gRPC methods:
Go¶
server.RegisterRESTRoute("GET", "/v1/users/{user_id}", "/pkg.UserService/GetUser")
server.RegisterRESTRoute("POST", "/v1/users", "/pkg.UserService/CreateUser")
Rust¶
let rest_routes = vec![
RestRoute::new("GET", "/v1/users/{user_id}", "/pkg.UserService/GetUser"),
RestRoute::new("POST", "/v1/users", "/pkg.UserService/CreateUser"),
];
handle_http_connection(stream, handler, rest_routes, is_http2).await;
Path parameters like {user_id} are automatically extracted and merged into the JSON request body (or bound via the Binder function in Go).
Bidirectional Streaming¶
Helix supports full-duplex gRPC bidirectional streaming over HTTP/2.
Go¶
server.RegisterMethod("/pkg.Service/StreamData", runtime.MethodInfo{
IsStreaming: true,
StreamHandler: func(stream runtime.ServerStream) error {
for {
var req pb.DataRequest
if err := stream.Recv(&req); err == io.EOF {
return nil // client done
} else if err != nil {
return err
}
resp := &pb.DataResponse{Result: process(req)}
if err := stream.Send(resp); err != nil {
return err
}
}
},
})
Rust¶
struct MyStreamHandler;
#[async_trait::async_trait]
impl HttpStreamingHandler for MyStreamHandler {
fn is_streaming(&self, path: &str) -> bool {
path == "/pkg.Service/StreamData"
}
async fn handle_stream(
&self,
_path: &str,
mut stream: Box<dyn ServerStream>,
) -> Result<(), String> {
loop {
match stream.recv().await? {
None => return Ok(()), // client done
Some(payload) => {
let req = MyRequest::decode(&payload[..]).unwrap();
let resp_bytes = process_and_encode(req);
stream.send(resp_bytes).await?;
}
}
}
}
}
// Wire it up:
handle_http_connection_streaming(stream, handler, routes, streaming, true).await;
Wire Protocol¶
Each frame follows the standard gRPC length-prefixed format:
┌─────────┬──────────────┬─────────────────┐
│ 1 byte │ 4 bytes │ N bytes │
│ compress│ length │ protobuf │
│ flag │ (big-end.) │ payload │
└─────────┴──────────────┴─────────────────┘
Interceptors & Middleware¶
Go — Unary Server Interceptors¶
// Logging interceptor
server.AddInterceptor(func(
ctx context.Context,
req interface{},
info *runtime.UnaryServerInfo,
handler runtime.UnaryHandler,
) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("[%s] %v", info.FullMethod, time.Since(start))
return resp, err
})
Interceptors chain in registration order. Each interceptor wraps the next, forming a middleware pipeline.
Metadata Propagation¶
Request headers are automatically extracted into a typed metadata context.
Go¶
// Inside a handler:
if md, ok := runtime.FromContext(ctx); ok {
traceID := md.Get("x-trace-id")
// ...
}
Rust¶
// Inside an async handler (uses tokio task-local storage):
if let Some(trace_ids) = helix_rt::get_metadata("x-trace-id") {
println!("trace: {}", trace_ids[0]);
}
Error Mapping¶
Helix defines a unified ErrorCode enum that maps across protocols:
| Helix Code | gRPC Status | HTTP Status | Thrift Exception |
|---|---|---|---|
NotFound |
5 (NOT_FOUND) | 404 | ApplicationError |
InvalidArgument |
3 (INVALID_ARGUMENT) | 400 | ApplicationError |
Internal |
13 (INTERNAL) | 500 | ApplicationError |
Unauthenticated |
16 (UNAUTHENTICATED) | 401 | ApplicationError |
PermissionDenied |
7 (PERMISSION_DENIED) | 403 | ApplicationError |
Client Pooling & Load Balancing¶
Go¶
pool := runtime.NewClientConnPool([]string{
"127.0.0.1:9001",
"127.0.0.1:9002",
"127.0.0.1:9003",
})
pool.SetBalancer(&runtime.RoundRobinBalancer{})
conn, err := pool.Get()
// Use conn...
pool.Put(conn)
Rust¶
let pool = ClientConnPool::new(vec![
"127.0.0.1:9001".parse().unwrap(),
"127.0.0.1:9002".parse().unwrap(),
]);
pool.set_balancer(Box::new(RoundRobinBalancer::new()));
let conn = pool.get().await?;
// Use conn...
pool.put(conn).await;
Service Discovery¶
Helix provides a Resolver interface for pluggable service discovery:
Go¶
// Static resolver (built-in)
resolver := runtime.NewStaticResolver([]string{
"10.0.1.1:9000",
"10.0.1.2:9000",
})
addrs := resolver.Resolve("my-service")
// Custom resolver (e.g., Consul, etcd):
type MyResolver struct{}
func (r *MyResolver) Resolve(service string) []string {
// Query your service registry...
}
Rust¶
let resolver = StaticResolver::new(vec![
"10.0.1.1:9000".parse().unwrap(),
"10.0.1.2:9000".parse().unwrap(),
]);
let addrs = resolver.resolve("my-service").await;
Shared-Memory (SHM) Transport¶
For same-host IPC, Helix provides a POSIX shared-memory transport that bypasses the kernel network stack entirely.
Go¶
// Server side
shmListener, err := runtime.NewShmListener("/helix-shm-region", 4096)
defer shmListener.Close()
conn, _ := shmListener.Accept()
// Client side
conn, err := runtime.NewShmConn("/helix-shm-region", 4096)
conn.Write(data)
Rust¶
use helix_rt::ShmConn;
// Open or create a shared-memory region
let shm = ShmConn::new("/helix-shm-region", 4096)?;
shm.write(b"hello from rust")?;
let data = shm.read()?;
[!WARNING] SHM transport requires both processes to run on the same host. The memory region name must match between client and server. Ensure proper cleanup of
/dev/shm/entries.
Production Features¶
Helix RPC includes built-in support for standard production features.
1. Deadline / Timeout Propagation¶
Helix automatically extracts the grpc-timeout header and applies it to the context, terminating long-running requests early and returning a DEADLINE_EXCEEDED (status code 4) error if the deadline is reached.
Go¶
In Go, the deadline is automatically injected into the request context.Context. You can check for deadline expiration using standard Go practices:
func (s *myService) GetUserProfile(ctx context.Context, req *generated.UserProfile) (*generated.UserProfile, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(10 * time.Millisecond):
// do work...
}
}
Rust¶
In Rust, the handler future is automatically wrapped in a tokio::time::timeout block if a deadline is present. Standard tokio::select! or .await on downstream calls automatically propagates the cancellation.
2. Health Checking Service¶
Helix RPC registers a standard gRPC health-checking service at /grpc.health.v1.Health/Check.
Go¶
To set service-specific health status on the server:
server := runtime.NewServer("127.0.0.1:8080")
server.Health.SetServingStatus("my.Service", runtime.HealthServing)
Rust¶
// In Rust, HealthChecker is cloned and passed to connection handlers:
let hc = helix_rt::HealthChecker::new();
hc.set_serving_status("my.Service", helix_rt::HealthStatus::Serving).await;
3. Per-Message Compression¶
Helix RPC supports gzip compression on gRPC message frames.
Go¶
Client requests can specify grpc-encoding: gzip to compress message payloads. The server will dynamically decode compressed payloads and write compressed responses if grpc-encoding is requested by the client.
Rust¶
// GzipCompressor is exported by default:
use helix_rt::{Compressor, GzipCompressor};
let compressor = GzipCompressor;
let compressed = compressor.compress(&payload)?;
Performance Tuning¶
HTTP/2 Window Sizes¶
Helix configures generous HTTP/2 flow control windows by default:
| Setting | Default | Description |
|---|---|---|
| Connection window | 2 MB | Total buffered data per connection |
| Stream window | 1 MB | Buffered data per individual stream |
| Max concurrent streams | 250 | Parallel RPCs per connection |
Benchmark Results (vs. CloudWeGo Kitex)¶
| Metric | Helix | Kitex | Improvement |
|---|---|---|---|
| Thrift Serialization | ~95% faster | baseline | Static stubs vs. reflection |
| Memory per RPC | Lower | Higher | Zero-reflection, no schema cache |
| ARM64 Performance | Native speed | Degraded* | No x86-only codepaths |
*Kitex's dynamicgo fast-path is x86-only; falls back to reflection on ARM64.
[!TIP] For production deployments, consider: 1. Setting
GOMAXPROCSto match container CPU shares. 2. Ensuring the client connection pools (ClientPool) match thread/goroutine counts.
Publishing the Go Runtime¶
Since the Go runtime is housed inside a subdirectory of the main repository, it must be versioned and published using a subdirectory tag prefix to be correctly recognized by the Go module proxy.
To publish a new version of the Go runtime (e.g. v0.1.0):
# 1. Tag the repository with the runtime-go prefix
git tag runtime-go/v0.1.0
# 2. Push the tag to GitHub
git push origin runtime-go/v0.1.0
Once pushed, the Go proxy will automatically index it at:
github.com/helix-rpc/helix/runtime-go@v0.1.0
[!TIP] For production deployments, consider: - Using the SHM transport for sidecar/proxy patterns on the same host. - Enabling interceptors for OpenTelemetry tracing propagation. - Using the
StaticResolveras a starting point, then implement a customResolverfor Consul/etcd/Kubernetes service discovery.