Skip to content

Commit b7ebe36

Browse files
committed
added slice fields support
slice fields length configuration via struct tags; generated code optimizations; code improvements; readme updated;
1 parent ab6f574 commit b7ebe36

File tree

9 files changed

+524
-76
lines changed

9 files changed

+524
-76
lines changed

README.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,45 @@
11
# Simser
2-
**Sim**ple **ser**ialization code generator for GO structs with unexported fields
2+
**Sim**ple **ser**ialization code generator for GO structs with unexported fields.
3+
4+
Like `binary.Read`, but a bit different and without reflection.
5+
6+
## Features
7+
8+
### Core
9+
- go module mode only
10+
- simple sequential [de]serialization of simple structs
11+
- supports non-exported fields, as well as exported
12+
- basic (`int32`, `float64`, etc.), named (`type My uint64`, etc.) field types, and arrays of them (struct type fields,
13+
array of arrays and slice of arrays are not supported right now)
14+
- no reflection in generated code, it is simple and fast
15+
- possibility to select type[s] to serialize via `-types` CLI flag
16+
- possibility to select output function names (and exportability, as implied by Go)
17+
18+
### Advanced
19+
- slice field length can be set to any expression that returns `int`, by using tags. Currently [de]serialized instance can be referred to as "`o`", within expression.
20+
E.g. `simser:"len=o.PreviousIntegerField-5"`.
21+
Or `simser:"len=otherFunc()"`
22+
Remember that only fields that get read _before_ the slice field will have meaningful values (unless some tricks were used)
23+
24+
## Usage
25+
26+
#### Basic
27+
28+
`//go:generate go run github.com/am4n0w4r/simser -types=Header,body`
29+
`//go:generate go run github.com/am4n0w4r/simser -types=all`
30+
31+
- `-types` (required): can be a comma-separated list of types you want to process, or reserved keyword `all` for processing all top-level `struct`s, found in file.
32+
`all` usage and requiredness of the argument can change in the future.
33+
34+
#### Custom
35+
36+
`//go:generate go run github.com/am4n0w4r/simser -types=Header -read-fn-name=customReadFnName -write-fn-name=CustomWriteFnName`
37+
38+
- `-read-fn-name` (optional): custom name for deserializing function. Is set per-file.
39+
- `-write-fn-name` (optional): custom name for deserializing function. Is set per-file.
340

4-
This is a generator that should help with simple sequential reading/writing of structs to/from binary file.
5-
`binary.Read` actually does the same thing and, probably, does this well, but it has a drawback - it doesn't work with non-exported fields.
6-
Also, `binary` stuff is a runtime handler, so it has some overhead, while code, generated by this module tries to be simple and fast.
741

842
## Project state
943

10-
A bit messy, probably unoptimal, but simple and working. It was developed quickly from scratch, to serve a purpose, so the code itself is rather not perfect, but generated code should be at least ok.
11-
It's the first time I saw go's ast, so it was a lot of try-and-fail behind the scenes. Feel free to file issues.
44+
A bit messy, not very optimal, but simple and working. It was developed quickly from scratch, to serve a particular practical purpose, so the code itself is rather not perfect, but generated code should be good and do the job.
45+
It's the first time I worked with go's ast, so it was a lot of try-and-fail behind the scenes. Feel free to file issues.

internal/domain/domain.go

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"errors"
2121
"fmt"
2222
"go/types"
23+
"strconv"
2324
"strings"
2425
)
2526

@@ -31,8 +32,8 @@ type InputStruct struct {
3132
fields []StructField
3233
}
3334

