Skip to content

Commit 5c864ee

Browse files
fuhryclaude
andcommitted
use tool to generate ProgramType and AttachType enums
## Summary Add a tool that generates the `ProgramType` and `AttachType` enums automatically from Linux headers installed on the system. The generated files are checked in, in order to preserve building in a single step and support building on systems which do not have linux-api-headers installed. The tool that generates Go from C-language enums was generated by Claude and lightly modified by me. ## Test Plan Confirmed the generated output makes sense and constant names match the existing ones. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-Off-By: Dan Fuhry <fuhry@dropbox.com>
1 parent 0936b84 commit 5c864ee

4 files changed

Lines changed: 419 additions & 83 deletions

File tree

attach_type.gen.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// @generated by bpf_enum_gen. DO NOT EDIT.
2+
// Source: /usr/include/linux/bpf.h
3+
4+
package goebpf
5+
6+
// AttachType is generated from enum bpf_attach_type in /usr/include/linux/bpf.h
7+
type AttachType int
8+
9+
const (
10+
AttachTypeCgroupInetIngress AttachType = iota
11+
AttachTypeCgroupInetEgress
12+
AttachTypeCgroupInetSockCreate
13+
AttachTypeCgroupSockOps
14+
AttachTypeSkSkbStreamParser
15+
AttachTypeSkSkbStreamVerdict
16+
AttachTypeCgroupDevice
17+
AttachTypeSkMsgVerdict
18+
AttachTypeCgroupInet4Bind
19+
AttachTypeCgroupInet6Bind
20+
AttachTypeCgroupInet4Connect
21+
AttachTypeCgroupInet6Connect
22+
AttachTypeCgroupInet4PostBind
23+
AttachTypeCgroupInet6PostBind
24+
AttachTypeCgroupUdp4Sendmsg
25+
AttachTypeCgroupUdp6Sendmsg
26+
AttachTypeLircMode2
27+
AttachTypeFlowDissector
28+
AttachTypeCgroupSysctl
29+
AttachTypeCgroupUdp4Recvmsg
30+
AttachTypeCgroupUdp6Recvmsg
31+
AttachTypeCgroupGetsockopt
32+
AttachTypeCgroupSetsockopt
33+
AttachTypeTraceRawTp
34+
AttachTypeTraceFentry
35+
AttachTypeTraceFexit
36+
AttachTypeModifyReturn
37+
AttachTypeLsmMac
38+
AttachTypeTraceIter
39+
AttachTypeCgroupInet4Getpeername
40+
AttachTypeCgroupInet6Getpeername
41+
AttachTypeCgroupInet4Getsockname
42+
AttachTypeCgroupInet6Getsockname
43+
AttachTypeXdpDevmap
44+
AttachTypeCgroupInetSockRelease
45+
AttachTypeXdpCpumap
46+
AttachTypeSkLookup
47+
AttachTypeXdp
48+
AttachTypeSkSkbVerdict
49+
AttachTypeSkReuseportSelect
50+
AttachTypeSkReuseportSelectOrMigrate
51+
AttachTypePerfEvent
52+
AttachTypeTraceKprobeMulti
53+
AttachTypeLsmCgroup
54+
AttachTypeStructOps
55+
AttachTypeNetfilter
56+
AttachTypeTcxIngress
57+
AttachTypeTcxEgress
58+
AttachTypeTraceUprobeMulti
59+
AttachTypeCgroupUnixConnect
60+
AttachTypeCgroupUnixSendmsg
61+
AttachTypeCgroupUnixRecvmsg
62+
AttachTypeCgroupUnixGetpeername
63+
AttachTypeCgroupUnixGetsockname
64+
AttachTypeNetkitPrimary
65+
AttachTypeNetkitPeer
66+
AttachTypeTraceKprobeSession
67+
AttachTypeTraceUprobeSession
68+
AttachTypeTraceFsession
69+
maxBpfAttachType
70+
)

