-
Notifications
You must be signed in to change notification settings - Fork 848
[POC] bpf2go/struct_ops: implement StructOps support #1973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,6 +79,10 @@ func (n templateName) Programs() string { | |
| return string(n) + "Programs" | ||
| } | ||
|
|
||
| func (n templateName) StructOps() string { | ||
| return string(n) + "StructOps" | ||
| } | ||
|
|
||
| func (n templateName) CloseHelper() string { | ||
| return "_" + toUpperFirst(string(n)) + "Close" | ||
| } | ||
|
|
@@ -104,6 +108,76 @@ type GenerateArgs struct { | |
| Output io.Writer | ||
| // Function which transforms the input into a valid go identifier. Uses the default behaviour if nil | ||
| Identifier func(string) string | ||
| // StructOps to be emitted | ||
| StructOps []StructOpsSpec | ||
| } | ||
|
|
||
| type StructOpsSpec struct { | ||
| Name string | ||
| Type *btf.Struct | ||
| } | ||
|
|
||
| // generateStructOpsShadowType produces a Go struct definition that mirrors the memory | ||
| // layout of a BTF struct, specifically for use with struct_ops maps. | ||
| func generateStructOpsShadowType(goTypeName string, st *btf.Struct, gf *btf.GoFormatter) (string, error) { | ||
| var sb strings.Builder | ||
|
|
||
| sb.WriteString(fmt.Sprintf("// %s is a struct type for the struct_ops map.\n", goTypeName)) | ||
| sb.WriteString(fmt.Sprintf("type %s struct {\n", goTypeName)) | ||
| sb.WriteString("\t_ structs.HostLayout\n") | ||
|
|
||
| prevOffset := uint32(0) | ||
| for _, m := range st.Members { | ||
| offset := m.Offset.Bytes() | ||
| if padding := offset - prevOffset; padding > 0 { | ||
| sb.WriteString(fmt.Sprintf("\t_ [%d]byte\n", padding)) | ||
| } | ||
|
|
||
| fieldName := gf.Identifier(m.Name) | ||
| if fieldName == "" { | ||
| continue | ||
| } | ||
|
|
||
| var fieldType string | ||
| underlying := btf.UnderlyingType(m.Type) | ||
|
|
||
| if ptr, ok := btf.As[*btf.Pointer](underlying); ok { | ||
| if _, ok := btf.As[*btf.FuncProto](btf.UnderlyingType(ptr.Target)); ok { | ||
| fieldType = "*ebpf.Program" | ||
| } | ||
| } | ||
|
|
||
| if fieldType == "" { | ||
| if _, ok := btf.As[*btf.Struct](underlying); ok { | ||
| size, _ := btf.Sizeof(m.Type) | ||
| fieldType = fmt.Sprintf("[%d]byte", size) | ||
| } else if _, ok := btf.As[*btf.Union](underlying); ok { | ||
| size, _ := btf.Sizeof(m.Type) | ||
| fieldType = fmt.Sprintf("[%d]byte", size) | ||
| } | ||
|
Comment on lines
+151
to
+157
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also deviates from the So question, did we do this because recursive emitting is difficult or is there a technical reason? If its just difficult, usage of the |
||
| } | ||
|
|
||
| if fieldType == "" { | ||
| decl, err := gf.TypeDeclaration("T", m.Type) | ||
| if err != nil { | ||
| size, _ := btf.Sizeof(m.Type) | ||
| fieldType = fmt.Sprintf("[%d]byte", size) | ||
| } else { | ||
| fieldType = strings.TrimPrefix(decl, "type T ") | ||
| fieldType = strings.TrimRight(fieldType, " \n\t;") | ||
| } | ||
| } | ||
|
|
||
| sb.WriteString(fmt.Sprintf("\t%s %s `ebpf:\"%s\"`\n", fieldName, fieldType, m.Name)) | ||
| size, err := btf.Sizeof(m.Type) | ||
| if err != nil { | ||
| return "", fmt.Errorf("field %s: %w", m.Name, err) | ||
| } | ||
| prevOffset = offset + uint32(size) | ||
| } | ||
|
|
||
| sb.WriteString("}\n") | ||
| return sb.String(), nil | ||
| } | ||
|
|
||
| // Generate bindings for a BPF ELF file. | ||
|
|
@@ -149,6 +223,12 @@ func Generate(args GenerateArgs) error { | |
| typeNames[typ] = args.Stem + args.Identifier(typ.TypeName()) | ||
| } | ||
|
|
||
| structOps := make(map[string]string) | ||
| for _, stOps := range args.StructOps { | ||
| goTypeName := "StructOps" + args.Identifier(stOps.Name) | ||
| structOps[stOps.Name] = goTypeName | ||
| } | ||
|
|
||
| // 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) | ||
|
|
@@ -174,6 +254,16 @@ func Generate(args GenerateArgs) error { | |
| typeDecls = append(typeDecls, decl) | ||
| } | ||
|
|
||
| for _, st := range args.StructOps { | ||
| goTypeName := args.Stem + "StructOps" + args.Identifier(st.Name) | ||
| decl, err := generateStructOpsShadowType(goTypeName, st.Type, gf) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| typeDecls = append(typeDecls, decl) | ||
| needsStructsPkg = true | ||
| } | ||
|
|
||
| ctx := struct { | ||
| Module string | ||
| Package string | ||
|
|
@@ -185,6 +275,7 @@ func Generate(args GenerateArgs) error { | |
| TypeDeclarations []string | ||
| File string | ||
| NeedsStructsPkg bool | ||
| StructOps map[string]string | ||
| }{ | ||
| b2gInt.CurrentModule, | ||
| args.Package, | ||
|
|
@@ -196,6 +287,7 @@ func Generate(args GenerateArgs) error { | |
| typeDecls, | ||
| args.ObjectFile, | ||
| needsStructsPkg, | ||
| structOps, | ||
| } | ||
|
|
||
| var buf bytes.Buffer | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -94,6 +94,14 @@ type {{ .Name.Objects }} struct { | |
| {{ .Name.Programs }} | ||
| {{ .Name.Maps }} | ||
| {{ .Name.Variables }} | ||
| {{ .Name.StructOps }} | ||
| } | ||
|
|
||
| // {{ .Name.StructOps }} contains all struct_ops types. | ||
| type {{ .Name.StructOps }} struct { | ||
| {{- range $name, $id := .StructOps }} | ||
| {{ $id }} {{ $.Name }}{{ $id }} `ebpf:"{{ $name }}"` | ||
| {{- end }} | ||
|
Comment on lines
+100
to
+104
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would consider surrounding this with an |
||
| } | ||
|
|
||
| func (o *{{ .Name.Objects }}) Close() error { | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -116,7 +116,7 @@ func copyMapOfSpecs[T interface{ Copy() T }](m map[string]T) map[string]T { | |||||||||||||
| // Returns an error if any of the eBPF objects can't be found, or | ||||||||||||||
| // if the same Spec is assigned multiple times. | ||||||||||||||
| func (cs *CollectionSpec) Assign(to interface{}) error { | ||||||||||||||
| getValue := func(typ reflect.Type, name string) (interface{}, error) { | ||||||||||||||
| getValue := func(typ reflect.Type, name string, fieldVal reflect.Value) (interface{}, error) { | ||||||||||||||
| switch typ { | ||||||||||||||
| case reflect.TypeOf((*ProgramSpec)(nil)): | ||||||||||||||
| if p := cs.Programs[name]; p != nil { | ||||||||||||||
|
|
@@ -183,7 +183,19 @@ func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions) | |||||||||||||
| assignedProgs := make(map[string]bool) | ||||||||||||||
| assignedVars := make(map[string]bool) | ||||||||||||||
|
|
||||||||||||||
| getValue := func(typ reflect.Type, name string) (interface{}, error) { | ||||||||||||||
| getValue := func(typ reflect.Type, name string, val reflect.Value) (interface{}, error) { | ||||||||||||||
| ms := cs.Maps[name] | ||||||||||||||
| if ms != nil && ms.Type == StructOpsMap { | ||||||||||||||
| vType := typ | ||||||||||||||
| if vType.Kind() == reflect.Ptr { | ||||||||||||||
| vType = vType.Elem() | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if vType.Kind() == reflect.Struct && typ != reflect.TypeOf((*Map)(nil)) { | ||||||||||||||
| return loader.loadStructOps(name, val) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+187
to
+197
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code should go into |
||||||||||||||
|
|
||||||||||||||
| switch typ { | ||||||||||||||
|
|
||||||||||||||
| case reflect.TypeOf((*Program)(nil)): | ||||||||||||||
|
|
@@ -544,6 +556,38 @@ func (cl *collectionLoader) loadVariable(varName string) (*Variable, error) { | |||||||||||||
| return v, nil | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // loadStructOps synchronizes a user-provided shadow struct with its MapSpec | ||||||||||||||
| // before the map is created in the kernel. | ||||||||||||||
| // | ||||||||||||||
| // If the field is already instantiated by the user, its values are patched | ||||||||||||||
| // into the MapSpec's raw data buffer. This allows setting initial flags, | ||||||||||||||
| // parameters, and function pointers before the kernel performs | ||||||||||||||
| // struct_ops validation. | ||||||||||||||
| func (cl *collectionLoader) loadStructOps(name string, field reflect.Value) (interface{}, error) { | ||||||||||||||
| ms := cl.coll.Maps[name] | ||||||||||||||
| if ms == nil { | ||||||||||||||
| return nil, fmt.Errorf("map %s: not found in loader", name) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if field.IsValid() && field.Kind() == reflect.Struct { | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you invert this there is far less code indented.
Suggested change
|
||||||||||||||
| userType, ok := btf.As[*btf.Struct](ms.Value) | ||||||||||||||
| if !ok { | ||||||||||||||
| return nil, fmt.Errorf("map %s: value type is not a Struct", name) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| rawData, ok := ms.Contents[0].Value.([]byte) | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||||||||||||||
| if !ok { | ||||||||||||||
| return nil, fmt.Errorf("map %s: spec contents are not a byte slice", name) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if err := structOpsPatchValue(field, userType, rawData); err != nil { | ||||||||||||||
| return nil, fmt.Errorf("patching spec from field for %s: %q", name, err) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| return field.Interface(), nil | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // populateDeferredMaps iterates maps holding programs or other maps and loads | ||||||||||||||
| // any dependencies. Populates all maps in cl and freezes them if specified. | ||||||||||||||
| func (cl *collectionLoader) populateDeferredMaps() error { | ||||||||||||||
|
|
@@ -845,7 +889,7 @@ func (coll *Collection) Assign(to interface{}) error { | |||||||||||||
|
|
||||||||||||||
| // Assign() only transfers already-loaded Maps and Programs. No extra | ||||||||||||||
| // loading is done. | ||||||||||||||
| getValue := func(typ reflect.Type, name string) (interface{}, error) { | ||||||||||||||
| getValue := func(typ reflect.Type, name string, fieldVal reflect.Value) (interface{}, error) { | ||||||||||||||
| switch typ { | ||||||||||||||
|
|
||||||||||||||
| case reflect.TypeOf((*Program)(nil)): | ||||||||||||||
|
|
@@ -999,7 +1043,7 @@ func ebpfFields(structVal reflect.Value, visited map[reflect.Type]bool) ([]struc | |||||||||||||
| // getValue is called for every tagged field of 'to' and must return the value | ||||||||||||||
| // to be assigned to the field with the given typ and name. | ||||||||||||||
| func assignValues(to interface{}, | ||||||||||||||
| getValue func(typ reflect.Type, name string) (interface{}, error)) error { | ||||||||||||||
| getValue func(typ reflect.Type, name string, val reflect.Value) (interface{}, error)) error { | ||||||||||||||
|
|
||||||||||||||
| toValue := reflect.ValueOf(to) | ||||||||||||||
| if toValue.Type().Kind() != reflect.Ptr { | ||||||||||||||
|
|
@@ -1037,7 +1081,7 @@ func assignValues(to interface{}, | |||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Get the eBPF object referred to by the tag. | ||||||||||||||
| value, err := getValue(field.Type, tag) | ||||||||||||||
| value, err := getValue(field.Type, tag, field.value) | ||||||||||||||
| if err != nil { | ||||||||||||||
| return fmt.Errorf("field %s: %w", field.Name, err) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic of emitting a
*ebpf.Programfor pointers to functions seems to be the only reason for having this logic here. I think it would be better to adjust theGoFormatterin btf/format.go to do this instead, so you can reuse the rest of the existing logic for writing go types.