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
34 changes: 34 additions & 0 deletions cmd/bpf2go/gen/link.go
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 {
Copy link
Collaborator

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.

// Destination object file name
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
}
72 changes: 72 additions & 0 deletions cmd/bpf2go/gen/link_test.go
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and everywhere: use qt.Assert().

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")
}
}
160 changes: 113 additions & 47 deletions cmd/bpf2go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.sourceFiles[i] = absPath
b2g.sourceFiles = append(b2g.sourceFiles, absPath)

}
b2g.sourceFile = sourceFile

if b2g.makeBase != "" {
var err error
b2g.makeBase, err = filepath.Abs(b2g.makeBase)
if err != nil {
return nil, err
Expand Down Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} else ... { is not necessary here since the opposing case returns. The typical pattern is:

_, err := ...
if errors.Is(err, fs.ErrNotExist) { // Note: os.IsFoo functions are unofficially deprecated.
    return fmt.Errorf(...)
}
if err != nil {
    return err
}

Also, the PathError returned by os.Stat always contains the path provided to the function call, so it's not necessary to generate your own error. Doing so also swallows the fs.ErrNotExist sentinel, making error handling confusing for the user.

return err
}
}

if !b2g.disableStripping {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider initializing targetObjFileName := objFileName and drop the the else below.

// 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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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)
}

Expand Down
Loading
Loading