From 244a66fa8fdb877a912b217ddd5c84bc988f8761 Mon Sep 17 00:00:00 2001 From: Kirill Korotaev Date: Sat, 11 Apr 2020 20:48:34 +0300 Subject: [PATCH] Add "nocopy" json tag to return unsafe string reference When large amounts of data are unmarshaled for immediate non long live use than copying of strings from original buffer can be avoided and instead a reference to the original buffer returned in the field value. Note, if the value requires unescaping, than it will be processed as normally done with copying. Signed-off-by: Kirill Korotaev --- Makefile | 4 +++- README.md | 12 ++++++++++++ gen/decoder.go | 3 +++ gen/encoder.go | 3 +++ tests/nocopy.go | 7 +++++++ tests/nocopy_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/nocopy.go create mode 100644 tests/nocopy_test.go diff --git a/Makefile b/Makefile index f1c386b..92f3c43 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,8 @@ generate: build ./tests/type_declaration.go \ ./tests/members_escaped.go \ ./tests/members_unescaped.go \ - ./tests/intern.go + ./tests/intern.go \ + ./tests/nocopy.go \ bin/easyjson -all ./tests/data.go bin/easyjson -all ./tests/nothing.go @@ -44,6 +45,7 @@ generate: build bin/easyjson ./tests/members_escaped.go bin/easyjson -disable_members_unescape ./tests/members_unescaped.go bin/easyjson ./tests/intern.go + bin/easyjson ./tests/nocopy.go test: generate go test \ diff --git a/README.md b/README.md index 91494c9..67ee8fc 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,18 @@ Additional option notes: * `-build_tags` will add the specified build tags to generated Go sources. +## Structure json tag options + +Besides standart json tag options like 'omitempty' the following are supported: + +* 'nocopy' - disables allocation and copying of string values, making them + refer to original json buffer memory. This works great for short lived + objects which are not hold in memory after decoding and immediate usage. + Note if string requires unescaping it will be processed as normally. +* 'intern' - string "interning" (deduplication) to save memory when the very + same string dictionory values are often met all over the structure. + See below for more details. + ## Generated Marshaler/Unmarshaler Funcs For Go struct types, easyjson generates the funcs `MarshalEasyJSON` / diff --git a/gen/decoder.go b/gen/decoder.go index b2b6bdc..4fddbff 100644 --- a/gen/decoder.go +++ b/gen/decoder.go @@ -121,6 +121,9 @@ func (g *Generator) genTypeDecoderNoCheck(t reflect.Type, out string, tags field if tags.intern && t.Kind() == reflect.String { dec = "in.StringIntern()" } + if tags.noCopy && t.Kind() == reflect.String { + dec = "in.UnsafeString()" + } fmt.Fprintln(g.out, ws+out+" = "+g.getType(t)+"("+dec+")") return nil } diff --git a/gen/encoder.go b/gen/encoder.go index f9650e7..893d9e8 100644 --- a/gen/encoder.go +++ b/gen/encoder.go @@ -59,6 +59,7 @@ type fieldTags struct { asString bool required bool intern bool + noCopy bool } // parseFieldTags parses the json field tag into a structure. @@ -81,6 +82,8 @@ func parseFieldTags(f reflect.StructField) fieldTags { ret.required = true case s == "intern": ret.intern = true + case s == "nocopy": + ret.noCopy = true } } diff --git a/tests/nocopy.go b/tests/nocopy.go new file mode 100644 index 0000000..3d3c7f3 --- /dev/null +++ b/tests/nocopy.go @@ -0,0 +1,7 @@ +package tests + +//easyjson:json +type NocopyStruct struct { + A string `json:"a"` + B string `json:"b,nocopy"` +} diff --git a/tests/nocopy_test.go b/tests/nocopy_test.go new file mode 100644 index 0000000..fdd0651 --- /dev/null +++ b/tests/nocopy_test.go @@ -0,0 +1,41 @@ +package tests + +import ( + "reflect" + "testing" + "unsafe" + + "github.com/mailru/easyjson" +) + +// verifies if string pointer belongs to the given buffer or outside of it +func strBelongsTo(s string, buf []byte) bool { + sPtr := (*reflect.StringHeader)(unsafe.Pointer(&s)).Data + bufPtr := (*reflect.SliceHeader)(unsafe.Pointer(&buf)).Data + + if bufPtr <= sPtr && sPtr < bufPtr+uintptr(len(buf)) { + return true + } + return false +} + +func TestNocopy(t *testing.T) { + data := []byte(`{"a": "valueA", "b": "valueB"}`) + exp := NocopyStruct{ + A: "valueA", + B: "valueB", + } + res := NocopyStruct{} + + easyjson.Unmarshal(data, &res) + if !reflect.DeepEqual(exp, res) { + t.Errorf("TestNocopy(): got=%+v, exp=%+v", res, exp) + } + + if strBelongsTo(res.A, data) { + t.Error("TestNocopy(): field A was not copied and refers to buffer") + } + if !strBelongsTo(res.B, data) { + t.Error("TestNocopy(): field B was copied rather than refer to bufferr") + } +}