-
Notifications
You must be signed in to change notification settings - Fork 769
bpf2go: support multiple source files #1762
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4500480
38f89b6
0104501
438ad98
70844c7
e83058d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
//go:build !windows | ||
|
||
package gen | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
) | ||
|
||
// LinkArgs specifies the arguments for linking multiple BPF object files together. | ||
type LinkArgs struct { | ||
// Destination object file name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mention that paths must be absolute? |
||
Dest string | ||
// Source object files to link together | ||
Sources []string | ||
} | ||
|
||
// Link combines multiple BPF object files into a single object file. | ||
func Link(args LinkArgs) error { | ||
if len(args.Sources) == 0 { | ||
return fmt.Errorf("no source files to link") | ||
} | ||
|
||
cmd := exec.Command("bpftool", "gen", "object", args.Dest) | ||
cmd.Args = append(cmd.Args, args.Sources...) | ||
cmd.Stderr = os.Stderr | ||
|
||
if err := cmd.Run(); err != nil { | ||
return fmt.Errorf("bpftool gen object returned error: %w", err) | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
//go:build !windows | ||
|
||
package gen | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/cilium/ebpf/internal/testutils" | ||
) | ||
|
||
const ( | ||
func1 = `__attribute__((section("socket"), used)) int func1() { return 1; }` | ||
func2 = `__attribute__((section("socket"), used)) int func2() { return 2; }` | ||
) | ||
|
||
func TestLink(t *testing.T) { | ||
if testing.Short() { | ||
t.SkipNow() | ||
} | ||
|
||
dir := t.TempDir() | ||
mustWriteFile(t, dir, "func1.c", func1) | ||
mustWriteFile(t, dir, "func2.c", func2) | ||
|
||
// Compile first object | ||
obj1 := filepath.Join(dir, "func1.o") | ||
err := Compile(CompileArgs{ | ||
CC: testutils.ClangBin(t), | ||
DisableStripping: true, | ||
Workdir: dir, | ||
Source: filepath.Join(dir, "func1.c"), | ||
Dest: obj1, | ||
}) | ||
if err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here and everywhere: use |
||
t.Fatal("Can't compile func1:", err) | ||
} | ||
|
||
// Compile second object | ||
obj2 := filepath.Join(dir, "func2.o") | ||
err = Compile(CompileArgs{ | ||
CC: testutils.ClangBin(t), | ||
DisableStripping: true, | ||
Workdir: dir, | ||
Source: filepath.Join(dir, "func2.c"), | ||
Dest: obj2, | ||
}) | ||
if err != nil { | ||
t.Fatal("Can't compile func2:", err) | ||
} | ||
|
||
// Link both objects | ||
linked := filepath.Join(dir, "linked.o") | ||
err = Link(LinkArgs{ | ||
Dest: linked, | ||
Sources: []string{obj1, obj2}, | ||
}) | ||
if err != nil { | ||
t.Fatal("Can't link objects:", err) | ||
} | ||
|
||
// Verify the linked file exists and has content | ||
stat, err := os.Stat(linked) | ||
if err != nil { | ||
t.Fatal("Can't stat linked file:", err) | ||
} | ||
|
||
if stat.Size() == 0 { | ||
t.Error("Linked file is empty") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -21,13 +21,14 @@ import ( | |||||
"github.com/cilium/ebpf/cmd/bpf2go/gen" | ||||||
) | ||||||
|
||||||
const helpText = `Usage: %[1]s [options] <ident> <source file> [-- <C flags>] | ||||||
const helpText = `Usage: %[1]s [options] <ident> <source files...> [-- <C flags>] | ||||||
|
||||||
ident is used as the stem of all generated Go types and functions, and | ||||||
must be a valid Go identifier. | ||||||
|
||||||
source is a single C file that is compiled using the specified compiler | ||||||
(usually some version of clang). | ||||||
source files are C files that are compiled using the specified compiler | ||||||
(usually some version of clang) and linked together into a single | ||||||
BPF program. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specifying multiple source files incurs an extra dependency (bpftool). I'd document that clearly here. |
||||||
|
||||||
You can pass options to the compiler by appending them after a '--' argument | ||||||
or by supplying -cflags. Flags passed as arguments take precedence | ||||||
|
@@ -60,8 +61,8 @@ func run(stdout io.Writer, args []string) (err error) { | |||||
type bpf2go struct { | ||||||
stdout io.Writer | ||||||
verbose bool | ||||||
// Absolute path to a .c file. | ||||||
sourceFile string | ||||||
// Absolute paths to .c files. | ||||||
sourceFiles []string | ||||||
// Absolute path to a directory where .go are written | ||||||
outputDir string | ||||||
// Alternative output stem. If empty, identStem is used. | ||||||
|
@@ -186,13 +187,18 @@ func newB2G(stdout io.Writer, args []string) (*bpf2go, error) { | |||||
|
||||||
b2g.identStem = args[0] | ||||||
|
||||||
sourceFile, err := filepath.Abs(args[1]) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
sourceFiles := args[1:] | ||||||
b2g.sourceFiles = make([]string, len(sourceFiles)) | ||||||
for i, source := range sourceFiles { | ||||||
absPath, err := filepath.Abs(source) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("convert source file to absolute path: %w", err) | ||||||
} | ||||||
b2g.sourceFiles[i] = absPath | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: just appending to b2g.sourceFiles without pre-allocating the slice is probably fine / easier to reason about.
Suggested change
|
||||||
} | ||||||
b2g.sourceFile = sourceFile | ||||||
|
||||||
if b2g.makeBase != "" { | ||||||
var err error | ||||||
b2g.makeBase, err = filepath.Abs(b2g.makeBase) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
|
@@ -298,10 +304,13 @@ func getBool(key string, defaultVal bool) bool { | |||||
} | ||||||
|
||||||
func (b2g *bpf2go) convertAll() (err error) { | ||||||
if _, err := os.Stat(b2g.sourceFile); os.IsNotExist(err) { | ||||||
return fmt.Errorf("file %s doesn't exist", b2g.sourceFile) | ||||||
} else if err != nil { | ||||||
return err | ||||||
// Check all source files exist | ||||||
for _, source := range b2g.sourceFiles { | ||||||
if _, err := os.Stat(source); os.IsNotExist(err) { | ||||||
return fmt.Errorf("file %s doesn't exist", source) | ||||||
} else if err != nil { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
_, err := ...
if errors.Is(err, fs.ErrNotExist) { // Note: os.IsFoo functions are unofficially deprecated.
return fmt.Errorf(...)
}
if err != nil {
return err
} Also, the |
||||||
return err | ||||||
} | ||||||
} | ||||||
|
||||||
if !b2g.disableStripping { | ||||||
|
@@ -320,6 +329,55 @@ func (b2g *bpf2go) convertAll() (err error) { | |||||
return nil | ||||||
} | ||||||
|
||||||
// compileOne compiles a single source file and returns any dependencies found during compilation. | ||||||
func (b2g *bpf2go) compileOne(tgt gen.Target, cwd, source, objFileName string) (deps []dependency, err error) { | ||||||
var depInput *os.File | ||||||
cFlags := slices.Clone(b2g.cFlags) | ||||||
if b2g.makeBase != "" { | ||||||
depInput, err = os.CreateTemp("", "bpf2go") | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
defer depInput.Close() | ||||||
defer os.Remove(depInput.Name()) | ||||||
|
||||||
cFlags = append(cFlags, | ||||||
// Output dependency information. | ||||||
"-MD", | ||||||
// Create phony targets so that deleting a dependency doesn't | ||||||
// break the build. | ||||||
"-MP", | ||||||
// Write it to temporary file | ||||||
"-MF"+depInput.Name(), | ||||||
) | ||||||
} | ||||||
|
||||||
// Compile to final object file name | ||||||
err = gen.Compile(gen.CompileArgs{ | ||||||
CC: b2g.cc, | ||||||
Strip: b2g.strip, | ||||||
DisableStripping: b2g.disableStripping, | ||||||
Flags: cFlags, | ||||||
Target: tgt, | ||||||
Workdir: cwd, | ||||||
Source: source, | ||||||
Dest: objFileName, | ||||||
}) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("compile %s: %w", source, err) | ||||||
} | ||||||
|
||||||
// Parse dependencies if enabled | ||||||
if b2g.makeBase != "" { | ||||||
deps, err = parseDependencies(cwd, depInput) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("parse dependencies for %s: %w", source, err) | ||||||
} | ||||||
} | ||||||
|
||||||
return deps, nil | ||||||
} | ||||||
|
||||||
func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { | ||||||
removeOnError := func(f *os.File) { | ||||||
if err != nil { | ||||||
|
@@ -341,6 +399,7 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { | |||||
} | ||||||
|
||||||
objFileName := filepath.Join(absOutPath, stem+".o") | ||||||
goFileName := filepath.Join(absOutPath, stem+".go") | ||||||
|
||||||
cwd, err := os.Getwd() | ||||||
if err != nil { | ||||||
|
@@ -354,39 +413,48 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { | |||||
return fmt.Errorf("remove obsolete output: %w", err) | ||||||
} | ||||||
|
||||||
var depInput *os.File | ||||||
cFlags := slices.Clone(b2g.cFlags) | ||||||
if b2g.makeBase != "" { | ||||||
depInput, err = os.CreateTemp("", "bpf2go") | ||||||
// Compile each source file | ||||||
var allDeps []dependency | ||||||
var tmpObjFileNames []string | ||||||
for _, source := range b2g.sourceFiles { | ||||||
// Determine the target object file name | ||||||
var targetObjFileName string | ||||||
if len(b2g.sourceFiles) > 1 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider initializing |
||||||
// For multiple source files, use a temporary file | ||||||
tmpObj, err := os.CreateTemp("", filepath.Base(source)) | ||||||
if err != nil { | ||||||
return fmt.Errorf("create temporary object file: %w", err) | ||||||
} | ||||||
tmpObj.Close() | ||||||
defer os.Remove(tmpObj.Name()) | ||||||
targetObjFileName = tmpObj.Name() | ||||||
tmpObjFileNames = append(tmpObjFileNames, targetObjFileName) | ||||||
} else { | ||||||
// For single source file, use the final object file name | ||||||
targetObjFileName = objFileName | ||||||
} | ||||||
|
||||||
deps, err := b2g.compileOne(tgt, cwd, source, targetObjFileName) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
defer depInput.Close() | ||||||
defer os.Remove(depInput.Name()) | ||||||
|
||||||
cFlags = append(cFlags, | ||||||
// Output dependency information. | ||||||
"-MD", | ||||||
// Create phony targets so that deleting a dependency doesn't | ||||||
// break the build. | ||||||
"-MP", | ||||||
// Write it to temporary file | ||||||
"-MF"+depInput.Name(), | ||||||
) | ||||||
if len(deps) > 0 { | ||||||
// There is always at least a dependency for the main file. | ||||||
deps[0].file = goFileName | ||||||
allDeps = append(allDeps, deps...) | ||||||
} | ||||||
} | ||||||
|
||||||
err = gen.Compile(gen.CompileArgs{ | ||||||
CC: b2g.cc, | ||||||
Strip: b2g.strip, | ||||||
DisableStripping: b2g.disableStripping, | ||||||
Flags: cFlags, | ||||||
Target: tgt, | ||||||
Workdir: cwd, | ||||||
Source: b2g.sourceFile, | ||||||
Dest: objFileName, | ||||||
}) | ||||||
if err != nil { | ||||||
return fmt.Errorf("compile: %w", err) | ||||||
// If we have multiple object files, link them together | ||||||
if len(tmpObjFileNames) > 1 { | ||||||
err = gen.Link(gen.LinkArgs{ | ||||||
Dest: objFileName, | ||||||
Sources: tmpObjFileNames, | ||||||
}) | ||||||
if err != nil { | ||||||
return fmt.Errorf("link object files: %w", err) | ||||||
} | ||||||
} | ||||||
|
||||||
if b2g.disableStripping { | ||||||
|
@@ -428,7 +496,6 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { | |||||
} | ||||||
|
||||||
// Write out generated go | ||||||
goFileName := filepath.Join(absOutPath, stem+".go") | ||||||
goFile, err := os.Create(goFileName) | ||||||
if err != nil { | ||||||
return err | ||||||
|
@@ -456,9 +523,10 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { | |||||
return | ||||||
} | ||||||
|
||||||
deps, err := parseDependencies(cwd, depInput) | ||||||
if err != nil { | ||||||
return fmt.Errorf("can't read dependency information: %s", err) | ||||||
// Merge dependencies if we have multiple source files | ||||||
var finalDeps []dependency | ||||||
if len(allDeps) > 0 { | ||||||
finalDeps = mergeDependencies(allDeps) | ||||||
} | ||||||
|
||||||
depFileName := goFileName + ".d" | ||||||
|
@@ -468,9 +536,7 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { | |||||
} | ||||||
defer depOutput.Close() | ||||||
|
||||||
// There is always at least a dependency for the main file. | ||||||
deps[0].file = goFileName | ||||||
if err := adjustDependencies(depOutput, b2g.makeBase, deps); err != nil { | ||||||
if err := adjustDependencies(depOutput, b2g.makeBase, finalDeps); err != nil { | ||||||
return fmt.Errorf("can't adjust dependency information: %s", err) | ||||||
} | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document that this only supports host endianness.