Skip to content

Commit

Permalink
bpf2go: recognise go package paths in C includes
Browse files Browse the repository at this point in the history
Recognise go package paths in C includes when -gopkg-includes flag is
passed, e.g. #include "github.com/cilium/ebpf/foo/bar.h"

It is handy for sharing code between multiple ebpf blobs withing a
project. (The alternative is "../.." paths which are brittle, or
configuring include paths via CFLAGS in a Makefile which is something an
average Golang programmer is not comfortable with.)

Even better, it enables sharing ebpf code between multiple projects
using go modules as delivery vehicle.

Signed-off-by: Nick Zavaritsky <mejedi@gmail.com>
  • Loading branch information
mejedi committed Jun 14, 2024
1 parent f82b29b commit d3daf1c
Show file tree
Hide file tree
Showing 4 changed files with 355 additions and 7 deletions.
6 changes: 6 additions & 0 deletions cmd/bpf2go/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type compileArgs struct {
target string
// Depfile will be written here if depName is not empty
dep io.Writer
// LLVM vfsoverlay file path
vfsoverlay string
}

func compile(args compileArgs) error {
Expand All @@ -40,6 +42,10 @@ func compile(args compileArgs) error {
"-mcpu=v1",
}

if args.vfsoverlay != "" {
overrideFlags = append(overrideFlags, "-ivfsoverlay", args.vfsoverlay, "-iquote", vfsRootDir)
}

cmd := exec.Command(args.cc, append(overrideFlags, args.cFlags...)...)
cmd.Stderr = os.Stderr

Expand Down
44 changes: 37 additions & 7 deletions cmd/bpf2go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"crypto/sha256"
"errors"
"flag"
"fmt"
Expand Down Expand Up @@ -108,6 +109,10 @@ type bpf2go struct {
// Base directory of the Makefile. Enables outputting make-style dependencies
// in .d files.
makeBase string
// Recognise go package paths in C #include-s.
gopkgIncludes bool
// LLVM vfsoverlay file path (generated when gopkgIncludes is true).
vfsoverlay string
}

func newB2G(stdout io.Writer, args []string) (*bpf2go, error) {
Expand All @@ -132,6 +137,7 @@ func newB2G(stdout io.Writer, args []string) (*bpf2go, error) {
fs.StringVar(&b2g.outputStem, "output-stem", "", "alternative stem for names of generated files (defaults to ident)")
outDir := fs.String("output-dir", "", "target directory of generated files (defaults to current directory)")
outPkg := fs.String("go-package", "", "package for output go file (default as ENV GOPACKAGE)")
fs.BoolVar(&b2g.gopkgIncludes, "gopkg-includes", false, "recognise go package paths in C #include-s")
fs.SetOutput(b2g.stdout)
fs.Usage = func() {
fmt.Fprintf(fs.Output(), helpText, fs.Name())
Expand Down Expand Up @@ -297,6 +303,29 @@ func (b2g *bpf2go) convertAll() (err error) {
}
}

if b2g.gopkgIncludes {
mods, err := listMods(b2g.outputDir, "all")
if err != nil {
return fmt.Errorf("listing go modules: %w", err)
}
var mainModAndDirectDeps []mod
for _, m := range mods {
if m.Indirect {
continue
}
mainModAndDirectDeps = append(mainModAndDirectDeps, m)
}
vfs, err := createVfs(mainModAndDirectDeps)
if err != nil {
return fmt.Errorf("creating LLVM vfsoverlay: %w", err)
}
vfsID := sha256.Sum256([]byte(b2g.outputDir))
b2g.vfsoverlay, err = persistVfs(vfsID, vfs)
if err != nil {
return fmt.Errorf("persisting LLVM vfsoverlay: %w", err)
}
}

for target, arches := range b2g.targetArches {
if err := b2g.convert(target, arches); err != nil {
return err
Expand Down Expand Up @@ -363,13 +392,14 @@ func (b2g *bpf2go) convert(tgt target, goarches []goarch) (err error) {

var dep bytes.Buffer
err = compile(compileArgs{
cc: b2g.cc,
cFlags: cFlags,
target: tgt.clang,
dir: cwd,
source: b2g.sourceFile,
dest: objFileName,
dep: &dep,
cc: b2g.cc,
cFlags: cFlags,
target: tgt.clang,
dir: cwd,
source: b2g.sourceFile,
dest: objFileName,
dep: &dep,
vfsoverlay: b2g.vfsoverlay,
})
if err != nil {
return err
Expand Down
227 changes: 227 additions & 0 deletions cmd/bpf2go/vfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package main

import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
)

// vfs is LLVM virtual file system parsed from YAML file
//
// In a nutshell, it is a tree of "directory" nodes with leafs being
// either "file" (a reference to file) or "directory-remap" (a reference
// to directory).
//
// https://github.com/llvm/llvm-project/blob/llvmorg-18.1.0/llvm/include/llvm/Support/VirtualFileSystem.h#L637
type vfs struct {
Version int `json:"version"`
CaseSensitive bool `json:"case-sensitive"`
Roots []vfsItem `json:"roots"`
}

type vfsItem struct {
Name string `json:"name"`
Type vfsItemType `json:"type"`
Contents []vfsItem `json:"contents,omitempty"`
ExternalContents string `json:"external-contents,omitempty"`

// specificity is used internally to resolve conflicts
//
// We populate vfs from go modules. Surprisingly, module file
// trees may overlap, e.g. "github.com/aws/aws-sdk-go-v2" and
// "github.com/aws/aws-sdk-go-v2/internal/endpoints/v2".
specificity int
}

type vfsItemType string

const (
vfsFile vfsItemType = "file"
vfsDirectory vfsItemType = "directory"
vfsDirectoryRemap vfsItemType = "directory-remap"
)

// vfsAdd adds "directory-remap" entry for dir under vpath
func vfsAdd(root *vfsItem, vpath, dir string) error {
return root.vfsAdd(vpath, dir, vfsDirectoryRemap, len(vpath), defaultFS{})
}

type defaultFS struct{}

func (defaultFS) Open(name string) (fs.File, error) {
return os.Open(name)
}

func (defaultFS) Stat(name string) (fs.FileInfo, error) {
return os.Stat(name)
}

func (vi *vfsItem) vfsAdd(vpath, path string, typ vfsItemType, specificity int, fs fs.FS) error {
for _, name := range strings.Split(vpath, "/") {
if name == "" {
continue
}
idx := vi.index(name)
if idx == -1 {
switch vi.Type {
case vfsDirectoryRemap:
newItem := vfsItem{Name: vi.Name, Type: vfsDirectory}
if err := vfsPopulateFromDir(&newItem, vi.ExternalContents, vi.specificity, fs); err != nil {
return err
}
*vi, idx = newItem, newItem.index(name)
case vfsFile:
vi.Type, vi.ExternalContents = vfsDirectory, ""
}
if idx == -1 {
idx = len(vi.Contents)
vi.Contents = append(vi.Contents, vfsItem{Name: name, Type: vfsDirectory})
}
}
vi = &vi.Contents[idx]
}
switch vi.Type {
case vfsDirectory:
if len(vi.Contents) == 0 {
vi.Type, vi.ExternalContents, vi.specificity = typ, path, specificity
return nil
}
if typ == vfsFile {
return nil
}
return vfsPopulateFromDir(vi, path, specificity, fs)
case vfsDirectoryRemap, vfsFile:
if vi.specificity <= specificity {
vi.Type, vi.ExternalContents, vi.specificity = typ, path, specificity
}
}
return nil
}

func vfsPopulateFromDir(vi *vfsItem, dir string, specificity int, fsys fs.FS) error {
entries, err := fs.ReadDir(fsys, dir)
if err != nil {
return err
}
for _, entry := range entries {
path := filepath.Join(dir, entry.Name())
info, err := fs.Stat(fsys, path) // follows symlinks
if err != nil {
return err
}
typ := vfsFile
if info.Mode().IsDir() {
typ = vfsDirectoryRemap
}
if err := vi.vfsAdd(entry.Name(), path, typ, specificity, fsys); err != nil {
return err
}
}
return nil
}

func (vi *vfsItem) index(name string) int {
return slices.IndexFunc(vi.Contents, func(item vfsItem) bool {
return item.Name == name
})
}

// persistVfs stores vfs in user cache dir under a path derived from
// id; the file should stay around for the benefit of a language
// server / IDE
func persistVfs(id [sha256.Size]byte, vfs *vfs) (string, error) {
idHex := hex.EncodeToString(id[:])
cacheDir, err := os.UserCacheDir()
if err != nil {
return "", err
}
dir := filepath.Join(cacheDir, "bpf2go", "llvm-overlay", idHex[:2])
name := filepath.Join(dir, idHex[2:])
if err := os.MkdirAll(dir, 0700); err != nil {
return "", err
}
tempName, err := persistVfsTemp(dir, vfs)
if err != nil {
return "", err
}
if err := os.Rename(tempName, name); err != nil {
_ = os.Remove(tempName)
return "", err
}
return name, nil
}

func persistVfsTemp(dir string, vfs *vfs) (string, error) {
temp, err := os.CreateTemp(dir, "tmp")
if err != nil {
return "", err
}
enc := json.NewEncoder(temp)
err = enc.Encode(vfs)
_ = temp.Close()
if err != nil {
_ = os.Remove(temp.Name())
return "", err
}
return temp.Name(), nil
}

// mod describes go module as returned by `go list -json -m`
type mod struct {
Path, Dir string
Indirect bool
}

func listMods(dir string, args ...string) ([]mod, error) {
cmd := exec.Command("go", append([]string{"list", "-json", "-m"}, args...)...)
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return nil, err
}
return parseMods(bytes.NewReader(out))
}

func parseMods(r io.Reader) ([]mod, error) {
var res []mod
dec := json.NewDecoder(r)
for {
var mod mod

err := dec.Decode(&mod)
if err == io.EOF {
return res, nil
}
if err != nil {
return nil, err
}

res = append(res, mod)
}
}

// vfsRootDir is the (virtual) directory where we mount go module sources
// for the C includes to pick them, e.g. "<vfsRootDir>/github.com/cilium/ebpf".
const vfsRootDir = "/.vfsoverlay.d"

func createVfs(mods []mod) (*vfs, error) {
roots := [1]vfsItem{{Name: vfsRootDir, Type: vfsDirectory}}
for _, m := range mods {
if m.Dir == "" {
return nil, fmt.Errorf("%s is missing locally: consider 'go mod download'", m.Path)
}
if err := vfsAdd(&roots[0], m.Path, m.Dir); err != nil {
return nil, err
}
}
return &vfs{CaseSensitive: true, Roots: roots[:]}, nil
}
Loading

0 comments on commit d3daf1c

Please sign in to comment.