Skip to content

Commit f786a42

Browse files
json: refactor ref-cycle handling
Also set UnsupportedValueError.Value (better stdlib compat).
1 parent dabc507 commit f786a42

File tree

2 files changed

+95
-17
lines changed

2 files changed

+95
-17
lines changed

json/codec.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"sort"
1111
"strconv"
1212
"strings"
13+
"sync"
1314
"sync/atomic"
1415
"time"
1516
"unicode"
@@ -32,13 +33,28 @@ type codec struct {
3233

3334
type encoder struct {
3435
flags AppendFlags
35-
// ptrDepth tracks the depth of pointer cycles, when it reaches the value
36+
// refDepth tracks the depth of pointer cycles, when it reaches the value
3637
// of startDetectingCyclesAfter, the ptrSeen map is allocated and the
3738
// encoder starts tracking pointers it has seen as an attempt to detect
3839
// whether it has entered a pointer cycle and needs to error before the
3940
// goroutine runs out of stack space.
40-
ptrDepth uint32
41-
ptrSeen map[unsafe.Pointer]struct{}
41+
//
42+
// This relies on encoder being passed as a value,
43+
// and encoder methods calling each other in a traditional stack
44+
// (not using trampoline techniques),
45+
// since refDepth is never decremented.
46+
refDepth uint32
47+
refSeen cycleMap
48+
}
49+
50+
type cycleKey struct {
51+
ptr unsafe.Pointer
52+
}
53+
54+
type cycleMap map[cycleKey]struct{}
55+
56+
var cycleMapPool = sync.Pool{
57+
New: func() any { return make(cycleMap) },
4258
}
4359

4460
type decoder struct {

json/encode.go

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -812,22 +812,23 @@ func (e encoder) encodeEmbeddedStructPointer(b []byte, p unsafe.Pointer, t refle
812812
}
813813

814814
func (e encoder) encodePointer(b []byte, p unsafe.Pointer, t reflect.Type, encode encodeFunc) ([]byte, error) {
815-
if p = *(*unsafe.Pointer)(p); p != nil {
816-
if e.ptrDepth++; e.ptrDepth >= startDetectingCyclesAfter {
817-
if _, seen := e.ptrSeen[p]; seen {
818-
// TODO: reconstruct the reflect.Value from p + t so we can set
819-
// the erorr's Value field?
820-
return b, &UnsupportedValueError{Str: fmt.Sprintf("encountered a cycle via %s", t)}
821-
}
822-
if e.ptrSeen == nil {
823-
e.ptrSeen = make(map[unsafe.Pointer]struct{})
824-
}
825-
e.ptrSeen[p] = struct{}{}
826-
defer delete(e.ptrSeen, p)
815+
// p was a pointer to the actual user data pointer:
816+
// dereference it to operate on the user data pointer.
817+
p = *(*unsafe.Pointer)(p)
818+
if p == nil {
819+
return e.encodeNull(b, nil)
820+
}
821+
822+
if shouldCheckForRefCycle(&e) {
823+
key := cycleKey{ptr: p}
824+
if hasRefCycle(&e, key) {
825+
return b, refCycleError(t, p)
827826
}
828-
return encode(e, b, p)
827+
828+
defer freeRefCycleInfo(&e, key)
829829
}
830-
return e.encodeNull(b, nil)
830+
831+
return encode(e, b, p)
831832
}
832833

833834
func (e encoder) encodeInterface(b []byte, p unsafe.Pointer) ([]byte, error) {
@@ -986,3 +987,64 @@ func appendCompactEscapeHTML(dst []byte, src []byte) []byte {
986987

987988
return dst
988989
}
990+
991+
// shouldCheckForRefCycle determines whether checking for reference cycles
992+
// is reasonable to do at this time.
993+
//
994+
// When true, checkRefCycle should be called and any error handled,
995+
// and then a deferred call to freeRefCycleInfo should be made.
996+
//
997+
// This should only be called from encoder methods that are possible points
998+
// that could directly contribute to a reference cycle.
999+
func shouldCheckForRefCycle(e *encoder) bool {
1000+
// Note: do not combine this with checkRefCycle,
1001+
// because checkRefCycle is too large to be inlined,
1002+
// and a non-inlined depth check leads to ~5%+ benchmark degradation.
1003+
e.refDepth++
1004+
return e.refDepth >= startDetectingCyclesAfter
1005+
}
1006+
1007+
// refCycleError constructs an [UnsupportedValueError].
1008+
func refCycleError(t reflect.Type, p unsafe.Pointer) error {
1009+
v := reflect.NewAt(t, p)
1010+
return &UnsupportedValueError{
1011+
Value: v,
1012+
Str: fmt.Sprintf("encountered a cycle via %s", t),
1013+
}
1014+
}
1015+
1016+
// hasRefCycle returns an error if a reference cycle was detected.
1017+
// The data pointer passed in should be equivalent to one of:
1018+
//
1019+
// - A normal Go pointer, e.g. `unsafe.Pointer(&T)`
1020+
// - The pointer to a map header, e.g. `*(*unsafe.Pointer)(&map[K]V)`
1021+
//
1022+
// Many [encoder] methods accept a pointer-to-a-pointer,
1023+
// and so those may need to be derenced in order to safely pass them here.
1024+
func hasRefCycle(e *encoder, key cycleKey) bool {
1025+
_, seen := e.refSeen[key]
1026+
if seen {
1027+
return true
1028+
}
1029+
1030+
if e.refSeen == nil {
1031+
e.refSeen = cycleMapPool.Get().(cycleMap)
1032+
}
1033+
1034+
e.refSeen[key] = struct{}{}
1035+
1036+
return false
1037+
}
1038+
1039+
// freeRefCycle performs the cleanup operation for [checkRefCycle].
1040+
// p must be the same value passed into a prior call to checkRefCycle.
1041+
func freeRefCycleInfo(e *encoder, key cycleKey) {
1042+
delete(e.refSeen, key)
1043+
if len(e.refSeen) == 0 {
1044+
// There are no remaining elements,
1045+
// so we can release this map for later reuse.
1046+
m := e.refSeen
1047+
e.refSeen = nil
1048+
cycleMapPool.Put(m)
1049+
}
1050+
}

0 commit comments

Comments
 (0)