Skip to content

Commit d7d3c70

Browse files
committed
feat(go): implement union type for xlang serialization
Add support for tagged union types in Go Fory, enabling cross-language serialization compatibility with other languages like C++, Java, Python, and Rust that already support union/variant types. Changes: - Add UNION (38) and NONE (39) type constants to types.go - Implement Union struct and unionSerializer in union.go - Add RegisterUnionType API to Fory for registering union alternatives - Add comprehensive tests in union_test.go The implementation follows the same binary protocol as other languages: 1. Write variant index (varuint32) 2. In xlang mode, write type info for the active alternative 3. Write the value data using the alternative's serializer Fixes #3031
1 parent 3348074 commit d7d3c70

File tree

3 files changed

+546
-0
lines changed

3 files changed

+546
-0
lines changed

go/fory/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ const (
9898
FLOAT32_ARRAY = 36
9999
// FLOAT64_ARRAY one dimensional float64 array
100100
FLOAT64_ARRAY = 37
101+
// UNION a tagged union type that can hold one of several alternative types
102+
UNION = 38
103+
// NONE represents an empty/unit value with no data (e.g., for empty union alternatives)
104+
NONE = 39
101105

102106
// UINT8 Unsigned 8-bit little-endian integer
103107
UINT8 = 64

go/fory/union.go

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package fory
19+
20+
import (
21+
"fmt"
22+
"reflect"
23+
)
24+
25+
// Union represents a tagged union type that can hold one of several alternative types.
26+
// It's equivalent to Rust's enum, C++'s std::variant, or Python's typing.Union.
27+
//
28+
// The Value field holds the actual value, which must be one of the types specified
29+
// when registering the Union type.
30+
//
31+
// Example usage:
32+
//
33+
// // Create a union that can hold int32 or string
34+
// union := fory.Union{Value: int32(42)}
35+
// // or
36+
// union := fory.Union{Value: "hello"}
37+
type Union struct {
38+
Value interface{}
39+
}
40+
41+
// NewUnion creates a new Union with the given value.
42+
func NewUnion(value interface{}) Union {
43+
return Union{Value: value}
44+
}
45+
46+
// IsNil returns true if the union holds no value.
47+
func (u Union) IsNil() bool {
48+
return u.Value == nil
49+
}
50+
51+
// unionSerializer serializes Union types.
52+
//
53+
// Serialization format:
54+
// 1. Write variant index (varuint32) - identifies which alternative type is active
55+
// 2. In xlang mode, write type info for the active alternative
56+
// 3. Write the value data using the alternative's serializer
57+
type unionSerializer struct {
58+
type_ reflect.Type
59+
alternativeTypes []reflect.Type
60+
typeResolver *TypeResolver
61+
alternativeTypeInfo []*TypeInfo
62+
}
63+
64+
// newUnionSerializer creates a new serializer for Union types with the specified alternatives.
65+
// The alternativeTypes slice defines the allowed types in order - the index is used as the variant index.
66+
func newUnionSerializer(typeResolver *TypeResolver, alternativeTypes []reflect.Type) *unionSerializer {
67+
typeInfos := make([]*TypeInfo, len(alternativeTypes))
68+
return &unionSerializer{
69+
type_: reflect.TypeOf(Union{}),
70+
alternativeTypes: alternativeTypes,
71+
typeResolver: typeResolver,
72+
alternativeTypeInfo: typeInfos,
73+
}
74+
}
75+
76+
// findAlternativeIndex finds the index of the type that matches the given value.
77+
// Returns -1 if no match is found.
78+
func (s *unionSerializer) findAlternativeIndex(value reflect.Value) int {
79+
if !value.IsValid() || (value.Kind() == reflect.Interface && value.IsNil()) {
80+
return -1
81+
}
82+
83+
valueType := value.Type()
84+
if valueType.Kind() == reflect.Interface {
85+
valueType = value.Elem().Type()
86+
}
87+
88+
for i, altType := range s.alternativeTypes {
89+
if valueType == altType {
90+
return i
91+
}
92+
// Also check if the value is assignable to the alternative type
93+
if valueType.AssignableTo(altType) {
94+
return i
95+
}
96+
// For pointer types, check the elem type
97+
if valueType.Kind() == reflect.Ptr && altType.Kind() == reflect.Ptr {
98+
if valueType.Elem() == altType.Elem() {
99+
return i
100+
}
101+
}
102+
}
103+
return -1
104+
}
105+
106+
func (s *unionSerializer) Write(ctx *WriteContext, refMode RefMode, writeType bool, value reflect.Value) error {
107+
buf := ctx.Buffer()
108+
109+
// Get the Union value
110+
var union Union
111+
if value.Kind() == reflect.Ptr {
112+
if value.IsNil() {
113+
buf.WriteInt8(NullFlag)
114+
return nil
115+
}
116+
union = value.Elem().Interface().(Union)
117+
} else {
118+
union = value.Interface().(Union)
119+
}
120+
121+
// Handle null union value
122+
if union.Value == nil {
123+
switch refMode {
124+
case RefModeTracking, RefModeNullOnly:
125+
buf.WriteInt8(NullFlag)
126+
}
127+
return nil
128+
}
129+
130+
// Write ref flag for non-null
131+
switch refMode {
132+
case RefModeTracking:
133+
refWritten, err := ctx.RefResolver().WriteRefOrNull(buf, value)
134+
if err != nil {
135+
return err
136+
}
137+
if refWritten {
138+
return nil
139+
}
140+
case RefModeNullOnly:
141+
buf.WriteInt8(NotNullValueFlag)
142+
}
143+
144+
// Write type info if needed
145+
if writeType {
146+
buf.WriteVaruint32Small7(uint32(UNION))
147+
}
148+
149+
return s.WriteData(ctx, value)
150+
}
151+
152+
func (s *unionSerializer) WriteData(ctx *WriteContext, value reflect.Value) error {
153+
buf := ctx.Buffer()
154+
155+
// Get the Union value
156+
var union Union
157+
if value.Kind() == reflect.Ptr {
158+
union = value.Elem().Interface().(Union)
159+
} else {
160+
union = value.Interface().(Union)
161+
}
162+
163+
// Find which alternative type matches the value
164+
innerValue := reflect.ValueOf(union.Value)
165+
activeIndex := s.findAlternativeIndex(innerValue)
166+
167+
if activeIndex < 0 {
168+
return fmt.Errorf("union value type %T doesn't match any alternative in %v", union.Value, s.alternativeTypes)
169+
}
170+
171+
// Write the active variant index
172+
buf.WriteVaruint32(uint32(activeIndex))
173+
174+
// Get the serializer for the active alternative
175+
altType := s.alternativeTypes[activeIndex]
176+
serializer, err := ctx.TypeResolver().getSerializerByType(altType, false)
177+
if err != nil {
178+
return fmt.Errorf("no serializer for union alternative type %v: %w", altType, err)
179+
}
180+
181+
// In xlang mode, write type info for the alternative
182+
if ctx.TypeResolver().isXlang {
183+
typeInfo, err := ctx.TypeResolver().getTypeInfo(innerValue, true)
184+
if err != nil {
185+
return err
186+
}
187+
if err := ctx.TypeResolver().WriteTypeInfo(buf, typeInfo); err != nil {
188+
return err
189+
}
190+
}
191+
192+
// Write the value data
193+
return serializer.WriteData(ctx, innerValue)
194+
}
195+
196+
func (s *unionSerializer) Read(ctx *ReadContext, refMode RefMode, readType bool, value reflect.Value) error {
197+
buf := ctx.Buffer()
198+
199+
switch refMode {
200+
case RefModeTracking:
201+
refID, err := ctx.RefResolver().TryPreserveRefId(buf)
202+
if err != nil {
203+
return err
204+
}
205+
if int8(refID) < NotNullValueFlag {
206+
obj := ctx.RefResolver().GetReadObject(refID)
207+
if obj.IsValid() {
208+
if value.Kind() == reflect.Ptr {
209+
value.Elem().Set(obj)
210+
} else {
211+
value.Set(obj)
212+
}
213+
}
214+
return nil
215+
}
216+
case RefModeNullOnly:
217+
flag := buf.ReadInt8()
218+
if flag == NullFlag {
219+
return nil
220+
}
221+
}
222+
223+
if readType {
224+
typeId := buf.ReadVaruint32Small7()
225+
if TypeId(typeId) != UNION {
226+
return fmt.Errorf("expected UNION type id %d, got %d", UNION, typeId)
227+
}
228+
}
229+
230+
return s.ReadData(ctx, s.type_, value)
231+
}
232+
233+
func (s *unionSerializer) ReadData(ctx *ReadContext, type_ reflect.Type, value reflect.Value) error {
234+
buf := ctx.Buffer()
235+
236+
// Read the stored variant index
237+
storedIndex := buf.ReadVaruint32()
238+
239+
// Validate index is within bounds
240+
if int(storedIndex) >= len(s.alternativeTypes) {
241+
return fmt.Errorf("union index out of bounds: %d (max: %d)", storedIndex, len(s.alternativeTypes)-1)
242+
}
243+
244+
// Get the alternative type
245+
altType := s.alternativeTypes[storedIndex]
246+
247+
// Get serializer for this alternative
248+
serializer, err := ctx.TypeResolver().getSerializerByType(altType, false)
249+
if err != nil {
250+
return fmt.Errorf("no serializer for union alternative type %v: %w", altType, err)
251+
}
252+
253+
// In xlang mode, read type info for the alternative
254+
if ctx.TypeResolver().isXlang {
255+
// Read the type info - we need to pass a value for the ReadTypeInfo function
256+
dummyValue := reflect.New(altType).Elem()
257+
_, err := ctx.TypeResolver().ReadTypeInfo(buf, dummyValue)
258+
if err != nil {
259+
return err
260+
}
261+
}
262+
263+
// Create a value to hold the alternative data
264+
altValue := reflect.New(altType).Elem()
265+
266+
// Read the value data
267+
if err := serializer.ReadData(ctx, altType, altValue); err != nil {
268+
return err
269+
}
270+
271+
// Set the union value
272+
union := Union{Value: altValue.Interface()}
273+
if value.Kind() == reflect.Ptr {
274+
value.Elem().Set(reflect.ValueOf(union))
275+
} else {
276+
value.Set(reflect.ValueOf(union))
277+
}
278+
279+
return nil
280+
}
281+
282+
func (s *unionSerializer) ReadWithTypeInfo(ctx *ReadContext, refMode RefMode, typeInfo *TypeInfo, value reflect.Value) error {
283+
return s.Read(ctx, refMode, false, value)
284+
}
285+
286+
// RegisterUnionType registers a Union type with the specified alternative types.
287+
// The alternative types are the types that the union can hold.
288+
// Returns an error if registration fails.
289+
//
290+
// Example:
291+
//
292+
// f := fory.NewFory()
293+
// err := f.RegisterUnionType(reflect.TypeOf(int32(0)), reflect.TypeOf(""))
294+
// if err != nil {
295+
// panic(err)
296+
// }
297+
func (f *Fory) RegisterUnionType(alternativeTypes ...reflect.Type) error {
298+
if len(alternativeTypes) == 0 {
299+
return fmt.Errorf("union must have at least one alternative type")
300+
}
301+
302+
unionType := reflect.TypeOf(Union{})
303+
serializer := newUnionSerializer(f.typeResolver, alternativeTypes)
304+
305+
// Register the union type with the serializer
306+
f.typeResolver.typeToSerializers[unionType] = serializer
307+
308+
// Also register pointer type
309+
ptrUnionType := reflect.PtrTo(unionType)
310+
f.typeResolver.typeToSerializers[ptrUnionType] = &ptrToValueSerializer{
311+
valueSerializer: serializer,
312+
}
313+
314+
return nil
315+
}

0 commit comments

Comments
 (0)