cmd/bpf_enum_gen/main.go

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
// bpf_enum_gen generates Go constants from a C enum in a header file.
2+
// Usage: bpf_enum_gen -header <file> -enum <enum_name> -type <GoType> -pkg <package> -out <file>
3+
//
4+
// The C enum members are converted to Go identifiers by stripping the
5+
// longest common prefix (e.g. "BPF_PROG_TYPE_"), then title-casing each
6+
// underscore-separated word. The __MAX_* sentinel becomes a private
7+
// constant named maxBpf<suffix>.
8+
package main
9+
10+
import (
11+
"bufio"
12+
"flag"
13+
"fmt"
14+
"os"
15+
"strings"
16+
"text/template"
17+
"unicode"
18+
)
19+
20+
const outputTemplate = `// @generated by bpf_enum_gen. DO NOT EDIT.
21+
// Source: {{.Header}}
22+
23+
package {{.Package}}
24+
25+
// {{.GoType}} is generated from enum {{.EnumName}} in {{.Header}}
26+
type {{.GoType}} int
27+
28+
const (
29+
{{- range .Members}}
30+
{{- if .First}}
31+
{{.Name}} {{$.GoType}} = iota
32+
{{- else}}
33+
{{.Name}}
34+
{{- end}}
35+
{{- end}}
36+
)
37+
`
38+
39+
type member struct {
40+
Name string
41+
First bool
42+
}
43+
44+
type templateData struct {
45+
Header string
46+
Package string
47+
GoType string
48+
EnumName string
49+
Members []member
50+
}
51+
52+
func main() {
53+
header := flag.String("header", "", "path to C header file")
54+
enumName := flag.String("enum", "", "C enum name (e.g. bpf_prog_type)")
55+
goType := flag.String("type", "", "Go type name (e.g. ProgramType)")
56+
pkg := flag.String("pkg", "goebpf", "Go package name")
57+
out := flag.String("out", "", "output file (default: stdout)")
58+
flag.Parse()
59+
60+
if *header == "" || *enumName == "" || *goType == "" {
61+
fmt.Fprintln(os.Stderr, "bpf_enum_gen: -header, -enum, and -type are required")
62+
flag.Usage()
63+
os.Exit(1)
64+
}
65+
66+
members, err := parseEnum(*header, *enumName)
67+
if err != nil {
68+
fmt.Fprintf(os.Stderr, "bpf_enum_gen: %v\n", err)
69+
os.Exit(1)
70+
}
71+
72+
goNames := toGoNames(members, *goType)
73+
74+
data := templateData{
75+
Header: *header,
76+
Package: *pkg,
77+
GoType: *goType,
78+
EnumName: *enumName,
79+
Members: goNames,
80+
}
81+
82+
w := os.Stdout
83+
if *out != "" {
84+
f, err := os.Create(*out)
85+
if err != nil {
86+
fmt.Fprintf(os.Stderr, "bpf_enum_gen: %v\n", err)
87+
os.Exit(1)
88+
}
89+
defer f.Close()
90+
w = f
91+
}
92+
93+
tmpl := template.Must(template.New("output").Parse(outputTemplate))
94+
if err := tmpl.Execute(w, data); err != nil {
95+
fmt.Fprintf(os.Stderr, "bpf_enum_gen: %v\n", err)
96+
os.Exit(1)
97+
}
98+
}
99+
100+
// parseEnum extracts the raw C member names from the named enum in the file.
101+
func parseEnum(path, enumName string) ([]string, error) {
102+
f, err := os.Open(path)
103+
if err != nil {
104+
return nil, err
105+
}
106+
defer f.Close()
107+
108+
target := "enum " + enumName + " {"
109+
var members []string
110+
inEnum := false
111+
depth := 0
112+
113+
scanner := bufio.NewScanner(f)
114+
for scanner.Scan() {
115+
line := strings.TrimSpace(scanner.Text())
116+
117+
// Strip line comments
118+
if idx := strings.Index(line, "//"); idx >= 0 {
119+
line = strings.TrimSpace(line[:idx])
120+
}
121+
if idx := strings.Index(line, "/*"); idx >= 0 {
122+
line = strings.TrimSpace(line[:idx])
123+
}
124+
125+
if !inEnum {
126+
if strings.Contains(line, target) {
127+
inEnum = true
128+
depth = strings.Count(line, "{") - strings.Count(line, "}")
129+
// Grab any member on the same line as the opening brace
130+
after := line[strings.Index(line, "{")+1:]
131+
collectMembers(after, &members)
132+
}
133+
continue
134+
}
135+
136+
depth += strings.Count(line, "{") - strings.Count(line, "}")
137+
if depth <= 0 {
138+
// Last line of enum body before closing brace
139+
before := line
140+
if idx := strings.LastIndex(line, "}"); idx >= 0 {
141+
before = line[:idx]
142+
}
143+
collectMembers(before, &members)
144+
break
145+
}
146+
collectMembers(line, &members)
147+
}
148+
149+
if err := scanner.Err(); err != nil {
150+
return nil, err
151+
}
152+
if !inEnum {
153+
return nil, fmt.Errorf("enum %q not found in %s", enumName, path)
154+
}
155+
return members, nil
156+
}
157+
158+
// collectMembers extracts comma-separated C identifiers (member names) from a
159+
// partial enum body line, stripping optional = <value> assignments.
160+
func collectMembers(line string, out *[]string) {
161+
// Strip inline comments again (already done at line level, but handle residue)
162+
if idx := strings.Index(line, "/*"); idx >= 0 {
163+
line = line[:idx]
164+
}
165+
for _, tok := range strings.Split(line, ",") {
166+
tok = strings.TrimSpace(tok)
167+
if tok == "" {
168+
continue
169+
}
170+
// Strip = value
171+
if idx := strings.Index(tok, "="); idx >= 0 {
172+
tok = strings.TrimSpace(tok[:idx])
173+
}
174+
if tok == "" || tok == "{" || tok == "}" {
175+
continue
176+
}
177+
// Must look like a C identifier
178+
if isIdent(tok) {
179+
*out = append(*out, tok)
180+
}
181+
}
182+
}
183+
184+
func isIdent(s string) bool {
185+
if s == "" {
186+
return false
187+
}
188+
for i, r := range s {
189+
if i == 0 && !unicode.IsLetter(r) && r != '_' {
190+
return false
191+
}
192+
if i > 0 && !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' {
193+
return false
194+
}
195+
}
196+
return true
197+
}
198+
199+
// toGoNames converts C enum member names to Go identifiers.
200+
//
201+
// Strategy:
202+
// 1. Find the longest common prefix that is a full underscore-delimited
203+
// token boundary (shared by all non-sentinel members).
204+
// 2. Strip that prefix, title-case the remaining words, prepend goType.
205+
// 3. __MAX_* sentinel → maxBpf<TitleCaseSuffix>.
206+
func toGoNames(cNames []string, goType string) []member {
207+
// Separate sentinel (__MAX_*) from real members
208+
var real []string
209+
var sentinels []string
210+
for _, n := range cNames {
211+
if strings.HasPrefix(n, "__MAX_") || (strings.HasPrefix(n, "__") && strings.HasSuffix(n, "_MAX")) {
212+
sentinels = append(sentinels, n)
213+
} else {
214+
real = append(real, n)
215+
}
216+
}
217+
218+
prefix := longestCommonTokenPrefix(real)
219+
220+
var result []member
221+
for i, n := range real {
222+
rest := strings.TrimPrefix(n, prefix)
223+
result = append(result, member{
224+
Name: goType + titleCase(rest),
225+
First: i == 0,
226+
})
227+
}
228+
229+
for range sentinels {
230+
// __MAX_BPF_PROG_TYPE → maxBpfProgramType (always derived from goType)
231+
// __MAX_BPF_ATTACH_TYPE → maxBpfAttachType
232+
result = append(result, member{Name: "maxBpf" + goType})
233+
}
234+
235+
return result
236+
}
237+
238+
// longestCommonTokenPrefix finds the longest prefix, measured in whole
239+
// underscore-delimited tokens, shared by all strings.
240+
func longestCommonTokenPrefix(ss []string) string {
241+
if len(ss) == 0 {
242+
return ""
243+
}
244+
parts := strings.Split(ss[0], "_")
245+
// Try prefixes from longest to shortest
246+
for length := len(parts); length > 0; length-- {
247+
candidate := strings.Join(parts[:length], "_") + "_"
248+
allMatch := true
249+
for _, s := range ss[1:] {
250+
if !strings.HasPrefix(s, candidate) {
251+
allMatch = false
252+
break
253+
}
254+
}
255+
if allMatch {
256+
return candidate
257+
}
258+
}
259+
return ""
260+
}
261+
262+
// titleCase converts SNAKE_CASE to TitleCase.
263+
// Special handling for well-known abbreviations to match existing names.
264+
func titleCase(s string) string {
265+
words := strings.Split(s, "_")
266+
var b strings.Builder
267+
for _, w := range words {
268+
if w == "" {
269+
continue
270+
}
271+
b.WriteString(titleCaseWord(w))
272+
}
273+
return b.String()
274+
}
275+
276+
// knownAbbreviations are substrings that should keep their capitalisation.
277+
// Checked case-insensitively; the value is the exact output spelling.
278+
var knownAbbreviations = map[string]string{
279+
"xdp": "Xdp",
280+
"skb": "Skb",
281+
"cls": "Cls",
282+
"act": "Act",
283+
"ops": "Ops",
284+
"msg": "Msg",
285+
"sk": "Sk",
286+
"lsm": "Lsm",
287+
"lrc": "Lrc",
288+
"udp": "Udp",
289+
"tcp": "Tcp",
290+
"ipv4": "Ipv4",
291+
"ipv6": "Ipv6",
292+
}
293+
294+
func titleCaseWord(w string) string {
295+
lower := strings.ToLower(w)
296+
if v, ok := knownAbbreviations[lower]; ok {
297+
return v
298+
}
299+
if len(w) == 0 {
300+
return w
301+
}
302+
return strings.ToUpper(w[:1]) + strings.ToLower(w[1:])
303+
}

0 commit comments

Comments
 (0)