2

I'm struggling with Golang Protobuf APIv2.

I'm trying to envelope protobuf messages so that I can invoke functions on a server; functions that aren't known at compile time. Something of the form:

syntax = "proto3";

package p;

import "google/protobuf/any.proto";

option go_package = "...";

message FunctionRequest {
    string name = 1;
    google.protobuf.Any parameters = 3;
}
message FunctionResponse {
    google.protobuf.Any result = 1;
    string error = 2;

}

I'm aiming to be generic in using protobufs to define the functions rather than using e.g. Go structs directly.

The Google SDK is complex and I've been unable to find examples.

E.g.

syntax = "proto3";

package e;

option go_package = "...";

service Adder {
    rpc add(AdderRequest) returns (AdderResponse) {};
}
message AdderRequest {
    int32 a = 1;
    int32 b = 2;
}
message AdderResponse {
    int32 result = 1;
}

IIUC this raw text file isn't useful directly and needs to be converted to a descriptor file:

protoc \
--proto_path=./protos \
--descriptor_set_out=descriptor.pb \
protos/adder.proto

I assumed (evidently incorrectly) that I could then read this in and unmarshal it to descriptorpb.FileDescriptorProto:


b, err := ioutil.ReadFile("/path/to/descriptor.pb")
if err != nil {
    log.Fatal(err)
}

fdProto := &descriptorpb.FileDescriptorProto{}
proto.Unmarshal(b, fdProto)

Question: How should I be doing this?

Assuming that the client will originate calls for services like Adder above, the server will need some to be able to unmarshal pbany.Any parameters into the correct type, invoke the function and reverse the process for the result.

Bonus questions:

  • Will the SDK require me to use protoregistry to manage these incoming types? Or is there a simpler way? The invocation will include the function and the types for the parameters and result so, it should always be possible to marshal messages on-the-fly.
  • Any examples using protoregistry? I'm confused by the seemingly disjointed Files and Types
DazWilkin
  • 32,823
  • 5
  • 47
  • 88

1 Answers1

6

Ok, for the main question:

b, err := ioutil.ReadFile("/path/to/descriptor.pb")
if err != nil {
    log.Fatal(err)
}

fds := &descriptorpb.FileDescriptorSet{}
proto.Unmarshal(b, fds)

NOTE a FileDescriptorSet not a FileDescriptorProto

I continue to be confused by the disjoint Files and Types but I suspect there's an intent that the developer be able to move e.g. messages obtained from a file into a type.

I solved my immediate problem:

package main

import (
    "fmt"
    "io/ioutil"
    "log"

    pb ".../protos"
    "google.golang.org/protobuf/proto"
    "google.golang.org/protobuf/reflect/protodesc"
    "google.golang.org/protobuf/reflect/protoreflect"
    "google.golang.org/protobuf/reflect/protoregistry"
    "google.golang.org/protobuf/types/descriptorpb"
    "google.golang.org/protobuf/types/dynamicpb"
    "google.golang.org/protobuf/types/known/anypb"
)

var _ protodesc.Resolver = (*protoregistry.Files)(nil)

const (
    projectRoot        = "."
    descriptorFilename = "descriptor.pb" // The output from `protoc --descriptor_set_out ...`
    protoFilename      = "adder.proto"   // The protoFilename will be one (of possibly several) in desc
    packageName        = "e"
    serviceName        = "Adder"
)

var (
    // By convention, the request message to be serviceName+"Request"
    message = service + "Request"
)

func toAny(t string, m protoreflect.ProtoMessage) (*anypb.Any, error) {
    v, err := proto.Marshal(m)
    if err != nil {
        return nil, err
    }

    return &anypb.Any{
        TypeUrl: t,
        Value:   v,
    }, nil
}
func readDescriptorSetFile(name string) (*descriptorpb.FileDescriptorSet, error) {
    b, err := ioutil.ReadFile(name)
    if err != nil {
        return nil, err
    }

    fds := &descriptorpb.FileDescriptorSet{}
    proto.Unmarshal(b, fds)
    return fds, nil
}
func byPath(ff *protoregistry.Files, path string, messageName string) (protoreflect.MessageDescriptor, error) {
    fd, err := ff.FindFileByPath(path)
    if err != nil {
        return nil, err
    }

    mds := fd.Messages()
    return mds.ByName(protoreflect.Name(messageName)), nil
}
func byName(ff *protoregistry.Files, packageName string, messageName string) (protoreflect.MessageDescriptor, error) {
    d, err := ff.FindDescriptorByName(protoreflect.FullName(fmt.Sprintf("%s.%s", packageName, messageName)))
    if err != nil {
        log.Fatal(err)
    }

    md, ok := d.(protoreflect.MessageDescriptor)
    if !ok {
        return nil, fmt.Errorf("type assertion to MessageDescriptor failed: %s", d.FullName())
    }
    return md, nil
}
func unmarshal(md protoreflect.MessageDescriptor, a *anypb.Any) (*dynamicpb.Message, error) {
    m := dynamicpb.NewMessage(md)
    err := proto.Unmarshal(a.GetValue(), m)
    if err != nil {
        return nil, err
    }
    return m, nil
}
func main() {

    // protoc \
    // --proto_path=./protos \
    // --descriptor_set_out=descriptor.pb \
    // protos/adder.proto

    fds, err := readDescriptorSetFile(fmt.Sprintf("%s/%s", projectRoot, descriptorFilename))

    ff, err := protodesc.NewFiles(fds)
    if err != nil {
        log.Fatal(err)
    }

    a, err := toAny(
        fmt.Sprintf("%s.%s", packageName, message),
        &pb.AdderRequest{
            A: 39,
            B: 3,
        })
    if err != nil {
        log.Fatal(err)
    }

    {
        md, err := byPath(ff, protoFilename, message)
        if err != nil {
            log.Fatal(err)
        }
        m, err := unmarshal(md, a)
        if err != nil {
            log.Fatal(err)
        }
        log.Printf("%+v", m)
    }
    {
        md, err := byName(ff, packageName, message)
        if err != nil {
            log.Fatal(err)
        }
        m, err := unmarshal(md, a)
        if err != nil {
            log.Fatal(err)
        }
        log.Printf("%+v", m)

    }
}

NOTE This may seem roundabout because I have pb.AdderRequest but I'm looking for a solution where this only available by definition.

NOTE The code duplicates the approach: byPath and byName as I'm unsure which approach I will want.

Community
  • 1
  • 1
DazWilkin
  • 32,823
  • 5
  • 47
  • 88