Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/bpf2go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ up-to-date list.
disable this behaviour using `-no-global-types`. You can add to the set of
types by specifying `-type foo` for each type you'd like to generate.

## Editor integration

Pass `-compdb path/to/compile_commands.json` (or set `BPF2GO_COMPDB`) to upsert
[JSON Compilation Database](https://clang.llvm.org/docs/JSONCompilationDatabase.html)
entry for each source. The file is written before compilation, so the entry is
kept up to date even when the build step itself is unable to run.

## Examples

See [examples/kprobe](../../examples/kprobe/main.go) for a fully worked out example.
82 changes: 82 additions & 0 deletions cmd/bpf2go/compdb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//go:build !windows

package main

import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"

"golang.org/x/sys/unix"
)

// A single JSON Compilation Database entry.
// See https://clang.llvm.org/docs/JSONCompilationDatabase.html
type compdbEntry struct {
Directory string `json:"directory"`
File string `json:"file"`
Arguments []string `json:"arguments"`
}

// writeCompDB upserts entry into the compdb at path.
// Concurrent writers are serialised through a sibling `.lock` file.
// The file itself is replaced atomically via rename.
func writeCompDB(path string, entry compdbEntry) error {
lock, err := os.OpenFile(path+".lock", os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return fmt.Errorf("open compdb lock: %w", err)
}
defer lock.Close()

if err := unix.Flock(int(lock.Fd()), unix.LOCK_EX); err != nil {
return fmt.Errorf("lock compdb: %w", err)
}

var db []compdbEntry
data, err := os.ReadFile(path)
switch {
case err == nil:
if err := json.Unmarshal(data, &db); err != nil {
return fmt.Errorf("parse %s: %w", path, err)
}
case errors.Is(err, fs.ErrNotExist):
// new file
default:
return err
}

i := slices.IndexFunc(db, func(e compdbEntry) bool {
return e.File == entry.File
})
if i >= 0 {
db[i] = entry
} else {
db = append(db, entry)
}

slices.SortFunc(db, func(a, b compdbEntry) int {
return strings.Compare(a.File, b.File)
})

tmp, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".tmp*")
if err != nil {
return err
}
defer os.Remove(tmp.Name())

enc := json.NewEncoder(tmp)
enc.SetIndent("", " ")
if err := enc.Encode(db); err != nil {
tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
return os.Rename(tmp.Name(), path)
}
117 changes: 117 additions & 0 deletions cmd/bpf2go/compdb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//go:build !windows

package main

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"testing"

"github.com/go-quicktest/qt"
)

func readCompDB(tb testing.TB, path string) []compdbEntry {
tb.Helper()
data, err := os.ReadFile(path)
qt.Assert(tb, qt.IsNil(err))
var db []compdbEntry
qt.Assert(tb, qt.IsNil(json.Unmarshal(data, &db)))
return db
}

func TestCompDBCreate(t *testing.T) {
path := filepath.Join(t.TempDir(), "compile_commands.json")

entry := compdbEntry{
Directory: "/tmp",
File: "/tmp/foo.c",
Arguments: []string{"clang", "-c", "/tmp/foo.c"},
}
qt.Assert(t, qt.IsNil(writeCompDB(path, entry)))

db := readCompDB(t, path)
qt.Assert(t, qt.HasLen(db, 1))
qt.Assert(t, qt.Equals(db[0].File, entry.File))
qt.Assert(t, qt.DeepEquals(db[0].Arguments, entry.Arguments))
}

func TestCompDBUpsert(t *testing.T) {
path := filepath.Join(t.TempDir(), "compile_commands.json")

a1 := compdbEntry{Directory: "/tmp", File: "/tmp/a.c", Arguments: []string{"clang", "old"}}
b := compdbEntry{Directory: "/tmp", File: "/tmp/b.c", Arguments: []string{"clang", "b"}}
a2 := compdbEntry{Directory: "/tmp", File: "/tmp/a.c", Arguments: []string{"clang", "new"}}

qt.Assert(t, qt.IsNil(writeCompDB(path, a1)))
qt.Assert(t, qt.IsNil(writeCompDB(path, b)))
qt.Assert(t, qt.IsNil(writeCompDB(path, a2)))

db := readCompDB(t, path)
qt.Assert(t, qt.HasLen(db, 2))
for _, e := range db {
if e.File == "/tmp/a.c" {
qt.Check(t, qt.DeepEquals(e.Arguments, []string{"clang", "new"}))
}
}
}

