This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 80784a79d feat(go): Implement compatible mode with metashare mode
(#2607)
80784a79d is described below
commit 80784a79da01af3257f1e63f666b24e1470cc830
Author: Zhong Junjie <[email protected]>
AuthorDate: Tue Sep 16 22:05:44 2025 +0800
feat(go): Implement compatible mode with metashare mode (#2607)
## Why?
Type-forward and backward-compatible serialization are crucial for
online services where different services update their data schemas and
deploy at different times. This ensures that a service running an older
schema can still communicate with a service using a newer one, and vice
versa.This PR is part of the work of it. The previous PR is #2554 , and
following pr are still work in process.
## What does this PR do?
This PR implements a metashare mode for the fory-go.
Specifically, it:
Provides a user option to enable compatible mode.
If compatible mode is enabled, the serialization process not only
serializes the data but also its detailed type information into a binary
format.
The reading peer, if compatible mode is enabled, will first attempt to
read this type information before deserializing the data. This allows it
to handle data serialized with a different schema.
This implementation is heavily inspired by the existing Java and Python
implementations, ensuring consistency across framework.
All progress and todos:
- [x] metadata encoding and decoding
- [x] Integrate into the main flow to enable metadata sharing
- [ ] Add support for collection types (map, slice, set, etc.)
- [ ] Add support for compressed type definitions
- [ ] Test with python
## Related issues
#2192
## Does this PR introduce any user-facing change?
<!--
If any user-facing interface changes, please [open an
issue](https://github.com/apache/fory/issues/new/choose) describing the
need to do so and update the document if necessary.
Delete section if not applicable.
-->
- [x] Does this PR introduce any public API change? Yes.
- [x] Does this PR introduce any binary protocol compatibility change?
No.
## Benchmark
<!--
When the PR has an impact on performance (if you don't know whether the
PR will have an impact on performance, you can submit the PR first, and
if it will have impact on performance, the code reviewer will explain
it), be sure to attach a benchmark data here.
Delete section if not applicable.
-->
---
.gitignore | 1 +
go/README.md | 32 +++++++
go/fory/context.go | 72 ++++++++++++++++
go/fory/fory.go | 131 ++++++++++++++++++++++++----
go/fory/fory_metashare_test.go | 181 +++++++++++++++++++++++++++++++++++++++
go/fory/set.go | 2 +-
go/fory/slice.go | 4 +-
go/fory/struct.go | 81 ++++++++++++++++--
go/fory/type.go | 110 ++++++++++++++++++++++--
go/fory/type_def.go | 68 ++++++++++++++-
go/fory/type_def_decoder.go | 13 ++-
go/fory/type_def_encoder_test.go | 2 +-
go/fory/type_test.go | 8 +-
13 files changed, 662 insertions(+), 43 deletions(-)
diff --git a/.gitignore b/.gitignore
index c1af3a735..ca557d35e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,4 @@ MODULE.bazel
MODULE.bazel.lock
.DS_Store
**/.DS_Store
+.vscode/
diff --git a/go/README.md b/go/README.md
index d4e9ca894..7dd44f912 100644
--- a/go/README.md
+++ b/go/README.md
@@ -146,6 +146,38 @@ fory --force -file <your file>
- Does generated code work across Go versions? Yes, it’s plain Go code; keep
your toolchain consistent in CI.
- Can I mix generated and non-generated structs? Yes, adoption is incremental
and per file.
+## Configuration Options
+
+Fory Go supports several configuration options through the functional options
pattern:
+
+### Compatible Mode (Metashare)
+
+Compatible mode enables meta information sharing, which allows for schema
evolution:
+
+```go
+// Enable compatible mode with metashare
+fory := NewForyWithOptions(WithCompatible(true))
+```
+
+### Reference Tracking
+
+Enable reference tracking:
+
+```go
+fory := NewForyWithOptions(WithRefTracking(true))
+```
+
+### Combined Options
+
+You can combine multiple options:
+
+```go
+fory := NewForyWithOptions(
+ WithCompatible(true),
+ WithRefTracking(true),
+)
+```
+
## How to test
```bash
diff --git a/go/fory/context.go b/go/fory/context.go
new file mode 100644
index 000000000..69ac98822
--- /dev/null
+++ b/go/fory/context.go
@@ -0,0 +1,72 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package fory
+
+import "reflect"
+
+// MetaContext used to share data across multiple serialization calls
+type MetaContext struct {
+ // typeMap make sure each type just fully serialize once, the following
serialization will use the index
+ typeMap map[reflect.Type]uint32
+ // record typeDefs need to be serialized during one serialization
+ writingTypeDefs []*TypeDef
+ // read from peer
+ readTypeInfos []TypeInfo
+ // scopedMetaShareEnabled controls whether meta sharing is scoped to
single serialization
+ scopedMetaShareEnabled bool
+}
+
+// NewMetaContext creates a new MetaContext
+func NewMetaContext(scopedMetaShareEnabled bool) *MetaContext {
+ return &MetaContext{
+ typeMap: make(map[reflect.Type]uint32),
+ scopedMetaShareEnabled: scopedMetaShareEnabled,
+ }
+}
+
+// resetRead resets the read-related state of the MetaContext
+func (mc *MetaContext) resetRead() {
+ if mc.scopedMetaShareEnabled {
+ mc.readTypeInfos = mc.readTypeInfos[:0] // Reset slice but keep
capacity
+ } else {
+ mc.readTypeInfos = nil
+ }
+}
+
+// resetWrite resets the write-related state of the MetaContext
+func (mc *MetaContext) resetWrite() {
+ if mc.scopedMetaShareEnabled {
+ for k := range mc.typeMap {
+ delete(mc.typeMap, k)
+ }
+ mc.writingTypeDefs = mc.writingTypeDefs[:0] // Reset slice but
keep capacity
+ } else {
+ mc.typeMap = nil
+ mc.writingTypeDefs = nil
+ }
+}
+
+// SetScopedMetaShareEnabled sets the scoped meta share mode
+func (mc *MetaContext) SetScopedMetaShareEnabled(enabled bool) {
+ mc.scopedMetaShareEnabled = enabled
+}
+
+// IsScopedMetaShareEnabled returns whether scoped meta sharing is enabled
+func (mc *MetaContext) IsScopedMetaShareEnabled() bool {
+ return mc.scopedMetaShareEnabled
+}
diff --git a/go/fory/fory.go b/go/fory/fory.go
index becca516d..fc3b9eab3 100644
--- a/go/fory/fory.go
+++ b/go/fory/fory.go
@@ -24,15 +24,72 @@ import (
"sync"
)
-func NewFory(referenceTracking bool) *Fory {
+// Option represents a configuration option for Fory instances.
+// This follows the functional options pattern, allowing flexible configuration
+// by passing variadic option functions to constructors like
NewForyWithOptions.
+type Option func(*Fory)
+
+// WithCompatible sets the compatible mode for the Fory instance.
+// When compatible=true, scoped meta sharing is automatically enabled.
+func WithCompatible(compatible bool) Option {
+ return func(f *Fory) {
+ f.compatible = compatible
+ if compatible {
+ f.metaContext = NewMetaContext(true) // Enable scoped
meta sharing
+ } else {
+ f.metaContext = nil
+ }
+ }
+}
+
+// WithRefTracking sets the reference tracking mode for the Fory instance
+func WithRefTracking(refTracking bool) Option {
+ return func(f *Fory) {
+ f.refTracking = refTracking
+ }
+}
+
+// WithScopedMetaShare sets the scoped meta share mode for the Fory instance.
+// Note: Compatible mode automatically enables scoped meta sharing.
+// This option is mainly used for fine-grained control when compatible mode is
already enabled.
+func WithScopedMetaShare(enabled bool) Option {
+ return func(f *Fory) {
+ if f.metaContext == nil {
+ f.metaContext = NewMetaContext(enabled)
+ } else {
+ f.metaContext.SetScopedMetaShareEnabled(enabled)
+ }
+ }
+}
+
+func NewFory(refTracking bool) *Fory {
+ return NewForyWithOptions(WithRefTracking(refTracking))
+}
+
+// NewForyWithOptions creates a Fory instance with configurable options
+func NewForyWithOptions(options ...Option) *Fory {
fory := &Fory{
- refResolver: newRefResolver(referenceTracking),
- referenceTracking: referenceTracking,
- language: XLANG,
- buffer: NewByteBuffer(nil),
+ refResolver: nil,
+ refTracking: false,
+ language: XLANG,
+ buffer: NewByteBuffer(nil),
+ compatible: false,
}
- // Create a new type resolver for this instance
+
+ // Apply options
+ for _, option := range options {
+ option(fory)
+ }
+
+ // Create a new type resolver for this instance but copy generated
serializers from global resolver
fory.typeResolver = newTypeResolver(fory)
+
+ // Initialize meta context if compatible mode is enabled
+ if fory.compatible {
+ fory.metaContext = NewMetaContext(true)
+ }
+
+ fory.refResolver = newRefResolver(fory.refTracking)
return fory
}
@@ -105,14 +162,16 @@ const (
const MAGIC_NUMBER int16 = 0x62D4
type Fory struct {
- typeResolver *typeResolver
- refResolver *RefResolver
- referenceTracking bool
- language Language
- bufferCallback BufferCallback
- peerLanguage Language
- buffer *ByteBuffer
- buffers []*ByteBuffer
+ typeResolver *typeResolver
+ refResolver *RefResolver
+ refTracking bool
+ language Language
+ bufferCallback BufferCallback
+ peerLanguage Language
+ buffer *ByteBuffer
+ buffers []*ByteBuffer
+ compatible bool
+ metaContext *MetaContext
}
func (f *Fory) RegisterTagType(tag string, v interface{}) error {
@@ -246,7 +305,18 @@ func (f *Fory) readLength(buffer *ByteBuffer) int {
}
func (f *Fory) WriteReferencable(buffer *ByteBuffer, value reflect.Value)
error {
- return f.writeReferencableBySerializer(buffer, value, nil)
+ metaOffset := buffer.writerIndex
+ if f.compatible {
+ buffer.WriteInt32(-1)
+ }
+ if err := f.writeReferencableBySerializer(buffer, value, nil); err !=
nil {
+ return err
+ }
+ if f.compatible && f.metaContext != nil &&
len(f.metaContext.writingTypeDefs) > 0 {
+ buffer.PutInt32(metaOffset,
int32(buffer.writerIndex-metaOffset-4))
+ f.typeResolver.writeTypeDefs(buffer)
+ }
+ return nil
}
func (f *Fory) writeReferencableBySerializer(buffer *ByteBuffer, value
reflect.Value, serializer Serializer) error {
@@ -367,6 +437,19 @@ func (f *Fory) Deserialize(buf *ByteBuffer, v interface{},
buffers []*ByteBuffer
"produced with buffer_callback null")
}
}
+
+ if f.compatible {
+ typeDefOffset := buf.ReadInt32()
+ if typeDefOffset >= 0 {
+ save := buf.readerIndex
+ buf.SetReaderIndex(save + int(typeDefOffset))
+ if err := f.typeResolver.readTypeDefs(buf); err != nil {
+ return fmt.Errorf("failed to read typeDefs:
%w", err)
+ }
+ buf.SetReaderIndex(save)
+ }
+ }
+
if isXLangFlag {
return f.ReadReferencable(buf, reflect.ValueOf(v).Elem())
} else {
@@ -475,11 +558,25 @@ func (f *Fory) Reset() {
func (f *Fory) resetWrite() {
f.typeResolver.resetWrite()
f.refResolver.resetWrite()
+ if f.metaContext != nil {
+ if f.metaContext.IsScopedMetaShareEnabled() {
+ f.metaContext.resetWrite()
+ } else {
+ f.metaContext = nil
+ }
+ }
}
func (f *Fory) resetRead() {
f.typeResolver.resetRead()
f.refResolver.resetRead()
+ if f.metaContext != nil {
+ if f.metaContext.IsScopedMetaShareEnabled() {
+ f.metaContext.resetRead()
+ } else {
+ f.metaContext = nil
+ }
+ }
}
// methods for configure fory.
@@ -488,6 +585,6 @@ func (f *Fory) SetLanguage(language Language) {
f.language = language
}
-func (f *Fory) SetReferenceTracking(referenceTracking bool) {
- f.referenceTracking = referenceTracking
+func (f *Fory) SetRefTracking(refTracking bool) {
+ f.refTracking = refTracking
}
diff --git a/go/fory/fory_metashare_test.go b/go/fory/fory_metashare_test.go
new file mode 100644
index 000000000..cc71c2639
--- /dev/null
+++ b/go/fory/fory_metashare_test.go
@@ -0,0 +1,181 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package fory
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// Test structs for metashare testing
+
+type SimpleDataClass struct {
+ Name string
+ Age int32
+ Active bool
+}
+type ExtendedDataClass struct {
+ Name string
+ Age int32
+ Active bool
+ Email string // Additional field
+}
+
+type ReducedDataClass struct {
+ Name string
+ Age int32
+ // Missing 'active' field
+}
+
+func TestMetaShareEnabled(t *testing.T) {
+ fory := NewForyWithOptions(WithCompatible(true))
+
+ assert.True(t, fory.compatible, "Expected compatible mode to be
enabled")
+ assert.NotNil(t, fory.metaContext, "Expected metaContext to be
initialized when compatible=true")
+ assert.True(t, fory.metaContext.IsScopedMetaShareEnabled(), "Expected
scoped meta share to be enabled by default when compatible=true")
+}
+
+func TestMetaShareDisabled(t *testing.T) {
+ fory := NewForyWithOptions(WithCompatible(false))
+
+ assert.False(t, fory.compatible, "Expected compatible mode to be
disabled")
+ assert.Nil(t, fory.metaContext, "Expected metaContext to be nil when
compatible=false")
+}
+
+func TestSimpleDataClassSerialization(t *testing.T) {
+ fory := NewForyWithOptions(WithCompatible(true))
+
+ // Register the struct
+ err := fory.RegisterTagType("SimpleDataClass", SimpleDataClass{})
+ assert.NoError(t, err, "Failed to register type")
+
+ obj := SimpleDataClass{Name: "test", Age: 25, Active: true}
+
+ // Serialize
+ data, err := fory.Marshal(obj)
+ assert.NoError(t, err, "Failed to marshal")
+
+ // Deserialize
+ var deserialized SimpleDataClass
+ err = fory.Unmarshal(data, &deserialized)
+ assert.NoError(t, err, "Failed to unmarshal")
+
+ // Verify
+ assert.Equal(t, obj.Name, deserialized.Name)
+ assert.Equal(t, obj.Age, deserialized.Age)
+ assert.Equal(t, obj.Active, deserialized.Active)
+}
+
+func TestFieldSortingOrder(t *testing.T) {
+ fory := NewForyWithOptions(WithCompatible(true))
+
+ // Create a struct with fields in non-optimal order (only implemented
types)
+ // the final order should be: FloatField, IntField, BoolField,
ByteField, StringField
+ type UnsortedStruct struct {
+ StringField string
+ FloatField float64
+ BoolField bool
+ IntField int32
+ ByteField byte
+ }
+
+ err := fory.RegisterTagType("UnsortedStruct", UnsortedStruct{})
+ assert.NoError(t, err, "Failed to register type")
+
+ obj := UnsortedStruct{
+ StringField: "test",
+ FloatField: 3.14,
+ BoolField: true,
+ IntField: 42,
+ ByteField: 255,
+ }
+
+ // Serialize
+ data, err := fory.Marshal(obj)
+ assert.NoError(t, err, "Failed to marshal")
+
+ // Deserialize
+ var deserialized UnsortedStruct
+ err = fory.Unmarshal(data, &deserialized)
+ assert.NoError(t, err, "Failed to unmarshal")
+
+ // Verify all fields are correctly serialized/deserialized regardless
of order
+ assert.Equal(t, obj.FloatField, deserialized.FloatField)
+ assert.Equal(t, obj.IntField, deserialized.IntField)
+ assert.Equal(t, obj.BoolField, deserialized.BoolField)
+ assert.Equal(t, obj.ByteField, deserialized.ByteField)
+ assert.Equal(t, obj.StringField, deserialized.StringField)
+
+ t.Logf("Field sorting test passed - optimal order is applied during
field definition creation")
+}
+
+func TestSchemaEvolutionAddField(t *testing.T) {
+ // Test adding fields to existing struct using predefined types
+
+ // Serialize with SimpleDataClass (3 fields)
+ fory1 := NewForyWithOptions(WithCompatible(true))
+ err := fory1.RegisterTagType("TestStruct", SimpleDataClass{})
+ assert.NoError(t, err, "Failed to register SimpleDataClass")
+
+ originalObj := SimpleDataClass{Name: "test", Age: 25, Active: true}
+ data, err := fory1.Marshal(originalObj)
+ assert.NoError(t, err, "Failed to marshal SimpleDataClass")
+
+ // Deserialize with ExtendedDataClass (4 fields - adds Email field)
+ fory2 := NewForyWithOptions(WithCompatible(true))
+ err = fory2.RegisterTagType("TestStruct", ExtendedDataClass{})
+ assert.NoError(t, err, "Failed to register ExtendedDataClass")
+
+ var deserialized ExtendedDataClass
+ err = fory2.Unmarshal(data, &deserialized)
+ assert.NoError(t, err, "Failed to unmarshal to ExtendedDataClass")
+
+ // Verify common fields and default value for new field
+ assert.Equal(t, originalObj.Name, deserialized.Name)
+ assert.Equal(t, originalObj.Age, deserialized.Age)
+ assert.Equal(t, originalObj.Active, deserialized.Active)
+ assert.Equal(t, "", deserialized.Email, "Expected Email to be its
default value (empty string)")
+}
+
+func TestSchemaEvolutionRemoveField(t *testing.T) {
+ // Test removing fields from existing struct using predefined types
+
+ // Serialize with SimpleDataClass (3 fields)
+ fory1 := NewForyWithOptions(WithCompatible(true))
+ err := fory1.RegisterTagType("TestStruct", SimpleDataClass{})
+ assert.NoError(t, err, "Failed to register SimpleDataClass")
+
+ originalObj := SimpleDataClass{Name: "test", Age: 25, Active: true}
+ data, err := fory1.Marshal(originalObj)
+ assert.NoError(t, err, "Failed to marshal SimpleDataClass")
+
+ // Deserialize with ReducedDataClass (2 fields - removes Active)
+ fory2 := NewForyWithOptions(WithCompatible(true))
+ err = fory2.RegisterTagType("TestStruct", ReducedDataClass{})
+ assert.NoError(t, err, "Failed to register ReducedDataClass")
+
+ var deserialized ReducedDataClass
+ err = fory2.Unmarshal(data, &deserialized)
+ assert.NoError(t, err, "Failed to unmarshal to ReducedDataClass")
+
+ // Verify common fields
+ assert.Equal(t, originalObj.Name, deserialized.Name)
+ assert.Equal(t, originalObj.Age, deserialized.Age)
+ // Active field is removed, so it should not be present in deserialized
ReducedDataClass
+}
diff --git a/go/fory/set.go b/go/fory/set.go
index 0b3838c66..d64982b6a 100644
--- a/go/fory/set.go
+++ b/go/fory/set.go
@@ -110,7 +110,7 @@ func (s setSerializer) writeHeader(f *Fory, buf
*ByteBuffer, keys []reflect.Valu
}
// Enable reference tracking if configured
- if f.referenceTracking {
+ if f.refTracking {
collectFlag |= CollectionTrackingRef
}
diff --git a/go/fory/slice.go b/go/fory/slice.go
index 6a171cdc4..ef3810abd 100644
--- a/go/fory/slice.go
+++ b/go/fory/slice.go
@@ -102,7 +102,7 @@ func (s sliceSerializer) writeHeader(f *Fory, buf
*ByteBuffer, value reflect.Val
}
// Enable reference tracking if configured
- if f.referenceTracking {
+ if f.refTracking {
collectFlag |= CollectionTrackingRef
}
@@ -1093,7 +1093,7 @@ func (s stringSliceSerializer) Read(f *Fory, buf
*ByteBuffer, type_ reflect.Type
}
}
elem := readString(buf)
- if f.referenceTracking && refFlag == RefValueFlag {
+ if f.refTracking && refFlag == RefValueFlag {
// If value is not nil(reflect), then value is
a pointer to some variable, we can update the `value`,
// then record `value` in the reference
resolver.
f.refResolver.SetReadObject(nextReadRefId,
reflect.ValueOf(elem))
diff --git a/go/fory/struct.go b/go/fory/struct.go
index f65216005..2fd679a35 100644
--- a/go/fory/struct.go
+++ b/go/fory/struct.go
@@ -30,6 +30,7 @@ type structSerializer struct {
type_ reflect.Type
fieldsInfo structFieldsInfo
structHash int32
+ fieldDefs []FieldDef // defs obtained during reading
}
var UNKNOWN_TYPE_ID = int16(-1)
@@ -86,10 +87,20 @@ func (s *structSerializer) Read(f *Fory, buf *ByteBuffer,
type_ reflect.Type, va
value = value.Elem()
}
if s.fieldsInfo == nil {
- if infos, err := createStructFieldInfos(f, s.type_); err != nil
{
- return err
+ if len(s.fieldDefs) == 0 {
+ // Normal case: create from reflection
+ if infos, err := createStructFieldInfos(f, s.type_);
err != nil {
+ return err
+ } else {
+ s.fieldsInfo = infos
+ }
} else {
- s.fieldsInfo = infos
+ // Create from fieldDefs for forward/backward
compatibility
+ if infos, err := createStructFieldInfosFromFieldDefs(f,
s.fieldDefs, type_); err != nil {
+ return err
+ } else {
+ s.fieldsInfo = infos
+ }
}
}
if s.structHash == 0 {
@@ -100,12 +111,21 @@ func (s *structSerializer) Read(f *Fory, buf *ByteBuffer,
type_ reflect.Type, va
}
}
structHash := buf.ReadInt32()
- if structHash != s.structHash {
+ if !f.compatible && structHash != s.structHash {
return fmt.Errorf("hash %d is not consistent with %d for type
%s",
structHash, s.structHash, s.type_)
}
for _, fieldInfo_ := range s.fieldsInfo {
- fieldValue := value.Field(fieldInfo_.fieldIndex)
+ var fieldValue reflect.Value
+
+ if fieldInfo_.fieldIndex >= 0 {
+ // Field exists in current struct version
+ fieldValue = value.Field(fieldInfo_.fieldIndex)
+ } else {
+ // Field doesn't exist in current struct version,
create temporary value to discard
+ fieldValue = reflect.New(fieldInfo_.type_).Elem()
+ }
+
fieldSerializer := fieldInfo_.serializer
if fieldSerializer != nil {
if err := readBySerializer(f, buf, fieldValue,
fieldSerializer, fieldInfo_.referencable); err != nil {
@@ -204,6 +224,57 @@ func createStructFieldInfos(f *Fory, type_ reflect.Type)
(structFieldsInfo, erro
return fields, nil
}
+// createStructFieldInfosFromFieldDefs creates structFieldsInfo from fieldDefs
and builds field name mapping
+func createStructFieldInfosFromFieldDefs(f *Fory, fieldDefs []FieldDef, type_
reflect.Type) (structFieldsInfo, error) {
+ fieldNameToIndex := make(map[string]int)
+
+ // Build map from field names to struct field indices for quick lookup
+ for i := 0; i < type_.NumField(); i++ {
+ field := type_.Field(i)
+ fieldName := SnakeCase(field.Name)
+ fieldNameToIndex[fieldName] = i
+ }
+
+ var fields structFieldsInfo
+ current_field_names := make(map[string]int)
+
+ for i, def := range fieldDefs {
+ current_field_names[def.name] = i
+
+ fieldIndex := -1 // Default to -1 if field doesn't exist in
current struct
+ var fieldType reflect.Type
+
+ if structFieldIndex, exists := fieldNameToIndex[def.name];
exists {
+ fieldIndex = structFieldIndex
+ fieldType = type_.Field(structFieldIndex).Type
+ } else {
+ // Field doesn't exist in current struct version, we
need the type from FieldDef
+ if info, exists :=
f.typeResolver.typeIDToTypeInfo[int32(def.fieldType.TypeId())]; exists {
+ fieldType = info.Type
+ } else {
+ return nil, fmt.Errorf("unknown type for field
%s with typeId %d", def.name, def.fieldType.TypeId())
+ }
+ }
+
+ fieldSerializer, err := def.fieldType.getSerializer(f)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get serializer for
field %s: %w", def.name, err)
+ }
+
+ fieldInfo := &fieldInfo{
+ name: def.name,
+ fieldIndex: fieldIndex,
+ type_: fieldType,
+ referencable: def.nullable,
+ serializer: fieldSerializer,
+ }
+
+ fields = append(fields, fieldInfo)
+ }
+
+ return fields, nil
+}
+
type triple struct {
typeID int16
serializer Serializer
diff --git a/go/fory/type.go b/go/fory/type.go
index 8d1bc68f8..de0a65446 100644
--- a/go/fory/type.go
+++ b/go/fory/type.go
@@ -306,6 +306,10 @@ type typeResolver struct {
namespaceDecoder *meta.Decoder
typeNameEncoder *meta.Encoder
typeNameDecoder *meta.Decoder
+
+ // meta share related
+ typeToTypeDef map[reflect.Type]*TypeDef
+ defIdToTypeDef map[int64]*TypeDef
}
func newTypeResolver(fory *Fory) *typeResolver {
@@ -341,6 +345,9 @@ func newTypeResolver(fory *Fory) *typeResolver {
namespaceDecoder: meta.NewDecoder('.', '_'),
typeNameEncoder: meta.NewEncoder('$', '_'),
typeNameDecoder: meta.NewDecoder('$', '_'),
+
+ typeToTypeDef: make(map[reflect.Type]*TypeDef),
+ defIdToTypeDef: make(map[int64]*TypeDef),
}
// base type info for encode/decode types.
// composite types info will be constructed dynamically.
@@ -532,15 +539,10 @@ func (r *typeResolver) getTypeInfo(value reflect.Value,
create bool) (TypeInfo,
}
clean := strings.TrimPrefix(rawInfo, "*@")
clean = strings.TrimPrefix(clean, "@")
+ typeName = clean
if idx := strings.LastIndex(clean, "."); idx != -1 {
pkgPath = clean[:idx]
- } else {
- pkgPath = clean
- }
- if type_.Kind() == reflect.Ptr {
- typeName = type_.Elem().Name()
- } else {
- typeName = type_.Name()
+ typeName = clean[idx+1:]
}
// Handle special types that require explicit registration
@@ -716,6 +718,10 @@ func calcTypeHash(type_ reflect.Type) uint64 {
return h.Sum64()
}
+func (r *typeResolver) metaShareEnabled() bool {
+ return r.fory != nil && r.fory.metaContext != nil && r.fory.compatible
+}
+
func (r *typeResolver) writeTypeInfo(buffer *ByteBuffer, typeInfo TypeInfo)
error {
// Extract the internal type ID (lower 8 bits)
typeID := typeInfo.TypeID
@@ -729,6 +735,12 @@ func (r *typeResolver) writeTypeInfo(buffer *ByteBuffer,
typeInfo TypeInfo) erro
// For namespaced types, write additional metadata:
if IsNamespacedType(TypeId(internalTypeID)) {
+ if r.metaShareEnabled() {
+ if err := r.writeSharedTypeMeta(buffer, typeInfo); err
!= nil {
+ return err
+ }
+ return nil
+ }
// Write package path (namespace) metadata
if err := r.metaStringResolver.WriteMetaStringBytes(buffer,
typeInfo.PkgPathBytes); err != nil {
return err
@@ -742,6 +754,87 @@ func (r *typeResolver) writeTypeInfo(buffer *ByteBuffer,
typeInfo TypeInfo) erro
return nil
}
+func (r *typeResolver) writeSharedTypeMeta(buffer *ByteBuffer, typeInfo
TypeInfo) error {
+ context := r.fory.metaContext
+ typ := typeInfo.Type
+
+ if index, exists := context.typeMap[typ]; exists {
+ buffer.WriteVarUint32(index)
+ return nil
+ }
+
+ newIndex := uint32(len(context.typeMap))
+ buffer.WriteVarUint32(newIndex)
+ context.typeMap[typ] = newIndex
+
+ typeDef, err := r.getOrCreateTypeDef(typeInfo.Type)
+ if err != nil {
+ return err
+ }
+ context.writingTypeDefs = append(context.writingTypeDefs, typeDef)
+ return nil
+}
+
+func (r *typeResolver) getOrCreateTypeDef(typ reflect.Type) (*TypeDef, error) {
+ if existingTypeDef, exists := r.typeToTypeDef[typ]; exists {
+ return existingTypeDef, nil
+ }
+
+ zero := reflect.Zero(typ)
+ typeDef, err := buildTypeDef(r.fory, zero)
+ if err != nil {
+ return nil, err
+ }
+ r.typeToTypeDef[typ] = typeDef
+ return typeDef, nil
+}
+
+func (r *typeResolver) readSharedTypeMeta(buffer *ByteBuffer) (TypeInfo,
error) {
+ context := r.fory.metaContext
+ index := buffer.ReadVarInt32() // shared meta index id
+ if index < 0 || index >= int32(len(context.readTypeInfos)) {
+ return TypeInfo{}, fmt.Errorf("TypeInfo not found for index
%d", index)
+ }
+ info := context.readTypeInfos[index]
+ return info, nil
+}
+
+func (r *typeResolver) writeTypeDefs(buffer *ByteBuffer) {
+ context := r.fory.metaContext
+ sz := len(context.writingTypeDefs)
+ buffer.WriteVarUint32Small7(uint32(sz))
+ for _, typeDef := range context.writingTypeDefs {
+ typeDef.writeTypeDef(buffer)
+ }
+ context.writingTypeDefs = nil
+}
+
+func (r *typeResolver) readTypeDefs(buffer *ByteBuffer) error {
+ numTypeDefs := buffer.ReadVarUint32Small7()
+ context := r.fory.metaContext
+ for i := 0; i < numTypeDefs; i++ {
+ id := buffer.ReadInt64()
+ var td *TypeDef
+ if existingTd, exists := r.defIdToTypeDef[id]; exists {
+ skipTypeDef(buffer, id)
+ td = existingTd
+ } else {
+ newTd, err := readTypeDef(r.fory, buffer, id)
+ if err != nil {
+ return err
+ }
+ r.defIdToTypeDef[id] = newTd
+ td = newTd
+ }
+ typeInfo, err := td.buildTypeInfo()
+ if err != nil {
+ return err
+ }
+ context.readTypeInfos = append(context.readTypeInfos, typeInfo)
+ }
+ return nil
+}
+
func (r *typeResolver) createSerializer(type_ reflect.Type, mapInStruct bool)
(s Serializer, err error) {
kind := type_.Kind()
switch kind {
@@ -1015,6 +1108,9 @@ func (r *typeResolver) readTypeInfo(buffer *ByteBuffer)
(TypeInfo, error) {
internalTypeID = -internalTypeID
}
if IsNamespacedType(TypeId(internalTypeID)) {
+ if r.metaShareEnabled() {
+ return r.readSharedTypeMeta(buffer)
+ }
// Read namespace and type name metadata bytes
nsBytes, err := r.metaStringResolver.ReadMetaStringBytes(buffer)
if err != nil {
diff --git a/go/fory/type_def.go b/go/fory/type_def.go
index 193dde6f3..6d59bc05f 100644
--- a/go/fory/type_def.go
+++ b/go/fory/type_def.go
@@ -66,8 +66,21 @@ func (td *TypeDef) writeTypeDef(buffer *ByteBuffer) {
buffer.WriteBinary(td.encoded)
}
-func readTypeDef(fory *Fory, buffer *ByteBuffer) (*TypeDef, error) {
- return decodeTypeDef(fory, buffer)
+// buildTypeInfo constructs a TypeInfo from the TypeDef
+func (td *TypeDef) buildTypeInfo() (TypeInfo, error) {
+ info := TypeInfo{
+ Type: td.type_,
+ TypeID: int32(td.typeId),
+ Serializer: &structSerializer{type_: td.type_, fieldDefs:
td.fieldDefs},
+ PkgPathBytes: td.nsName,
+ NameBytes: td.typeName,
+ IsDynamic: td.typeId < 0,
+ }
+ return info, nil
+}
+
+func readTypeDef(fory *Fory, buffer *ByteBuffer, header int64) (*TypeDef,
error) {
+ return decodeTypeDef(fory, buffer, header)
}
func skipTypeDef(buffer *ByteBuffer, header int64) {
@@ -128,7 +141,7 @@ func buildFieldDefs(fory *Fory, value reflect.Value)
([]FieldDef, error) {
fieldValue := value.Field(i)
var fieldInfo FieldDef
- fieldName := field.Name
+ fieldName := SnakeCase(field.Name)
nameEncoding :=
fory.typeResolver.typeNameEncoder.ComputeEncodingWith(fieldName,
fieldNameEncodings)
@@ -140,11 +153,45 @@ func buildFieldDefs(fory *Fory, value reflect.Value)
([]FieldDef, error) {
name: fieldName,
nameEncoding: nameEncoding,
nullable: nullable(field.Type),
- trackingRef: fory.referenceTracking,
+ trackingRef: fory.refTracking,
fieldType: ft,
}
fieldInfos = append(fieldInfos, fieldInfo)
}
+
+ // Sort field definitions
+ if len(fieldInfos) > 1 {
+ // Extract serializers and names for sorting
+ serializers := make([]Serializer, len(fieldInfos))
+ fieldNames := make([]string, len(fieldInfos))
+ for i, fieldInfo := range fieldInfos {
+ serializer, err :=
fieldInfo.fieldType.getSerializer(fory)
+ if err != nil {
+ // If we can't get serializer, use nil (will be
handled by sortFields)
+ serializers[i] = nil
+ } else {
+ serializers[i] = serializer
+ }
+ fieldNames[i] = fieldInfo.name
+ }
+
+ // Use existing sortFields function to get optimal order
+ _, sortedNames := sortFields(fory.typeResolver, fieldNames,
serializers)
+
+ // Rebuild fieldInfos in the sorted order
+ nameToFieldInfo := make(map[string]FieldDef)
+ for _, fieldInfo := range fieldInfos {
+ nameToFieldInfo[fieldInfo.name] = fieldInfo
+ }
+
+ sortedFieldInfos := make([]FieldDef, len(fieldInfos))
+ for i, name := range sortedNames {
+ sortedFieldInfos[i] = nameToFieldInfo[name]
+ }
+
+ fieldInfos = sortedFieldInfos
+ }
+
return fieldInfos, nil
}
@@ -152,6 +199,7 @@ func buildFieldDefs(fory *Fory, value reflect.Value)
([]FieldDef, error) {
type FieldType interface {
TypeId() TypeId
write(*ByteBuffer)
+ getSerializer(*Fory) (Serializer, error)
}
// BaseFieldType provides common functionality for field types
@@ -164,6 +212,18 @@ func (b *BaseFieldType) write(buffer *ByteBuffer) {
buffer.WriteVarUint32Small7(uint32(b.typeId))
}
+func (o *BaseFieldType) getSerializer(fory *Fory) (Serializer, error) {
+ if o.typeId == EXTENSION || o.typeId == STRUCT || o.typeId ==
NAMED_STRUCT ||
+ o.typeId == COMPATIBLE_STRUCT || o.typeId ==
NAMED_COMPATIBLE_STRUCT || o.typeId == UNKNOWN_TYPE_ID {
+ return nil, nil
+ }
+ info, err := fory.typeResolver.getTypeInfoById(o.typeId)
+ if err != nil {
+ return nil, err
+ }
+ return info.Serializer, nil
+}
+
// readFieldInfo reads field type info from the buffer according to the TypeId
func readFieldType(buffer *ByteBuffer) (FieldType, error) {
typeId := buffer.ReadVarUint32Small7()
diff --git a/go/fory/type_def_decoder.go b/go/fory/type_def_decoder.go
index f4b2d8ec2..d98cff6a8 100644
--- a/go/fory/type_def_decoder.go
+++ b/go/fory/type_def_decoder.go
@@ -19,6 +19,7 @@ package fory
import (
"fmt"
+ "reflect"
)
/*
@@ -29,9 +30,9 @@ typeDef are layout as following:
- next variable bytes: type id (varint) or ns name + type name
- next variable bytes: field definitions (see below)
*/
-func decodeTypeDef(fory *Fory, buffer *ByteBuffer) (*TypeDef, error) {
+func decodeTypeDef(fory *Fory, buffer *ByteBuffer, header int64) (*TypeDef,
error) {
// Read 8-byte global header
- globalHeader := uint64(buffer.ReadInt64())
+ globalHeader := header
hasFieldsMeta := (globalHeader & HAS_FIELDS_META_FLAG) != 0
isCompressed := (globalHeader & COMPRESS_META_FLAG) != 0
metaSize := int(globalHeader & META_SIZE_MASK)
@@ -61,6 +62,7 @@ func decodeTypeDef(fory *Fory, buffer *ByteBuffer) (*TypeDef,
error) {
// Read name or type ID according to the registerByName flag
var typeId TypeId
var nsBytes, nameBytes *MetaStringBytes
+ var type_ reflect.Type
if registeredByName {
// Read namespace and type name for namespaced types
readingNsBytes, err :=
fory.typeResolver.metaStringResolver.ReadMetaStringBytes(metaBuffer)
@@ -78,8 +80,14 @@ func decodeTypeDef(fory *Fory, buffer *ByteBuffer)
(*TypeDef, error) {
return nil, fmt.Errorf("type not registered")
}
typeId = TypeId(info.TypeID)
+ type_ = info.Type
} else {
typeId = TypeId(metaBuffer.ReadVarInt32())
+ if info, err := fory.typeResolver.getTypeInfoById(typeId); err
!= nil {
+ return nil, fmt.Errorf("failed to get type info by id
%d: %w", typeId, err)
+ } else {
+ type_ = info.Type
+ }
}
// Read fields information
@@ -97,6 +105,7 @@ func decodeTypeDef(fory *Fory, buffer *ByteBuffer)
(*TypeDef, error) {
// Create TypeDef
typeDef := NewTypeDef(typeId, nsBytes, nameBytes, registeredByName,
isCompressed, fieldInfos)
typeDef.encoded = encoded
+ typeDef.type_ = type_
return typeDef, nil
}
diff --git a/go/fory/type_def_encoder_test.go b/go/fory/type_def_encoder_test.go
index 0c74954fb..e65474d32 100644
--- a/go/fory/type_def_encoder_test.go
+++ b/go/fory/type_def_encoder_test.go
@@ -57,7 +57,7 @@ func TestTypeDefEncodingDecoding(t *testing.T) {
originalTypeDef.writeTypeDef(buffer)
// Decode the TypeDef
- decodedTypeDef, err := readTypeDef(fory, buffer)
+ decodedTypeDef, err := readTypeDef(fory, buffer,
int64(buffer.ReadInt64()))
if err != nil {
t.Fatalf("Failed to decode TypeDef: %v", err)
}
diff --git a/go/fory/type_test.go b/go/fory/type_test.go
index 5e0add767..b26c5674a 100644
--- a/go/fory/type_test.go
+++ b/go/fory/type_test.go
@@ -25,10 +25,10 @@ import (
func TestTypeResolver(t *testing.T) {
fory := &Fory{
- refResolver: newRefResolver(false),
- referenceTracking: false,
- language: XLANG,
- buffer: NewByteBuffer(nil),
+ refResolver: newRefResolver(false),
+ refTracking: false,
+ language: XLANG,
+ buffer: NewByteBuffer(nil),
}
typeResolver := newTypeResolver(fory)
type A struct {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]