user4442671
user4442671

Reputation:

How to load a .proto file in protoregistry

I need to simply load the contents of a few .proto files directly as descriptor so I can manipulate them via reflection as per https://pkg.go.dev/google.golang.org/protobuf/reflect/protoregistry

From what I can gather, I need to

  1. Load the contents of the file as a string.
  2. Use prototext to Unmarshal the string into a descriptorpb.FileDescriptorProto
  3. Initialize a ProtoFileDescriptor with that
  4. Finally I can register the ProtoFileDescriptor in a registry.

Do I really need to jump through all of these hoops, or am I completely missing another API?

Upvotes: 2

Views: 2692

Answers (2)

BearsEars
BearsEars

Reputation: 1109

I was digging around trying to figure out a way to register proto files with protoregistry without using the protoc executable on the host machine. I came upon this code in open policy agent that does it with bufbuild/protocompile (which describes itself as "the spiritual successor to the github.com/jhump/protoreflect/desc/protoparse package"):

https://github.com/open-policy-agent/conftest/blob/68e075c067f82f415a070ce019554894a8228cf7/parser/textproto/textproto.go#L101

In my case, I wanted to not only register messages and types with protoregistry but I also wanted to set up a generic service handler as described in gRPC GO Single Generic Service Handler so I modified the open policy agent code a bit:

func (s *server) registerServicesFromProtoFileDynamicpbNoProtoc(protoFileName string) error {

    // Skip this file if we already registered it.
    if _, err := protoregistry.GlobalFiles.FindFileByPath(protoFileName); err == nil {
        return nil
    }

    fh, err := os.Open(protoFileName)
    if err != nil {
        return fmt.Errorf("open file: %w", err)
    }
    defer fh.Close()

    handler := reporter.NewHandler(nil)
    node, err := parser.Parse(protoFileName, fh, handler)
    if err != nil {
        return fmt.Errorf("parse proto: %w", err)
    }

    res, err := parser.ResultFromAST(node, true, handler)
    if err != nil {
        return fmt.Errorf("convert from AST: %w", err)
    }

    fd, err := protodesc.NewFile(res.FileDescriptorProto(), protoregistry.GlobalFiles)
    if err != nil {
        return fmt.Errorf("convert to FileDescriptor: %w", err)
    }

    if err := protoregistry.GlobalFiles.RegisterFile(fd); err != nil {
        return fmt.Errorf("register file: %w", err)
    }

    for i := 0; i < fd.Messages().Len(); i++ {
        msg := fd.Messages().Get(i)
        if err := protoregistry.GlobalTypes.RegisterMessage(dynamicpb.NewMessageType(msg)); err != nil {
            return fmt.Errorf("register message %q: %w", msg.FullName(), err)
        }
    }
    for i := 0; i < fd.Extensions().Len(); i++ {
        ext := fd.Extensions().Get(i)
        if err := protoregistry.GlobalTypes.RegisterExtension(dynamicpb.NewExtensionType(ext)); err != nil {
            return fmt.Errorf("register extension %q: %w", ext.FullName(), err)
        }
    }

    for svcNum := 0; svcNum < fd.Services().Len(); svcNum++ {
        svc := fd.Services().Get(svcNum)
        serviceName := string(svc.FullName())
        s.sdMap2[serviceName] = svc
        gsd := grpc.ServiceDesc{ServiceName: serviceName, HandlerType: (*interface{})(nil)}
        for methodNum := 0; methodNum < svc.Methods().Len(); methodNum++ {
            m := svc.Methods().Get(methodNum)
            gsd.Methods = append(gsd.Methods, grpc.MethodDesc{MethodName: string(m.Name()), Handler: s.GenericHandler})
        }
        s.grpcServer.RegisterService(&gsd, s)
    }

    return nil
}

My server struct was also modified from the generic service handler post above:

type server struct {
    tunnel.UnimplementedRequestTunnelServer
    sdMap                  map[string]*desc.ServiceDescriptor         // this uses the github.com/jhump/protoreflect/desc package that I didn't want to use
    sdMap2                 map[string]protoreflect.ServiceDescriptor  // Instead, I use the official protoreflect package
    grpcServer             *grpc.Server
}

Upvotes: 1

user4442671
user4442671

Reputation:

In case someone else lands on this question:

func registerProtoFile(src_dir string, filename string) error {
    // First, convert the .proto file to a file descriptor set
    tmp_file := filename + "tmp.pb"
    cmd := exec.Command("protoc", 
        "--descriptor_set_out=" + tmp_file, 
        "-I"+src_dir  
        path.Join(src_dir, filename))

    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    err := cmd.Run()
    if err != nil {
        return err
    }

    defer os.Remove(tmp_file)

    // Now load that temporary file as a file descriptor set protobuf
    protoFile, err := ioutil.ReadFile(tmp_file)
    if err != nil {
        return err
    }

    pb_set := new(descriptorpb.FileDescriptorSet)
    if err := proto.Unmarshal(protoFile, pb_set); err != nil {
        return err
    }

    // We know protoc was invoked with a single .proto file
    pb := pb_set.GetFile()[0]

    // Initialize the File descriptor object
    fd, err := protodesc.NewFile(pb, protoregistry.GlobalFiles)
    if err != nil {
        return err
    }

    // and finally register it.
    return protoregistry.GlobalFiles.RegisterFile(fd)
}

Upvotes: 1

Related Questions