func TestCompDBSorted(t *testing.T) {
path := filepath.Join(t.TempDir(), "compile_commands.json")

for _, name := range []string{"/tmp/z.c", "/tmp/a.c", "/tmp/m.c"} {
qt.Assert(t, qt.IsNil(writeCompDB(path, compdbEntry{
Directory: "/tmp", File: name, Arguments: []string{"clang"},
})))
}

db := readCompDB(t, path)
qt.Assert(t, qt.HasLen(db, 3))
qt.Check(t, qt.Equals(db[0].File, "/tmp/a.c"))
qt.Check(t, qt.Equals(db[1].File, "/tmp/m.c"))
qt.Check(t, qt.Equals(db[2].File, "/tmp/z.c"))
}

func TestCompDBConcurrent(t *testing.T) {
path := filepath.Join(t.TempDir(), "compile_commands.json")

const n = 16
errs := make(chan error, n)
var wg sync.WaitGroup
wg.Add(n)
for i := range n {
go func(i int) {
defer wg.Done()
errs <- writeCompDB(path, compdbEntry{
Directory: "/tmp",
File: fmt.Sprintf("/tmp/f%02d.c", i),
Arguments: []string{"clang"},
})
}(i)
}
wg.Wait()
close(errs)

for err := range errs {
qt.Check(t, qt.IsNil(err))
}

db := readCompDB(t, path)
qt.Assert(t, qt.HasLen(db, n))
}

func TestCompDBMalformedExisting(t *testing.T) {
path := filepath.Join(t.TempDir(), "compile_commands.json")
qt.Assert(t, qt.IsNil(os.WriteFile(path, []byte("{not json"), 0644)))

err := writeCompDB(path, compdbEntry{
Directory: "/tmp", File: "/tmp/a.c", Arguments: []string{"clang"},
})
qt.Assert(t, qt.IsNotNil(err))

data, rerr := os.ReadFile(path)
qt.Assert(t, qt.IsNil(rerr))
qt.Assert(t, qt.Equals(string(data), "{not json"))
}
29 changes: 21 additions & 8 deletions cmd/bpf2go/gen/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,28 @@ func insertDefaultFlags(flags []string) []string {
return result
}

