netlink: add struct tag-based attribute codec#287
Conversation
Add a reflection-based Codec that maps struct fields tagged with `netlink:"<index>[,opts]"` to netlink attributes, providing a declarative alternative to manual AttributeEncoder/AttributeDecoder usage. Supported types: scalars (uint*/int* with optional be/le byte order), strings, []byte, bools (flags), nested structs, pointers (optional), slices (multi/indexed layouts), and sub-messages via resolver functions. Struct metadata is cached per type to avoid repeated reflection overhead. See #286.
c30837a to
40190a8
Compare
|
what will be the use cases for this? can you provide some realistic ones? |
Taking nftables as an example, for each attribute set you end up manually writing serialization and deserialization, which adds up to a lot of boilerplate. A typical table encode/decode looks something like this: func tableFromAttrs(ad *netlink.AttributeDecoder) Table {
var t Table
ad.ByteOrder = binary.BigEndian
for ad.Next() {
switch ad.Type() {
case unix.NFTA_TABLE_NAME:
t.Name = ad.String()
case unix.NFTA_TABLE_FLAGS:
t.Flags = ad.Uint32()
case unix.NFTA_TABLE_USE:
v := ad.Uint32()
t.Use = &v
case unix.NFTA_TABLE_HANDLE:
v := ad.Uint64()
t.Handle = &v
}
}
return t
}
func tableToAttrs(t *Table) ([]byte, error) {
ae := netlink.NewAttributeEncoder()
ae.ByteOrder = binary.BigEndian
ae.String(unix.NFTA_TABLE_NAME, t.Name)
ae.Uint32(unix.NFTA_TABLE_FLAGS, t.Flags)
if t.Use != nil {
ae.Uint32(unix.NFTA_TABLE_USE, *t.Use)
}
if t.Handle != nil {
ae.Uint64(unix.NFTA_TABLE_HANDLE, *t.Handle)
}
return ae.Encode()
}With a tag-based codec, the struct definition becomes the single source of truth similar to how encoding/json or gopkg.in/yaml.v3 work: type Table struct {
Name string `netlink:"1,be"` // NFTA_TABLE_NAME
Flags uint32 `netlink:"2,be"` // NFTA_TABLE_FLAGS
Use *uint32 `netlink:"3,be"` // NFTA_TABLE_USE
Handle *uint64 `netlink:"4,be"` // NFTA_TABLE_HANDLE
}
codec := netlink.NewAttributeCodec(nil)
// encode
b, err := codec.Marshal(t)
// decode
var t Table
err := codec.Unmarshal(b, &t)I started exploring this while building a YNL code generator for Go. In YNL, operations often use subsets of a shared attribute set. For example, newtable, gettable, deltable, and destroytable all reference table-attrs but each uses different fields. A naive generator would emit a separate struct (plus encode/decode boilerplate) for every operation/direction combination, which quickly explodes in families like nftables with dozens of operations. With a tag-based codec, the generator can instead emit one struct per attribute set using pointer fields for optional attributes, and get serialization for free. |
|
Here’s an example of a more advanced case where discrimination is needed. The tag options type Expression interface {
exprType() string
}
type ExprMeta struct {
Dreg *uint32 `netlink:"1,be"`
Key uint32 `netlink:"2,be"`
Sreg *uint32 `netlink:"3,be"`
}
func (*ExprMeta) exprType() string { return "meta" }
type ExprCounter struct {
Bytes uint64 `netlink:"1,be"`
Packets uint64 `netlink:"2,be"`
}
func (*ExprCounter) exprType() string { return "counter" }
// "Data" is resolved based the "Name" field.
type ExprAttrs struct {
Name string `netlink:"1,be"`
Data Expression `netlink:"2,submsg=expr-ops,selector=Name"`
}
type RuleAttrs struct {
Table string `netlink:"1,be"`
Chain string `netlink:"2,be"`
Exprs []ExprAttrs `netlink:"4,indexed"`
}
codec := netlink.NewAttributeCodec(&netlink.AttributeCodecConfig{
Resolvers: map[string]netlink.SubMessageResolver{
"expr-ops": func(selector any) (any, error) {
switch selector.(string) {
case "meta":
return &ExprMeta{}, nil
case "counter":
return &ExprCounter{}, nil
default:
return nil, fmt.Errorf("unknown expression: %s", selector)
}
},
},
})
rule := RuleAttrs{
Table: "filter",
Chain: "input",
Exprs: []ExprAttrs{
{Name: "meta", Data: &ExprMeta{Key: 16, Dreg: ptr(uint32(1))}},
// "cmp" expression omitted for brevity
{Name: "counter", Data: &ExprCounter{}},
},
}
b, err := codec.Marshal(rule)
var got RuleAttrs
err = codec.Unmarshal(b, &got)
// got.Exprs[0].Data.(*ExprMeta).Key == 16
// got.Exprs[1].Data.(*ExprCounter) != nil |
|
|
||
| // Unmarshal decodes netlink attribute bytes into a struct. | ||
| func (c *Codec) Unmarshal(b []byte, v any) error { | ||
| rv := reflect.ValueOf(v) |
There was a problem hiding this comment.
I generally avoid functions like reflect.ValueOf() because they hinder the Go compiler's ability to perform dead code elimination. This can lead to a significant and unnecessary increase in the final binary size. While hand-writing logic for known structs is more verbose, the resulting performance and footprint optimizations are well worth the manual effort.
There was a problem hiding this comment.
Thanks for having a look! Yeah I am not too keen on it being slower. Admittedly, I hadn't considered the binary size consequences. The idea was that it might be useful for use cases where the performance tradeoffs are acceptable. If there are no such use cases then having and maintaining this feature is probably not worth it.
Add a reflection-based Codec that maps struct fields tagged with
netlink:"<index>[,opts]"to netlink attributes, providing a declarative alternative to manual AttributeEncoder/AttributeDecoder usage.Supported types: scalars (uint*/int* with optional be/le byte order), strings, []byte, bools (flags), nested structs, pointers (optional), slices (multi/indexed layouts), and sub-messages via resolver functions.
Struct metadata is cached per type to avoid repeated reflection overhead.
See #286.