Skip to content

Commit 1d835dc

Browse files
committed
bpf2go: support multiple source files
1 parent 49a06b1 commit 1d835dc

File tree

4 files changed

+196
-48
lines changed

4 files changed

+196
-48
lines changed

cmd/bpf2go/main.go

Lines changed: 118 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ import (
2121
"github.com/cilium/ebpf/cmd/bpf2go/gen"
2222
)
2323

24-
const helpText = `Usage: %[1]s [options] <ident> <source file> [-- <C flags>]
24+
const helpText = `Usage: %[1]s [options] <ident> <source files...> [-- <C flags>]
2525
2626
ident is used as the stem of all generated Go types and functions, and
2727
must be a valid Go identifier.
2828
29-
source is a single C file that is compiled using the specified compiler
30-
(usually some version of clang).
29+
source files are C files that are compiled using the specified compiler
30+
(usually some version of clang) and linked together into a single
31+
BPF program.
3132
3233
You can pass options to the compiler by appending them after a '--' argument
3334
or by supplying -cflags. Flags passed as arguments take precedence
@@ -60,8 +61,8 @@ func run(stdout io.Writer, args []string) (err error) {
6061
type bpf2go struct {
6162
stdout io.Writer
6263
verbose bool
63-
// Absolute path to a .c file.
64-
sourceFile string
64+
// Absolute paths to .c files.
65+
sourceFiles []string
6566
// Absolute path to a directory where .go are written
6667
outputDir string
6768
// Alternative output stem. If empty, identStem is used.
@@ -186,13 +187,18 @@ func newB2G(stdout io.Writer, args []string) (*bpf2go, error) {
186187

187188
b2g.identStem = args[0]
188189

189-
sourceFile, err := filepath.Abs(args[1])
190-
if err != nil {
191-
return nil, err
190+
sourceFiles := args[1:]
191+
b2g.sourceFiles = make([]string, len(sourceFiles))
192+
for i, source := range sourceFiles {
193+
absPath, err := filepath.Abs(source)
194+
if err != nil {
195+
return nil, fmt.Errorf("convert source file to absolute path: %w", err)
196+
}
197+
b2g.sourceFiles[i] = absPath
192198
}
193-
b2g.sourceFile = sourceFile
194199

195200
if b2g.makeBase != "" {
201+
var err error
196202
b2g.makeBase, err = filepath.Abs(b2g.makeBase)
197203
if err != nil {
198204
return nil, err
@@ -298,10 +304,13 @@ func getBool(key string, defaultVal bool) bool {
298304
}
299305

300306
func (b2g *bpf2go) convertAll() (err error) {
301-
if _, err := os.Stat(b2g.sourceFile); os.IsNotExist(err) {
302-
return fmt.Errorf("file %s doesn't exist", b2g.sourceFile)
303-
} else if err != nil {
304-
return err
307+
// Check all source files exist
308+
for _, source := range b2g.sourceFiles {
309+
if _, err := os.Stat(source); os.IsNotExist(err) {
310+
return fmt.Errorf("file %s doesn't exist", source)
311+
} else if err != nil {
312+
return err
313+
}
305314
}
306315

307316
if !b2g.disableStripping {
@@ -320,6 +329,65 @@ func (b2g *bpf2go) convertAll() (err error) {
320329
return nil
321330
}
322331

332+
// compileOne compiles a single source file and returns the temporary object file name
333+
// and any dependencies found during compilation.
334+
func (b2g *bpf2go) compileOne(tgt gen.Target, cwd, source, objFileName, outputStem string) (tmpObjFileName string, deps []dependency, err error) {
335+
var depInput *os.File
336+
cFlags := slices.Clone(b2g.cFlags)
337+
if b2g.makeBase != "" {
338+
depInput, err = os.CreateTemp("", "bpf2go")
339+
if err != nil {
340+
return "", nil, err
341+
}
342+
defer depInput.Close()
343+
defer os.Remove(depInput.Name())
344+
345+
cFlags = append(cFlags,
346+
// Output dependency information.
347+
"-MD",
348+
// Create phony targets so that deleting a dependency doesn't
349+
// break the build.
350+
"-MP",
351+
// Write it to temporary file
352+
"-MF"+depInput.Name(),
353+
)
354+
}
355+
356+
// Compile to final object file name first
357+
err = gen.Compile(gen.CompileArgs{
358+
CC: b2g.cc,
359+
Strip: b2g.strip,
360+
DisableStripping: b2g.disableStripping,
361+
Flags: cFlags,
362+
Target: tgt,
363+
Workdir: cwd,
364+
Source: source,
365+
Dest: objFileName,
366+
})
367+
if err != nil {
368+
return "", nil, fmt.Errorf("compile %s: %w", source, err)
369+
}
370+
371+
// Move the compiled object to a temporary file
372+
tmpObjFileName = filepath.Join(filepath.Dir(objFileName), fmt.Sprintf("%s_%s_%s.o",
373+
outputStem,
374+
filepath.Base(source),
375+
tgt.Suffix()))
376+
if err := os.Rename(objFileName, tmpObjFileName); err != nil {
377+
return "", nil, fmt.Errorf("move object file: %w", err)
378+
}
379+
380+
// Parse dependencies if enabled
381+
if b2g.makeBase != "" {
382+
deps, err = parseDependencies(cwd, depInput)
383+
if err != nil {
384+
return "", nil, fmt.Errorf("parse dependencies for %s: %w", source, err)
385+
}
386+
}
387+
388+
return tmpObjFileName, deps, nil
389+
}
390+
323391
func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
324392
removeOnError := func(f *os.File) {
325393
if err != nil {
@@ -341,6 +409,7 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
341409
}
342410

343411
objFileName := filepath.Join(absOutPath, stem+".o")
412+
goFileName := filepath.Join(absOutPath, stem+".go")
344413

345414
cwd, err := os.Getwd()
346415
if err != nil {
@@ -354,39 +423,43 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
354423
return fmt.Errorf("remove obsolete output: %w", err)
355424
}
356425

357-
var depInput *os.File
358-
cFlags := slices.Clone(b2g.cFlags)
359-
if b2g.makeBase != "" {
360-
depInput, err = os.CreateTemp("", "bpf2go")
426+
// Compile each source file
427+
var allDeps []dependency
428+
var tmpObjFileNames []string
429+
for _, source := range b2g.sourceFiles {
430+
tmpObjFileName, deps, err := b2g.compileOne(tgt, cwd, source, objFileName, outputStem)
361431
if err != nil {
362432
return err
363433
}
364-
defer depInput.Close()
365-
defer os.Remove(depInput.Name())
434+
tmpObjFileNames = append(tmpObjFileNames, tmpObjFileName)
366435

367-
cFlags = append(cFlags,
368-
// Output dependency information.
369-
"-MD",
370-
// Create phony targets so that deleting a dependency doesn't
371-
// break the build.
372-
"-MP",
373-
// Write it to temporary file
374-
"-MF"+depInput.Name(),
375-
)
436+
if len(deps) > 0 {
437+
// There is always at least a dependency for the main file.
438+
deps[0].file = goFileName
439+
allDeps = append(allDeps, deps...)
440+
}
376441
}
377442

378-
err = gen.Compile(gen.CompileArgs{
379-
CC: b2g.cc,
380-
Strip: b2g.strip,
381-
DisableStripping: b2g.disableStripping,
382-
Flags: cFlags,
383-
Target: tgt,
384-
Workdir: cwd,
385-
Source: b2g.sourceFile,
386-
Dest: objFileName,
387-
})
388-
if err != nil {
389-
return fmt.Errorf("compile: %w", err)
443+
// If we have multiple object files, link them together
444+
if len(tmpObjFileNames) > 1 {
445+
cmd := exec.Command("bpftool", "gen", "object", objFileName)
446+
cmd.Args = append(cmd.Args, tmpObjFileNames...)
447+
cmd.Stderr = os.Stderr
448+
if err := cmd.Run(); err != nil {
449+
return fmt.Errorf("link object files: %w", err)
450+
}
451+
} else {
452+
// Single file, just rename it back to the final name
453+
if err := os.Rename(tmpObjFileNames[0], objFileName); err != nil {
454+
return fmt.Errorf("rename object file: %w", err)
455+
}
456+
}
457+
458+
// Clean up temporary object files
459+
for _, tmpObj := range tmpObjFileNames {
460+
if err := os.Remove(tmpObj); err != nil && !os.IsNotExist(err) {
461+
return fmt.Errorf("remove temporary object file: %w", err)
462+
}
390463
}
391464

392465
if b2g.disableStripping {
@@ -428,7 +501,6 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
428501
}
429502

430503
// Write out generated go
431-
goFileName := filepath.Join(absOutPath, stem+".go")
432504
goFile, err := os.Create(goFileName)
433505
if err != nil {
434506
return err
@@ -456,9 +528,10 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
456528
return
457529
}
458530

459-
deps, err := parseDependencies(cwd, depInput)
460-
if err != nil {
461-
return fmt.Errorf("can't read dependency information: %s", err)
531+
// Merge dependencies if we have multiple source files
532+
var finalDeps []dependency
533+
if len(allDeps) > 0 {
534+
finalDeps = mergeDependencies(allDeps)
462535
}
463536

464537
depFileName := goFileName + ".d"
@@ -468,9 +541,7 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
468541
}
469542
defer depOutput.Close()
470543

471-
// There is always at least a dependency for the main file.
472-
deps[0].file = goFileName
473-
if err := adjustDependencies(depOutput, b2g.makeBase, deps); err != nil {
544+
if err := adjustDependencies(depOutput, b2g.makeBase, finalDeps); err != nil {
474545
return fmt.Errorf("can't adjust dependency information: %s", err)
475546
}
476547

cmd/bpf2go/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func TestConvertGOARCH(t *testing.T) {
159159
identStem: "test",
160160
cc: testutils.ClangBin(t),
161161
disableStripping: true,
162-
sourceFile: tmp + "/test.c",
162+
sourceFiles: []string{tmp + "/test.c"},
163163
outputDir: tmp,
164164
}
165165

cmd/bpf2go/makedep.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,48 @@ func parseDependencies(baseDir string, in io.Reader) ([]dependency, error) {
106106
}
107107
return deps, nil
108108
}
109+
110+
// mergeDependencies combines multiple dependency slices into one, merging prerequisites
111+
// for files that appear in multiple slices.
112+
func mergeDependencies(depsSlices ...[]dependency) []dependency {
113+
// Map to track merged dependencies by file
114+
merged := make(map[string][]string)
115+
116+
// Process each slice of dependencies
117+
for _, deps := range depsSlices {
118+
for _, dep := range deps {
119+
// If we've seen this file before, merge prerequisites
120+
if existing, ok := merged[dep.file]; ok {
121+
// Combine prerequisites, avoiding duplicates
122+
prereqs := make(map[string]struct{})
123+
for _, p := range existing {
124+
prereqs[p] = struct{}{}
125+
}
126+
for _, p := range dep.prerequisites {
127+
prereqs[p] = struct{}{}
128+
}
129+
130+
// Convert back to slice
131+
merged[dep.file] = make([]string, 0, len(prereqs))
132+
for p := range prereqs {
133+
merged[dep.file] = append(merged[dep.file], p)
134+
}
135+
} else {
136+
// First time seeing this file, just copy prerequisites
137+
merged[dep.file] = make([]string, len(dep.prerequisites))
138+
copy(merged[dep.file], dep.prerequisites)
139+
}
140+
}
141+
}
142+
143+
// Convert map back to slice
144+
result := make([]dependency, 0, len(merged))
145+
for file, prereqs := range merged {
146+
result = append(result, dependency{
147+
file: file,
148+
prerequisites: prereqs,
149+
})
150+
}
151+
152+
return result
153+
}

cmd/bpf2go/makedep_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55
import (
66
"bytes"
77
"reflect"
8+
"sort"
89
"strings"
910
"testing"
1011
)
@@ -59,3 +60,34 @@ nothing:
5960
t.Error("Output doesn't match")
6061
}
6162
}
63+
64+
func TestMergeDependencies(t *testing.T) {
65+
deps1 := []dependency{
66+
{"/foo/main.c", []string{"/foo/bar.h", "/foo/baz.h"}},
67+
{"/foo/other.c", []string{"/foo/bar.h"}},
68+
}
69+
70+
deps2 := []dependency{
71+
{"/foo/main.c", []string{"/foo/qux.h"}},
72+
{"/foo/third.c", []string{"/foo/bar.h"}},
73+
}
74+
75+
merged := mergeDependencies(deps1, deps2)
76+
77+
// Sort merged dependencies for stable comparison
78+
sort.Slice(merged, func(i, j int) bool {
79+
return merged[i].file < merged[j].file
80+
})
81+
82+
want := []dependency{
83+
{"/foo/main.c", []string{"/foo/bar.h", "/foo/baz.h", "/foo/qux.h"}},
84+
{"/foo/other.c", []string{"/foo/bar.h"}},
85+
{"/foo/third.c", []string{"/foo/bar.h"}},
86+
}
87+
88+
if !reflect.DeepEqual(merged, want) {
89+
t.Logf("Have: %#v", merged)
90+
t.Logf("Want: %#v", want)
91+
t.Error("Merged dependencies don't match")
92+
}
93+
}

0 commit comments

Comments
 (0)