34-
func NewInputStruct(name string, typ *types.Struct) InputStruct {
35-
return InputStruct{
35+
func NewInputStruct(name string, typ *types.Struct) *InputStruct {
36+
return &InputStruct{
3637
name: name,
3738
typ: typ,
3839
}
@@ -50,45 +51,69 @@ func (s *InputStruct) SetFields(f []StructField) { s.fields = f }
5051
type StructField struct {
5152
name string
5253
typ FieldType
54+
tag map[string]string
5355
}
5456

55-
func NewStructField(name string, typ FieldType) StructField {
57+
func NewStructField(name string, typ FieldType, tag map[string]string) StructField {
5658
return StructField{
5759
name: name,
5860
typ: typ,
61+
tag: tag,
5962
}
6063
}
6164

6265
func (f StructField) Name() string { return f.name }
6366
func (f StructField) Type() FieldType { return f.typ }
6467

65-
//
68+
// Type
6669

6770
type FieldType interface {
6871
Name() string
69-
Size() int // Total size of the field
70-
// Don't confuse with IsInt. Checks if type is intX OR uintX
72+
// Total size of the field.
73+
// If Size() < 0, then size is not fixed, and SizeExpr() should be used.
74+
Size() int
75+
// Expression that determines the size of the field.
76+
// Should always be constructed in a way to return int type.
77+
SizeExpr() string
78+
// Don't confuse with IsInt. Checks if type is intX OR uintX OR byte
7179
IsInteger() bool
80+
// Should return true if the type is an array or slice
81+
IsSequence() bool
82+
}
83+
84+
// Field type that is a sequence, i.e. array or slice, which have a number of certain elements
85+
type SequenceFieldType interface { // No need to make this a more generic collection, as [de]serialization is sequential
86+
ElType() FieldType
87+
LenExpr() string
7288
}
7389

90+
// Simple
91+
7492
type SimpleFieldType struct {
7593
name string
7694
size int
7795
underlying *SimpleFieldType
7896
}
7997

8098
func NewSimpleFieldType(name string, size int, underlying *SimpleFieldType) *SimpleFieldType {
99+
if size < 1 {
100+
panic(fmt.Sprintf("simple type size < 1. This should be caught earlier. Please, file a bug to the repo. Type %s, size %d",
101+
name, size))
102+
}
81103
return &SimpleFieldType{
82104
name: name,
83105
size: size,
84106
underlying: underlying,
85107
}
86108
}
87109

88-
func (t SimpleFieldType) Name() string { return t.name }
110+
func (t SimpleFieldType) Name() string { return t.name }
111+
89112
func (t SimpleFieldType) Size() int { return t.size }
113+
func (t SimpleFieldType) SizeExpr() string { return strconv.Itoa(t.size) }
90114
func (t SimpleFieldType) BitSize() int { return t.size * 8 }
91115
func (t SimpleFieldType) Underlying() *SimpleFieldType { return t.underlying }
116+
func (t SimpleFieldType) IsSequence() bool { return false }
92117

93118
func (bt SimpleFieldType) IsInteger() bool {
94119
tmp := &bt
@@ -98,20 +123,65 @@ func (bt SimpleFieldType) IsInteger() bool {
98123
return strings.HasPrefix(tmp.name, "int") || strings.HasPrefix(tmp.name, "uint") || tmp.name == "byte"
99124
}
100125

126+
// Array
127+
101128
type ArrayFieldType struct {
102129
length int
103130
elType FieldType
104131
}
105132

106133
func NewArrayFieldType(length int, el FieldType) *ArrayFieldType {
134+
if length < 0 {
135+
panic(fmt.Sprintf("array length < 1. This should be caught earlier. Please, file a bug to the repo. Length %d",
136+
length))
137+
}
107138
return &ArrayFieldType{
108139
length: length,
109140
elType: el,
110141
}
111142
}
112143

113-
func (at ArrayFieldType) Name() string { return fmt.Sprintf("[]%s", at.elType.Name()) }
114-
func (at ArrayFieldType) Size() int { return at.elType.Size() * at.length }
115-
func (at ArrayFieldType) Length() int { return at.length }
116-
func (at ArrayFieldType) ElType() FieldType { return at.elType }
117-
func (at ArrayFieldType) IsInteger() bool { return false }
144+
func (t ArrayFieldType) Name() string { return fmt.Sprintf("[]%s", t.elType.Name()) }
145+
146+
func (t ArrayFieldType) Size() int {
147+
if !IsFixedSize(t.elType.Size()) {
148+
return -1
149+
}
150+
return t.elType.Size() * t.length
151+
}
152+
func (t ArrayFieldType) SizeExpr() string {
153+
if IsFixedSize(t) {
154+
return strconv.Itoa(t.elType.Size() * t.length)
155+
}
156+
return fmt.Sprintf("(%s) * %d", t.elType.SizeExpr(), t.length)
157+
}
158+
func (t ArrayFieldType) Length() int { return t.length }
159+
func (t ArrayFieldType) LenExpr() string { return strconv.Itoa(t.length) }
160+
func (t ArrayFieldType) ElType() FieldType { return t.elType }
161+
func (t ArrayFieldType) IsInteger() bool { return false }
162+
func (t ArrayFieldType) IsSequence() bool { return true }
163+
164+
// Slice
165+
166+
type SliceFieldType struct {
167+
lenExpr string // expression used to calculate length.
168+
elType FieldType
169+
}
170+
171+
func NewSliceFieldType(lenExpr string, el FieldType) *SliceFieldType {
172+
return &SliceFieldType{
173+
lenExpr: strings.TrimSpace(lenExpr),
174+
elType: el,
175+
}
176+
}
177+
178+
func (t SliceFieldType) Name() string { return fmt.Sprintf("[]%s", t.elType.Name()) }
179+
180+
func (t SliceFieldType) Size() int { return -1 }
181+
func (t SliceFieldType) SizeExpr() string {
182+
return fmt.Sprintf("%s * %s", ParenthesizeIntExpr(t.elType.SizeExpr()), ParenthesizeIntExpr(t.lenExpr))
183+
}
184+
func (t SliceFieldType) LenExpr() string { return t.lenExpr }
185+
func (t SliceFieldType) ElType() FieldType { return t.elType }
186+
func (t SliceFieldType) IsInteger() bool { return false }
187+
func (t SliceFieldType) IsSequence() bool { return true }

internal/domain/functions.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2023 am4n0w4r
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package domain
16+
17+
import (
18+
"fmt"
19+
"strconv"
20+
"strings"
21+
)
22+
23+
/******************************************************************/
24+
/* This file should be kept as small as possible. */
25+
/* Ony functions that are used in domain methods and ALSO outside */
26+
/******************************************************************/
27+
28+
// Adds parentheses to Go's int expression if it is
29+
// a) not a simple int value,
30+
// b) not already cast to int.
31+
func ParenthesizeIntExpr(expr string) string {
32+
if _, err := strconv.Atoi(expr); err == nil {
33+
return expr
34+
}
35+
if !strings.HasPrefix(expr, "int") || !strings.HasSuffix(expr, ")") {
36+
return fmt.Sprintf("(%s)", expr)
37+
}
38+
pos := 3
39+
for expr[pos] == ' ' {
40+
pos++
41+
}
42+
if expr[pos] != '(' {
43+
return fmt.Sprintf("(%s)", expr)
44+
}
45+
return expr
46+
}
47+
48+
// Returns true if total argument's size can be interpreted as known at declaration time.
49+
// E.g. (primitives, arrays). NOT slices.
50+
func IsFixedSize[
51+
T StructField | SimpleFieldType | ArrayFieldType | SliceFieldType | int](a T) bool {
52+
53+
switch arg := any(a).(type) {
54+
case StructField:
55+
return arg.Type().Size() >= 0
56+
case SimpleFieldType:
57+
return arg.Size() >= 0
58+
case ArrayFieldType:
59+
return arg.Size() >= 0
60+
case SliceFieldType:
61+
return arg.Size() >= 0
62+
case int:
63+
return arg >= 0
64+
default:
65+
panic(fmt.Errorf("unknown type %T", a))
66+
}
67+
}

internal/generator/generator.go

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,54 @@ func GenStructCode(s domain.InputStruct, out *Output, readFnName, writeFnName st
2323
return nil
2424
}
2525

26-
totalBufLen := 0
27-
for i := 0; i < s.FieldCount(); i++ {
28-
totalBufLen += s.Field(i).Type().Size()
29-
}
26+
sizeGroups := getFieldSizeGroups(s)
3027

31-
// Generate func (o 'typename')LoadFrom(io.Reader) (*typename, error)
28+
// Generate func (o 'typename')LoadFrom(io.Reader) (*'typename', error)
3229
{
3330
out.AppendImport("io")
3431

3532
out.AppendF("func (o *%s) %s(r io.Reader) (n int, err error) {\n", s.Name(), readFnName)
36-
out.AppendF("b := make([]byte, %d)\n", totalBufLen)
37-
out.AppendF("p := 0\n")
33+
out.AppendF("p, nRead := 0, 0\n")
34+
35+
out.AppendF("toRead := ")
36+
if domain.IsFixedSize(sizeGroups[0]) {
37+
out.AppendF("%d\n", sizeGroups[0])
38+
} else {
39+
out.AppendF("%s\n", s.Field(0).Type().SizeExpr())
40+
}
41+
42+
for i := 0; i < s.FieldCount(); i++ {
43+
if s.Field(i).Type().IsSequence() {
44+
out.AppendF("sLen, sElSize := 0, 0\n")
45+
break
46+
}
47+
}
48+
3849
out.LF()
39-
out.Append(tpl_ReadNbytesIntoBuf("b", uint(totalBufLen))).LF()
50+
out.AppendF("b := make([]byte, toRead)\n")
51+
out.Append(tpl_ReadBytesIntoBuf("b")).LF()
4052
out.LF()
4153

4254
for i := 0; i < s.FieldCount(); i++ {
4355
field := s.Field(i)
44-
out.AppendF("// %s", field.Name()).LF()
56+
out.AppendF("\n// %s\n", field.Name())
57+
if i != 0 {
58+
if size, ok := sizeGroups[i]; ok {
59+
if !domain.IsFixedSize(size) {
60+
if field.Type().IsSequence() {
61+
seqType := field.Type().(domain.SequenceFieldType)
62+
out.AppendF("sLen, sElSize = %s, %s\n", seqType.LenExpr(), seqType.ElType().SizeExpr())
63+
}
64+
out.Append("p, toRead = 0, sLen * sElSize\n")
65+
} else {
66+
out.AppendF("p, toRead = 0, %d\n", size)
67+
}
68+
out.AppendF("if toRead > cap(b) {\n")
69+
out.AppendF("b = make([]byte, toRead)\n")
70+
out.Append("}\n")
71+
out.Append(tpl_ReadBytesIntoBuf("b")).LF()
72+
}
73+
}
4574
s, err := tpl_ReadField(field, "b")
4675
if err != nil {
4776
return err
@@ -60,13 +89,25 @@ func GenStructCode(s domain.InputStruct, out *Output, readFnName, writeFnName st
6089
{
6190
out.AppendImport("io")
6291

63-
out.AppendF("func (s *%s) %s(w io.Writer) (n int, err error) {\n", s.Name(), writeFnName)
64-
out.AppendF("b := make([]byte, 0, %d)\n", totalBufLen)
92+
out.AppendF("func (o *%s) %s(w io.Writer) (n int, err error) {\n", s.Name(), writeFnName)
93+
94+
out.AppendF("b := make([]byte, 0, ")
95+
sb := fstringBuilder{}
96+
constSize := 0
97+
for i, size := range sizeGroups {
98+
if !domain.IsFixedSize(size) {
99+
expr := s.Field(i).Type().SizeExpr()
100+
sb.WriteFString("+ %s", domain.ParenthesizeIntExpr(expr))
101+
continue
102+
}
103+
constSize += size
104+
}
105+
out.AppendF("%d %s)\n", constSize, sb.String())
65106
out.LF()
66107

67108
for i := 0; i < s.FieldCount(); i++ {
68109
field := s.Field(i)
69-
out.AppendF("// %s", field.Name()).LF()
110+
out.AppendF("\n// %s", field.Name()).LF()
70111
s, err := tpl_WriteField(field, "b")
71112
if err != nil {
72113
return err

0 commit comments

Comments
 (0)