This guide will walk you through the steps to create a plugin for protoc, the protocol buffer compiler. A plugin allows you to generate custom code based on your .proto files.

Step 1: Set Up Your Project

Create a new directory for your plugin and initialize a new Go module:

mkdir myprotocplugin
cd myprotocplugin
go mod init myprotocplugin

Step 2: Define the Main Function

Your plugin will read a CodeGeneratorRequest from standard input and write a CodeGeneratorResponse to standard output. Start by creating a main.go file:

package main

import (
    "google.golang.org/protobuf/compiler/protogen"
)

func main() {
    protogen.Options{}.Run(func(gen *protogen.Plugin) error {
        for _, file := range gen.Files {
            if !file.Generate {
                continue
            }
            generateFile(gen, file)
        }
        return nil
    })
}

Step 3: Generate Code

Create a function to generate code for each file:

func generateFile(gen *protogen.Plugin, file *protogen.File) {
    filename := file.GeneratedFilenamePrefix + ".pb.go"
    g := gen.NewGeneratedFile(filename, file.GoImportPath)

    g.P("// Code generated by protoc-gen-myplugin. DO NOT EDIT.")
    g.P()
    g.P("package ", file.GoPackageName)
    g.P()

    for _, message := range file.Messages {
        generateMessage(g, message)
    }

    for _, service := range file.Services {
        generateService(g, service)
    }
}

Step 4: Generate Message Code

Create a function to generate code for each message:

func generateMessage(g *protogen.GeneratedFile, message *protogen.Message) {
    g.P("// ", message.GoIdent.GoName, " is a message.")
    g.P("type ", message.GoIdent.GoName, " struct {")
    for _, field := range message.Fields {
        g.P(field.GoName, " ", field.GoType, " `json:\"", field.Desc.JSONName(), "\"`")
    }
    g.P("}")
    g.P()
}

Step 5: Generate Service Code

Create a function to generate code for each service:

func generateService(g *protogen.GeneratedFile, service *protogen.Service) {
    g.P("// ", service.GoName, " is a service.")
    g.P("type ", service.GoName, " interface {")
    for _, method := range service.Methods {
        g.P(method.GoName, "(", method.Input.GoIdent, ") (", method.Output.GoIdent, ", error)")
    }
    g.P("}")
    g.P()
}

Step 6: Handle Dependencies

If you need to add dependencies to the generated file, use the QualifiedGoIdent method. This method ensures that the correct import path is added to the file’s imports and returns the fully qualified name of the identifier.

Here is an example:

ErrorfName := g.QualifiedGoIdent(protogen.GoIdent{
    GoName:      "Errorf",
    GoImportPath: "fmt",
})

In this example, the QualifiedGoIdent method will add the fmt package to the file’s imports and return the correct name of the Errorf function. The ErrorfName variable will contain the string fmt.Errorf.

Step 7: Build and Test Your Plugin

Build your plugin:

go build -o protoc-gen-myplugin

Now, you need to copy your executable into one of the directories listed in your PATH environment variable to make it available for the protoc utility.

Test your plugin with protoc:

protoc --myplugin_out=. --myplugin_opt=paths=source_relative echosrv.proto

Conclusion

You have now created a basic plugin for protoc. This plugin reads .proto files and generates Go code based on the messages and services defined in those files. You can extend this plugin to generate more complex code as needed.