moonrpc — a real gRPC implementation for MoonBit (← gRPC). v0.8.1 adds Channel::stream_take, which drives a streaming call inline — no background reader — and returns once it has collected a bounded number of reply messages, so a caller can consume the first N of an unbounded subscription (etcd's Watch, a long server-streaming feed) and close, keeping the whole exchange in one task. v0.8.0 completes the client channel stack. Load balancing picks a READY sub-connection per call (pick_first or round_robin) from a connection pool that tracks each address through the gRPC connectivity states (IDLE/CONNECTING/READY/TRANSIENT_FAILURE/SHUTDOWN); a ManagedChannel dials several backends over real sockets and routes calls through the picker. Name resolution self-builds a DNS message codec (RFC 1035, A/AAAA with name compression) and a UDP resolver, so dns_connect turns a hostname into the backends it dials. Retry and hedging policies drive re-issue and parallel attempts — hedging fires a fresh attempt every hedging delay and commits on the first fatal status, cancelling the rest. Client keepalive pings an idle connection and closes it on a missed ACK. A multiplexed channel (MuxChannel) carries many concurrent calls over one connection through a shared read pump with per-stream inboxes and serialized writes, drives keepalive, and supports interactive bidirectional streaming — send while receiving, replies read message by message. v0.7.0 adds per-message gzip: a self-built DEFLATE (RFC 1951, stored/fixed/dynamic-Huffman) and gzip (RFC 1952, CRC-32 + ISIZE) reader that decompresses gzip requests on the server and gzip responses on the client, and client-side retry with exponential backoff bounded by the call deadline. It also hardens every decoder against malformed input: the gRPC length prefix, HPACK integer/string, protobuf, and health decoders now reject an out-of-range or overflowing length instead of aborting; flow-control windows saturate rather than wrap; peer SETTINGS are validated; the client reassembles a response header block across CONTINUATION; and header blocks and message sizes are capped. v0.6.1 self-builds a pure protobuf wire runtime (PbWriter/PbReader for varint, zigzag, fixed32/64, and length-delimited fields) and Server Reflection: a descriptor model and the grpc.reflection.v1.ServerReflection service (with its v1alpha alias) answering ListServices, FileContainingSymbol, and FileByFilename. v0.6 added the client half — a real Channel, a long-lived multiplexed h2c connection over @socket.Tcp driven by a pure H2Client engine symmetric to the server — plus the grpc.health.v1.Health service, server interceptors, -bin metadata, google.rpc.Status rich errors, and client-side grpc-timeout enforcement. The server engine (H2Server) serves all four call kinds over the self-built HTTP/2 (h2c) transport, driving the RFC 7540 frame layer, the stream state machine, complete HPACK (RFC 7541, with Huffman + dynamic table), and connection- and stream-level flow control. Includes the shared length-prefixed framing and the 17-code gRPC status model.
Dependencies
let server = @net.GrpcServer::new()
// unary: one request, one reply.
server.register("/greet.Greeter/SayHello", req => handle(req))
// server-streaming: one request, an ordered run of replies.
server.register_server_streaming("/count.C/Up", (ctx, req) => [
first(req), second(req), third(req),
])
// client-streaming: many requests collected, one reply at half-close.
server.register_client_streaming("/sum.S/Add", (ctx, msgs) => fold(msgs))
// bidi: each request echoed as it arrives, a farewell at half-close.
server.register_bidi("/chat.C/Echo", ctx => @moonrpc.BidiHandler::{
on_message: m => [reply_to(m)],
on_end: () => [b"bye"],
})
server.serve(port=50051) // a real gRPC / in-process client gets the replieslet engine = @moonrpc.H2Server::new()
engine.register("/echo.Echo/Say", req => req)
let out = engine.feed(frame) // -> [HEADERS(:status 200), DATA(reply), HEADERS(grpc-status:0)]let frame = @moonrpc.encode_message(b"hello grpc")
// [0x00][0x00 0x00 0x00 0x0A]["hello grpc"] — 1-byte flag + 4-byte big-endian length + payload
let (compressed, payload) = @moonrpc.decode_message(frame).unwrap()
// (false, b"hello grpc")let f = @moonrpc.Frame::Headers(
stream_id=1, fragment=block, end_stream=true, end_headers=true,
priority=None, padding=0,
)
let bytes = f.encode() // 9-octet header + payload
let (frame, consumed) = @moonrpc.decode_frame(bytes) // raises Incomplete until whole
@moonrpc.has_connection_preface(buf) // PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n (§3.5)let s = @moonrpc.Stream::new(1)
s.send(@moonrpc.StreamEvent::Headers(end_stream=false)) // -> Open
s.recv(@moonrpc.StreamEvent::Data(end_stream=true)) // -> HalfClosedRemotelet enc = @moonrpc.HpackEncoder::new(huffman=true)
let block = enc.encode([{ name: b":method", value: b"GET" },
{ name: b":authority", value: b"www.example.com" }])
let dec = @moonrpc.HpackDecoder::new()
let headers = dec.decode(block) // back to the same header list
@moonrpc.huffman_encode(b"www.example.com") // f1e3 c2e5 f23a 6ba0 ab90 f4ff — §C.4.1@moonrpc.Status::code(NotFound) // 5
@moonrpc.Status::name(Unauthenticated) // "UNAUTHENTICATED" (all 17 canonical grpc-status codes)
let m : @moonrpc.Method = { service: "greet.Greeter", name: "SayHello" }
m.path() // "/greet.Greeter/SayHello"impl Show for FrameErrorimpl Show for HpackErrorimpl Show for StreamErrorpub(all) enum Frame {
Data(stream_id~ : Int, data~ : Bytes, end_stream~ : Bool, padding~ : Int)
Headers(stream_id~ : Int, fragment~ : Bytes, end_stream~ : Bool, end_headers~ : Bool, priority~ : Priority?, padding~ : Int)
Priority(stream_id~ : Int, priority~ : Priority)
RstStream(stream_id~ : Int, error_code~ : Int)
Settings(params~ : Array[(Int, Int)], ack~ : Bool)
PushPromise(stream_id~ : Int, promised_id~ : Int, fragment~ : Bytes, end_headers~ : Bool, padding~ : Int)
Ping(payload~ : Bytes, ack~ : Bool)
GoAway(last_stream_id~ : Int, error_code~ : Int, debug~ : Bytes)
WindowUpdate(stream_id~ : Int, increment~ : Int)
Continuation(stream_id~ : Int, fragment~ : Bytes, end_headers~ : Bool)
Unknown(ftype~ : Int, flags~ : Int, stream_id~ : Int, payload~ : Bytes)
} derive(Eq)pub struct H2Server {
handlers : Map[String, Handler]
encoder : HpackEncoder
decoder : HpackDecoder
streams : Map[Int, SrvStream]
conn_recv_window : Int
conn_send_window : Int
remote_initial_window : Int
remote_max_frame : Int
goaway_received : Bool
}fn H2Server::register_bidi(self : H2Server, path : String, factory : (RpcContext) -> BidiHandler) -> Unitfn H2Server::register_client_streaming(self : H2Server, path : String, handler : (RpcContext, Array[Bytes]) -> Bytes) -> Unitfn H2Server::register_server_streaming(self : H2Server, path : String, handler : (RpcContext, Bytes) -> Array[Bytes]) -> Unitfn H2Server::register_unary(self : H2Server, path : String, handler : (RpcContext, Bytes) -> Bytes) -> Unitpub(all) enum Handler {
Unary((RpcContext, Bytes) -> Bytes)
ServerStreaming((RpcContext, Bytes) -> Array[Bytes])
ClientStreaming((RpcContext, Array[Bytes]) -> Bytes)
Bidi((RpcContext) -> BidiHandler)
}pub(all) struct Method {
service : String
name : String
}pub(all) struct SrvStream {
id : Int
state : StreamState
header_block : Buffer
data : Buffer
req_off : Int
req_msgs : Array[Bytes]
bidi_processed : Int
headers_complete : Bool
end_stream_recv : Bool
path : String
ctx : RpcContext
recv_window : Int
send_window : Int
out : Bytes
out_off : Int
started : Bool
started_response : Bool
response_ended : Bool
finalized : Bool
trailers_only : Bool
status_code : Int
headers_sent : Bool
trailers_sent : Bool
bidi : BidiHandler?
}pub(all) enum Status {
Ok
Cancelled
Unknown
InvalidArgument
DeadlineExceeded
NotFound
AlreadyExists
PermissionDenied
ResourceExhausted
FailedPrecondition
Aborted
OutOfRange
Unimplemented
Internal
Unavailable
DataLoss
Unauthenticated
} derive(Eq)pub(all) enum StreamEvent {
Headers(end_stream~ : Bool)
Data(end_stream~ : Bool)
Reserve
RstStream
} derive(Eq)impl Show for StreamStatelet connection_preface : Bytesfn decode_message(data : Bytes) -> (Bool, Bytes)?let default_max_frame_size : Intlet default_window_size : Intfn encode_message(payload : Bytes, compressed? : Bool) -> Byteslet error_no_error : Intlet flag_end_stream : Intfn has_connection_preface(data : Bytes) -> Boolfn hpack_decode_int(data : Bytes, offset : Int, prefix_bits : Int) -> (Int, Int)fn hpack_decode_string(data : Bytes, offset : Int) -> (Bytes, Int)fn hpack_encode_int(value : Int, prefix_bits : Int) -> Bytesfn hpack_encode_size_update(new_max : Int) -> Bytesfn hpack_encode_string(octets : Bytes) -> Bytesfn hpack_encode_string_auto(octets : Bytes) -> Bytesfn hpack_encode_string_huffman(octets : Bytes) -> Bytesfn hpack_static_entry(index : Int) -> (String, String)?fn hpack_string_is_huffman(data : Bytes, offset : Int) -> Boolfn huffman_encode(input : Bytes) -> Bytesfn huffman_encoded_length(input : Bytes) -> Intfn parse_grpc_timeout(v : Bytes) -> Int?let settings_header_table_size : Intfn stream_id_valid_for_initiator(id : Int, by_client~ : Bool) -> Boolfn stream_is_client_initiated(id : Int) -> Boolfn stream_is_server_initiated(id : Int) -> Boolmoonrpc — a real gRPC implementation for MoonBit (← gRPC). v0.8.1 adds Channel::stream_take, which drives a streaming call inline — no background reader — and returns once it has collected a bounded number of reply messages, so a caller can consume the first N of an unbounded subscription (etcd's Watch, a long server-streaming feed) and close, keeping the whole exchange in one task. v0.8.0 completes the client channel stack. Load balancing picks a READY sub-connection per call (pick_first or round_robin) from a connection pool that tracks each address through the gRPC connectivity states (IDLE/CONNECTING/READY/TRANSIENT_FAILURE/SHUTDOWN); a ManagedChannel dials several backends over real sockets and routes calls through the picker. Name resolution self-builds a DNS message codec (RFC 1035, A/AAAA with name compression) and a UDP resolver, so dns_connect turns a hostname into the backends it dials. Retry and hedging policies drive re-issue and parallel attempts — hedging fires a fresh attempt every hedging delay and commits on the first fatal status, cancelling the rest. Client keepalive pings an idle connection and closes it on a missed ACK. A multiplexed channel (MuxChannel) carries many concurrent calls over one connection through a shared read pump with per-stream inboxes and serialized writes, drives keepalive, and supports interactive bidirectional streaming — send while receiving, replies read message by message. v0.7.0 adds per-message gzip: a self-built DEFLATE (RFC 1951, stored/fixed/dynamic-Huffman) and gzip (RFC 1952, CRC-32 + ISIZE) reader that decompresses gzip requests on the server and gzip responses on the client, and client-side retry with exponential backoff bounded by the call deadline. It also hardens every decoder against malformed input: the gRPC length prefix, HPACK integer/string, protobuf, and health decoders now reject an out-of-range or overflowing length instead of aborting; flow-control windows saturate rather than wrap; peer SETTINGS are validated; the client reassembles a response header block across CONTINUATION; and header blocks and message sizes are capped. v0.6.1 self-builds a pure protobuf wire runtime (PbWriter/PbReader for varint, zigzag, fixed32/64, and length-delimited fields) and Server Reflection: a descriptor model and the grpc.reflection.v1.ServerReflection service (with its v1alpha alias) answering ListServices, FileContainingSymbol, and FileByFilename. v0.6 added the client half — a real Channel, a long-lived multiplexed h2c connection over @socket.Tcp driven by a pure H2Client engine symmetric to the server — plus the grpc.health.v1.Health service, server interceptors, -bin metadata, google.rpc.Status rich errors, and client-side grpc-timeout enforcement. The server engine (H2Server) serves all four call kinds over the self-built HTTP/2 (h2c) transport, driving the RFC 7540 frame layer, the stream state machine, complete HPACK (RFC 7541, with Huffman + dynamic table), and connection- and stream-level flow control. Includes the shared length-prefixed framing and the 17-code gRPC status model.
Dependencies