Skip to content

Commit 3d563d4

Browse files
committed
bpf2go: enable ebpf code reuse across go packages
Extract imports from "bpf2go.hfiles.go" in the output dir, scan packages for header files, and expose headers to clang. C code consumes headers by providing a go package path in include directive, e.g. bpf2go.hfiles.go: package awesome import ( _ "example.org/foo" ) frob.c: #include "example.org/foo/foo.h" It is handy for sharing code between multiple ebpf blobs withing a project. Even better, it enables sharing ebpf code between multiple projects using go modules as delivery vehicle. By listing build dependencies in a .go file, we ensure that they are properly reflected in go.mod. Signed-off-by: Nick Zavaritsky <[email protected]>
1 parent d0c8fc1 commit 3d563d4

File tree

4 files changed

+275
-14
lines changed

4 files changed

+275
-14
lines changed

cmd/bpf2go/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ up-to-date list.
3434
disable this behaviour using `-no-global-types`. You can add to the set of
3535
types by specifying `-type foo` for each type you'd like to generate.
3636

37+
## eBPF packages
38+
39+
The tool can pull header files from other Go packages. To enable, create
40+
`bpf2go.hfiles.go` in the output dir. Add packages you wish to pull headers
41+
from as imports, e.g.:
42+
43+
```
44+
// bpf2go.hfiles.go
45+
package awesome
46+
import _ "example.org/foo"
47+
```
48+
49+
Write `#include "example.org/foo/foo.h"` to include `foo.h` from `example.org/foo`.
50+
3751
## Examples
3852

3953
See [examples/kprobe](../../examples/kprobe/main.go) for a fully worked out example.

cmd/bpf2go/main.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import (
44
"errors"
55
"flag"
66
"fmt"
7+
"go/build"
8+
"go/parser"
9+
"go/token"
710
"io"
811
"os"
912
"os/exec"
1013
"path/filepath"
1114
"regexp"
1215
"slices"
1316
"sort"
17+
"strconv"
1418
"strings"
1519

1620
"github.com/cilium/ebpf"
@@ -273,6 +277,10 @@ func (b2g *bpf2go) convertAll() (err error) {
273277
}
274278
}
275279

