Repository navigation
feat(cbor): add RegisterDecoder for per-type decoders #4206
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
Open
danielntmd
wants to merge
1
commit into
main
Choose a base branch
from
danielntmd/cbor-wrapper
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| package cbor | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "reflect" | ||
| "sync/atomic" | ||
| "time" | ||
| "unsafe" | ||
|
|
||
| "github.com/NethermindEth/juno/utils/cbor/fxamacker" | ||
| "github.com/NethermindEth/juno/utils/log" | ||
| "go.uber.org/zap" | ||
| ) | ||
|
|
||
| // A decoder reads CBOR into a destination. | ||
| type decoder struct { | ||
| unmarshal func(data []byte, v any) error | ||
| unmarshalFirst func(data []byte, v any) ([]byte, error) | ||
| } | ||
|
|
||
| // fxamackerDecoder is the default decoder. | ||
| // It decodes types with no registered decoder and is the current fallback. | ||
| var fxamackerDecoder = decoder{fxamacker.Unmarshal, fxamacker.UnmarshalFirst} | ||
|
|
||
| // decoders finds the decoder of a destination by its type, without hashing the interface. A | ||
| // destination is looked up by its exact pointer type, *T or **T, so the pointer inside the | ||
| // interface is always the one its decoder was written for, and the unsafe casts only erase and | ||
| // restore that type. This assumes gc's layout of an interface (the type word followed by the data | ||
| // word), which [reflect] also relies on. | ||
| var decoders = map[unsafe.Pointer]*typeDecoder{} | ||
|
|
||
| // decoderFor returns the decoder of v. | ||
| // Either the one registered for its type, or fxamacker. | ||
| func decoderFor(v any) decoder { | ||
| value := (*eface)(unsafe.Pointer(&v)) | ||
| d, ok := decoders[value.typ] | ||
| if !ok || value.data == nil || !d.isZero(value.data) { | ||
| return fxamackerDecoder | ||
| } | ||
| return d.decoder | ||
| } | ||
|
danielntmd marked this conversation as resolved.
Resolved
danielntmd marked this conversation as resolved.
Resolved
|
||
|
|
||
| // eface is the layout of an interface value with no methods. | ||
| type eface struct { | ||
| typ, data unsafe.Pointer | ||
| } | ||
|
|
||
| // typeWord returns the dynamic type of v as the runtime keeps it. | ||
| func typeWord(v any) unsafe.Pointer { | ||
| return (*eface)(unsafe.Pointer(&v)).typ | ||
| } | ||
|
|
||
| // dest returns the pointer that v, a pointer to a registered type, holds. | ||
| func dest(v any) unsafe.Pointer { | ||
| return (*eface)(unsafe.Pointer(&v)).data | ||
| } | ||
|
|
||
| // A typeDecoder decodes into destinations of one pointer type, *T or **T. | ||
| // Registering a decoder for T creates one of each. | ||
| type typeDecoder struct { | ||
| name string // destination type | ||
| // decoder is what decoderFor returns for the type, built by bind. | ||
| decoder decoder | ||
| // decodeUnsafe is the registered decoder with the destination as the pointer itself. | ||
| decodeUnsafe func(data []byte, dst unsafe.Pointer) ([]byte, error) | ||
| // reset zeroes dst, undoing a partial read. | ||
| reset func(dst unsafe.Pointer) | ||
| // isZero reports whether dst holds the zero value, which for **T is a nil pointer. | ||
| isZero func(dst unsafe.Pointer) bool | ||
| } | ||
|
|
||
| // logger reports failed decodes. | ||
| var logger atomic.Pointer[log.StructuredLogger] | ||
|
|
||
| // SetLogger sets where a decode that falls back to fxamacker is reported. The reports are sampled, | ||
| // so a hot read path cannot flood the log. Call it once, at startup. | ||
| // todo(danielntmd): It reports errors only, not a wrong value returned without one. The fallback | ||
| // and this logger are meant to be removed eventually. | ||
| func SetLogger(l log.StructuredLogger) { | ||
| // The first few reports in each interval, then every thereafter'th. | ||
| const ( | ||
| interval = time.Minute | ||
| first = 5 | ||
| thereafter = 100 | ||
| ) | ||
| l = log.Sampled(l, interval, first, thereafter) | ||
| logger.Store(&l) | ||
| } | ||
|
|
||
| // unmarshalFirst reads one item into v and returns the bytes after it. | ||
| func (d *typeDecoder) unmarshalFirst(data []byte, v any) ([]byte, error) { | ||
| rest, err := d.decodeUnsafe(data, dest(v)) | ||
| if err != nil { | ||
| if l := logger.Load(); l != nil { | ||
| (*l).Warn("cbor: decoder failed, decoding with fxamacker", | ||
| zap.String("type", d.name), zap.Error(err)) | ||
| } | ||
| d.reset(dest(v)) | ||
| return fxamackerDecoder.unmarshalFirst(data, v) // fallback | ||
| } | ||
| return rest, nil | ||
| } | ||
|
|
||
| // unmarshal reads exactly one item into v. | ||
| func (d *typeDecoder) unmarshal(data []byte, v any) error { | ||
| rest, err := d.unmarshalFirst(data, v) | ||
| if err == nil && len(rest) != 0 { | ||
| d.reset(dest(v)) | ||
| return fmt.Errorf("cbor: %d bytes of extraneous data", len(rest)) | ||
| } | ||
| return err | ||
| } | ||
|
|
||
| // bind builds the decoder that decoderFor returns for d, once, so that a decode allocates nothing | ||
| // to call it. | ||
| func bind(d *typeDecoder) *typeDecoder { | ||
| d.decoder = decoder{d.unmarshal, d.unmarshalFirst} | ||
| return d | ||
| } | ||
|
|
||
| // valueDecoder returns the decoder of *T destinations. | ||
| func valueDecoder[T any](decode func([]byte, *T) ([]byte, error)) *typeDecoder { | ||
| typ := reflect.TypeFor[T]() | ||
| zeroBytes := make([]byte, typ.Size()) | ||
| return bind(&typeDecoder{ | ||
| name: reflect.TypeFor[*T]().String(), | ||
| // *T and unsafe.Pointer are passed the same way, so the decoder is called directly. | ||
| decodeUnsafe: *(*func([]byte, unsafe.Pointer) ([]byte, error))(unsafe.Pointer(&decode)), | ||
|
danielntmd marked this conversation as resolved.
Resolved
|
||
| reset: func(dst unsafe.Pointer) { | ||
| *(*T)(dst) = *new(T) | ||
| }, | ||
| // All-zero bytes are the zero value, but not the reverse (an empty string can still | ||
| // point somewhere), so [reflect] decides the rest. | ||
| isZero: func(dst unsafe.Pointer) bool { | ||
| return bytes.Equal(unsafe.Slice((*byte)(dst), len(zeroBytes)), zeroBytes) || | ||
| reflect.NewAt(typ, dst).Elem().IsZero() | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| // Head bytes of the two items a pointer treats as nil. | ||
| // (RFC 8949, section 3.3) | ||
| const ( | ||
| simpleNull = 0xf6 | ||
| simpleUndefined = 0xf7 | ||
| ) | ||
|
|
||
| // pointerDecoder returns the decoder of **T destinations. | ||
| func pointerDecoder[T any](decode func([]byte, *T) ([]byte, error)) *typeDecoder { | ||
| return bind(&typeDecoder{ | ||
| name: reflect.TypeFor[**T]().String(), | ||
| decodeUnsafe: func(data []byte, dst unsafe.Pointer) ([]byte, error) { | ||
| // For parity with fxamacker, null and undefined leave the pointer nil. | ||
| if len(data) > 0 && (data[0] == simpleNull || data[0] == simpleUndefined) { | ||
| return data[1:], nil | ||
| } | ||
| value := new(T) | ||
| rest, err := decode(data, value) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| *(**T)(dst) = value | ||
| return rest, nil | ||
| }, | ||
| reset: func(dst unsafe.Pointer) { | ||
| *(**T)(dst) = nil | ||
| }, | ||
| isZero: func(dst unsafe.Pointer) bool { | ||
| return *(**T)(dst) == nil | ||
| }, | ||
| }) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
With metrics this is avoidable, but this logger will be removed when fallback is removed.