whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
bpf2go
|
||||
===
|
||||
|
||||
`bpf2go` compiles a C source file into eBPF bytecode and then emits a
|
||||
Go file containing the eBPF. The goal is to avoid loading the
|
||||
eBPF from disk at runtime and to minimise the amount of manual
|
||||
work required to interact with eBPF programs. It takes inspiration
|
||||
from `bpftool gen skeleton`.
|
||||
|
||||
Invoke the program using go generate:
|
||||
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go foo path/to/src.c -- -I/path/to/include
|
||||
|
||||
This will emit `foo_bpfel.go` and `foo_bpfeb.go`, with types using `foo`
|
||||
as a stem. The two files contain compiled BPF for little and big
|
||||
endian systems, respectively.
|
||||
|
||||
You can use environment variables to affect all bpf2go invocations
|
||||
across a project, e.g. to set specific C flags:
|
||||
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cflags "$BPF_CFLAGS" foo path/to/src.c
|
||||
|
||||
By exporting `$BPF_CFLAGS` from your build system you can then control
|
||||
all builds from a single location.
|
||||
|
||||
## Generated types
|
||||
|
||||
`bpf2go` generates Go types for all map keys and values by default. You can
|
||||
disable this behaviour using `-no-global-types`. You can add to the set of
|
||||
types by specifying `-type foo` for each type you'd like to generate.
|
||||
|
||||
## Examples
|
||||
|
||||
See [examples/kprobe](../../examples/kprobe/main.go) for a fully worked out example.
|
||||
@@ -0,0 +1,210 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type compileArgs struct {
|
||||
// Which compiler to use
|
||||
cc string
|
||||
cFlags []string
|
||||
// Absolute working directory
|
||||
dir string
|
||||
// Absolute input file name
|
||||
source string
|
||||
// Absolute output file name
|
||||
dest string
|
||||
// Target to compile for, defaults to "bpf".
|
||||
target string
|
||||
// Depfile will be written here if depName is not empty
|
||||
dep io.Writer
|
||||
}
|
||||
|
||||
func compile(args compileArgs) error {
|
||||
// Default cflags that can be overridden by args.cFlags
|
||||
overrideFlags := []string{
|
||||
// Code needs to be optimized, otherwise the verifier will often fail
|
||||
// to understand it.
|
||||
"-O2",
|
||||
// Clang defaults to mcpu=probe which checks the kernel that we are
|
||||
// compiling on. This isn't appropriate for ahead of time
|
||||
// compiled code so force the most compatible version.
|
||||
"-mcpu=v1",
|
||||
}
|
||||
|
||||
cmd := exec.Command(args.cc, append(overrideFlags, args.cFlags...)...)
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
inputDir := filepath.Dir(args.source)
|
||||
relInputDir, err := filepath.Rel(args.dir, inputDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := args.target
|
||||
if target == "" {
|
||||
target = "bpf"
|
||||
}
|
||||
|
||||
// C flags that can't be overridden.
|
||||
cmd.Args = append(cmd.Args,
|
||||
"-target", target,
|
||||
"-c", args.source,
|
||||
"-o", args.dest,
|
||||
// Don't include clang version
|
||||
"-fno-ident",
|
||||
// Don't output inputDir into debug info
|
||||
"-fdebug-prefix-map="+inputDir+"="+relInputDir,
|
||||
"-fdebug-compilation-dir", ".",
|
||||
// We always want BTF to be generated, so enforce debug symbols
|
||||
"-g",
|
||||
fmt.Sprintf("-D__BPF_TARGET_MISSING=%q", "GCC error \"The eBPF is using target specific macros, please provide -target that is not bpf, bpfel or bpfeb\""),
|
||||
)
|
||||
cmd.Dir = args.dir
|
||||
|
||||
var depFile *os.File
|
||||
if args.dep != nil {
|
||||
depFile, err = os.CreateTemp("", "bpf2go")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer depFile.Close()
|
||||
defer os.Remove(depFile.Name())
|
||||
|
||||
cmd.Args = append(cmd.Args,
|
||||
// Output dependency information.
|
||||
"-MD",
|
||||
// Create phony targets so that deleting a dependency doesn't
|
||||
// break the build.
|
||||
"-MP",
|
||||
// Write it to temporary file
|
||||
"-MF"+depFile.Name(),
|
||||
)
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("can't execute %s: %s", args.cc, err)
|
||||
}
|
||||
|
||||
if depFile != nil {
|
||||
if _, err := io.Copy(args.dep, depFile); err != nil {
|
||||
return fmt.Errorf("error writing depfile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func adjustDependencies(baseDir string, deps []dependency) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for _, dep := range deps {
|
||||
relativeFile, err := filepath.Rel(baseDir, dep.file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(dep.prerequisites) == 0 {
|
||||
_, err := fmt.Fprintf(&buf, "%s:\n\n", relativeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var prereqs []string
|
||||
for _, prereq := range dep.prerequisites {
|
||||
relativePrereq, err := filepath.Rel(baseDir, prereq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prereqs = append(prereqs, relativePrereq)
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(&buf, "%s: \\\n %s\n\n", relativeFile, strings.Join(prereqs, " \\\n "))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type dependency struct {
|
||||
file string
|
||||
prerequisites []string
|
||||
}
|
||||
|
||||
func parseDependencies(baseDir string, in io.Reader) ([]dependency, error) {
|
||||
abs := func(path string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(baseDir, path)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(in)
|
||||
var line strings.Builder
|
||||
var deps []dependency
|
||||
for scanner.Scan() {
|
||||
buf := scanner.Bytes()
|
||||
if line.Len()+len(buf) > 1024*1024 {
|
||||
return nil, errors.New("line too long")
|
||||
}
|
||||
|
||||
if bytes.HasSuffix(buf, []byte{'\\'}) {
|
||||
line.Write(buf[:len(buf)-1])
|
||||
continue
|
||||
}
|
||||
|
||||
line.Write(buf)
|
||||
if line.Len() == 0 {
|
||||
// Skip empty lines
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line.String(), ":", 2)
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("invalid line without ':'")
|
||||
}
|
||||
|
||||
// NB: This doesn't handle filenames with spaces in them.
|
||||
// It seems like make doesn't do that either, so oh well.
|
||||
var prereqs []string
|
||||
for _, prereq := range strings.Fields(parts[1]) {
|
||||
prereqs = append(prereqs, abs(prereq))
|
||||
}
|
||||
|
||||
deps = append(deps, dependency{
|
||||
abs(string(parts[0])),
|
||||
prereqs,
|
||||
})
|
||||
line.Reset()
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// There is always at least a dependency for the main file.
|
||||
if len(deps) == 0 {
|
||||
return nil, fmt.Errorf("empty dependency file")
|
||||
}
|
||||
return deps, nil
|
||||
}
|
||||
|
||||
// strip DWARF debug info from file by executing exe.
|
||||
func strip(exe, file string) error {
|
||||
cmd := exec.Command(exe, "-g", file)
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("%s: %s", exe, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const minimalSocketFilter = `__attribute__((section("socket"), used)) int main() { return 0; }`
|
||||
|
||||
func TestCompile(t *testing.T) {
|
||||
dir := mustWriteTempFile(t, "test.c", minimalSocketFilter)
|
||||
|
||||
var dep bytes.Buffer
|
||||
err := compile(compileArgs{
|
||||
cc: clangBin(t),
|
||||
dir: dir,
|
||||
source: filepath.Join(dir, "test.c"),
|
||||
dest: filepath.Join(dir, "test.o"),
|
||||
dep: &dep,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("Can't compile:", err)
|
||||
}
|
||||
|
||||
stat, err := os.Stat(filepath.Join(dir, "test.o"))
|
||||
if err != nil {
|
||||
t.Fatal("Can't stat output:", err)
|
||||
}
|
||||
|
||||
if stat.Size() == 0 {
|
||||
t.Error("Compilation creates an empty file")
|
||||
}
|
||||
|
||||
if dep.Len() == 0 {
|
||||
t.Error("Compilation doesn't generate depinfo")
|
||||
}
|
||||
|
||||
if _, err := parseDependencies(dir, &dep); err != nil {
|
||||
t.Error("Can't parse dependencies:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReproducibleCompile(t *testing.T) {
|
||||
clangBin := clangBin(t)
|
||||
dir := mustWriteTempFile(t, "test.c", minimalSocketFilter)
|
||||
|
||||
err := compile(compileArgs{
|
||||
cc: clangBin,
|
||||
dir: dir,
|
||||
source: filepath.Join(dir, "test.c"),
|
||||
dest: filepath.Join(dir, "a.o"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("Can't compile:", err)
|
||||
}
|
||||
|
||||
err = compile(compileArgs{
|
||||
cc: clangBin,
|
||||
dir: dir,
|
||||
source: filepath.Join(dir, "test.c"),
|
||||
dest: filepath.Join(dir, "b.o"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("Can't compile:", err)
|
||||
}
|
||||
|
||||
aBytes, err := os.ReadFile(filepath.Join(dir, "a.o"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bBytes, err := os.ReadFile(filepath.Join(dir, "b.o"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(aBytes, bBytes) {
|
||||
t.Error("Compiling the same file twice doesn't give the same result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriggerMissingTarget(t *testing.T) {
|
||||
dir := mustWriteTempFile(t, "test.c", `_Pragma(__BPF_TARGET_MISSING);`)
|
||||
|
||||
err := compile(compileArgs{
|
||||
cc: clangBin(t),
|
||||
dir: dir,
|
||||
source: filepath.Join(dir, "test.c"),
|
||||
dest: filepath.Join(dir, "a.o"),
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("No error when compiling __BPF_TARGET_MISSING")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDependencies(t *testing.T) {
|
||||
const input = `main.go: /foo/bar baz
|
||||
|
||||
frob: /gobble \
|
||||
gubble
|
||||
|
||||
nothing:
|
||||
`
|
||||
|
||||
have, err := parseDependencies("/foo", strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatal("Can't parse dependencies:", err)
|
||||
}
|
||||
|
||||
want := []dependency{
|
||||
{"/foo/main.go", []string{"/foo/bar", "/foo/baz"}},
|
||||
{"/foo/frob", []string{"/gobble", "/foo/gubble"}},
|
||||
{"/foo/nothing", nil},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(have, want) {
|
||||
t.Logf("Have: %#v", have)
|
||||
t.Logf("Want: %#v", want)
|
||||
t.Error("Result doesn't match")
|
||||
}
|
||||
|
||||
output, err := adjustDependencies("/foo", want)
|
||||
if err != nil {
|
||||
t.Error("Can't adjust dependencies")
|
||||
}
|
||||
|
||||
const wantOutput = `main.go: \
|
||||
bar \
|
||||
baz
|
||||
|
||||
frob: \
|
||||
../gobble \
|
||||
gubble
|
||||
|
||||
nothing:
|
||||
|
||||
`
|
||||
|
||||
if have := string(output); have != wantOutput {
|
||||
t.Logf("Have:\n%s", have)
|
||||
t.Logf("Want:\n%s", wantOutput)
|
||||
t.Error("Output doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteTempFile(t *testing.T, name, contents string) string {
|
||||
t.Helper()
|
||||
|
||||
tmp, err := os.MkdirTemp("", "bpf2go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { os.RemoveAll(tmp) })
|
||||
|
||||
tmpFile := filepath.Join(tmp, name)
|
||||
if err := os.WriteFile(tmpFile, []byte(contents), 0660); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return tmp
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Program bpf2go embeds eBPF in Go.
|
||||
//
|
||||
// Please see the README for details how to use it.
|
||||
package main
|
||||
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"go/build/constraint"
|
||||
)
|
||||
|
||||
// buildTags is a comma-separated list of build tags.
|
||||
//
|
||||
// This follows the pre-Go 1.17 syntax and is kept for compatibility reasons.
|
||||
type buildTags struct {
|
||||
Expr constraint.Expr
|
||||
}
|
||||
|
||||
var _ flag.Value = (*buildTags)(nil)
|
||||
|
||||
func (bt *buildTags) String() string {
|
||||
if bt.Expr == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return (bt.Expr).String()
|
||||
}
|
||||
|
||||
func (bt *buildTags) Set(value string) error {
|
||||
ct, err := constraint.Parse("// +build " + value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bt.Expr = ct
|
||||
return nil
|
||||
}
|
||||
|
||||
func andConstraints(x, y constraint.Expr) constraint.Expr {
|
||||
if x == nil {
|
||||
return y
|
||||
}
|
||||
|
||||
if y == nil {
|
||||
return x
|
||||
}
|
||||
|
||||
return &constraint.AndExpr{X: x, Y: y}
|
||||
}
|
||||
|
||||
func orConstraints(x, y constraint.Expr) constraint.Expr {
|
||||
if x == nil {
|
||||
return y
|
||||
}
|
||||
|
||||
if y == nil {
|
||||
return x
|
||||
}
|
||||
|
||||
return &constraint.OrExpr{X: x, Y: y}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build/constraint"
|
||||
"go/token"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
const helpText = `Usage: %[1]s [options] <ident> <source file> [-- <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).
|
||||
|
||||
You can pass options to the compiler by appending them after a '--' argument
|
||||
or by supplying -cflags. Flags passed as arguments take precedence
|
||||
over flags passed via -cflags. Additionally, the program expands quotation
|
||||
marks in -cflags. This means that -cflags 'foo "bar baz"' is passed to the
|
||||
compiler as two arguments "foo" and "bar baz".
|
||||
|
||||
The program expects GOPACKAGE to be set in the environment, and should be invoked
|
||||
via go generate. The generated files are written to the current directory.
|
||||
|
||||
Some options take defaults from the environment. Variable name is mentioned
|
||||
next to the respective option.
|
||||
|
||||
Options:
|
||||
|
||||
`
|
||||
|
||||
// Targets understood by bpf2go.
|
||||
//
|
||||
// Targets without a Linux string can't be used directly and are only included
|
||||
// for the generic bpf, bpfel, bpfeb targets.
|
||||
var targetByGoArch = map[string]target{
|
||||
"386": {"bpfel", "x86"},
|
||||
"amd64": {"bpfel", "x86"},
|
||||
"amd64p32": {"bpfel", ""},
|
||||
"arm": {"bpfel", "arm"},
|
||||
"arm64": {"bpfel", "arm64"},
|
||||
"loong64": {"bpfel", ""},
|
||||
"mipsle": {"bpfel", ""},
|
||||
"mips64le": {"bpfel", ""},
|
||||
"mips64p32le": {"bpfel", ""},
|
||||
"ppc64le": {"bpfel", "powerpc"},
|
||||
"riscv64": {"bpfel", ""},
|
||||
"armbe": {"bpfeb", "arm"},
|
||||
"arm64be": {"bpfeb", "arm64"},
|
||||
"mips": {"bpfeb", ""},
|
||||
"mips64": {"bpfeb", ""},
|
||||
"mips64p32": {"bpfeb", ""},
|
||||
"ppc64": {"bpfeb", "powerpc"},
|
||||
"s390": {"bpfeb", "s390"},
|
||||
"s390x": {"bpfeb", "s390"},
|
||||
"sparc": {"bpfeb", "sparc"},
|
||||
"sparc64": {"bpfeb", "sparc"},
|
||||
}
|
||||
|
||||
func run(stdout io.Writer, pkg, outputDir string, args []string) (err error) {
|
||||
b2g, err := newB2G(stdout, pkg, outputDir, args)
|
||||
switch {
|
||||
case err == nil:
|
||||
return b2g.convertAll()
|
||||
case errors.Is(err, flag.ErrHelp):
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type bpf2go struct {
|
||||
stdout io.Writer
|
||||
// Absolute path to a .c file.
|
||||
sourceFile string
|
||||
// Absolute path to a directory where .go are written
|
||||
outputDir string
|
||||
// Alternative output stem. If empty, identStem is used.
|
||||
outputStem string
|
||||
// Valid go package name.
|
||||
pkg string
|
||||
// Valid go identifier.
|
||||
identStem string
|
||||
// Targets to build for.
|
||||
targetArches map[target][]string
|
||||
// C compiler.
|
||||
cc string
|
||||
// Command used to strip DWARF.
|
||||
strip string
|
||||
disableStripping bool
|
||||
// C flags passed to the compiler.
|
||||
cFlags []string
|
||||
skipGlobalTypes bool
|
||||
// C types to include in the generatd output.
|
||||
cTypes cTypes
|
||||
// Build tags to be included in the output.
|
||||
tags buildTags
|
||||
// Base directory of the Makefile. Enables outputting make-style dependencies
|
||||
// in .d files.
|
||||
makeBase string
|
||||
}
|
||||
|
||||
func newB2G(stdout io.Writer, pkg, outputDir string, args []string) (*bpf2go, error) {
|
||||
b2g := &bpf2go{
|
||||
stdout: stdout,
|
||||
pkg: pkg,
|
||||
outputDir: outputDir,
|
||||
}
|
||||
|
||||
fs := flag.NewFlagSet("bpf2go", flag.ContinueOnError)
|
||||
fs.StringVar(&b2g.cc, "cc", getEnv("BPF2GO_CC", "clang"),
|
||||
"`binary` used to compile C to BPF ($BPF2GO_CC)")
|
||||
fs.StringVar(&b2g.strip, "strip", getEnv("BPF2GO_STRIP", ""),
|
||||
"`binary` used to strip DWARF from compiled BPF ($BPF2GO_STRIP)")
|
||||
fs.BoolVar(&b2g.disableStripping, "no-strip", false, "disable stripping of DWARF")
|
||||
flagCFlags := fs.String("cflags", getEnv("BPF2GO_CFLAGS", ""),
|
||||
"flags passed to the compiler, may contain quoted arguments ($BPF2GO_CFLAGS)")
|
||||
fs.Var(&b2g.tags, "tags", "Comma-separated list of Go build tags to include in generated files")
|
||||
flagTarget := fs.String("target", "bpfel,bpfeb", "clang target(s) to compile for (comma separated)")
|
||||
fs.StringVar(&b2g.makeBase, "makebase", getEnv("BPF2GO_MAKEBASE", ""),
|
||||
"write make compatible depinfo files relative to `directory` ($BPF2GO_MAKEBASE)")
|
||||
fs.Var(&b2g.cTypes, "type", "`Name` of a type to generate a Go declaration for, may be repeated")
|
||||
fs.BoolVar(&b2g.skipGlobalTypes, "no-global-types", false, "Skip generating types for map keys and values, etc.")
|
||||
fs.StringVar(&b2g.outputStem, "output-stem", "", "alternative stem for names of generated files (defaults to ident)")
|
||||
|
||||
fs.SetOutput(b2g.stdout)
|
||||
fs.Usage = func() {
|
||||
fmt.Fprintf(fs.Output(), helpText, fs.Name())
|
||||
fs.PrintDefaults()
|
||||
fmt.Fprintln(fs.Output())
|
||||
printTargets(fs.Output())
|
||||
}
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b2g.pkg == "" {
|
||||
return nil, errors.New("missing package, are you running via go generate?")
|
||||
}
|
||||
|
||||
if b2g.cc == "" {
|
||||
return nil, errors.New("no compiler specified")
|
||||
}
|
||||
|
||||
args, cFlags := splitCFlagsFromArgs(fs.Args())
|
||||
|
||||
if *flagCFlags != "" {
|
||||
splitCFlags, err := splitArguments(*flagCFlags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Command line arguments take precedence over C flags
|
||||
// from the flag.
|
||||
cFlags = append(splitCFlags, cFlags...)
|
||||
}
|
||||
|
||||
for _, cFlag := range cFlags {
|
||||
if strings.HasPrefix(cFlag, "-M") {
|
||||
return nil, fmt.Errorf("use -makebase instead of %q", cFlag)
|
||||
}
|
||||
}
|
||||
|
||||
b2g.cFlags = cFlags[:len(cFlags):len(cFlags)]
|
||||
|
||||
if len(args) < 2 {
|
||||
return nil, errors.New("expected at least two arguments")
|
||||
}
|
||||
|
||||
b2g.identStem = args[0]
|
||||
if !token.IsIdentifier(b2g.identStem) {
|
||||
return nil, fmt.Errorf("%q is not a valid identifier", b2g.identStem)
|
||||
}
|
||||
|
||||
sourceFile, err := filepath.Abs(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b2g.sourceFile = sourceFile
|
||||
|
||||
if b2g.makeBase != "" {
|
||||
b2g.makeBase, err = filepath.Abs(b2g.makeBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if b2g.outputStem != "" && strings.ContainsRune(b2g.outputStem, filepath.Separator) {
|
||||
return nil, fmt.Errorf("-output-stem %q must not contain path separation characters", b2g.outputStem)
|
||||
}
|
||||
|
||||
targetArches, err := collectTargets(strings.Split(*flagTarget, ","))
|
||||
if errors.Is(err, errInvalidTarget) {
|
||||
printTargets(b2g.stdout)
|
||||
fmt.Fprintln(b2g.stdout)
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(targetArches) == 0 {
|
||||
return nil, fmt.Errorf("no targets specified")
|
||||
}
|
||||
b2g.targetArches = targetArches
|
||||
|
||||
// Try to find a suitable llvm-strip, possibly with a version suffix derived
|
||||
// from the clang binary.
|
||||
if b2g.strip == "" {
|
||||
b2g.strip = "llvm-strip"
|
||||
if strings.HasPrefix(b2g.cc, "clang") {
|
||||
b2g.strip += strings.TrimPrefix(b2g.cc, "clang")
|
||||
}
|
||||
}
|
||||
|
||||
return b2g, nil
|
||||
}
|
||||
|
||||
// cTypes collects the C type names a user wants to generate Go types for.
|
||||
//
|
||||
// Names are guaranteed to be unique, and only a subset of names is accepted so
|
||||
// that we may extend the flag syntax in the future.
|
||||
type cTypes []string
|
||||
|
||||
var _ flag.Value = (*cTypes)(nil)
|
||||
|
||||
func (ct *cTypes) String() string {
|
||||
if ct == nil {
|
||||
return "[]"
|
||||
}
|
||||
return fmt.Sprint(*ct)
|
||||
}
|
||||
|
||||
const validCTypeChars = `[a-z0-9_]`
|
||||
|
||||
var reValidCType = regexp.MustCompile(`(?i)^` + validCTypeChars + `+$`)
|
||||
|
||||
func (ct *cTypes) Set(value string) error {
|
||||
if !reValidCType.MatchString(value) {
|
||||
return fmt.Errorf("%q contains characters outside of %s", value, validCTypeChars)
|
||||
}
|
||||
|
||||
i := sort.SearchStrings(*ct, value)
|
||||
if i >= len(*ct) {
|
||||
*ct = append(*ct, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
if (*ct)[i] == value {
|
||||
return fmt.Errorf("duplicate type %q", value)
|
||||
}
|
||||
|
||||
*ct = append((*ct)[:i], append([]string{value}, (*ct)[i:]...)...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val, ok := os.LookupEnv(key); ok {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !b2g.disableStripping {
|
||||
b2g.strip, err = exec.LookPath(b2g.strip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for target, arches := range b2g.targetArches {
|
||||
if err := b2g.convert(target, arches); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b2g *bpf2go) convert(tgt target, arches []string) (err error) {
|
||||
removeOnError := func(f *os.File) {
|
||||
if err != nil {
|
||||
os.Remove(f.Name())
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
outputStem := b2g.outputStem
|
||||
if outputStem == "" {
|
||||
outputStem = strings.ToLower(b2g.identStem)
|
||||
}
|
||||
stem := fmt.Sprintf("%s_%s", outputStem, tgt.clang)
|
||||
if tgt.linux != "" {
|
||||
stem = fmt.Sprintf("%s_%s_%s", outputStem, tgt.clang, tgt.linux)
|
||||
}
|
||||
|
||||
objFileName := filepath.Join(b2g.outputDir, stem+".o")
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var archConstraint constraint.Expr
|
||||
for _, arch := range arches {
|
||||
tag := &constraint.TagExpr{Tag: arch}
|
||||
archConstraint = orConstraints(archConstraint, tag)
|
||||
}
|
||||
|
||||
constraints := andConstraints(archConstraint, b2g.tags.Expr)
|
||||
|
||||
cFlags := make([]string, len(b2g.cFlags))
|
||||
copy(cFlags, b2g.cFlags)
|
||||
if tgt.linux != "" {
|
||||
cFlags = append(cFlags, "-D__TARGET_ARCH_"+tgt.linux)
|
||||
}
|
||||
|
||||
var dep bytes.Buffer
|
||||
err = compile(compileArgs{
|
||||
cc: b2g.cc,
|
||||
cFlags: cFlags,
|
||||
target: tgt.clang,
|
||||
dir: cwd,
|
||||
source: b2g.sourceFile,
|
||||
dest: objFileName,
|
||||
dep: &dep,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(b2g.stdout, "Compiled", objFileName)
|
||||
|
||||
if !b2g.disableStripping {
|
||||
if err := strip(b2g.strip, objFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(b2g.stdout, "Stripped", objFileName)
|
||||
}
|
||||
|
||||
spec, err := ebpf.LoadCollectionSpec(objFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't load BPF from ELF: %s", err)
|
||||
}
|
||||
|
||||
maps, programs, types, err := collectFromSpec(spec, b2g.cTypes, b2g.skipGlobalTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write out generated go
|
||||
goFileName := filepath.Join(b2g.outputDir, stem+".go")
|
||||
goFile, err := os.Create(goFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer removeOnError(goFile)
|
||||
|
||||
err = output(outputArgs{
|
||||
pkg: b2g.pkg,
|
||||
stem: b2g.identStem,
|
||||
constraints: constraints,
|
||||
maps: maps,
|
||||
programs: programs,
|
||||
types: types,
|
||||
obj: filepath.Base(objFileName),
|
||||
out: goFile,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't write %s: %s", goFileName, err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(b2g.stdout, "Wrote", goFileName)
|
||||
|
||||
if b2g.makeBase == "" {
|
||||
return
|
||||
}
|
||||
|
||||
deps, err := parseDependencies(cwd, &dep)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't read dependency information: %s", err)
|
||||
}
|
||||
|
||||
// There is always at least a dependency for the main file.
|
||||
deps[0].file = goFileName
|
||||
depFile, err := adjustDependencies(b2g.makeBase, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't adjust dependency information: %s", err)
|
||||
}
|
||||
|
||||
depFileName := goFileName + ".d"
|
||||
if err := os.WriteFile(depFileName, depFile, 0666); err != nil {
|
||||
return fmt.Errorf("can't write dependency file: %s", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(b2g.stdout, "Wrote", depFileName)
|
||||
return nil
|
||||
}
|
||||
|
||||
type target struct {
|
||||
clang string
|
||||
linux string
|
||||
}
|
||||
|
||||
func printTargets(w io.Writer) {
|
||||
var arches []string
|
||||
for arch, archTarget := range targetByGoArch {
|
||||
if archTarget.linux == "" {
|
||||
continue
|
||||
}
|
||||
arches = append(arches, arch)
|
||||
}
|
||||
sort.Strings(arches)
|
||||
|
||||
fmt.Fprint(w, "Supported targets:\n")
|
||||
fmt.Fprint(w, "\tbpf\n\tbpfel\n\tbpfeb\n")
|
||||
for _, arch := range arches {
|
||||
fmt.Fprintf(w, "\t%s\n", arch)
|
||||
}
|
||||
}
|
||||
|
||||
var errInvalidTarget = errors.New("unsupported target")
|
||||
|
||||
func collectTargets(targets []string) (map[target][]string, error) {
|
||||
result := make(map[target][]string)
|
||||
for _, tgt := range targets {
|
||||
switch tgt {
|
||||
case "bpf", "bpfel", "bpfeb":
|
||||
var goarches []string
|
||||
for arch, archTarget := range targetByGoArch {
|
||||
if archTarget.clang == tgt {
|
||||
// Include tags for all goarches that have the same endianness.
|
||||
goarches = append(goarches, arch)
|
||||
}
|
||||
}
|
||||
sort.Strings(goarches)
|
||||
result[target{tgt, ""}] = goarches
|
||||
|
||||
case "native":
|
||||
tgt = runtime.GOARCH
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
archTarget, ok := targetByGoArch[tgt]
|
||||
if !ok || archTarget.linux == "" {
|
||||
return nil, fmt.Errorf("%q: %w", tgt, errInvalidTarget)
|
||||
}
|
||||
|
||||
var goarches []string
|
||||
for goarch, lt := range targetByGoArch {
|
||||
if lt == archTarget {
|
||||
// Include tags for all goarches that have the same
|
||||
// target.
|
||||
goarches = append(goarches, goarch)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(goarches)
|
||||
result[archTarget] = goarches
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
outputDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := run(os.Stdout, os.Getenv("GOPACKAGE"), outputDir, os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
clangBin := clangBin(t)
|
||||
dir := mustWriteTempFile(t, "test.c", minimalSocketFilter)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
modRoot := filepath.Clean(filepath.Join(cwd, "../.."))
|
||||
if _, err := os.Stat(filepath.Join(modRoot, "go.mod")); os.IsNotExist(err) {
|
||||
t.Fatal("No go.mod file in", modRoot)
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "bpf2go-module-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
execInModule := func(name string, args ...string) {
|
||||
t.Helper()
|
||||
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Dir = tmpDir
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
if out := string(out); out != "" {
|
||||
t.Log(out)
|
||||
}
|
||||
t.Fatalf("Can't execute %s: %v", name, args)
|
||||
}
|
||||
}
|
||||
|
||||
module := currentModule()
|
||||
|
||||
execInModule("go", "mod", "init", "bpf2go-test")
|
||||
|
||||
execInModule("go", "mod", "edit",
|
||||
// Require the module. The version doesn't matter due to the replace
|
||||
// below.
|
||||
fmt.Sprintf("-require=%s@v0.0.0", module),
|
||||
// Replace the module with the current version.
|
||||
fmt.Sprintf("-replace=%s=%s", module, modRoot),
|
||||
)
|
||||
|
||||
err = run(io.Discard, "foo", tmpDir, []string{
|
||||
"-cc", clangBin,
|
||||
"bar",
|
||||
filepath.Join(dir, "test.c"),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Can't run:", err)
|
||||
}
|
||||
|
||||
for _, arch := range []string{
|
||||
"amd64", // little-endian
|
||||
"s390x", // big-endian
|
||||
} {
|
||||
t.Run(arch, func(t *testing.T) {
|
||||
goBin := exec.Command("go", "build", "-mod=mod")
|
||||
goBin.Dir = tmpDir
|
||||
goBin.Env = append(os.Environ(),
|
||||
"GOOS=linux",
|
||||
"GOARCH="+arch,
|
||||
)
|
||||
out, err := goBin.CombinedOutput()
|
||||
if err != nil {
|
||||
if out := string(out); out != "" {
|
||||
t.Log(out)
|
||||
}
|
||||
t.Error("Can't compile package:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
err := run(&stdout, "", "", []string{"-help"})
|
||||
if err != nil {
|
||||
t.Fatal("Can't execute -help")
|
||||
}
|
||||
|
||||
if stdout.Len() == 0 {
|
||||
t.Error("-help doesn't write to stdout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableStripping(t *testing.T) {
|
||||
dir := mustWriteTempFile(t, "test.c", minimalSocketFilter)
|
||||
|
||||
err := run(io.Discard, "foo", dir, []string{
|
||||
"-cc", clangBin(t),
|
||||
"-strip", "binary-that-certainly-doesnt-exist",
|
||||
"-no-strip",
|
||||
"bar",
|
||||
filepath.Join(dir, "test.c"),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Can't run with stripping disabled:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectTargets(t *testing.T) {
|
||||
clangArches := make(map[string][]string)
|
||||
linuxArchesLE := make(map[string][]string)
|
||||
linuxArchesBE := make(map[string][]string)
|
||||
for arch, archTarget := range targetByGoArch {
|
||||
clangArches[archTarget.clang] = append(clangArches[archTarget.clang], arch)
|
||||
if archTarget.clang == "bpfel" {
|
||||
linuxArchesLE[archTarget.linux] = append(linuxArchesLE[archTarget.linux], arch)
|
||||
continue
|
||||
}
|
||||
linuxArchesBE[archTarget.linux] = append(linuxArchesBE[archTarget.linux], arch)
|
||||
}
|
||||
for i := range clangArches {
|
||||
sort.Strings(clangArches[i])
|
||||
}
|
||||
for i := range linuxArchesLE {
|
||||
sort.Strings(linuxArchesLE[i])
|
||||
}
|
||||
for i := range linuxArchesBE {
|
||||
sort.Strings(linuxArchesBE[i])
|
||||
}
|
||||
|
||||
nativeTarget := make(map[target][]string)
|
||||
for arch, archTarget := range targetByGoArch {
|
||||
if arch == runtime.GOARCH {
|
||||
if archTarget.clang == "bpfel" {
|
||||
nativeTarget[archTarget] = linuxArchesLE[archTarget.linux]
|
||||
} else {
|
||||
nativeTarget[archTarget] = linuxArchesBE[archTarget.linux]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
targets []string
|
||||
want map[target][]string
|
||||
}{
|
||||
{
|
||||
[]string{"bpf", "bpfel", "bpfeb"},
|
||||
map[target][]string{
|
||||
{"bpf", ""}: nil,
|
||||
{"bpfel", ""}: clangArches["bpfel"],
|
||||
{"bpfeb", ""}: clangArches["bpfeb"],
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"amd64", "386"},
|
||||
map[target][]string{
|
||||
{"bpfel", "x86"}: linuxArchesLE["x86"],
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"amd64", "arm64be"},
|
||||
map[target][]string{
|
||||
{"bpfeb", "arm64"}: linuxArchesBE["arm64"],
|
||||
{"bpfel", "x86"}: linuxArchesLE["x86"],
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"native"},
|
||||
nativeTarget,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
name := strings.Join(test.targets, ",")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
have, err := collectTargets(test.targets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(test.want, have); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectTargetsErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target string
|
||||
}{
|
||||
{"unknown", "frood"},
|
||||
{"no linux target", "mips64p32le"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := collectTargets([]string{test.target})
|
||||
if err == nil {
|
||||
t.Fatal("Function did not return an error")
|
||||
}
|
||||
t.Log("Error message:", err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertGOARCH(t *testing.T) {
|
||||
tmp := mustWriteTempFile(t, "test.c",
|
||||
`
|
||||
#ifndef __TARGET_ARCH_x86
|
||||
#error __TARGET_ARCH_x86 is not defined
|
||||
#endif`,
|
||||
)
|
||||
|
||||
b2g := bpf2go{
|
||||
pkg: "test",
|
||||
stdout: io.Discard,
|
||||
identStem: "test",
|
||||
cc: clangBin(t),
|
||||
disableStripping: true,
|
||||
sourceFile: tmp + "/test.c",
|
||||
outputDir: tmp,
|
||||
}
|
||||
|
||||
if err := b2g.convert(targetByGoArch["amd64"], nil); err != nil {
|
||||
t.Fatal("Can't target GOARCH:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCTypes(t *testing.T) {
|
||||
var ct cTypes
|
||||
valid := []string{
|
||||
"abcdefghijklmnopqrstuvqxyABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_",
|
||||
"y",
|
||||
}
|
||||
for _, value := range valid {
|
||||
if err := ct.Set(value); err != nil {
|
||||
t.Fatalf("Set returned an error for %q: %s", value, err)
|
||||
}
|
||||
}
|
||||
qt.Assert(t, ct, qt.ContentEquals, cTypes(valid))
|
||||
|
||||
for _, value := range []string{
|
||||
"",
|
||||
" ",
|
||||
" frood",
|
||||
"foo\nbar",
|
||||
".",
|
||||
",",
|
||||
"+",
|
||||
"-",
|
||||
} {
|
||||
ct = nil
|
||||
if err := ct.Set(value); err == nil {
|
||||
t.Fatalf("Set did not return an error for %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
ct = nil
|
||||
qt.Assert(t, ct.Set("foo"), qt.IsNil)
|
||||
qt.Assert(t, ct.Set("foo"), qt.IsNotNil)
|
||||
}
|
||||
|
||||
func TestParseArgs(t *testing.T) {
|
||||
const (
|
||||
pkg = "eee"
|
||||
outputDir = "."
|
||||
csource = "testdata/minimal.c"
|
||||
stem = "a"
|
||||
)
|
||||
|
||||
t.Run("makebase", func(t *testing.T) {
|
||||
basePath, _ := filepath.Abs("barfoo")
|
||||
args := []string{"-makebase", basePath, stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.makeBase, qt.Equals, basePath)
|
||||
})
|
||||
|
||||
t.Run("makebase from env", func(t *testing.T) {
|
||||
basePath, _ := filepath.Abs("barfoo")
|
||||
args := []string{stem, csource}
|
||||
t.Setenv("BPF2GO_MAKEBASE", basePath)
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.makeBase, qt.Equals, basePath)
|
||||
})
|
||||
|
||||
t.Run("makebase flag overrides env", func(t *testing.T) {
|
||||
basePathFlag, _ := filepath.Abs("barfoo")
|
||||
basePathEnv, _ := filepath.Abs("foobar")
|
||||
args := []string{"-makebase", basePathFlag, stem, csource}
|
||||
t.Setenv("BPF2GO_MAKEBASE", basePathEnv)
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.makeBase, qt.Equals, basePathFlag)
|
||||
})
|
||||
|
||||
t.Run("cc defaults to clang", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cc, qt.Equals, "clang")
|
||||
})
|
||||
|
||||
t.Run("cc", func(t *testing.T) {
|
||||
args := []string{"-cc", "barfoo", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cc, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("cc from env", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
t.Setenv("BPF2GO_CC", "barfoo")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cc, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("cc flag overrides env", func(t *testing.T) {
|
||||
args := []string{"-cc", "barfoo", stem, csource}
|
||||
t.Setenv("BPF2GO_CC", "foobar")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cc, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("strip defaults to llvm-strip", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.strip, qt.Equals, "llvm-strip")
|
||||
})
|
||||
|
||||
t.Run("strip", func(t *testing.T) {
|
||||
args := []string{"-strip", "barfoo", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.strip, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("strip from env", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
t.Setenv("BPF2GO_STRIP", "barfoo")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.strip, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("strip flag overrides env", func(t *testing.T) {
|
||||
args := []string{"-strip", "barfoo", stem, csource}
|
||||
t.Setenv("BPF2GO_STRIP", "foobar")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.strip, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("no strip defaults to false", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.disableStripping, qt.IsFalse)
|
||||
})
|
||||
|
||||
t.Run("no strip", func(t *testing.T) {
|
||||
args := []string{"-no-strip", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.disableStripping, qt.IsTrue)
|
||||
})
|
||||
|
||||
t.Run("cflags flag", func(t *testing.T) {
|
||||
args := []string{"-cflags", "x y z", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"x", "y", "z"})
|
||||
})
|
||||
|
||||
t.Run("cflags multi flag", func(t *testing.T) {
|
||||
args := []string{"-cflags", "x y z", "-cflags", "u v", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"u", "v"})
|
||||
})
|
||||
|
||||
t.Run("cflags flag and args", func(t *testing.T) {
|
||||
args := []string{"-cflags", "x y z", "stem", csource, "--", "u", "v"}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"x", "y", "z", "u", "v"})
|
||||
})
|
||||
|
||||
t.Run("cflags from env", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
t.Setenv("BPF2GO_CFLAGS", "x y z")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"x", "y", "z"})
|
||||
})
|
||||
|
||||
t.Run("cflags flag overrides env", func(t *testing.T) {
|
||||
args := []string{"-cflags", "u v", stem, csource}
|
||||
t.Setenv("BPF2GO_CFLAGS", "x y z")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"u", "v"})
|
||||
})
|
||||
}
|
||||
|
||||
func clangBin(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("Not compiling with -short")
|
||||
}
|
||||
|
||||
// Use a recent clang version for local development, but allow CI to run
|
||||
// against oldest supported clang.
|
||||
clang := "clang-14"
|
||||
if minVersion := os.Getenv("CI_MIN_CLANG_VERSION"); minVersion != "" {
|
||||
clang = fmt.Sprintf("clang-%s", minVersion)
|
||||
}
|
||||
|
||||
t.Log("Testing against", clang)
|
||||
return clang
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"go/build/constraint"
|
||||
"go/token"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/btf"
|
||||
"github.com/cilium/ebpf/internal"
|
||||
)
|
||||
|
||||
//go:embed output.tpl
|
||||
var commonRaw string
|
||||
|
||||
var commonTemplate = template.Must(template.New("common").Parse(commonRaw))
|
||||
|
||||
type templateName string
|
||||
|
||||
func (n templateName) maybeExport(str string) string {
|
||||
if token.IsExported(string(n)) {
|
||||
return toUpperFirst(str)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (n templateName) Bytes() string {
|
||||
return "_" + toUpperFirst(string(n)) + "Bytes"
|
||||
}
|
||||
|
||||
func (n templateName) Specs() string {
|
||||
return string(n) + "Specs"
|
||||
}
|
||||
|
||||
func (n templateName) ProgramSpecs() string {
|
||||
return string(n) + "ProgramSpecs"
|
||||
}
|
||||
|
||||
func (n templateName) MapSpecs() string {
|
||||
return string(n) + "MapSpecs"
|
||||
}
|
||||
|
||||
func (n templateName) Load() string {
|
||||
return n.maybeExport("load" + toUpperFirst(string(n)))
|
||||
}
|
||||
|
||||
func (n templateName) LoadObjects() string {
|
||||
return n.maybeExport("load" + toUpperFirst(string(n)) + "Objects")
|
||||
}
|
||||
|
||||
func (n templateName) Objects() string {
|
||||
return string(n) + "Objects"
|
||||
}
|
||||
|
||||
func (n templateName) Maps() string {
|
||||
return string(n) + "Maps"
|
||||
}
|
||||
|
||||
func (n templateName) Programs() string {
|
||||
return string(n) + "Programs"
|
||||
}
|
||||
|
||||
func (n templateName) CloseHelper() string {
|
||||
return "_" + toUpperFirst(string(n)) + "Close"
|
||||
}
|
||||
|
||||
type outputArgs struct {
|
||||
// Package of the resulting file.
|
||||
pkg string
|
||||
// The prefix of all names declared at the top-level.
|
||||
stem string
|
||||
// Build constraints included in the resulting file.
|
||||
constraints constraint.Expr
|
||||
// Maps to be emitted.
|
||||
maps []string
|
||||
// Programs to be emitted.
|
||||
programs []string
|
||||
// Types to be emitted.
|
||||
types []btf.Type
|
||||
// Filename of the ELF object to embed.
|
||||
obj string
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func output(args outputArgs) error {
|
||||
maps := make(map[string]string)
|
||||
for _, name := range args.maps {
|
||||
maps[name] = internal.Identifier(name)
|
||||
}
|
||||
|
||||
programs := make(map[string]string)
|
||||
for _, name := range args.programs {
|
||||
programs[name] = internal.Identifier(name)
|
||||
}
|
||||
|
||||
typeNames := make(map[btf.Type]string)
|
||||
for _, typ := range args.types {
|
||||
typeNames[typ] = args.stem + internal.Identifier(typ.TypeName())
|
||||
}
|
||||
|
||||
// Ensure we don't have conflicting names and generate a sorted list of
|
||||
// named types so that the output is stable.
|
||||
types, err := sortTypes(typeNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
module := currentModule()
|
||||
|
||||
gf := &btf.GoFormatter{
|
||||
Names: typeNames,
|
||||
Identifier: internal.Identifier,
|
||||
}
|
||||
|
||||
ctx := struct {
|
||||
*btf.GoFormatter
|
||||
Module string
|
||||
Package string
|
||||
Constraints constraint.Expr
|
||||
Name templateName
|
||||
Maps map[string]string
|
||||
Programs map[string]string
|
||||
Types []btf.Type
|
||||
TypeNames map[btf.Type]string
|
||||
File string
|
||||
}{
|
||||
gf,
|
||||
module,
|
||||
args.pkg,
|
||||
args.constraints,
|
||||
templateName(args.stem),
|
||||
maps,
|
||||
programs,
|
||||
types,
|
||||
typeNames,
|
||||
args.obj,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := commonTemplate.Execute(&buf, &ctx); err != nil {
|
||||
return fmt.Errorf("can't generate types: %s", err)
|
||||
}
|
||||
|
||||
return internal.WriteFormatted(buf.Bytes(), args.out)
|
||||
}
|
||||
|
||||
func collectFromSpec(spec *ebpf.CollectionSpec, cTypes []string, skipGlobalTypes bool) (maps, programs []string, types []btf.Type, _ error) {
|
||||
for name := range spec.Maps {
|
||||
// Skip .rodata, .data, .bss, etc. sections
|
||||
if !strings.HasPrefix(name, ".") {
|
||||
maps = append(maps, name)
|
||||
}
|
||||
}
|
||||
|
||||
for name := range spec.Programs {
|
||||
programs = append(programs, name)
|
||||
}
|
||||
|
||||
types, err := collectCTypes(spec.Types, cTypes)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("collect C types: %w", err)
|
||||
}
|
||||
|
||||
// Collect map key and value types, unless we've been asked not to.
|
||||
if skipGlobalTypes {
|
||||
return maps, programs, types, nil
|
||||
}
|
||||
|
||||
for _, typ := range collectMapTypes(spec.Maps) {
|
||||
switch btf.UnderlyingType(typ).(type) {
|
||||
case *btf.Datasec:
|
||||
// Avoid emitting .rodata, .bss, etc. for now. We might want to
|
||||
// name these types differently, etc.
|
||||
continue
|
||||
|
||||
case *btf.Int:
|
||||
// Don't emit primitive types by default.
|
||||
continue
|
||||
}
|
||||
|
||||
types = append(types, typ)
|
||||
}
|
||||
|
||||
return maps, programs, types, nil
|
||||
}
|
||||
|
||||
func collectCTypes(types *btf.Spec, names []string) ([]btf.Type, error) {
|
||||
var result []btf.Type
|
||||
for _, cType := range names {
|
||||
typ, err := types.AnyTypeByName(cType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, typ)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// collectMapTypes returns a list of all types used as map keys or values.
|
||||
func collectMapTypes(maps map[string]*ebpf.MapSpec) []btf.Type {
|
||||
var result []btf.Type
|
||||
for _, m := range maps {
|
||||
if m.Key != nil && m.Key.TypeName() != "" {
|
||||
result = append(result, m.Key)
|
||||
}
|
||||
|
||||
if m.Value != nil && m.Value.TypeName() != "" {
|
||||
result = append(result, m.Value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// sortTypes returns a list of types sorted by their (generated) Go type name.
|
||||
//
|
||||
// Duplicate Go type names are rejected.
|
||||
func sortTypes(typeNames map[btf.Type]string) ([]btf.Type, error) {
|
||||
var types []btf.Type
|
||||
var names []string
|
||||
for typ, name := range typeNames {
|
||||
i := sort.SearchStrings(names, name)
|
||||
if i >= len(names) {
|
||||
types = append(types, typ)
|
||||
names = append(names, name)
|
||||
continue
|
||||
}
|
||||
|
||||
if names[i] == name {
|
||||
return nil, fmt.Errorf("type name %q is used multiple times", name)
|
||||
}
|
||||
|
||||
types = append(types[:i], append([]btf.Type{typ}, types[i:]...)...)
|
||||
names = append(names[:i], append([]string{name}, names[i:]...)...)
|
||||
}
|
||||
|
||||
return types, nil
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
{{ with .Constraints }}//go:build {{ . }}{{ end }}
|
||||
|
||||
package {{ .Package }}
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"{{ .Module }}"
|
||||
)
|
||||
|
||||
{{- if .Types }}
|
||||
{{- range $type := .Types }}
|
||||
{{ $.TypeDeclaration (index $.TypeNames $type) $type }}
|
||||
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
|
||||
// {{ .Name.Load }} returns the embedded CollectionSpec for {{ .Name }}.
|
||||
func {{ .Name.Load }}() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader({{ .Name.Bytes }})
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load {{ .Name }}: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// {{ .Name.LoadObjects }} loads {{ .Name }} and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *{{ .Name.Objects }}
|
||||
// *{{ .Name.Programs }}
|
||||
// *{{ .Name.Maps }}
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func {{ .Name.LoadObjects }}(obj interface{}, opts *ebpf.CollectionOptions) (error) {
|
||||
spec, err := {{ .Name.Load }}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// {{ .Name.Specs }} contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type {{ .Name.Specs }} struct {
|
||||
{{ .Name.ProgramSpecs }}
|
||||
{{ .Name.MapSpecs }}
|
||||
}
|
||||
|
||||
// {{ .Name.Specs }} contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type {{ .Name.ProgramSpecs }} struct {
|
||||
{{- range $name, $id := .Programs }}
|
||||
{{ $id }} *ebpf.ProgramSpec `ebpf:"{{ $name }}"`
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
// {{ .Name.MapSpecs }} contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type {{ .Name.MapSpecs }} struct {
|
||||
{{- range $name, $id := .Maps }}
|
||||
{{ $id }} *ebpf.MapSpec `ebpf:"{{ $name }}"`
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
// {{ .Name.Objects }} contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to {{ .Name.LoadObjects }} or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type {{ .Name.Objects }} struct {
|
||||
{{ .Name.Programs }}
|
||||
{{ .Name.Maps }}
|
||||
}
|
||||
|
||||
func (o *{{ .Name.Objects }}) Close() error {
|
||||
return {{ .Name.CloseHelper }}(
|
||||
&o.{{ .Name.Programs }},
|
||||
&o.{{ .Name.Maps }},
|
||||
)
|
||||
}
|
||||
|
||||
// {{ .Name.Maps }} contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to {{ .Name.LoadObjects }} or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type {{ .Name.Maps }} struct {
|
||||
{{- range $name, $id := .Maps }}
|
||||
{{ $id }} *ebpf.Map `ebpf:"{{ $name }}"`
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
func (m *{{ .Name.Maps }}) Close() error {
|
||||
return {{ .Name.CloseHelper }}(
|
||||
{{- range $id := .Maps }}
|
||||
m.{{ $id }},
|
||||
{{- end }}
|
||||
)
|
||||
}
|
||||
|
||||
// {{ .Name.Programs }} contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to {{ .Name.LoadObjects }} or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type {{ .Name.Programs }} struct {
|
||||
{{- range $name, $id := .Programs }}
|
||||
{{ $id }} *ebpf.Program `ebpf:"{{ $name }}"`
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
func (p *{{ .Name.Programs }}) Close() error {
|
||||
return {{ .Name.CloseHelper }}(
|
||||
{{- range $id := .Programs }}
|
||||
p.{{ $id }},
|
||||
{{- end }}
|
||||
)
|
||||
}
|
||||
|
||||
func {{ .Name.CloseHelper }}(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//go:embed {{ .File }}
|
||||
var {{ .Name.Bytes }} []byte
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/btf"
|
||||
"github.com/cilium/ebpf/internal"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestOrderTypes(t *testing.T) {
|
||||
a := &btf.Int{}
|
||||
b := &btf.Int{}
|
||||
c := &btf.Int{}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
in map[btf.Type]string
|
||||
out []btf.Type
|
||||
}{
|
||||
{
|
||||
"order",
|
||||
map[btf.Type]string{
|
||||
a: "foo",
|
||||
b: "bar",
|
||||
c: "baz",
|
||||
},
|
||||
[]btf.Type{b, c, a},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := sortTypes(test.in)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, len(result), qt.Equals, len(test.out))
|
||||
for i, o := range test.out {
|
||||
if result[i] != o {
|
||||
t.Fatalf("Index %d: expected %p got %p", i, o, result[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
in map[btf.Type]string
|
||||
}{
|
||||
{
|
||||
"duplicate names",
|
||||
map[btf.Type]string{
|
||||
a: "foo",
|
||||
b: "foo",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := sortTypes(test.in)
|
||||
qt.Assert(t, err, qt.IsNotNil)
|
||||
qt.Assert(t, result, qt.IsNil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var typesEqual = qt.CmpEquals(cmp.Comparer(func(a, b btf.Type) bool {
|
||||
return a == b
|
||||
}))
|
||||
|
||||
func TestCollectFromSpec(t *testing.T) {
|
||||
spec, err := ebpf.LoadCollectionSpec(fmt.Sprintf("testdata/minimal-%s.elf", internal.ClangEndian))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
map1 := spec.Maps["map1"]
|
||||
|
||||
maps, programs, types, err := collectFromSpec(spec, nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
qt.Assert(t, maps, qt.ContentEquals, []string{"map1"})
|
||||
qt.Assert(t, programs, qt.ContentEquals, []string{"filter"})
|
||||
qt.Assert(t, types, typesEqual, []btf.Type{map1.Key, map1.Value})
|
||||
|
||||
_, _, types, err = collectFromSpec(spec, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
qt.Assert(t, types, typesEqual, ([]btf.Type)(nil))
|
||||
|
||||
_, _, types, err = collectFromSpec(spec, []string{"barfoo"}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
qt.Assert(t, types, typesEqual, []btf.Type{map1.Value})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/cilium/ebpf/internal/testutils"
|
||||
)
|
||||
|
||||
func TestLoadingSpec(t *testing.T) {
|
||||
spec, err := loadTest()
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("Can't load spec:", err)
|
||||
}
|
||||
|
||||
if spec == nil {
|
||||
t.Fatal("Got a nil spec")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadingObjects(t *testing.T) {
|
||||
var objs testObjects
|
||||
err := loadTestObjects(&objs, nil)
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("Can't load objects:", err)
|
||||
}
|
||||
defer objs.Close()
|
||||
|
||||
if objs.Filter == nil {
|
||||
t.Error("Loading returns an object with nil programs")
|
||||
}
|
||||
|
||||
if objs.Map1 == nil {
|
||||
t.Error("Loading returns an object with nil maps")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypes(t *testing.T) {
|
||||
if testEHOOPY != 0 {
|
||||
t.Error("Expected testEHOOPY to be 0, got", testEHOOPY)
|
||||
}
|
||||
if testEFROOD != 1 {
|
||||
t.Error("Expected testEFROOD to be 0, got", testEFROOD)
|
||||
}
|
||||
|
||||
e := testE(0)
|
||||
if size := unsafe.Sizeof(e); size != 4 {
|
||||
t.Error("Expected size of exampleE to be 4, got", size)
|
||||
}
|
||||
|
||||
bf := testBarfoo{}
|
||||
if size := unsafe.Sizeof(bf); size != 16 {
|
||||
t.Error("Expected size of exampleE to be 16, got", size)
|
||||
}
|
||||
if reflect.TypeOf(bf.Bar).Kind() != reflect.Int64 {
|
||||
t.Error("Expected testBarfoo.Bar to be int64")
|
||||
}
|
||||
if reflect.TypeOf(bf.Baz).Kind() != reflect.Bool {
|
||||
t.Error("Expected testBarfoo.Baz to be bool")
|
||||
}
|
||||
if reflect.TypeOf(bf.Boo) != reflect.TypeOf(e) {
|
||||
t.Error("Expected testBarfoo.Boo to be exampleE")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Package test checks that the code generated by bpf2go conforms to a
|
||||
// specific API.
|
||||
package test
|
||||
|
||||
// $BPF_CLANG and $BPF_CFLAGS are set by the Makefile.
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG test ../testdata/minimal.c
|
||||
@@ -0,0 +1,133 @@
|
||||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
type testBarfoo struct {
|
||||
Bar int64
|
||||
Baz bool
|
||||
_ [3]byte
|
||||
Boo testE
|
||||
}
|
||||
|
||||
type testE uint32
|
||||
|
||||
const (
|
||||
testEHOOPY testE = 0
|
||||
testEFROOD testE = 1
|
||||
)
|
||||
|
||||
// loadTest returns the embedded CollectionSpec for test.
|
||||
func loadTest() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader(_TestBytes)
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load test: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// loadTestObjects loads test and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *testObjects
|
||||
// *testPrograms
|
||||
// *testMaps
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func loadTestObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
|
||||
spec, err := loadTest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// testSpecs contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testSpecs struct {
|
||||
testProgramSpecs
|
||||
testMapSpecs
|
||||
}
|
||||
|
||||
// testSpecs contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testProgramSpecs struct {
|
||||
Filter *ebpf.ProgramSpec `ebpf:"filter"`
|
||||
}
|
||||
|
||||
// testMapSpecs contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testMapSpecs struct {
|
||||
Map1 *ebpf.MapSpec `ebpf:"map1"`
|
||||
}
|
||||
|
||||
// testObjects contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testObjects struct {
|
||||
testPrograms
|
||||
testMaps
|
||||
}
|
||||
|
||||
func (o *testObjects) Close() error {
|
||||
return _TestClose(
|
||||
&o.testPrograms,
|
||||
&o.testMaps,
|
||||
)
|
||||
}
|
||||
|
||||
// testMaps contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testMaps struct {
|
||||
Map1 *ebpf.Map `ebpf:"map1"`
|
||||
}
|
||||
|
||||
func (m *testMaps) Close() error {
|
||||
return _TestClose(
|
||||
m.Map1,
|
||||
)
|
||||
}
|
||||
|
||||
// testPrograms contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testPrograms struct {
|
||||
Filter *ebpf.Program `ebpf:"filter"`
|
||||
}
|
||||
|
||||
func (p *testPrograms) Close() error {
|
||||
return _TestClose(
|
||||
p.Filter,
|
||||
)
|
||||
}
|
||||
|
||||
func _TestClose(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//
|
||||
//go:embed test_bpfeb.o
|
||||
var _TestBytes []byte
|
||||
Binary file not shown.
@@ -0,0 +1,133 @@
|
||||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
//go:build 386 || amd64 || amd64p32 || arm || arm64 || loong64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
type testBarfoo struct {
|
||||
Bar int64
|
||||
Baz bool
|
||||
_ [3]byte
|
||||
Boo testE
|
||||
}
|
||||
|
||||
type testE uint32
|
||||
|
||||
const (
|
||||
testEHOOPY testE = 0
|
||||
testEFROOD testE = 1
|
||||
)
|
||||
|
||||
// loadTest returns the embedded CollectionSpec for test.
|
||||
func loadTest() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader(_TestBytes)
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load test: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// loadTestObjects loads test and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *testObjects
|
||||
// *testPrograms
|
||||
// *testMaps
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func loadTestObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
|
||||
spec, err := loadTest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// testSpecs contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testSpecs struct {
|
||||
testProgramSpecs
|
||||
testMapSpecs
|
||||
}
|
||||
|
||||
// testSpecs contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testProgramSpecs struct {
|
||||
Filter *ebpf.ProgramSpec `ebpf:"filter"`
|
||||
}
|
||||
|
||||
// testMapSpecs contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testMapSpecs struct {
|
||||
Map1 *ebpf.MapSpec `ebpf:"map1"`
|
||||
}
|
||||
|
||||
// testObjects contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testObjects struct {
|
||||
testPrograms
|
||||
testMaps
|
||||
}
|
||||
|
||||
func (o *testObjects) Close() error {
|
||||
return _TestClose(
|
||||
&o.testPrograms,
|
||||
&o.testMaps,
|
||||
)
|
||||
}
|
||||
|
||||
// testMaps contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testMaps struct {
|
||||
Map1 *ebpf.Map `ebpf:"map1"`
|
||||
}
|
||||
|
||||
func (m *testMaps) Close() error {
|
||||
return _TestClose(
|
||||
m.Map1,
|
||||
)
|
||||
}
|
||||
|
||||
// testPrograms contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testPrograms struct {
|
||||
Filter *ebpf.Program `ebpf:"filter"`
|
||||
}
|
||||
|
||||
func (p *testPrograms) Close() error {
|
||||
return _TestClose(
|
||||
p.Filter,
|
||||
)
|
||||
}
|
||||
|
||||
func _TestClose(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//
|
||||
//go:embed test_bpfel.o
|
||||
var _TestBytes []byte
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
#include "../../../testdata/common.h"
|
||||
|
||||
char __license[] __section("license") = "MIT";
|
||||
|
||||
enum e { HOOPY, FROOD };
|
||||
|
||||
typedef long long int longint;
|
||||
|
||||
typedef struct {
|
||||
longint bar;
|
||||
_Bool baz;
|
||||
enum e boo;
|
||||
} barfoo;
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__type(key, enum e);
|
||||
__type(value, barfoo);
|
||||
__uint(max_entries, 1);
|
||||
} map1 __section(".maps");
|
||||
|
||||
volatile const enum e my_constant = FROOD;
|
||||
|
||||
volatile const barfoo struct_const;
|
||||
|
||||
__section("socket") int filter() {
|
||||
return my_constant + struct_const.bar;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func splitCFlagsFromArgs(in []string) (args, cflags []string) {
|
||||
for i, arg := range in {
|
||||
if arg == "--" {
|
||||
return in[:i], in[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func splitArguments(in string) ([]string, error) {
|
||||
var (
|
||||
result []string
|
||||
builder strings.Builder
|
||||
escaped bool
|
||||
delim = ' '
|
||||
)
|
||||
|
||||
for _, r := range strings.TrimSpace(in) {
|
||||
if escaped {
|
||||
builder.WriteRune(r)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
switch r {
|
||||
case '\\':
|
||||
escaped = true
|
||||
|
||||
case delim:
|
||||
current := builder.String()
|
||||
builder.Reset()
|
||||
|
||||
if current != "" || delim != ' ' {
|
||||
// Only append empty words if they are not
|
||||
// delimited by spaces
|
||||
result = append(result, current)
|
||||
}
|
||||
delim = ' '
|
||||
|
||||
case '"', '\'', ' ':
|
||||
if delim == ' ' {
|
||||
delim = r
|
||||
continue
|
||||
}
|
||||
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
builder.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
if delim != ' ' {
|
||||
return nil, fmt.Errorf("missing `%c`", delim)
|
||||
}
|
||||
|
||||
if escaped {
|
||||
return nil, errors.New("unfinished escape")
|
||||
}
|
||||
|
||||
// Add the last word
|
||||
if builder.Len() > 0 {
|
||||
result = append(result, builder.String())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func toUpperFirst(str string) string {
|
||||
first, n := utf8.DecodeRuneInString(str)
|
||||
return string(unicode.ToUpper(first)) + str[n:]
|
||||
}
|
||||
|
||||
func currentModule() string {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
// Fall back to constant since bazel doesn't support BuildInfo.
|
||||
return "github.com/cilium/ebpf"
|
||||
}
|
||||
|
||||
return bi.Main.Path
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitArguments(t *testing.T) {
|
||||
testcases := []struct {
|
||||
in string
|
||||
out []string
|
||||
}{
|
||||
{`foo`, []string{"foo"}},
|
||||
{`foo bar`, []string{"foo", "bar"}},
|
||||
{`foo bar`, []string{"foo", "bar"}},
|
||||
{`\\`, []string{`\`}},
|
||||
{`\\\`, nil},
|
||||
{`foo\ bar`, []string{"foo bar"}},
|
||||
{`foo "" bar`, []string{"foo", "", "bar"}},
|
||||
{`"bar baz"`, []string{"bar baz"}},
|
||||
{`'bar baz'`, []string{"bar baz"}},
|
||||
{`'bar " " baz'`, []string{`bar " " baz`}},
|
||||
{`"bar \" baz"`, []string{`bar " baz`}},
|
||||
{`"`, nil},
|
||||
{`'`, nil},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
have, err := splitArguments(testcase.in)
|
||||
if testcase.out == nil {
|
||||
if err == nil {
|
||||
t.Errorf("Test should fail for: %s", testcase.in)
|
||||
}
|
||||
} else if !reflect.DeepEqual(testcase.out, have) {
|
||||
t.Logf("Have: %q\n", have)
|
||||
t.Logf("Want: %q\n", testcase.out)
|
||||
t.Errorf("Test fails for: %s", testcase.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user