Skip to content
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

bpf2go: generate assignment structs and Go types for Variables and VariableSpecs #1610

Merged
merged 4 commits into from
Dec 18, 2024
Merged
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
3 changes: 3 additions & 0 deletions btf/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ func (gf *GoFormatter) writeTypeLit(typ Type, depth int) error {
case *Datasec:
err = gf.writeDatasecLit(v, depth)

case *Var:
err = gf.writeTypeLit(v.Type, depth)

default:
return fmt.Errorf("type %T: %w", v, ErrNotSupported)
}
Expand Down
1 change: 1 addition & 0 deletions btf/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func TestGoTypeDeclaration(t *testing.T) {
},
"type t struct { _ [4]byte; g uint32; _ [8]byte; }",
},
{&Var{Type: &Int{Size: 4}}, "type t uint32"},
}

for _, test := range tests {
Expand Down
14 changes: 14 additions & 0 deletions btf/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,20 @@ func UnderlyingType(typ Type) Type {
return &cycle{typ}
}

// QualifiedType returns the type with all qualifiers removed.
func QualifiedType(typ Type) Type {
result := typ
for depth := 0; depth <= maxResolveDepth; depth++ {
switch v := (result).(type) {
case qualifier:
result = v.qualify()
default:
return result
}
}
return &cycle{typ}
}

// As returns typ if is of type T. Otherwise it peels qualifiers and Typedefs
// until it finds a T.
//
Expand Down
17 changes: 17 additions & 0 deletions cmd/bpf2go/gen/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ func (n templateName) MapSpecs() string {
return string(n) + "MapSpecs"
}

func (n templateName) VariableSpecs() string {
return string(n) + "VariableSpecs"
}

func (n templateName) Load() string {
return n.maybeExport("load" + toUpperFirst(string(n)))
}
Expand All @@ -65,6 +69,10 @@ func (n templateName) Maps() string {
return string(n) + "Maps"
}

func (n templateName) Variables() string {
return string(n) + "Variables"
}

func (n templateName) Programs() string {
return string(n) + "Programs"
}
Expand All @@ -82,6 +90,8 @@ type GenerateArgs struct {
Constraints constraint.Expr
// Maps to be emitted.
Maps []string
// Variables to be emitted.
Variables []string
// Programs to be emitted.
Programs []string
// Types to be emitted.
Expand Down Expand Up @@ -121,6 +131,11 @@ func Generate(args GenerateArgs) error {
maps[name] = args.Identifier(name)
}

variables := make(map[string]string)
for _, name := range args.Variables {
variables[name] = args.Identifier(name)
}

programs := make(map[string]string)
for _, name := range args.Programs {
programs[name] = args.Identifier(name)
Expand Down Expand Up @@ -151,6 +166,7 @@ func Generate(args GenerateArgs) error {
Constraints constraint.Expr
Name templateName
Maps map[string]string
Variables map[string]string
Programs map[string]string
Types []btf.Type
TypeNames map[btf.Type]string
Expand All @@ -162,6 +178,7 @@ func Generate(args GenerateArgs) error {
args.Constraints,
templateName(args.Stem),
maps,
variables,
programs,
types,
typeNames,
Expand Down
22 changes: 21 additions & 1 deletion cmd/bpf2go/gen/output.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ func {{ .Name.LoadObjects }}(obj interface{}, opts *ebpf.CollectionOptions) (err
type {{ .Name.Specs }} struct {
{{ .Name.ProgramSpecs }}
{{ .Name.MapSpecs }}
{{ .Name.VariableSpecs }}
}

// {{ .Name.Specs }} contains programs before they are loaded into the kernel.
// {{ .Name.ProgramSpecs }} contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type {{ .Name.ProgramSpecs }} struct {
Expand All @@ -74,12 +75,22 @@ type {{ .Name.MapSpecs }} struct {
{{- end }}
}

// {{ .Name.VariableSpecs }} contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type {{ .Name.VariableSpecs }} struct {
{{- range $name, $id := .Variables }}
{{ $id }} *ebpf.VariableSpec `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 }}
{{ .Name.Variables }}
}

func (o *{{ .Name.Objects }}) Close() error {
Expand All @@ -106,6 +117,15 @@ func (m *{{ .Name.Maps }}) Close() error {
)
}

// {{ .Name.Variables }} contains all global variables after they have been loaded into the kernel.
//
// It can be passed to {{ .Name.LoadObjects }} or ebpf.CollectionSpec.LoadAndAssign.
type {{ .Name.Variables }} struct {
{{- range $name, $id := .Variables }}
{{ $id }} *ebpf.Variable `ebpf:"{{ $name }}"`
{{- 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.
Expand Down
29 changes: 24 additions & 5 deletions cmd/bpf2go/gen/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"testing"

"github.com/go-quicktest/qt"
"github.com/google/go-cmp/cmp"

"github.com/cilium/ebpf/btf"
"github.com/cilium/ebpf/cmd/bpf2go/internal"
Expand Down Expand Up @@ -79,10 +78,6 @@ func TestPackageImport(t *testing.T) {
qt.Assert(t, qt.StringContains(buf.String(), fmt.Sprintf(`"%s"`, internal.CurrentModule)))
}

var typesEqualComparer = cmp.Comparer(func(a, b btf.Type) bool {
return a == b
})

func TestCustomIdentifier(t *testing.T) {
var buf bytes.Buffer
args := GenerateArgs{
Expand All @@ -97,3 +92,27 @@ func TestCustomIdentifier(t *testing.T) {
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.StringContains(buf.String(), "DO_THING"))
}

func TestObjects(t *testing.T) {
var buf bytes.Buffer
args := GenerateArgs{
Package: "foo",
Stem: "bar",
Maps: []string{"map1"},
Variables: []string{"var_1"},
Programs: []string{"prog_foo_1"},
Output: &buf,
}
err := Generate(args)
qt.Assert(t, qt.IsNil(err))

str := buf.String()

qt.Assert(t, qt.StringContains(str, "Map1 *ebpf.MapSpec `ebpf:\"map1\"`"))
qt.Assert(t, qt.StringContains(str, "Var1 *ebpf.VariableSpec `ebpf:\"var_1\"`"))
qt.Assert(t, qt.StringContains(str, "ProgFoo1 *ebpf.ProgramSpec `ebpf:\"prog_foo_1\"`"))

qt.Assert(t, qt.StringContains(str, "Map1 *ebpf.Map `ebpf:\"map1\"`"))
qt.Assert(t, qt.StringContains(str, "Var1 *ebpf.Variable `ebpf:\"var_1\"`"))
qt.Assert(t, qt.StringContains(str, "ProgFoo1 *ebpf.Program `ebpf:\"prog_foo_1\"`"))
}
89 changes: 70 additions & 19 deletions cmd/bpf2go/gen/types.go
Original file line number Diff line number Diff line change
@@ -1,44 +1,95 @@
package gen

import (
"cmp"
"slices"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/btf"
)

// CollectGlobalTypes finds all types which are used in the global scope.
//
// This currently includes the types of map keys and values.
// This currently includes the types of variables, map keys and values.
func CollectGlobalTypes(spec *ebpf.CollectionSpec) []btf.Type {
var types []btf.Type
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 = collectMapTypes(types, spec.Maps)
types = collectVariableTypes(types, spec.Variables)

types = append(types, typ)
}
slices.SortStableFunc(types, func(a, b btf.Type) int {
return cmp.Compare(a.TypeName(), b.TypeName())
})

return types
}

// 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
// collectMapTypes collects all types used by MapSpecs.
func collectMapTypes(types []btf.Type, maps map[string]*ebpf.MapSpec) []btf.Type {
for _, m := range maps {
if m.Key != nil && m.Key.TypeName() != "" {
result = append(result, m.Key)
types = addType(types, m.Key)
}

if m.Value != nil && m.Value.TypeName() != "" {
result = append(result, m.Value)
types = addType(types, m.Value)
}
}
return result

return types
}

// collectVariableTypes collects all types used by VariableSpecs.
func collectVariableTypes(types []btf.Type, vars map[string]*ebpf.VariableSpec) []btf.Type {
for _, vs := range vars {
v := vs.Type()
if v == nil {
continue
}

types = addType(types, v.Type)
}

return types
}

// addType adds a type to types if not already present. Types that don't need to
// be generated are not added to types.
func addType(types []btf.Type, incoming btf.Type) []btf.Type {
incoming = selectType(incoming)
if incoming == nil {
return types
}

// Strip only the qualifiers (not typedefs) from the incoming type. Retain
// typedefs since they carry the name of the anonymous type they point to,
// without which we can't generate a named Go type.
incoming = btf.QualifiedType(incoming)
if incoming.TypeName() == "" {
return types
}

exists := func(existing btf.Type) bool {
return existing.TypeName() == incoming.TypeName()
}
if !slices.ContainsFunc(types, exists) {
types = append(types, incoming)
}
return types
}

func selectType(t btf.Type) btf.Type {
// Obtain a concrete type with qualifiers and typedefs stripped.
switch ut := btf.UnderlyingType(t).(type) {
case *btf.Struct, *btf.Union, *btf.Enum:
return t

// Collect the array's element type. Note: qualifiers on array-type variables
// typically appear after the array, e.g. a const volatile int[4] is actually
// an array of const volatile ints.
case *btf.Array:
return selectType(ut.Type)
}

return nil
}
27 changes: 21 additions & 6 deletions cmd/bpf2go/gen/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,34 @@ import (
"github.com/cilium/ebpf/internal/testutils"

"github.com/go-quicktest/qt"
"github.com/google/go-cmp/cmp"
)

func mustAnyTypeByName(t *testing.T, spec *ebpf.CollectionSpec, name string) btf.Type {
t.Helper()

typ, err := spec.Types.AnyTypeByName(name)
qt.Assert(t, qt.IsNil(err))
return typ
}

func TestCollectGlobalTypes(t *testing.T) {
spec, err := ebpf.LoadCollectionSpec(testutils.NativeFile(t, "../testdata/minimal-%s.elf"))
if err != nil {
t.Fatal(err)
}

map1 := spec.Maps["map1"]
bar := mustAnyTypeByName(t, spec, "bar")
barfoo := mustAnyTypeByName(t, spec, "barfoo")
baz := mustAnyTypeByName(t, spec, "baz")
e := mustAnyTypeByName(t, spec, "e")
ubar := mustAnyTypeByName(t, spec, "ubar")

types := CollectGlobalTypes(spec)
if err != nil {
t.Fatal(err)
}
qt.Assert(t, qt.CmpEquals(types, []btf.Type{map1.Key, map1.Value}, typesEqualComparer))
got := CollectGlobalTypes(spec)
qt.Assert(t, qt.IsNil(err))

want := []btf.Type{bar, barfoo, baz, e, ubar}
qt.Assert(t, qt.CmpEquals(got, want, cmp.Comparer(func(a, b btf.Type) bool {
return a.TypeName() == b.TypeName()
})))
}
6 changes: 6 additions & 0 deletions cmd/bpf2go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,11 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
}
}

var variables []string
for name := range spec.Variables {
variables = append(variables, name)
}

var programs []string
for name := range spec.Programs {
programs = append(programs, name)
Expand Down Expand Up @@ -397,6 +402,7 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
Stem: b2g.identStem,
Constraints: constraints,
Maps: maps,
Variables: variables,
Programs: programs,
Types: types,
ObjectFile: filepath.Base(objFileName),
Expand Down
Loading
Loading