Skip to content

Commit

Permalink
bpf2go: generate Go types used in global variables
Browse files Browse the repository at this point in the history
This commit generates Go type declarations for global variables declared
in a C BPF program. Some improvements to CollectGlobalTypes were
due:

- types are now deduplicated by name
- types are now selected based on allow-list instead of explicitly
  rejecting Datasec and Int
- the output is sorted by type name
- arrays' inner types are now emitted to interact with Variable{Spec}

Also added a new btf.QualifiedType(), a subset of btf.UnderlyingType()
to remove all qualifiers up to a Typedef, since Typedefs are named, and
typically directly point to the anonymous type they alias.

Typedefs should only be stripped for evaluating the concrete underlying
type, and they should be included when yielding types to the Go renderer.

Signed-off-by: Timo Beckers <[email protected]>
Co-authored-by: Simone Magnani <[email protected]>
  • Loading branch information
ti-mo and smagnani96 committed Dec 18, 2024
1 parent 1e8f079 commit 580ff21
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 26 deletions.
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
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()
})))
}
23 changes: 23 additions & 0 deletions cmd/bpf2go/test/test_bpfeb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified cmd/bpf2go/test/test_bpfeb.o
Binary file not shown.
23 changes: 23 additions & 0 deletions cmd/bpf2go/test/test_bpfel.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified cmd/bpf2go/test/test_bpfel.o
Binary file not shown.
Binary file modified cmd/bpf2go/testdata/minimal-eb.elf
Binary file not shown.
Binary file modified cmd/bpf2go/testdata/minimal-el.elf
Binary file not shown.
21 changes: 20 additions & 1 deletion cmd/bpf2go/testdata/minimal.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,35 @@ typedef struct {
enum e boo;
} barfoo;

typedef struct {
uint64_t a;
} baz;

struct bar {
uint64_t a;
uint32_t b;
};

union ubar {
uint32_t a;
uint64_t b;
};

struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, enum e);
__type(value, barfoo);
__uint(max_entries, 1);
} map1 __section(".maps");

volatile const int an_int;
volatile const enum e my_constant = FROOD;

volatile const int int_array[2];
volatile const barfoo struct_const;
volatile const baz struct_array[2];

volatile struct bar struct_var;
volatile union ubar union_var;

__section("socket") int filter() {
return my_constant + struct_const.bar;
Expand Down

0 comments on commit 580ff21

Please sign in to comment.