280+
if err := b2g.addHeaders(); err != nil {
281+
return fmt.Errorf("adding headers: %w", err)
282+
}
283+
276284
for target, arches := range b2g.targetArches {
277285
if err := b2g.convert(target, arches); err != nil {
278286
return err
@@ -282,6 +290,79 @@ func (b2g *bpf2go) convertAll() (err error) {
282290
return nil
283291
}
284292

293+
// addHeaders exposes header files from packages listed in
294+
// $OUTPUT_DIR/bpf2go.hfiles.go to clang. C consumes them by giving a
295+
// golang package path in include, e.g.
296+
// #include "github.com/cilium/ebpf/foo/bar.h".
297+
func (b2g *bpf2go) addHeaders() error {
298+
f, err := os.Open(filepath.Join(b2g.outputDir, "bpf2go.hfiles.go"))
299+
if os.IsNotExist(err) {
300+
return nil
301+
}
302+
if err != nil {
303+
return err
304+
}
305+
defer f.Close()
306+
307+
fmt.Fprintf(b2g.stdout, "Processing packages listed in %s\n", f.Name())
308+
309+
fset := token.NewFileSet()
310+
ast, err := parser.ParseFile(fset, f.Name(), f, parser.ImportsOnly)
311+
if err != nil {
312+
return err
313+
}
314+
315+
var pkgs []*build.Package
316+
buildCtx := build.Default
317+
buildCtx.Dir = b2g.outputDir
318+
for _, s := range ast.Imports {
319+
path, _ := strconv.Unquote(s.Path.Value)
320+
if build.IsLocalImport(path) {
321+
return fmt.Errorf("local imports are not supported: %s", path)
322+
}
323+
pkg, err := buildCtx.Import(path, b2g.outputDir, 0)
324+
if err != nil {
325+
return err
326+
}
327+
if pkg.Dir == "" {
328+
return fmt.Errorf("%s is missing locally: consider 'go mod download'", path)
329+
}
330+
if len(hfiles(pkg)) == 0 {
331+
fmt.Fprintf(b2g.stdout, "Package doesn't contain .h files: %s\n", path)
332+
continue
333+
}
334+
pkgs = append(pkgs, pkg)
335+
}
336+
337+
if len(pkgs) == 0 {
338+
return nil
339+
}
340+
341+
vfs, err := createVfs(pkgs)
342+
if err != nil {
343+
return err
344+
}
345+
346+
path, err := persistVfs(vfs)
347+
if err != nil {
348+
return err
349+
}
350+
351+
b2g.cFlags = append([]string{"-ivfsoverlay", path, "-iquote", vfsRootDir}, b2g.cFlags...)
352+
return nil
353+
}
354+
355+
// hfiles lists .h files in a package
356+
func hfiles(pkg *build.Package) []string {
357+
var res []string
358+
for _, h := range pkg.HFiles { // includes .hpp, etc
359+
if strings.HasSuffix(h, ".h") {
360+
res = append(res, h)
361+
}
362+
}
363+
return res
364+
}
365+
285366
func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
286367
removeOnError := func(f *os.File) {
287368
if err != nil {

cmd/bpf2go/main_test.go

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"path/filepath"
1010
"strings"
1111
"testing"
12+
"testing/fstest"
1213

1314
"github.com/go-quicktest/qt"
1415

@@ -32,24 +33,12 @@ func TestRun(t *testing.T) {
3233
}
3334

3435
modDir := t.TempDir()
35-
execInModule := func(name string, args ...string) {
36-
t.Helper()
37-
38-
cmd := exec.Command(name, args...)
39-
cmd.Dir = modDir
40-
if out, err := cmd.CombinedOutput(); err != nil {
41-
if out := string(out); out != "" {
42-
t.Log(out)
43-
}
44-
t.Fatalf("Can't execute %s: %v", name, args)
45-
}
46-
}
4736

4837
module := internal.CurrentModule
4938

50-
execInModule("go", "mod", "init", "bpf2go-test")
39+
execInDir(t, modDir, "go", "mod", "init", "bpf2go-test")
5140

52-
execInModule("go", "mod", "edit",
41+
execInDir(t, modDir, "go", "mod", "edit",
5342
// Require the module. The version doesn't matter due to the replace
5443
// below.
5544
fmt.Sprintf("-require=%[email protected]", module),
@@ -106,6 +95,76 @@ func main() {
10695
}
10796
}
10897

98+
func execInDir(t *testing.T, dir, name string, args ...string) {
99+
t.Helper()
100+
101+
cmd := exec.Command(name, args...)
102+
cmd.Dir = dir
103+
if out, err := cmd.CombinedOutput(); err != nil {
104+
if out := string(out); out != "" {
105+
t.Log(out)
106+
}
107+
t.Fatalf("Can't execute %s: %v", name, args)
108+
}
109+
}
110+
111+
func TestImports(t *testing.T) {
112+
dir := t.TempDir()
113+
114+
f := func(s string) *fstest.MapFile { return &fstest.MapFile{Data: []byte(s)} }
115+
err := os.CopyFS(dir, fstest.MapFS{
116+
"foo/foo.go": f("package foo"),
117+
"foo/foo.h": f("#define EXAMPLE_ORG__FOO__FOO_H 1"),
118+
"bar/nested/nested.go": f("package nested"),
119+
"bar/nested/nested.h": f("#define EXAMPLE_ORG__BAR__NESTED__NESTED_H 1"),
120+
"bar/bpf2go.hfiles.go": f(`
121+
package bar
122+
import (
123+
_ "example.org/bar/nested"
124+
_ "example.org/foo"
125+
)`),
126+
"bar/bar.c": f(`
127+
//go:build ignore
128+
129+
// include from current module, package listed in bpf2go.hfiles.go
130+
#include "example.org/bar/nested/nested.h"
131+
#ifndef EXAMPLE_ORG__BAR__NESTED__NESTED_H
132+
#error "example.org/bar/nested/nested.h: unexpected file contents"
133+
#endif
134+
135+
// include from external module, package listed in bpf2go.hfiles.go
136+
#include "example.org/foo/foo.h"
137+
#ifndef EXAMPLE_ORG__FOO__FOO_H
138+
#error "example.org/foo/foo.h: unexpected file contents"
139+
#endif`)})
140+
if err != nil {
141+
t.Fatal("extracting assets", err)
142+
}
143+
144+
fooModDir := filepath.Join(dir, "foo")
145+
execInDir(t, fooModDir, "go", "mod", "init", "example.org/foo")
146+
147+
barModDir := filepath.Join(dir, "bar")
148+
execInDir(t, barModDir, "go", "mod", "init", "example.org/bar")
149+
execInDir(t, barModDir, "go", "mod", "edit", "-require=example.org/[email protected]")
150+
151+
execInDir(t, dir, "go", "work", "init")
152+
execInDir(t, dir, "go", "work", "use", fooModDir)
153+
execInDir(t, dir, "go", "work", "use", barModDir)
154+
155+
err = run(io.Discard, []string{
156+
"-go-package", "bar",
157+
"-output-dir", barModDir,
158+
"-cc", testutils.ClangBin(t),
159+
"bar",
160+
filepath.Join(barModDir, "bar.c"),
161+
})
162+
163+
if err != nil {
164+
t.Fatal("Can't run:", err)
165+
}
166+
}
167+
109168
func TestHelp(t *testing.T) {
110169
var stdout bytes.Buffer
111170
err := run(&stdout, []string{"-help"})

cmd/bpf2go/vfs.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"go/build"
7+
"os"
8+
"path/filepath"
9+
"slices"
10+
"strings"
11+
)
12+
13+
// vfs is LLVM virtual file system parsed from a file
14+
//
15+
// In a nutshell, it is a tree of "directory" nodes with leafs being
16+
// either "file" (a reference to file) or "directory-remap" (a reference
17+
// to directory).
18+
//
19+
// https://github.com/llvm/llvm-project/blob/llvmorg-18.1.0/llvm/include/llvm/Support/VirtualFileSystem.h#L637
20+
type vfs struct {
21+
Version int `json:"version"`
22+
CaseSensitive bool `json:"case-sensitive"`
23+
Roots []vfsItem `json:"roots"`
24+
}
25+
26+
type vfsItem struct {
27+
Name string `json:"name"`
28+
Type vfsItemType `json:"type"`
29+
Contents []vfsItem `json:"contents,omitempty"`
30+
ExternalContents string `json:"external-contents,omitempty"`
31+
}
32+
33+
type vfsItemType string
34+
35+
const (
36+
vfsFile vfsItemType = "file"
37+
vfsDirectory vfsItemType = "directory"
38+
)
39+
40+
func (vi *vfsItem) addDir(path string) (*vfsItem, error) {
41+
for _, name := range strings.Split(path, "/") {
42+
idx := vi.index(name)
43+
if idx == -1 {
44+
idx = len(vi.Contents)
45+
vi.Contents = append(vi.Contents, vfsItem{Name: name, Type: vfsDirectory})
46+
}
47+
vi = &vi.Contents[idx]
48+
if vi.Type != vfsDirectory {
49+
return nil, fmt.Errorf("adding %q: non-directory object already exists", path)
50+
}
51+
}
52+
return vi, nil
53+
}
54+
55+
func (vi *vfsItem) index(name string) int {
56+
return slices.IndexFunc(vi.Contents, func(item vfsItem) bool {
57+
return item.Name == name
58+
})
59+
}
60+
61+
func persistVfs(vfs *vfs) (_ string, retErr error) {
62+
temp, err := os.CreateTemp("", "")
63+
if err != nil {
64+
return "", err
65+
}
66+
defer func() {
67+
temp.Close()
68+
if retErr != nil {
69+
os.Remove(temp.Name())
70+
}
71+
}()
72+
73+
if err = json.NewEncoder(temp).Encode(vfs); err != nil {
74+
return "", err
75+
}
76+
77+
return temp.Name(), nil
78+
}
79+
80+
// vfsRootDir is the (virtual) directory where we mount go module sources
81+
// for the C includes to pick them, e.g. "<vfsRootDir>/github.com/cilium/ebpf".
82+
const vfsRootDir = "/.vfsoverlay.d"
83+
84+
// createVfs produces a vfs from a list of packages. It creates a
85+
// (virtual) directory tree reflecting package import paths and adds
86+
// links to header files. E.g. for github.com/foo/bar containing awesome.h:
87+
//
88+
// github.com/
89+
// foo/
90+
// bar/
91+
// awesome.h -> $HOME/go/pkg/mod/github.com/foo/bar@version/awesome.h
92+
func createVfs(pkgs []*build.Package) (*vfs, error) {
93+
roots := [1]vfsItem{{Name: vfsRootDir, Type: vfsDirectory}}
94+
for _, pkg := range pkgs {
95+
var headers []vfsItem
96+
for _, h := range hfiles(pkg) {
97+
headers = append(headers, vfsItem{Name: h, Type: vfsFile,
98+
ExternalContents: filepath.Join(pkg.Dir, h)})
99+
}
100+
dir, err := roots[0].addDir(pkg.ImportPath)
101+
if err != nil {
102+
return nil, err
103+
}
104+
dir.Contents = headers // NB don't append inplace, same package could be imported twice
105+
}
106+
return &vfs{CaseSensitive: true, Roots: roots[:]}, nil
107+
}

0 commit comments

Comments
 (0)