Sign in

    moonrpc

    Real gRPC for MoonBit over HTTP/2: all four call kinds, metadata, deadlines, rich errors, health checking, server reflection, retries, hedging and load balancing, with pure client and server engines that run on every backend.

    grpc
    rpc
    protobuf
    http2
    moonbit
    Download zip
    Version
    0.20.2
    License
    Apache-2.0
    Last updated
    3 hours ago
    Downloads
    544

    #moonrpc

    Real gRPC for MoonBit — ← gRPC.

    Check and Test License mooncakes

    Moved on mooncakes from Lfan-ke/moonrpc to moonbitstack/moonrpc.

    moonrpc is gRPC over HTTP/2, not gRPC-Web. HTTP/2 framing (RFC 9113) and HPACK (RFC 7541) come from moonhttp; this module is everything gRPC puts on top: all four call kinds, metadata, deadlines, status and rich errors, flow control, health checking, server reflection, retries, hedging and load balancing. The client and server engines are pure functions over frames and run on every backend; only the socket driver in net is native.

    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 answered 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() // listens on 12000

    The handler sees the call's RpcContext: the request metadata, the grpc-timeout deadline in milliseconds, and slots for response initial and trailing metadata.

    #Packages

    Each layer is its own subpackage, so a program links the layers it uses and no others. The root re-exports all of them, so @moonrpc.H2Server and @moonrpc.encode_message work as they always did.

    PackageWhat it holdsDepends on
    moonrpc/wiremessage framing, Status, Method, metadata rules, keepalive, HTTP/2 defaultsmoonhttp, moonvar, moonbase
    moonrpc/protoPbWriter/PbReader, the descriptor model and its codecmoonvar
    moonrpc/streamthe stream state machine, RpcContext, the four handler kindsmoonhttp
    moonrpc/balanceretry, hedging, connectivity states, balancers, DNS, the channel poolwire, moondate, moonpool, moonvar
    moonrpc/clientH2Client, CallReplywire, moonhttp, moonzip
    moonrpc/serverH2Server, interceptorswire, proto, stream, moonhttp, moonzip, moonbase
    moonrpc/servicehealth checking and server reflectionserver, stream, proto, moonvar
    moonrpc/netGrpcServer, Channel, MuxChannel, ManagedChannel over TCP (native)the root, moonbitlang/async

    A client links nothing of the server and a server nothing of the client; protobuf alone is proto.

    Constructors are written without the package. pub using re-exports types but not their constructors, so @moonrpc.Serving does not resolve. Write the bare constructor and let the expected type pick it: health.set_status("greet.Greeter", Serving). Where nothing gives the type, name the subpackage: @wire.Status::NotFound.

    #The Channel client

    A Channel is one long-lived h2c connection; every call multiplexes over it on its own client-allocated stream id, sharing the connection's HPACK and flow-control state.

    let chan = @net.Channel::connect("127.0.0.1", @net.port)

    // unary.
    let reply = chan.unary("/greet.Greeter/SayHello", request)
    reply.messages[0] // the reply message; reply.grpc_status is 0 on success

    // server-streaming: one request, every framed reply reassembled in order.
    let out = chan.server_streaming("/count.C/Up", request) // out.messages

    // client-streaming: many requests, one reply.
    let sum = chan.client_streaming("/sum.S/Add", [a, b, c])

    // a deadline: sent as grpc-timeout and enforced locally — if it elapses the
    // stream is reset and grpc_status comes back DEADLINE_EXCEEDED (4).
    let bounded = chan.unary("/slow.S/Wait", request, timeout_millis=200)

    MuxChannel carries many concurrent calls and interactive bidirectional streams over one connection; ManagedChannel dials several backends, tracks each through the gRPC connectivity states and picks one per call (pick_first or round_robin).

    #Health and interceptors

    The standard grpc.health.v1.Health service registers in one call, and server interceptors wrap unary and server-streaming handlers:

    let health = @moonrpc.HealthService::new()
    health.set_status("greet.Greeter", Serving)
    server.register_health(health) // Check + Watch on /grpc.health.v1.Health/*

    engine.add_unary_interceptor((ctx, req, next) => {
    // inspect, then proceed — or return without calling next to short-circuit.
    next(ctx, req)
    })

    #Protobuf and Server Reflection

    PbWriter and PbReader carry the proto3 binary format: varint, zigzag, fixed32/fixed64 and length-delimited fields. The base-128 varint and the zigzag fold are moonvar's, since DWARF writes the same integer; what is here is the field structure built over it.

    let w = @moonrpc.PbWriter::new()
    w.string_(1, "testing") // field 1, a length-delimited string
    w.int32(2, -1) // field 2, a varint (negative -> ten octets)
    let r = @moonrpc.PbReader::new(w.to_bytes())
    let (field, wire) = r.read_tag() // (1, LengthDelim)
    r.read_string() // "testing"

    On top of it, a descriptor model builds and parses FileDescriptorProto and FileDescriptorSet, and grpc.reflection.v1.ServerReflection (with its v1alpha alias) answers ListServices, FileContainingSymbol and FileByFilename, so grpcurl can list and describe a service. CI runs exactly that against cmd/greetd.

    let greeter : @moonrpc.ServiceDescriptor = {
    name: "Greeter",
    methods: [
    { name: "SayHello", input_type: ".greet.HelloRequest",
    output_type: ".greet.HelloReply",
    client_streaming: false, server_streaming: false },
    ],
    }
    let refl = @moonrpc.ReflectionService::new()
    refl.add_file(@moonrpc.FileDescriptor::new("greet.proto", "greet", services=[greeter]))
    server.register_reflection(refl)

    #The engines

    Everything below serve is a pure engine: feed turns a decoded frame into the frames to write back, so the whole server path (HPACK, flow control, dispatch, HEADERS + DATA + grpc-status trailers) runs in memory on every backend. H2Client is the same on the other side, and the root package's tests drive the two against each other with no socket.

    let 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)]

    What the server refuses: a request line that is not gRPC's (:method other than POST, a content-type not starting application/grpc, no te: trailers) gets an HTTP status instead of 200; SETTINGS_MAX_CONCURRENT_STREAMS is advertised and enforced with REFUSED_STREAM; overrunning a flow-control window is FLOW_CONTROL_ERROR; a field block must arrive contiguously (RFC 9113 §6.10); a call already past its grpc-timeout never reaches its handler.

    #Framing and status

    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")

    @moonrpc.Status::code(NotFound) // 5
    @moonrpc.Status::name(Unauthenticated) // "UNAUTHENTICATED", one of the 17 codes

    let m : @moonrpc.Method = { service: "greet.Greeter", name: "SayHello" }
    m.path() // "/greet.Greeter/SayHello"

    #Not yet

    • TLS. moonbitlang/async's TLS exposes no ALPN, so HTTP/2 runs as h2c until moontls carries it over TCP.
    • Response and request compression; incoming gzip is decompressed, outgoing messages are not compressed.
    • Concurrent dispatch on one connection: a slow handler holds up the other streams, which is also why health Watch sends once and server-side deadlines are not preemptive.
    • The rest of A6 retries (jitter, throttling, pushback, transparent retry), connection backoff, client interceptors, channelz and service config.
    • Descriptor fidelity for enums, nested types, imports and options, and transitive dependencies in reflection.

    #License

    Apache-2.0.

    BidiHandler

    A live bidirectional call. on_message is invoked once per fully-received request message and returns the reply messages to send right then; on_end runs after the client half-closes and returns the final replies. Both feed the same flow-controlled response stream, so responses interleave with requests.

    CallReply

    The completed result of a call: the HTTP :status, the numeric grpc-status (-1 if the peer never sent one), the response initial metadata, the reply messages in order, and the trailing metadata.

    ClientCall

    One client-side call: its stream id, the request side (the length-prefixed request body still to send, the send window, and whether the request has been half-closed), and the response side (the accumulating DATA with a cursor over the messages already pulled out, the recv window, the captured :status and grpc-status, and the response initial and trailing metadata).

    ConnectionPool

    A pool of sub-connections, one per resolved address (in resolver order), that picks a READY one per an LbPolicy.

    ConnectivityState

    A sub-connection's connectivity state (← gRPC connectivity.State).

    DnsRecord

    One answer resource record, carrying the textual address for A/AAAA records (empty for other types).

    DnsResponse

    A decoded DNS response: the transaction id echoed back, the response code (0 = NOERROR), and the answer records.

    FieldDescriptor

    One field of a message (FieldDescriptorProto). type_name is the fully-qualified name of the referenced message or enum for TypeMessage / TypeEnum, and empty for scalars.

    FieldLabel

    The FieldDescriptorProto.Label: proto3 fields are LabelOptional unless repeated.

    FieldType

    The FieldDescriptorProto.Type enum (proto3's scalar and composite field types). TypeGroup is intentionally absent — groups are removed from proto3.

    FileDescriptor

    A single .proto file (FileDescriptorProto): its filename, package, the message and service types it defines, and the syntax level. This is the unit Server Reflection returns.

    H2Client

    The client side of one HTTP/2 connection: the HPACK codec pair (stateful across every call on the connection), the live calls keyed by stream id, the next odd stream id to allocate (§5.1.1), and the connection-level flow-control state bounded by the peer's SETTINGS.

    H2Server

    The server side of one HTTP/2 connection: the HPACK codec pair, the live streams, the connection-level flow-control windows, and the peer's settings that bound what we may send. Persistent across the whole connection because HPACK and flow control are stateful.

    Handler

    A registered method, in one of gRPC's four cardinalities. The reply shape mirrors the request shape: streaming handlers produce an ordered Array[Bytes] of messages, each framed as its own length-prefixed gRPC message on the wire.

    HealthService

    A grpc.health.v1.Health service backed by a per-service status table. The empty key "" is the overall-server status; a fresh service reports the whole server SERVING.

    HedgingPolicy

    A method's hedging policy. max_attempts counts every parallel attempt (gRPC caps it at 5); a new attempt originates every hedging_delay_millis while attempts remain. A finished attempt commits the whole call unless its status is one of non_fatal, in which case hedging continues.

    Keepalive

    Keepalive state for one connection: the idle threshold that triggers a PING, the ACK deadline that declares the connection dead, and the timers tracking the last activity and any in-flight PING.

    LbPolicy

    A client-side load-balancing policy (← gRPC loadBalancingConfig).

    LoadBalancer

    A load balancer over a resolver's addresses, applying an LbPolicy to choose the next one.

    MessageDescriptor

    A message type (DescriptorProto): its (simple) name and its fields.

    Method

    A fully-qualified RPC method: package.Service and the method name.

    MethodDescriptor

    One RPC method (MethodDescriptorProto): its name, the fully-qualified request and response message names, and the two streaming flags that together pick the call cardinality.

    PbError

    A raised protobuf decode failure: Truncated when the buffer ends inside a field, BadWireType for a group or unknown wire type, Overflow for a varint longer than ten octets, and BadUtf8 for an invalid string field.

    PbReader

    A forward cursor over an encoded protobuf message. read_tag pulls the next field's number and wire type; the typed readers then consume its body. skip discards an unknown field's body so a decoder tolerates fields it does not know (protobuf forward compatibility).

    PbWriter

    An append-only protobuf message encoder. Field writers append a tag and the field body; to_bytes yields the finished message. Fields are written in the caller's order — protobuf places no ordering requirement on distinct fields, and a message struct's encoder writes them by ascending number by convention.

    ReflectionRequest

    A decoded ServerReflectionRequest, reduced to the one message_request oneof arm that was set. Extension-number queries are surfaced as Unsupported, which the service answers with an UNIMPLEMENTED error response.

    ReflectionResponse

    A decoded ServerReflectionResponse, reduced to the one message_response arm.

    ReflectionService

    A grpc.reflection.v1.ServerReflection service backed by an in-memory descriptor database. Each added FileDescriptor is indexed by filename and by every symbol (package.Service / package.Message) it defines, so a FileContainingSymbol or FileByFilename query resolves to the right file, and ListServices enumerates every registered service.

    RetryPolicy

    A method's retry policy. max_attempts counts the first try plus every retry (gRPC caps it at 5); a call is retried only while attempts remain and its status is in retryable. Backoff starts at initial_backoff_millis and grows by backoff_multiplier each retry, capped at max_backoff_millis.

    RpcContext

    The context surfaced to a handler for one RPC call: the invoked :path, the request metadata (custom HEADERS, minus the pseudo- and reserved gRPC headers), the deadline parsed from grpc-timeout (in milliseconds, None when absent), and mutable slots for response initial metadata and trailing metadata the handler can set before it returns.

    ServiceDescriptor

    A service (ServiceDescriptorProto): its (simple) name and its methods.

    ServingStatus

    The grpc.health.v1.HealthCheckResponse.ServingStatus enum.

    SrvStream

    One server-side stream: its lifecycle state, the accumulating request header block and DATA (with a cursor over the length-prefixed messages already pulled out of it), the per-stream flow-control windows, and the response side — the bytes still to send, whether the initial HEADERS and the trailers have gone out, and any live bidi call state.

    Status

    The 17 canonical gRPC status codes (grpc-status).

    Stream

    A mutable stream: its identifier and current lifecycle state. send/recv advance the state in place, raising on an illegal transition.

    StreamError

    An illegal stream transition (RFC 7540 §5.1): a frame not permitted in the current state (typically a STREAM_CLOSED or PROTOCOL_ERROR condition).

    StreamEvent

    A state-changing stream event: the arrival or departure of the frames that drive §5.1 transitions. Headers/Data carry the END_STREAM flag; Reserve is a PUSH_PROMISE reserving this (promised) stream. Frames that never change stream state (PRIORITY, WINDOW_UPDATE, SETTINGS, PING) are intentionally absent.

    StreamInterceptor

    A server-streaming interceptor: (ctx, request, next) -> replies. It can pre-process the request, post-process the reply sequence, or short-circuit.

    StreamState

    The lifecycle state of a single HTTP/2 stream (RFC 7540 §5.1).

    SubConn

    One address the Channel dials, tracking its connectivity state.

    UnaryInterceptor

    A unary server interceptor: (ctx, request, next) -> reply, where next is the remainder of the chain. Call next(ctx, request) to proceed, or return without calling it to short-circuit.

    WireType

    The four protobuf wire types carried by a field tag's low three bits. The two group types (3 start-group, 4 end-group) are deprecated and unsupported, so from_code rejects them.

    decode_file_descriptor_set

    Decode a FileDescriptorSet into its files.

    decode_health_request

    fn decode_health_request(msg : Bytes) -> Bytes

    Decode the service field of a HealthCheckRequest, or empty bytes when the field is absent (the overall-server check). Unknown fields are skipped.

    decode_health_response

    fn decode_health_response(msg : Bytes) ->
    ServingStatus

    Decode the status field of a HealthCheckResponse; an absent field reads as the 0 default (StatusUnknown).

    decode_message

    fn decode_message(data : Bytes) -> (Bool, Bytes)?

    Decode one gRPC length-prefixed message from the front of data, returning (compressed, payload), or None if fewer than a full frame is present.

    decode_reflection_request

    Decode a ServerReflectionRequest down to its message_request oneof arm. host (field 1) is accepted and ignored; an unset oneof reads as Unsupported.

    decode_reflection_response

    Decode a ServerReflectionResponse down to its message_response arm — the half an in-process reflection client (and the tests) needs to read an answer.

    default_max_frame_size

    let default_max_frame_size : Int

    The HTTP/2 default (and minimum) SETTINGS_MAX_FRAME_SIZE (RFC 7540 §6.5.2).

    default_max_streams

    let default_max_streams : Int

    The SETTINGS_MAX_CONCURRENT_STREAMS the server advertises and enforces unless a caller says otherwise: 100, the value gRPC and nghttp2 both settle on. RFC 9113 §5.1.2 leaves the setting unset by default, which means unbounded — and every live stream holds a request buffer, so a peer that opens streams and never finishes them pins memory for as long as the connection lasts.

    default_window_size

    let default_window_size : Int

    The HTTP/2 default flow-control window and initial SETTINGS_INITIAL_WINDOW_SIZE (RFC 7540 §6.9.2): 65 535 octets.

    dns_decode_response

    fn dns_decode_response(msg : Bytes) ->
    DnsResponse

    Decode a DNS response: echoed id, response code, and every answer record (A/AAAA records carry their textual address). Questions are skipped; authority and additional sections are ignored.

    dns_encode_query

    fn dns_encode_query(id : Int, name : String, qtype : Int) -> Bytes

    Encode a standard recursion-desired query for name of type qtype (dns_type_a or dns_type_aaaa) in class IN, with transaction id id.

    dns_type_a

    let dns_type_a : Int

    The DNS record type for an IPv4 address.

    dns_type_aaaa

    let dns_type_aaaa : Int

    The DNS record type for an IPv6 address.

    encode_file_descriptor_set

    fn encode_file_descriptor_set(files : Array[
    FileDescriptor
    ]) -> Bytes

    Encode a FileDescriptorSet (protoc --descriptor_set_out): the concatenation of FileDescriptorProtos under repeated field 1.

    encode_grpc_timeout

    fn encode_grpc_timeout(millis : Int) -> Bytes

    Encode whole millis as a grpc-timeout header value (RFC gRPC HTTP/2 mapping): the m (millisecond) unit when the count fits the 8-digit field, else seconds with the S unit.

    encode_health_request

    fn encode_health_request(service : Bytes) -> Bytes

    Encode a HealthCheckRequest: field 1 (service, a length-delimited string). An empty service name encodes to the empty message, the wire form of the overall-server check.

    encode_health_response

    fn encode_health_response(status :
    ServingStatus
    ) -> Bytes

    Encode a HealthCheckResponse: field 1 (status, a varint enum). The SERVING default 0 still encodes to the empty message per protobuf default-omission.

    encode_message

    fn encode_message(payload : Bytes, compressed? : Bool) -> Bytes

    Encode a payload as a gRPC Length-Prefixed-Message: a 1-byte compression flag, a 4-byte big-endian length, then the payload. This is the framing every gRPC transport shares (gRPC-Web over HTTP/1.1 and real gRPC over HTTP/2 alike).

    encode_reflection_request

    fn encode_reflection_request(req :
    ReflectionRequest
    ) -> Bytes

    Encode a ServerReflectionRequest carrying a single oneof arm. Used by an in-process reflection client (and by the round-trip tests).

    health_check_path

    let health_check_path : String

    The gRPC HTTP/2 path of the Check method.

    health_watch_path

    let health_watch_path : String

    The gRPC HTTP/2 path of the Watch method.

    is_binary_metadata

    fn is_binary_metadata(name : Bytes) -> Bool

    True when a metadata key carries binary, which gRPC spells by suffixing the key with -bin.

    max_header_list_size

    let max_header_list_size : Int

    The cap on one accumulated header block (HEADERS plus its CONTINUATION frames). Without it a peer could stream endless non-final CONTINUATION frames and grow the buffer without bound (a CONTINUATION flood); 128 KiB is far above any real gRPC request's headers.

    max_message_size

    let max_message_size : Int

    The default cap on a single received gRPC message (4 MiB, matching gRPC's default MaxRecvMsgSize). A length prefix above this — including one whose 4 bytes decode to a negative Int because the high bit is set — is rejected rather than trusted, so a hostile prefix can neither slice out of bounds nor pin unbounded buffer.

    message_len

    fn message_len(data : BytesView, at? : Int) -> Int?

    The payload length of the length-prefixed message at at, or None when fewer than the five octets of a prefix are there.

    A prefix with its high bit set reads back negative, which is how a hostile length is told from a real one before anything is sliced on it. Every reader of this framing goes through here — there are five of them across the client, the server and the socket transport, and they have to agree.

    metadata_value_from_wire

    fn metadata_value_from_wire(name : Bytes, value : Bytes) -> Bytes

    Decode a metadata value coming off the wire: base64 for a -bin key, verbatim otherwise.

    Padding is optional on the wire, and a peer may pad or not; anything outside the alphabet is skipped rather than rejected, which is what the gRPC spec asks a receiver to tolerate.

    metadata_value_to_wire

    fn metadata_value_to_wire(name : Bytes, value : Bytes) -> Bytes

    Encode a metadata value for the wire: base64 for a -bin key, verbatim otherwise.

    parse_grpc_timeout

    fn parse_grpc_timeout(v : Bytes) -> Int?

    Parse a grpc-timeout value (RFC: up to 8 ASCII digits then a unit — H/M/S/m/u/n) to whole milliseconds, flooring sub-millisecond units. None for a malformed value.

    percent_decode

    fn percent_decode(v : Bytes) -> String

    Percent-decode a received grpc-message value — the inverse of the server's percent_encode (gRPC spec §"Responses"): each %XX escape becomes the byte its two hex digits name, every other byte passes through, and a malformed escape is left literal; the result is read as UTF-8.

    reflection_v1_path

    let reflection_v1_path : String

    The gRPC HTTP/2 path of the v1 reflection stream.

    reflection_v1alpha_path

    let reflection_v1alpha_path : String

    The gRPC HTTP/2 path of the legacy v1alpha reflection stream. grpcurl tries v1 first and falls back to this, so a server registers both.

    split_messages

    fn split_messages(data : Bytes) -> Array[Bytes]

    Split a run of concatenated length-prefixed messages into their payloads, stopping at the first one that has not fully arrived.

    stream_id_valid_for_initiator

    fn stream_id_valid_for_initiator(id : Int, by_client~ : Bool) -> Bool

    Whether a peer that is a client (by_client = true) or server may legally open stream id: clients use odd ids, servers use even ids, and 0 is the connection control stream, openable by neither (RFC 7540 §5.1.1).

    stream_is_client_initiated

    fn stream_is_client_initiated(id : Int) -> Bool

    Whether id is a client-initiated stream: a non-zero odd identifier.

    stream_is_server_initiated

    fn stream_is_server_initiated(id : Int) -> Bool

    Whether id is a server-initiated (pushed) stream: a non-zero even identifier.

    Source Files