-
Notifications
You must be signed in to change notification settings - Fork 730
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: ergonomic enums #1636
bpf2go: ergonomic enums #1636
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 |
---|---|---|
|
@@ -7,7 +7,6 @@ import ( | |
"go/build/constraint" | ||
"go/token" | ||
"io" | ||
"sort" | ||
"strings" | ||
"text/template" | ||
"unicode" | ||
|
@@ -141,22 +140,50 @@ func Generate(args GenerateArgs) error { | |
programs[name] = args.Identifier(name) | ||
} | ||
|
||
typeNames := make(map[btf.Type]string) | ||
for _, typ := range args.Types { | ||
// NB: This also deduplicates types. | ||
typeNames[typ] = args.Stem + args.Identifier(typ.TypeName()) | ||
tn := templateName(args.Stem) | ||
reservedNames := map[string]struct{}{ | ||
tn.Specs(): {}, | ||
tn.MapSpecs(): {}, | ||
tn.ProgramSpecs(): {}, | ||
tn.VariableSpecs(): {}, | ||
tn.Objects(): {}, | ||
tn.Maps(): {}, | ||
tn.Programs(): {}, | ||
tn.Variables(): {}, | ||
} | ||
|
||
// 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 | ||
typeByName := map[string]btf.Type{} | ||
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 |
||
nameByType := map[btf.Type]string{} | ||
for _, typ := range args.Types { | ||
// NB: This also deduplicates types. | ||
name := args.Stem + args.Identifier(typ.TypeName()) | ||
if _, reserved := reservedNames[name]; reserved { | ||
return fmt.Errorf("type name %q is reserved", name) | ||
} | ||
if otherType, ok := typeByName[name]; ok { | ||
if otherType == typ { | ||
continue | ||
} | ||
return fmt.Errorf("type name %q is used multiple times", name) | ||
} | ||
typeByName[name] = typ | ||
nameByType[typ] = name | ||
} | ||
|
||
gf := &btf.GoFormatter{ | ||
Names: typeNames, | ||
Names: nameByType, | ||
Identifier: args.Identifier, | ||
ShortEnumIdentifier: func(_, element string) string { | ||
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. Could this be made a part of 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 propose generating BOTH long and short identifiers to maintain backwards compatibility. |
||
elementName := args.Stem + args.Identifier(element) | ||
if _, nameTaken := typeByName[elementName]; nameTaken { | ||
return "" | ||
} | ||
if _, nameReserved := reservedNames[elementName]; nameReserved { | ||
return "" | ||
} | ||
reservedNames[elementName] = struct{}{} | ||
return elementName | ||
}, | ||
} | ||
|
||
ctx := struct { | ||
|
@@ -168,8 +195,7 @@ func Generate(args GenerateArgs) error { | |
Maps map[string]string | ||
Variables map[string]string | ||
Programs map[string]string | ||
Types []btf.Type | ||
TypeNames map[btf.Type]string | ||
Types map[string]btf.Type | ||
File string | ||
}{ | ||
gf, | ||
|
@@ -180,44 +206,18 @@ func Generate(args GenerateArgs) error { | |
maps, | ||
variables, | ||
programs, | ||
types, | ||
typeNames, | ||
typeByName, | ||
args.ObjectFile, | ||
} | ||
|
||
var buf bytes.Buffer | ||
if err := commonTemplate.Execute(&buf, &ctx); err != nil { | ||
return fmt.Errorf("can't generate types: %s", err) | ||
return fmt.Errorf("can't generate types: %v", err) | ||
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. Why %v over %s? This change feels a little arbitrary. 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 actually meant %w. |
||
} | ||
|
||
return internal.WriteFormatted(buf.Bytes(), args.Output) | ||
} | ||
|
||
// 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) { | ||
ti-mo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
} | ||
|
||
func toUpperFirst(str string) string { | ||
first, n := utf8.DecodeRuneInString(str) | ||
return string(unicode.ToUpper(first)) + str[n:] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ package gen | |
import ( | ||
"bytes" | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
"testing" | ||
|
||
|
@@ -12,58 +13,6 @@ import ( | |
"github.com/cilium/ebpf/cmd/bpf2go/internal" | ||
) | ||
|
||
func TestOrderTypes(t *testing.T) { | ||
ti-mo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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, qt.IsNil(err)) | ||
qt.Assert(t, qt.Equals(len(result), 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, qt.IsNotNil(err)) | ||
qt.Assert(t, qt.IsNil(result)) | ||
}) | ||
} | ||
} | ||
|
||
func TestPackageImport(t *testing.T) { | ||
var buf bytes.Buffer | ||
err := Generate(GenerateArgs{ | ||
|
@@ -116,3 +65,46 @@ func TestObjects(t *testing.T) { | |
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\"`")) | ||
} | ||
|
||
func TestEnums(t *testing.T) { | ||
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. Would it be possible to write this test without the range over conflict true/false? I'm not a big fan of treating invariants like a list of test cases, because you inevitably end up sprinkling These are technically separate tests; perhaps you could extract the series of |
||
for _, conflict := range []bool{false, true} { | ||
t.Run(fmt.Sprintf("conflict=%v", conflict), func(t *testing.T) { | ||
var buf bytes.Buffer | ||
args := GenerateArgs{ | ||
Package: "foo", | ||
Stem: "bar", | ||
Types: []btf.Type{ | ||
&btf.Enum{Name: "EnumName", Size: 4, Values: []btf.EnumValue{ | ||
{Name: "V1", Value: 1}, {Name: "V2", Value: 2}, {Name: "conflict", Value: 0}}}, | ||
}, | ||
Output: &buf, | ||
} | ||
if conflict { | ||
args.Types = append(args.Types, &btf.Struct{Name: "conflict", Size: 4}) | ||
} | ||
err := Generate(args) | ||
qt.Assert(t, qt.IsNil(err)) | ||
|
||
str := buf.String() | ||
|
||
qt.Assert(t, qt.Matches(str, wsSeparated("barEnumNameV1", "barEnumName", "=", "1"))) | ||
qt.Assert(t, qt.Matches(str, wsSeparated("barEnumNameV2", "barEnumName", "=", "2"))) | ||
qt.Assert(t, qt.Matches(str, wsSeparated("barEnumNameConflict", "barEnumName", "=", "0"))) | ||
|
||
// short enum element names, only generated if they don't conflict with other decls | ||
qt.Assert(t, qt.Matches(str, wsSeparated("barV1", "barEnumName", "=", "1"))) | ||
qt.Assert(t, qt.Matches(str, wsSeparated("barV2", "barEnumName", "=", "2"))) | ||
|
||
pred := qt.Matches(str, wsSeparated("barConflict", "barEnumName", "=", "0")) | ||
if conflict { | ||
qt.Assert(t, qt.Not(pred)) | ||
} else { | ||
qt.Assert(t, pred) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func wsSeparated(terms ...string) *regexp.Regexp { | ||
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. Is this strictly required? IMO it's harder to read 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.
The amount of whitespace depends on the length of other identifiers in the set since formatter inserts extra spaces for alignment. It doesn't improve readability indeed, but makes it easier to update test code in the future. |
||
return regexp.MustCompile(strings.Join(terms, `\s+`)) | ||
} |
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.
At first sight, I'd model this as a property to be specified during construction of the GoFormatter. Everytime the formatter generates an identifier (struct or otherwise), it can check uniqueness and push it to the set of seen names. I think it's a good feature to have in the formatter to prevent it from emitting invalid code, it doesn't need to be b2g-specific.
This way, you also avoid having to close over
reservedNames
andtypeByName
to get it into anShortEnumIdentifier
function.