// Compile C to a BPF ELF file.
func Compile(args CompileArgs) error {
cmd := exec.Command(args.CC, insertDefaultFlags(args.Flags)...)
cmd.Stderr = os.Stderr

// Argv returns the compiler invocation that Compile would run for args.
// Compiler binary + passed flags.
func Argv(args CompileArgs) ([]string, error) {
inputDir := filepath.Dir(args.Source)
relInputDir, err := filepath.Rel(args.Workdir, inputDir)
if err != nil {
return err
return nil, err
}

target := args.Target
if target == (Target{}) {
target.clang = "bpf"
}

argv := append([]string{args.CC}, insertDefaultFlags(args.Flags)...)

// C flags that can't be overridden.
if linux := target.linux; linux != "" {
cmd.Args = append(cmd.Args, "-D__TARGET_ARCH_"+linux)
argv = append(argv, "-D__TARGET_ARCH_"+linux)
}

cmd.Args = append(cmd.Args,
argv = append(argv,
"-Wunused-command-line-argument",
"-target", target.clang,
"-c", args.Source,
Expand All @@ -92,6 +92,19 @@ func Compile(args CompileArgs) error {
"-g",
fmt.Sprintf("-D__BPF_TARGET_MISSING=%q", "GCC error \"The eBPF is using target specific macros, please provide -target that is not bpf, bpfel or bpfeb\""),
)

return argv, nil
}

// Compile C to a BPF ELF file.
func Compile(args CompileArgs) error {
argv, err := Argv(args)
if err != nil {
return err
}

cmd := exec.Command(argv[0], argv[1:]...)
cmd.Stderr = os.Stderr
cmd.Dir = args.Workdir

if err := cmd.Run(); err != nil {
Expand Down
59 changes: 59 additions & 0 deletions cmd/bpf2go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"flag"
"fmt"
"io"
"maps"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -89,6 +90,9 @@ type bpf2go struct {
// Base directory of the Makefile. Enables outputting make-style dependencies
// in .d files.
makeBase string
// Path to a JSON compilation database. If non-empty, an entry for the source
// file is upserted before compilation starts.
compdb string
}

func (b2g *bpf2go) Debugln(a ...any) {
Expand All @@ -115,6 +119,8 @@ func newB2G(stdout io.Writer, args []string) (*bpf2go, error) {
flagTarget := fs.String("target", "bpfel,bpfeb", "clang target(s) to compile for (comma separated)")
fs.StringVar(&b2g.makeBase, "makebase", getEnv("BPF2GO_MAKEBASE", ""),
"write make compatible depinfo files relative to `directory` ($BPF2GO_MAKEBASE)")
fs.StringVar(&b2g.compdb, "compdb", getEnv("BPF2GO_COMPDB", ""),
"upsert an entry for the source into compile_commands.json at `path` ($BPF2GO_COMPDB)")
fs.Var(&b2g.cTypes, "type", "`Name` of a type to generate a Go declaration for, may be repeated")
fs.BoolVar(&b2g.skipGlobalTypes, "no-global-types", false, "Skip generating types for map keys and values, etc.")
fs.StringVar(&b2g.outputStem, "output-stem", "", "alternative stem for names of generated files (defaults to ident)")
Expand Down Expand Up @@ -202,6 +208,13 @@ func newB2G(stdout io.Writer, args []string) (*bpf2go, error) {
}
}

if b2g.compdb != "" {
b2g.compdb, err = filepath.Abs(b2g.compdb)
if err != nil {
return nil, err
}
}

if b2g.outputStem != "" && strings.ContainsRune(b2g.outputStem, filepath.Separator) {
return nil, fmt.Errorf("-output-stem %q must not contain path separation characters", b2g.outputStem)
}
Expand Down Expand Up @@ -307,6 +320,12 @@ func (b2g *bpf2go) convertAll() (err error) {
return err
}

if b2g.compdb != "" {
if err := b2g.writeCompDBEntry(); err != nil {
return fmt.Errorf("compdb: %w", err)
}
}

if !b2g.disableStripping {
b2g.strip, err = exec.LookPath(b2g.strip)
if err != nil {
Expand All @@ -323,6 +342,46 @@ func (b2g *bpf2go) convertAll() (err error) {
return nil
}

func (b2g *bpf2go) writeCompDBEntry() error {
targets := slices.SortedFunc(maps.Keys(b2g.targetArches), func(a, b gen.Target) int {
return strings.Compare(a.Suffix(), b.Suffix())
})
tgt := targets[0]

outputStem := b2g.outputStem
if outputStem == "" {
outputStem = strings.ToLower(b2g.identStem)
}
stem := fmt.Sprintf("%s_%s%s", outputStem, tgt.Suffix(), b2g.outputSuffix)

absOutPath, err := filepath.Abs(b2g.outputDir)
if err != nil {
return err
}
cwd, err := os.Getwd()
if err != nil {
return err
}

argv, err := gen.Argv(gen.CompileArgs{
CC: b2g.cc,
Flags: b2g.cFlags,
Workdir: cwd,
Source: b2g.sourceFile,
Dest: filepath.Join(absOutPath, stem+".o"),
Target: tgt,
})
if err != nil {
return err
}

return writeCompDB(b2g.compdb, compdbEntry{
Directory: filepath.Dir(b2g.sourceFile),
File: b2g.sourceFile,
Arguments: argv,
})
}

func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
removeOnError := func(f *os.File) {
if err != nil {
Expand Down
Loading