diff --git a/ci/run_ci.sh b/ci/run_ci.sh index 789f78cdad..4c9c0c1420 100755 --- a/ci/run_ci.sh +++ b/ci/run_ci.sh @@ -342,6 +342,10 @@ case $1 in go) echo "Executing fory go tests for go" cd "$ROOT/go/fory" + go install ./cmd/fory + cd "$ROOT/go/fory/tests" + go generate + cd "$ROOT/go/fory" go test -v echo "Executing fory go tests succeeds" ;; diff --git a/go/README.md b/go/README.md index a8f19d0146..d4e9ca894e 100644 --- a/go/README.md +++ b/go/README.md @@ -2,9 +2,149 @@ Fory is a blazingly fast multi-language serialization framework powered by just-in-time compilation and zero-copy. -Currently, Fory Go is implemented using reflection. In the future, we plan to implement a static code generator -to generate serializer code ahead to speed up serialization, or implement a JIT framework which generate ASM -instructions to speed up serialization. +Currently, Fory Go is implemented using reflection. We have also implemented a static code generator to generate serializer code ahead of time to speed up serialization. + +## Fory Go Codegen (optional) + +This repository includes an optional ahead-of-time (AOT) code generator for Fory. The runtime reflection-based path continues to work; codegen exists to provide additional performance, type safety and zero-reflection overhead for hot paths. You can adopt it incrementally, per package or per file. + +### Why codegen (rationale) + +- Faster (no reflection on the hot path) +- Type-safe serialization/deserialization with predictable layouts +- Smaller GC pressure and fewer allocations +- Compile-time guards to detect stale generated code when struct definitions change + +Note: Code generation is not mandatory. If you prefer simple workflows, you can keep using the reflection-based API. + +### Install the generator + +The generator binary is `fory`. + +- Go 1.16+ (recommended): + +```bash +go install github.com/apache/fory/go/fory/cmd/fory@latest +``` + +- Go 1.13+ + +```bash +# Inside a module-enabled environment +GO111MODULE=on go get -u github.com/apache/fory/go/fory/cmd/fory + +# Or clone the repo and install from source +git clone https://github.com/apache/fory.git +cd fory/go/fory +go install ./cmd/fory +``` + +Ensure $GOBIN or $GOPATH/bin is on your PATH so that `fory` is discoverable by `go generate`. + +### Usage: annotate and generate + +1. Mark structs for generation with `//fory:gen`, and add a `go:generate` directive. File-based generation is recommended. + +```go +package yourpkg + +//fory:gen +type User struct { + ID int64 `json:"id"` + Name string `json:"name"` +} + +//go:generate fory -file structs.go +``` + +Then run: + +```bash +go generate +``` + +The generator will create `structs_fory_gen.go` next to your source file and register serializers in init(). + +2. Explicit types (legacy mode) are also supported: + +```bash +fory -pkg ./models -type "User,Order" +``` + +### When to re-run `go generate` + +Re-run generation whenever any of the following change for generated structs: + +- Field additions/removals/renames +- Field type changes or tag changes +- New structs annotated with `//fory:gen` + +Fory adds a compile-time guard in generated files to detect stale code. If you forget to re-generate, your build will fail with a clear message. The generator also includes a smart auto-retry: when invoked via `go generate`, it detects this situation, removes the stale generated file, and retries automatically. You can force this behavior manually with: + +```bash +fory --force -file structs.go +``` + +### What gets generated (simplified example) + +Below is a minimal illustration. Actual output includes strongly-typed serializers, interface-compatible methods, registration, and a compile-time snapshot of your struct. + +```go +// Code generated by fory. DO NOT EDIT. +package yourpkg + +// Snapshot of User's underlying type at generation time. +type _User_expected struct { + ID int64 + Name string +} + +// Compile-time check: fails if User no longer matches the snapshot. +// If this fails, run: go generate +var _ = func(x User) { _ = _User_expected(x) } + +type User_ForyGenSerializer struct{} + +func (User_ForyGenSerializer) WriteTyped(f *fory.Fory, buf *fory.ByteBuffer, v *User) error { + // write fields in a stable order + buf.WriteInt64(v.ID) + fory.WriteString(buf, v.Name) + return nil +} + +func (User_ForyGenSerializer) ReadTyped(f *fory.Fory, buf *fory.ByteBuffer, v *User) error { + v.ID = buf.ReadInt64() + v.Name = fory.ReadString(buf) + return nil +} +``` + +### CI and version control (should I check in generated code?) + +Both models are supported; choose based on your workflow: + +- Check in generated code (recommended for libraries) + - Pros: Consumers can build without the generator; reproducible builds + - Cons: Larger diffs; must remember to re-generate before commit + +- Do not check in; generate in CI/release pipeline (recommended for apps) + - Add a step to your pipeline, e.g.: + - `go generate ./...` + - Optionally `fory --force -file ` for targeted regeneration + +Regardless of your choice, the compile-time guard ensures that stale code is noticed early. If a build fails due to the guard in a local environment, run: + +```bash +go generate +# If needed +fory --force -file +``` + +### FAQ + +- Is codegen required? No. Fory works without it via reflection. +- 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. ## How to test diff --git a/go/fory/cmd/fory/main.go b/go/fory/cmd/fory/main.go new file mode 100644 index 0000000000..b2157a3116 --- /dev/null +++ b/go/fory/cmd/fory/main.go @@ -0,0 +1,164 @@ +// 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 main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/apache/fory/go/fory/codegen" +) + +var ( + typeFlag = flag.String("type", "", "comma-separated list of types to generate code for (optional if using //fory:gen comments)") + pkgFlag = flag.String("pkg", ".", "package directory to search for types (legacy mode)") + fileFlag = flag.String("file", "", "source file to generate code for (new mode)") + forceFlag = flag.Bool("force", false, "force regeneration by removing existing generated files first") + helpFlag = flag.Bool("help", false, "show help message") + versionFlag = flag.Bool("version", false, "show version information") +) + +const version = "1.0.0" + +func main() { + flag.Parse() + + if *helpFlag { + showHelp() + return + } + + if *versionFlag { + fmt.Printf("fory version %s\n", version) + return + } + + // Configure generator options + opts := &codegen.GeneratorOptions{ + TypeList: *typeFlag, + PackageDir: *pkgFlag, + SourceFile: *fileFlag, + Force: *forceFlag, + } + + // Run the code generator with smart error handling + if err := codegen.Run(opts); err != nil { + // Check if this looks like a compile-time guard error + if isCompileGuardError(err.Error()) { + fmt.Fprintf(os.Stderr, "\n🚨 Compile-time guard detected struct changes!\n\n") + fmt.Fprintf(os.Stderr, "It looks like you've modified a struct but haven't regenerated the code.\n") + fmt.Fprintf(os.Stderr, "\nTo fix this, try one of these solutions:\n") + fmt.Fprintf(os.Stderr, " 1. Run with --force flag: fory --force %s\n", getRunArguments()) + fmt.Fprintf(os.Stderr, " 2. Delete generated files and run again:\n") + if *fileFlag != "" { + genFile := getGeneratedFileName(*fileFlag) + fmt.Fprintf(os.Stderr, " rm %s && go generate\n", genFile) + } else { + fmt.Fprintf(os.Stderr, " rm *_fory_gen.go && go generate\n") + } + fmt.Fprintf(os.Stderr, "\nThis protection ensures your generated code stays in sync with struct definitions.\n") + os.Exit(1) + } + + fmt.Fprintf(os.Stderr, "fory: %v\n", err) + os.Exit(1) + } +} + +func showHelp() { + fmt.Printf(`fory - Fast serialization code generator for Go + +Usage: + fory [options] + +Options: + -file string + source file to generate code for (new mode) + -pkg string + package directory to search for types (legacy mode) (default ".") + -type string + comma-separated list of types to generate code for (optional if using //fory:gen comments) + -force + force regeneration by removing existing generated files first + -help + show this help message + -version + show version information + +Examples: + # Generate for specific file using //fory:gen comments + fory -file structs.go + + # Generate for specific types in current package + fory -type "User,Order" + + # Generate for specific types in a directory + fory -pkg ./models -type "User,Order" + +Installation: + go install github.com/apache/fory/go/fory/cmd/fory + +For more information, visit: https://github.com/apache/fory +`) +} + +// isCompileGuardError checks if the error is due to compile-time guard conflicts +func isCompileGuardError(errMsg string) bool { + // Look for patterns indicating compile-time guard failures + patterns := []string{ + "cannot convert x (variable of type", + "to type _", "_expected", + "_expected struct", + } + + errMsgLower := strings.ToLower(errMsg) + for _, pattern := range patterns { + if strings.Contains(errMsgLower, strings.ToLower(pattern)) { + return true + } + } + return false +} + +// getRunArguments reconstructs command line arguments for error messages +func getRunArguments() string { + var args []string + if *fileFlag != "" { + args = append(args, "-file", *fileFlag) + } + if *typeFlag != "" { + args = append(args, "-type", *typeFlag) + } + if *pkgFlag != "." { + args = append(args, "-pkg", *pkgFlag) + } + return strings.Join(args, " ") +} + +// getGeneratedFileName returns the expected generated file name for a source file +func getGeneratedFileName(sourceFile string) string { + if sourceFile == "" { + return "*_fory_gen.go" + } + + base := strings.TrimSuffix(filepath.Base(sourceFile), ".go") + return fmt.Sprintf("%s_fory_gen.go", base) +} diff --git a/go/fory/codegen/decoder.go b/go/fory/codegen/decoder.go new file mode 100644 index 0000000000..345f283c69 --- /dev/null +++ b/go/fory/codegen/decoder.go @@ -0,0 +1,150 @@ +// 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 codegen + +import ( + "bytes" + "fmt" + "go/types" +) + +// generateReadTyped generates the strongly-typed Read method +func generateReadTyped(buf *bytes.Buffer, s *StructInfo) error { + hash := computeStructHash(s) + + fmt.Fprintf(buf, "// ReadTyped provides strongly-typed deserialization with no reflection overhead\n") + fmt.Fprintf(buf, "func (g %s_ForyGenSerializer) ReadTyped(f *fory.Fory, buf *fory.ByteBuffer, v *%s) error {\n", s.Name, s.Name) + + // Read and verify struct hash + fmt.Fprintf(buf, "\t// Read and verify struct hash\n") + fmt.Fprintf(buf, "\tif got := buf.ReadInt32(); got != %d {\n", hash) + fmt.Fprintf(buf, "\t\treturn fmt.Errorf(\"struct hash mismatch for %s: expected %d, got %%d\", got)\n", s.Name, hash) + fmt.Fprintf(buf, "\t}\n\n") + + // Read fields in sorted order + fmt.Fprintf(buf, "\t// Read fields in same order as write\n") + for _, field := range s.Fields { + if err := generateFieldReadTyped(buf, field); err != nil { + return err + } + } + + fmt.Fprintf(buf, "\treturn nil\n") + fmt.Fprintf(buf, "}\n\n") + return nil +} + +// generateReadInterface generates interface compatibility Read method +func generateReadInterface(buf *bytes.Buffer, s *StructInfo) error { + fmt.Fprintf(buf, "// Read provides reflect.Value interface compatibility\n") + fmt.Fprintf(buf, "func (g %s_ForyGenSerializer) Read(f *fory.Fory, buf *fory.ByteBuffer, type_ reflect.Type, value reflect.Value) error {\n", s.Name) + fmt.Fprintf(buf, "\t// Convert reflect.Value to concrete type and delegate to typed method\n") + fmt.Fprintf(buf, "\tvar v *%s\n", s.Name) + fmt.Fprintf(buf, "\tif value.Kind() == reflect.Ptr {\n") + fmt.Fprintf(buf, "\t\tif value.IsNil() {\n") + fmt.Fprintf(buf, "\t\t\t// For pointer types, allocate using type_.Elem()\n") + fmt.Fprintf(buf, "\t\t\tvalue.Set(reflect.New(type_.Elem()))\n") + fmt.Fprintf(buf, "\t\t}\n") + fmt.Fprintf(buf, "\t\tv = value.Interface().(*%s)\n", s.Name) + fmt.Fprintf(buf, "\t} else {\n") + fmt.Fprintf(buf, "\t\t// value must be addressable for read\n") + fmt.Fprintf(buf, "\t\tv = value.Addr().Interface().(*%s)\n", s.Name) + fmt.Fprintf(buf, "\t}\n") + fmt.Fprintf(buf, "\t// Delegate to strongly-typed method for maximum performance\n") + fmt.Fprintf(buf, "\treturn g.ReadTyped(f, buf, v)\n") + fmt.Fprintf(buf, "}\n\n") + return nil +} + +// generateFieldReadTyped generates field reading code for the typed method +func generateFieldReadTyped(buf *bytes.Buffer, field *FieldInfo) error { + fmt.Fprintf(buf, "\t// Field: %s (%s)\n", field.GoName, field.Type.String()) + + fieldAccess := fmt.Sprintf("v.%s", field.GoName) + + // Handle special named types first + if named, ok := field.Type.(*types.Named); ok { + typeStr := named.String() + switch typeStr { + case "time.Time": + fmt.Fprintf(buf, "\tusec := buf.ReadInt64()\n") + fmt.Fprintf(buf, "\t%s = fory.CreateTimeFromUnixMicro(usec)\n", fieldAccess) + return nil + case "github.com/apache/fory/go/fory.Date": + fmt.Fprintf(buf, "\tdays := buf.ReadInt32()\n") + fmt.Fprintf(buf, "\t// Handle zero date marker\n") + fmt.Fprintf(buf, "\tif days == int32(-2147483648) {\n") + fmt.Fprintf(buf, "\t\t%s = fory.Date{Year: 0, Month: 0, Day: 0}\n", fieldAccess) + fmt.Fprintf(buf, "\t} else {\n") + fmt.Fprintf(buf, "\t\tdiff := time.Duration(days) * 24 * time.Hour\n") + fmt.Fprintf(buf, "\t\tt := time.Date(1970, 1, 1, 0, 0, 0, 0, time.Local).Add(diff)\n") + fmt.Fprintf(buf, "\t\t%s = fory.Date{Year: t.Year(), Month: t.Month(), Day: t.Day()}\n", fieldAccess) + fmt.Fprintf(buf, "\t}\n") + return nil + } + } + + // Handle pointer types + if _, ok := field.Type.(*types.Pointer); ok { + // For pointer types, use ReadReferencable + fmt.Fprintf(buf, "\tf.ReadReferencable(buf, reflect.ValueOf(&%s).Elem())\n", fieldAccess) + return nil + } + + // Handle basic types + if basic, ok := field.Type.Underlying().(*types.Basic); ok { + switch basic.Kind() { + case types.Bool: + fmt.Fprintf(buf, "\t%s = buf.ReadBool()\n", fieldAccess) + case types.Int8: + fmt.Fprintf(buf, "\t%s = buf.ReadInt8()\n", fieldAccess) + case types.Int16: + fmt.Fprintf(buf, "\t%s = buf.ReadInt16()\n", fieldAccess) + case types.Int32: + fmt.Fprintf(buf, "\t%s = buf.ReadInt32()\n", fieldAccess) + case types.Int, types.Int64: + fmt.Fprintf(buf, "\t%s = buf.ReadInt64()\n", fieldAccess) + case types.Uint8: + fmt.Fprintf(buf, "\t%s = buf.ReadByte_()\n", fieldAccess) + case types.Uint16: + fmt.Fprintf(buf, "\t%s = uint16(buf.ReadInt16())\n", fieldAccess) + case types.Uint32: + fmt.Fprintf(buf, "\t%s = uint32(buf.ReadInt32())\n", fieldAccess) + case types.Uint, types.Uint64: + fmt.Fprintf(buf, "\t%s = uint64(buf.ReadInt64())\n", fieldAccess) + case types.Float32: + fmt.Fprintf(buf, "\t%s = buf.ReadFloat32()\n", fieldAccess) + case types.Float64: + fmt.Fprintf(buf, "\t%s = buf.ReadFloat64()\n", fieldAccess) + case types.String: + fmt.Fprintf(buf, "\t%s = fory.ReadString(buf)\n", fieldAccess) + default: + fmt.Fprintf(buf, "\t// TODO: unsupported basic type %s\n", basic.String()) + } + return nil + } + + // Handle struct types + if _, ok := field.Type.Underlying().(*types.Struct); ok { + fmt.Fprintf(buf, "\tf.ReadReferencable(buf, reflect.ValueOf(&%s).Elem())\n", fieldAccess) + return nil + } + + fmt.Fprintf(buf, "\t// TODO: unsupported type %s\n", field.Type.String()) + return nil +} diff --git a/go/fory/codegen/encoder.go b/go/fory/codegen/encoder.go new file mode 100644 index 0000000000..72a5e94cd2 --- /dev/null +++ b/go/fory/codegen/encoder.go @@ -0,0 +1,142 @@ +// 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 codegen + +import ( + "bytes" + "fmt" + "go/types" +) + +// generateWriteTyped generates the strongly-typed Write method +func generateWriteTyped(buf *bytes.Buffer, s *StructInfo) error { + hash := computeStructHash(s) + + fmt.Fprintf(buf, "// WriteTyped provides strongly-typed serialization with no reflection overhead\n") + fmt.Fprintf(buf, "func (g %s_ForyGenSerializer) WriteTyped(f *fory.Fory, buf *fory.ByteBuffer, v *%s) error {\n", s.Name, s.Name) + + // Write struct hash + fmt.Fprintf(buf, "\t// Write precomputed struct hash for compatibility checking\n") + fmt.Fprintf(buf, "\tbuf.WriteInt32(%d) // hash of %s structure\n\n", hash, s.Name) + + // Write fields in sorted order + fmt.Fprintf(buf, "\t// Write fields in sorted order\n") + for _, field := range s.Fields { + if err := generateFieldWriteTyped(buf, field); err != nil { + return err + } + } + + fmt.Fprintf(buf, "\treturn nil\n") + fmt.Fprintf(buf, "}\n\n") + return nil +} + +// generateWriteInterface generates interface compatibility Write method +func generateWriteInterface(buf *bytes.Buffer, s *StructInfo) error { + fmt.Fprintf(buf, "// Write provides reflect.Value interface compatibility\n") + fmt.Fprintf(buf, "func (g %s_ForyGenSerializer) Write(f *fory.Fory, buf *fory.ByteBuffer, value reflect.Value) error {\n", s.Name) + fmt.Fprintf(buf, "\t// Convert reflect.Value to concrete type and delegate to typed method\n") + fmt.Fprintf(buf, "\tvar v *%s\n", s.Name) + fmt.Fprintf(buf, "\tif value.Kind() == reflect.Ptr {\n") + fmt.Fprintf(buf, "\t\tv = value.Interface().(*%s)\n", s.Name) + fmt.Fprintf(buf, "\t} else {\n") + fmt.Fprintf(buf, "\t\t// Create a copy to get a pointer\n") + fmt.Fprintf(buf, "\t\ttemp := value.Interface().(%s)\n", s.Name) + fmt.Fprintf(buf, "\t\tv = &temp\n") + fmt.Fprintf(buf, "\t}\n") + fmt.Fprintf(buf, "\t// Delegate to strongly-typed method for maximum performance\n") + fmt.Fprintf(buf, "\treturn g.WriteTyped(f, buf, v)\n") + fmt.Fprintf(buf, "}\n\n") + return nil +} + +// generateFieldWriteTyped generates field writing code for the typed method +func generateFieldWriteTyped(buf *bytes.Buffer, field *FieldInfo) error { + fmt.Fprintf(buf, "\t// Field: %s (%s)\n", field.GoName, field.Type.String()) + + fieldAccess := fmt.Sprintf("v.%s", field.GoName) + + // Handle special named types first + if named, ok := field.Type.(*types.Named); ok { + typeStr := named.String() + switch typeStr { + case "time.Time": + fmt.Fprintf(buf, "\tbuf.WriteInt64(fory.GetUnixMicro(%s))\n", fieldAccess) + return nil + case "github.com/apache/fory/go/fory.Date": + fmt.Fprintf(buf, "\t// Handle zero date specially\n") + fmt.Fprintf(buf, "\tif %s.Year == 0 && %s.Month == 0 && %s.Day == 0 {\n", fieldAccess, fieldAccess, fieldAccess) + fmt.Fprintf(buf, "\t\tbuf.WriteInt32(int32(-2147483648)) // Special marker for zero date\n") + fmt.Fprintf(buf, "\t} else {\n") + fmt.Fprintf(buf, "\t\tdiff := time.Date(%s.Year, %s.Month, %s.Day, 0, 0, 0, 0, time.Local).Sub(time.Date(1970, 1, 1, 0, 0, 0, 0, time.Local))\n", fieldAccess, fieldAccess, fieldAccess) + fmt.Fprintf(buf, "\t\tbuf.WriteInt32(int32(diff.Hours() / 24))\n") + fmt.Fprintf(buf, "\t}\n") + return nil + } + } + + // Handle pointer types + if _, ok := field.Type.(*types.Pointer); ok { + // For all pointer types, use WriteReferencable + fmt.Fprintf(buf, "\tf.WriteReferencable(buf, reflect.ValueOf(%s))\n", fieldAccess) + return nil + } + + // Handle basic types + if basic, ok := field.Type.Underlying().(*types.Basic); ok { + switch basic.Kind() { + case types.Bool: + fmt.Fprintf(buf, "\tbuf.WriteBool(%s)\n", fieldAccess) + case types.Int8: + fmt.Fprintf(buf, "\tbuf.WriteInt8(%s)\n", fieldAccess) + case types.Int16: + fmt.Fprintf(buf, "\tbuf.WriteInt16(%s)\n", fieldAccess) + case types.Int32: + fmt.Fprintf(buf, "\tbuf.WriteInt32(%s)\n", fieldAccess) + case types.Int, types.Int64: + fmt.Fprintf(buf, "\tbuf.WriteInt64(%s)\n", fieldAccess) + case types.Uint8: + fmt.Fprintf(buf, "\tbuf.WriteByte_(%s)\n", fieldAccess) + case types.Uint16: + fmt.Fprintf(buf, "\tbuf.WriteInt16(int16(%s))\n", fieldAccess) + case types.Uint32: + fmt.Fprintf(buf, "\tbuf.WriteInt32(int32(%s))\n", fieldAccess) + case types.Uint, types.Uint64: + fmt.Fprintf(buf, "\tbuf.WriteInt64(int64(%s))\n", fieldAccess) + case types.Float32: + fmt.Fprintf(buf, "\tbuf.WriteFloat32(%s)\n", fieldAccess) + case types.Float64: + fmt.Fprintf(buf, "\tbuf.WriteFloat64(%s)\n", fieldAccess) + case types.String: + fmt.Fprintf(buf, "\tfory.WriteString(buf, %s)\n", fieldAccess) + default: + fmt.Fprintf(buf, "\t// TODO: unsupported basic type %s\n", basic.String()) + } + return nil + } + + // Handle struct types + if _, ok := field.Type.Underlying().(*types.Struct); ok { + fmt.Fprintf(buf, "\tf.WriteReferencable(buf, reflect.ValueOf(%s))\n", fieldAccess) + return nil + } + + fmt.Fprintf(buf, "\t// TODO: unsupported type %s\n", field.Type.String()) + return nil +} diff --git a/go/fory/codegen/generator.go b/go/fory/codegen/generator.go new file mode 100644 index 0000000000..a2c236685a --- /dev/null +++ b/go/fory/codegen/generator.go @@ -0,0 +1,495 @@ +// 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 codegen + +import ( + "bytes" + "fmt" + "go/format" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "time" + + "golang.org/x/tools/go/packages" +) + +// GeneratorOptions contains configuration for the code generator +type GeneratorOptions struct { + TypeList string // comma-separated list of types to generate code for + PackageDir string // package directory to search for types + SourceFile string // source file to generate code for (new mode) + Force bool // force regeneration by removing existing files first +} + +// Run executes the code generator with the given options +func Run(opts *GeneratorOptions) error { + // If force flag is set, clean up existing files first + if opts.Force { + log.Printf("Force flag detected, cleaning up existing generated files...") + if cleanupErr := cleanupGeneratedFiles(opts); cleanupErr != nil { + log.Printf("Warning: Failed to cleanup generated files: %v", cleanupErr) + } + } + + err := run(opts) + + // Check if the error is due to compile-time guard conflicts + if err != nil && isCompileGuardError(err.Error()) { + log.Printf("Detected compile-time guard conflict. Attempting to regenerate...") + + // Try to clean up and regenerate + if cleanupErr := cleanupGeneratedFiles(opts); cleanupErr != nil { + log.Printf("Warning: Failed to cleanup generated files: %v", cleanupErr) + } + + // Retry generation + log.Printf("Retrying code generation...") + return run(opts) + } + + return err +} + +func run(opts *GeneratorOptions) error { + // Determine mode: file-based or package-based + if opts.SourceFile != "" { + return runFileMode(opts) + } + return runPackageMode(opts) +} + +func runFileMode(opts *GeneratorOptions) error { + // Load packages including the specific file + cfg := &packages.Config{ + Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedName | packages.NeedFiles | packages.NeedTypesInfo, + } + + // Load the directory containing the file + dir := filepath.Dir(opts.SourceFile) + if dir == "" { + dir = "." + } + + pkgs, err := packages.Load(cfg, dir) + if err != nil { + return fmt.Errorf("loading packages: %w", err) + } + + if len(pkgs) == 0 { + return fmt.Errorf("no packages found") + } + + if packages.PrintErrors(pkgs) > 0 { + // Check if any errors are compile-time guard related + var allErrors []string + for _, pkg := range pkgs { + for _, err := range pkg.Errors { + allErrors = append(allErrors, err.Error()) + } + } + errorMsg := strings.Join(allErrors, "; ") + + // If this looks like a compile-time guard error, provide better context + if isCompileGuardError(errorMsg) { + return fmt.Errorf("compile-time guard detected struct changes: %s", errorMsg) + } + + return fmt.Errorf("errors in packages") + } + + // Process only the specified file + for _, pkg := range pkgs { + if err := processPackageFile(pkg, opts.SourceFile, opts.TypeList); err != nil { + return fmt.Errorf("processing file %s: %w", opts.SourceFile, err) + } + } + + return nil +} + +func runPackageMode(opts *GeneratorOptions) error { + // Legacy package-based mode + cfg := &packages.Config{ + Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedName | packages.NeedFiles | packages.NeedTypesInfo, + } + + pkgs, err := packages.Load(cfg, opts.PackageDir) + if err != nil { + return fmt.Errorf("loading packages: %w", err) + } + + if len(pkgs) == 0 { + return fmt.Errorf("no packages found") + } + + if packages.PrintErrors(pkgs) > 0 { + // Check if any errors are compile-time guard related + var allErrors []string + for _, pkg := range pkgs { + for _, err := range pkg.Errors { + allErrors = append(allErrors, err.Error()) + } + } + errorMsg := strings.Join(allErrors, "; ") + + // If this looks like a compile-time guard error, provide better context + if isCompileGuardError(errorMsg) { + return fmt.Errorf("compile-time guard detected struct changes: %s", errorMsg) + } + + return fmt.Errorf("errors in packages") + } + + // Process each package (legacy behavior) + for _, pkg := range pkgs { + if err := processPackage(pkg, opts.TypeList); err != nil { + return fmt.Errorf("processing package %s: %w", pkg.PkgPath, err) + } + } + + return nil +} + +func processPackageFile(pkg *packages.Package, sourceFile string, typeList string) error { + // Convert to absolute path for comparison + absSourceFile, err := filepath.Abs(sourceFile) + if err != nil { + return fmt.Errorf("getting absolute path for %s: %w", sourceFile, err) + } + + // Find target types from the specific file + var targetTypes []string + + // If type list is provided, use it + if typeList != "" { + targetTypes = strings.Split(typeList, ",") + } else { + // Auto-discover types with //fory:gen comments + discoveredTypes, err := discoverTypesFromFile(pkg, absSourceFile) + if err != nil { + return fmt.Errorf("discovering types from file: %w", err) + } + targetTypes = discoveredTypes + } + + if len(targetTypes) == 0 { + fmt.Printf("No types found to generate in %s\n", sourceFile) + return nil + } + + // Also check if there are any compilation errors + if len(pkg.Errors) > 0 { + for _, err := range pkg.Errors { + log.Printf("package error: %s", err) + } + } + + // Parse structs from package + structs, err := parseStructsFromPackage(pkg, targetTypes) + if err != nil { + return fmt.Errorf("parsing structs from package: %w", err) + } + + if len(structs) == 0 { + if len(targetTypes) > 0 { + scope := pkg.Types.Scope() + allNames := scope.Names() + log.Printf("Warning: No matching structs found for target types: %v", targetTypes) + log.Printf("Available types in package: %v", allNames) + return fmt.Errorf("no matching structs found for target types: %v", targetTypes) + } + log.Printf("No structs to generate (no target types specified)") + return nil + } + + // Generate code with file-based naming + log.Printf("Generating code for %d struct(s) from %s: %v", len(structs), sourceFile, getStructNames(structs)) + if err := generateCodeForFile(pkg, structs, sourceFile); err != nil { + return err + } + log.Printf("Successfully generated code for %s", sourceFile) + return nil +} + +func processPackage(pkg *packages.Package, typeList string) error { + // Find structs to generate code for + var targetTypes []string + if typeList != "" { + targetTypes = strings.Split(typeList, ",") + } + + // Also check if there are any compilation errors + if len(pkg.Errors) > 0 { + for _, err := range pkg.Errors { + log.Printf("package error: %s", err) + } + } + + // Parse structs from package + structs, err := parseStructsFromPackage(pkg, targetTypes) + if err != nil { + return fmt.Errorf("parsing structs from package: %w", err) + } + + if len(structs) == 0 { + if len(targetTypes) > 0 { + scope := pkg.Types.Scope() + allNames := scope.Names() + log.Printf("Warning: No matching structs found for target types: %v", targetTypes) + log.Printf("Available types in package: %v", allNames) + return fmt.Errorf("no matching structs found for target types: %v", targetTypes) + } + log.Printf("No structs to generate (no target types specified)") + return nil + } + + // Generate code (legacy package mode) + log.Printf("Generating code for %d struct(s): %v", len(structs), getStructNames(structs)) + if err := generateCode(pkg, structs); err != nil { + return err + } + log.Printf("Successfully generated code for package %s", pkg.Name) + return nil +} + +// generateCodeForFile generates code with file-based naming +func generateCodeForFile(pkg *packages.Package, structs []*StructInfo, sourceFile string) error { + var buf bytes.Buffer + + // Generate file header + fmt.Fprintf(&buf, "// Code generated by forygen. DO NOT EDIT.\n") + fmt.Fprintf(&buf, "// source: %s\n", sourceFile) + fmt.Fprintf(&buf, "// generated at: %s\n\n", time.Now().Format(time.RFC3339)) + fmt.Fprintf(&buf, "package %s\n\n", pkg.Name) + + // Determine which imports are needed + needsTime := false + needsReflect := false + + for _, s := range structs { + for _, field := range s.Fields { + typeStr := field.Type.String() + if typeStr == "time.Time" || typeStr == "github.com/apache/fory/go/fory.Date" { + needsTime = true + } + // We need reflect for the interface compatibility methods + needsReflect = true + } + } + + // Generate imports + fmt.Fprintf(&buf, "import (\n") + fmt.Fprintf(&buf, "\t\"fmt\"\n") + if needsReflect { + fmt.Fprintf(&buf, "\t\"reflect\"\n") + } + if needsTime { + fmt.Fprintf(&buf, "\t\"time\"\n") + } + fmt.Fprintf(&buf, "\t\"github.com/apache/fory/go/fory\"\n") + fmt.Fprintf(&buf, ")\n\n") + + // Generate init function to register serializers + fmt.Fprintf(&buf, "func init() {\n") + for _, s := range structs { + fmt.Fprintf(&buf, "\tfory.RegisterGeneratedSerializer((*%s)(nil), %s_ForyGenSerializer{})\n", s.Name, s.Name) + } + fmt.Fprintf(&buf, "}\n\n") + + // Generate serializers for each struct + for _, s := range structs { + if err := generateStructSerializer(&buf, s); err != nil { + return fmt.Errorf("generating serializer for %s: %w", s.Name, err) + } + } + + // Generate compile-time guards to ensure struct definitions haven't changed + structInfos := convertStructInfos(structs) + guardCode := generateCompileGuard(structInfos) + if guardCode != "" { + buf.WriteString(guardCode) + } + + // Format the generated code + formatted, err := format.Source(buf.Bytes()) + if err != nil { + return fmt.Errorf("formatting generated code: %w", err) + } + + // Create output filename based on source file: filename_fory_gen.go + base := strings.TrimSuffix(filepath.Base(sourceFile), ".go") + outputFile := filepath.Join(filepath.Dir(sourceFile), fmt.Sprintf("%s_fory_gen.go", base)) + + return ioutil.WriteFile(outputFile, formatted, 0644) +} + +// convertStructInfos converts []*StructInfo to []StructInfo +func convertStructInfos(structs []*StructInfo) []StructInfo { + result := make([]StructInfo, len(structs)) + for i, s := range structs { + result[i] = *s + } + return result +} + +// isCompileGuardError checks if the error is due to compile-time guard conflicts +func isCompileGuardError(errMsg string) bool { + // Look for patterns indicating compile-time guard failures + patterns := []string{ + "cannot convert x (variable of type", + "to type _", "_expected", + "_expected struct", + } + + errMsgLower := strings.ToLower(errMsg) + for _, pattern := range patterns { + if strings.Contains(errMsgLower, strings.ToLower(pattern)) { + return true + } + } + return false +} + +// cleanupGeneratedFiles removes generated files to allow regeneration +func cleanupGeneratedFiles(opts *GeneratorOptions) error { + if opts.SourceFile != "" { + // File-based mode: remove filename_fory_gen.go + base := strings.TrimSuffix(filepath.Base(opts.SourceFile), ".go") + genFile := filepath.Join(filepath.Dir(opts.SourceFile), fmt.Sprintf("%s_fory_gen.go", base)) + + if _, err := os.Stat(genFile); err == nil { + log.Printf("Removing generated file: %s", genFile) + return os.Remove(genFile) + } + } else { + // Package-based mode: need to load package to find generated file + // This is more complex and might need package analysis + log.Printf("Package-based cleanup not yet implemented") + } + + return nil +} + +// generateStructSerializer generates a complete serializer for a struct +func generateStructSerializer(buf *bytes.Buffer, s *StructInfo) error { + // Generate struct serializer type + fmt.Fprintf(buf, "type %s_ForyGenSerializer struct {}\n\n", s.Name) + + // Generate TypeId method + fmt.Fprintf(buf, "func (%s_ForyGenSerializer) TypeId() fory.TypeId {\n", s.Name) + fmt.Fprintf(buf, "\treturn fory.NAMED_STRUCT\n") + fmt.Fprintf(buf, "}\n\n") + + // Generate NeedWriteRef method + fmt.Fprintf(buf, "func (%s_ForyGenSerializer) NeedWriteRef() bool {\n", s.Name) + fmt.Fprintf(buf, "\treturn true\n") + fmt.Fprintf(buf, "}\n\n") + + // Generate strongly-typed Write method (delegate to encoder) + if err := generateWriteTyped(buf, s); err != nil { + return err + } + + // Generate strongly-typed Read method (delegate to decoder) + if err := generateReadTyped(buf, s); err != nil { + return err + } + + // Generate interface compatibility methods (delegate to encoder/decoder) + if err := generateWriteInterface(buf, s); err != nil { + return err + } + + if err := generateReadInterface(buf, s); err != nil { + return err + } + + return nil +} + +// generateCode generates code with package-based naming (legacy mode) +func generateCode(pkg *packages.Package, structs []*StructInfo) error { + var buf bytes.Buffer + + // Generate file header + fmt.Fprintf(&buf, "// Code generated by forygen. DO NOT EDIT.\n") + fmt.Fprintf(&buf, "// source: %s\n", pkg.PkgPath) + fmt.Fprintf(&buf, "// generated at: %s\n\n", time.Now().Format(time.RFC3339)) + fmt.Fprintf(&buf, "package %s\n\n", pkg.Name) + + // Determine which imports are needed + needsTime := false + needsReflect := false + + for _, s := range structs { + for _, field := range s.Fields { + typeStr := field.Type.String() + if typeStr == "time.Time" || typeStr == "github.com/apache/fory/go/fory.Date" { + needsTime = true + } + // We need reflect for the interface compatibility methods + needsReflect = true + } + } + + // Generate imports + fmt.Fprintf(&buf, "import (\n") + fmt.Fprintf(&buf, "\t\"fmt\"\n") + if needsReflect { + fmt.Fprintf(&buf, "\t\"reflect\"\n") + } + if needsTime { + fmt.Fprintf(&buf, "\t\"time\"\n") + } + fmt.Fprintf(&buf, "\t\"github.com/apache/fory/go/fory\"\n") + fmt.Fprintf(&buf, ")\n\n") + + // Generate init function to register serializers + fmt.Fprintf(&buf, "func init() {\n") + for _, s := range structs { + fmt.Fprintf(&buf, "\tfory.RegisterGeneratedSerializer((*%s)(nil), %s_ForyGenSerializer{})\n", s.Name, s.Name) + } + fmt.Fprintf(&buf, "}\n\n") + + // Generate serializers for each struct + for _, s := range structs { + if err := generateStructSerializer(&buf, s); err != nil { + return fmt.Errorf("generating serializer for %s: %w", s.Name, err) + } + } + + // Generate compile-time guards to ensure struct definitions haven't changed + structInfos := convertStructInfos(structs) + guardCode := generateCompileGuard(structInfos) + if guardCode != "" { + buf.WriteString(guardCode) + } + + // Format the generated code + formatted, err := format.Source(buf.Bytes()) + if err != nil { + return fmt.Errorf("formatting generated code: %w", err) + } + + // Write to output file (legacy package-based naming) + outputFile := filepath.Join(filepath.Dir(pkg.GoFiles[0]), fmt.Sprintf("%s_fory_gen.go", pkg.Name)) + return ioutil.WriteFile(outputFile, formatted, 0644) +} diff --git a/go/fory/codegen/guard.go b/go/fory/codegen/guard.go new file mode 100644 index 0000000000..c8d58c3e1a --- /dev/null +++ b/go/fory/codegen/guard.go @@ -0,0 +1,183 @@ +// 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 codegen + +import ( + "bytes" + "fmt" + "go/types" + "sort" + "strings" +) + +// generateCompileGuard generates compile-time checks to ensure struct definitions +// haven't changed since code generation. If a struct is modified, users must +// re-run go generate or compilation will fail. +func generateCompileGuard(structs []StructInfo) string { + if len(structs) == 0 { + return "" + } + + var buf bytes.Buffer + + buf.WriteString("\n// Compile-time guards: These ensure struct definitions haven't changed\n") + buf.WriteString("// since code generation. If you modify structs, re-run go generate.\n\n") + + for _, structInfo := range structs { + generateStructGuard(&buf, structInfo) + } + + return buf.String() +} + +func generateStructGuard(buf *bytes.Buffer, structInfo StructInfo) { + typeName := structInfo.Name + expectedTypeName := fmt.Sprintf("_%s_expected", typeName) + + // Generate the snapshot struct + buf.WriteString(fmt.Sprintf("// Snapshot of %s's underlying type at generation time.\n", typeName)) + buf.WriteString(fmt.Sprintf("type %s struct {\n", expectedTypeName)) + + // Sort fields to ensure consistent ordering (using pointers) + fields := make([]*FieldInfo, len(structInfo.Fields)) + copy(fields, structInfo.Fields) + sort.Slice(fields, func(i, j int) bool { + return fields[i].GoName < fields[j].GoName + }) + + for _, field := range fields { + buf.WriteString(fmt.Sprintf("\t%s %s", field.GoName, formatFieldType(*field))) + + // Add struct tag if present (we'll extract it from the original struct) + // For now, skip tags - they would require access to the original AST + + buf.WriteString("\n") + } + + buf.WriteString("}\n\n") + + // Generate the compile-time check function with better error message + buf.WriteString(fmt.Sprintf("// Compile-time check: this conversion is legal only if %s's underlying type\n", typeName)) + buf.WriteString(fmt.Sprintf("// is identical to %s (names, order, types, tags).\n", expectedTypeName)) + buf.WriteString(fmt.Sprintf("//\n")) + buf.WriteString(fmt.Sprintf("// If compilation fails here, it means you've modified the %s struct but haven't\n", typeName)) + buf.WriteString(fmt.Sprintf("// regenerated the code. Please run: go generate\n")) + buf.WriteString(fmt.Sprintf("//\n")) + buf.WriteString(fmt.Sprintf("// If go generate also fails, delete this file first: rm %s_fory_gen.go\n", strings.ToLower(typeName))) + buf.WriteString(fmt.Sprintf("// Then run: go generate\n")) + buf.WriteString(fmt.Sprintf("var _ = func(x %s) {\n", typeName)) + buf.WriteString(fmt.Sprintf("\t// ERROR: %s struct has changed! Run 'go generate' to fix this.\n", typeName)) + buf.WriteString(fmt.Sprintf("\t_ = %s(x)\n", expectedTypeName)) + buf.WriteString("}\n\n") +} + +func formatFieldType(field FieldInfo) string { + return formatGoType(field.Type) +} + +// formatGoType converts a Go type to its string representation +func formatGoType(t types.Type) string { + switch typ := t.(type) { + case *types.Basic: + return typ.Name() + case *types.Pointer: + return "*" + formatGoType(typ.Elem()) + case *types.Array: + return fmt.Sprintf("[%d]%s", typ.Len(), formatGoType(typ.Elem())) + case *types.Slice: + return "[]" + formatGoType(typ.Elem()) + case *types.Map: + return fmt.Sprintf("map[%s]%s", formatGoType(typ.Key()), formatGoType(typ.Elem())) + case *types.Chan: + dir := "" + switch typ.Dir() { + case types.SendOnly: + dir = "chan<- " + case types.RecvOnly: + dir = "<-chan " + default: + dir = "chan " + } + return dir + formatGoType(typ.Elem()) + case *types.Named: + // Handle named types like custom structs, interfaces, etc. + obj := typ.Obj() + if obj.Pkg() != nil && obj.Pkg().Name() != "" { + return obj.Pkg().Name() + "." + obj.Name() + } + return obj.Name() + case *types.Interface: + if typ.Empty() { + return "interface{}" + } + // For non-empty interfaces, we need to format method signatures + var methods []string + for i := 0; i < typ.NumMethods(); i++ { + method := typ.Method(i) + sig := method.Type().(*types.Signature) + methods = append(methods, formatMethodSignature(method.Name(), sig)) + } + return fmt.Sprintf("interface { %s }", strings.Join(methods, "; ")) + case *types.Struct: + // This shouldn't happen in field types typically, but handle it + return "struct{...}" + default: + // Fallback to the type's string representation + return t.String() + } +} + +func formatMethodSignature(name string, sig *types.Signature) string { + var params, results []string + + // Format parameters + if sig.Params() != nil { + for i := 0; i < sig.Params().Len(); i++ { + param := sig.Params().At(i) + paramStr := formatGoType(param.Type()) + if param.Name() != "" { + paramStr = param.Name() + " " + paramStr + } + params = append(params, paramStr) + } + } + + // Format results + if sig.Results() != nil { + for i := 0; i < sig.Results().Len(); i++ { + result := sig.Results().At(i) + resultStr := formatGoType(result.Type()) + if result.Name() != "" { + resultStr = result.Name() + " " + resultStr + } + results = append(results, resultStr) + } + } + + paramStr := strings.Join(params, ", ") + resultStr := strings.Join(results, ", ") + + if len(results) > 1 { + resultStr = "(" + resultStr + ")" + } + + if resultStr != "" { + return fmt.Sprintf("%s(%s) %s", name, paramStr, resultStr) + } + return fmt.Sprintf("%s(%s)", name, paramStr) +} diff --git a/go/fory/codegen/parser.go b/go/fory/codegen/parser.go new file mode 100644 index 0000000000..a8dad06fa8 --- /dev/null +++ b/go/fory/codegen/parser.go @@ -0,0 +1,170 @@ +// 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 codegen + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "path/filepath" + "strings" + + "golang.org/x/tools/go/packages" +) + +// discoverTypesFromFile scans the source file for //fory:gen comments +func discoverTypesFromFile(pkg *packages.Package, sourceFile string) ([]string, error) { + var discoveredTypes []string + + // Find the syntax tree for the specific file + for _, file := range pkg.Syntax { + // Get the file's position and convert to absolute path for comparison + filename := pkg.Fset.Position(file.Pos()).Filename + absFilename, err := filepath.Abs(filename) + if err != nil { + continue + } + + if absFilename != sourceFile { + continue + } + + // Scan for type declarations with //fory:gen comments + for _, decl := range file.Decls { + if genDecl, ok := decl.(*ast.GenDecl); ok && genDecl.Tok == token.TYPE { + for _, spec := range genDecl.Specs { + if typeSpec, ok := spec.(*ast.TypeSpec); ok { + // Check if it's a struct type + if _, ok := typeSpec.Type.(*ast.StructType); ok { + // Look for //fory:gen comment + if hasGenerateComment(genDecl.Doc) || hasGenerateComment(typeSpec.Doc) { + discoveredTypes = append(discoveredTypes, typeSpec.Name.Name) + } + } + } + } + } + } + } + + return discoveredTypes, nil +} + +// hasGenerateComment checks if comment group contains //fory:gen +func hasGenerateComment(commentGroup *ast.CommentGroup) bool { + if commentGroup == nil { + return false + } + + for _, comment := range commentGroup.List { + if strings.Contains(comment.Text, "fory:gen") { + return true + } + } + return false +} + +// extractStructInfo extracts metadata from a struct type +func extractStructInfo(name string, structType *types.Struct) (*StructInfo, error) { + var fields []*FieldInfo + + for i := 0; i < structType.NumFields(); i++ { + field := structType.Field(i) + if !field.Exported() { + continue // Skip unexported fields + } + + fieldInfo, err := analyzeField(field, i) + if err != nil { + return nil, fmt.Errorf("analyzing field %s: %w", field.Name(), err) + } + + if fieldInfo == nil { + continue // Skip unsupported fields + } + + fields = append(fields, fieldInfo) + } + + // Sort fields according to Fory protocol + sortFields(fields) + + return &StructInfo{ + Name: name, + Fields: fields, + }, nil +} + +// parseStructsFromPackage finds and parses structs from a package +func parseStructsFromPackage(pkg *packages.Package, targetTypes []string) ([]*StructInfo, error) { + var structs []*StructInfo + + // Check if package has types + if pkg.Types == nil { + return nil, fmt.Errorf("package %s has no type information", pkg.PkgPath) + } + + // Iterate through all types in the package + scope := pkg.Types.Scope() + allNames := scope.Names() + + for _, name := range allNames { + obj := scope.Lookup(name) + if obj == nil { + continue + } + + // Check if it's a named type + named, ok := obj.Type().(*types.Named) + if !ok { + continue + } + + // Check if underlying type is struct + structType, ok := named.Underlying().(*types.Struct) + if !ok { + continue + } + + // Check if we should generate code for this type + shouldGenerate := false + if len(targetTypes) > 0 { + for _, t := range targetTypes { + if strings.TrimSpace(t) == name { + shouldGenerate = true + break + } + } + } + + if !shouldGenerate { + continue + } + + // Extract struct information + structInfo, err := extractStructInfo(name, structType) + if err != nil { + return nil, fmt.Errorf("extracting struct info for %s: %w", name, err) + } + + structs = append(structs, structInfo) + } + + return structs, nil +} diff --git a/go/fory/codegen/utils.go b/go/fory/codegen/utils.go new file mode 100644 index 0000000000..00dde64249 --- /dev/null +++ b/go/fory/codegen/utils.go @@ -0,0 +1,324 @@ +// 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 codegen + +import ( + "crypto/md5" + "encoding/binary" + "go/types" + "sort" + "unicode" +) + +// FieldInfo contains metadata about a struct field +type FieldInfo struct { + GoName string // Original Go field name + SnakeName string // snake_case field name for sorting + Type types.Type // Go type information + Index int // Original field index in struct + IsPrimitive bool // Whether it's a Fory primitive type + IsPointer bool // Whether it's a pointer type + TypeID string // Fory TypeID for sorting + PrimitiveSize int // Size for primitive type sorting +} + +// StructInfo contains metadata about a struct to generate code for +type StructInfo struct { + Name string + Fields []*FieldInfo +} + +// toSnakeCase converts CamelCase to snake_case +func toSnakeCase(s string) string { + var result []rune + for i, r := range s { + if i > 0 && unicode.IsUpper(r) { + result = append(result, '_') + } + result = append(result, unicode.ToLower(r)) + } + return string(result) +} + +// isSupportedFieldType checks if a field type is supported +func isSupportedFieldType(t types.Type) bool { + // Handle pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + // Check named types + if named, ok := t.(*types.Named); ok { + typeStr := named.String() + switch typeStr { + case "time.Time", "github.com/apache/fory/go/fory.Date": + return true + } + // Check if it's another struct + if _, ok := named.Underlying().(*types.Struct); ok { + return true + } + } + + // Check basic types + if basic, ok := t.Underlying().(*types.Basic); ok { + switch basic.Kind() { + case types.Bool, types.Int8, types.Int16, types.Int32, types.Int, types.Int64, + types.Uint8, types.Uint16, types.Uint32, types.Uint, types.Uint64, + types.Float32, types.Float64, types.String: + return true + } + } + + return false +} + +// isPrimitiveType checks if a type is considered primitive in Fory +func isPrimitiveType(t types.Type) bool { + // Handle pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + // Check basic types + if basic, ok := t.Underlying().(*types.Basic); ok { + switch basic.Kind() { + case types.Bool, types.Int8, types.Int16, types.Int32, types.Int, types.Int64, + types.Uint8, types.Uint16, types.Uint32, types.Uint, types.Uint64, + types.Float32, types.Float64: + return true + } + } + + // String is also considered primitive in Fory context but nullable + if basic, ok := t.Underlying().(*types.Basic); ok && basic.Kind() == types.String { + return true + } + + return false +} + +// getTypeID returns the Fory TypeID for a given type +func getTypeID(t types.Type) string { + // Handle pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + // Check named types first + if named, ok := t.(*types.Named); ok { + typeStr := named.String() + switch typeStr { + case "time.Time": + return "TIMESTAMP" + case "github.com/apache/fory/go/fory.Date": + return "LOCAL_DATE" + } + // Struct types + if _, ok := named.Underlying().(*types.Struct); ok { + return "NAMED_STRUCT" + } + } + + // Check basic types + if basic, ok := t.Underlying().(*types.Basic); ok { + switch basic.Kind() { + case types.Bool: + return "BOOL" + case types.Int8: + return "INT8" + case types.Int16: + return "INT16" + case types.Int32: + return "INT32" + case types.Int, types.Int64: + return "INT64" + case types.Uint8: + return "UINT8" + case types.Uint16: + return "UINT16" + case types.Uint32: + return "UINT32" + case types.Uint, types.Uint64: + return "UINT64" + case types.Float32: + return "FLOAT32" + case types.Float64: + return "FLOAT64" + case types.String: + return "STRING" + } + } + + return "UNKNOWN" +} + +// getPrimitiveSize returns the byte size of a primitive type +func getPrimitiveSize(t types.Type) int { + // Handle pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + if basic, ok := t.Underlying().(*types.Basic); ok { + switch basic.Kind() { + case types.Bool, types.Int8, types.Uint8: + return 1 + case types.Int16, types.Uint16: + return 2 + case types.Int32, types.Uint32, types.Float32: + return 4 + case types.Int, types.Int64, types.Uint, types.Uint64, types.Float64: + return 8 + case types.String: + return 999 // Variable size, sort last among primitives + } + } + + return 0 +} + +// getTypeIDValue returns numeric value for type ID for sorting +func getTypeIDValue(typeID string) int { + // Map Fory TypeIDs to numeric values for sorting + typeIDMap := map[string]int{ + "BOOL": 1, + "INT8": 2, + "INT16": 3, + "INT32": 4, + "INT64": 5, + "UINT8": 6, + "UINT16": 7, + "UINT32": 8, + "UINT64": 9, + "FLOAT32": 10, + "FLOAT64": 11, + "STRING": 12, + "TIMESTAMP": 20, + "LOCAL_DATE": 21, + "NAMED_STRUCT": 30, + } + + if val, ok := typeIDMap[typeID]; ok { + return val + } + return 999 +} + +// sortFields sorts fields according to Fory protocol +func sortFields(fields []*FieldInfo) { + sort.Slice(fields, func(i, j int) bool { + f1, f2 := fields[i], fields[j] + + // Group primitives first + if f1.IsPrimitive && !f2.IsPrimitive { + return true + } + if !f1.IsPrimitive && f2.IsPrimitive { + return false + } + + if f1.IsPrimitive && f2.IsPrimitive { + // Sort primitives by size (descending), then by type ID, then by name + if f1.PrimitiveSize != f2.PrimitiveSize { + return f1.PrimitiveSize > f2.PrimitiveSize + } + if f1.TypeID != f2.TypeID { + return getTypeIDValue(f1.TypeID) < getTypeIDValue(f2.TypeID) + } + return f1.SnakeName < f2.SnakeName + } + + // Sort non-primitives by type ID, then by name + if f1.TypeID != f2.TypeID { + return getTypeIDValue(f1.TypeID) < getTypeIDValue(f2.TypeID) + } + return f1.SnakeName < f2.SnakeName + }) +} + +// computeStructHash computes a hash for struct schema compatibility +func computeStructHash(s *StructInfo) int32 { + h := md5.New() + + // Write struct name + h.Write([]byte(s.Name)) + + // Write sorted field information + for _, field := range s.Fields { + h.Write([]byte(field.SnakeName)) + h.Write([]byte(field.TypeID)) + // Add primitive size for better differentiation + if field.IsPrimitive { + sizeBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(sizeBytes, uint32(field.PrimitiveSize)) + h.Write(sizeBytes) + } + } + + hashBytes := h.Sum(nil) + // Take first 4 bytes as int32 + return int32(binary.LittleEndian.Uint32(hashBytes[:4])) +} + +// getStructNames extracts struct names from StructInfo slice +func getStructNames(structs []*StructInfo) []string { + names := make([]string, len(structs)) + for i, s := range structs { + names[i] = s.Name + } + return names +} + +// analyzeField analyzes a struct field and creates FieldInfo +func analyzeField(field *types.Var, index int) (*FieldInfo, error) { + fieldType := field.Type() + goName := field.Name() + snakeName := toSnakeCase(goName) + + // Check if field type is supported + if !isSupportedFieldType(fieldType) { + return nil, nil // Skip unsupported types + } + + // Analyze type information + isPrimitive := isPrimitiveType(fieldType) + isPointer := false + typeID := getTypeID(fieldType) + primitiveSize := getPrimitiveSize(fieldType) + + // Handle pointer types + if ptr, ok := fieldType.(*types.Pointer); ok { + isPointer = true + fieldType = ptr.Elem() + isPrimitive = isPrimitiveType(fieldType) + typeID = getTypeID(fieldType) + primitiveSize = getPrimitiveSize(fieldType) + } + + return &FieldInfo{ + GoName: goName, + SnakeName: snakeName, + Type: field.Type(), + Index: index, + IsPrimitive: isPrimitive, + IsPointer: isPointer, + TypeID: typeID, + PrimitiveSize: primitiveSize, + }, nil +} diff --git a/go/fory/fory.go b/go/fory/fory.go index 06bc2d6cf9..6431b324ce 100644 --- a/go/fory/fory.go +++ b/go/fory/fory.go @@ -25,6 +25,31 @@ import ( ) func NewFory(referenceTracking bool) *Fory { + fory := &Fory{ + refResolver: newRefResolver(referenceTracking), + referenceTracking: referenceTracking, + language: XLANG, + buffer: NewByteBuffer(nil), + } + // Create a new type resolver for this instance but copy generated serializers from global resolver + fory.typeResolver = newTypeResolver(fory) + + // Copy generated serializers from global resolver to this instance + if globalTypeResolver != nil { + for typ, serializer := range globalTypeResolver.typeToSerializers { + fory.typeResolver.typeToSerializers[typ] = serializer + } + for typeId, typ := range globalTypeResolver.typeIdToType { + fory.typeResolver.typeIdToType[typeId] = typ + } + } + + return fory +} + +// NewForyWithIsolatedTypes creates a Fory instance with isolated type resolver +// for use cases that need independent type registration +func NewForyWithIsolatedTypes(referenceTracking bool) *Fory { fory := &Fory{ refResolver: newRefResolver(referenceTracking), referenceTracking: referenceTracking, diff --git a/go/fory/go.mod b/go/fory/go.mod index 206c822042..3beb267602 100644 --- a/go/fory/go.mod +++ b/go/fory/go.mod @@ -23,4 +23,5 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/spaolacci/murmur3 v1.1.0 github.com/stretchr/testify v1.7.0 + golang.org/x/tools v0.1.12 ) diff --git a/go/fory/go.sum b/go/fory/go.sum index d34e0f7a78..e7b90738ee 100644 --- a/go/fory/go.sum +++ b/go/fory/go.sum @@ -8,6 +8,32 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/go/fory/string.go b/go/fory/string.go index 666ee32a1c..3e1396626b 100644 --- a/go/fory/string.go +++ b/go/fory/string.go @@ -151,3 +151,15 @@ func readUTF8(buf *ByteBuffer, size int) string { data := buf.ReadBinary(size) return string(data) // Direct UTF-8 conversion } + +// WriteString provides public API for zero-reflection string serialization +// This method is specifically designed for code generation to avoid reflection overhead +func WriteString(buf *ByteBuffer, value string) error { + return writeString(buf, value) +} + +// ReadString provides public API for zero-reflection string deserialization +// This method is specifically designed for code generation to avoid reflection overhead +func ReadString(buf *ByteBuffer) string { + return readString(buf) +} diff --git a/go/fory/tests/generator_test.go b/go/fory/tests/generator_test.go new file mode 100644 index 0000000000..452554783f --- /dev/null +++ b/go/fory/tests/generator_test.go @@ -0,0 +1,63 @@ +// 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/apache/fory/go/fory" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +//go:generate fory -file structs.go + +func TestValidationDemo(t *testing.T) { + // 1. Create test instance + original := &ValidationDemo{ + A: 12345, // int32 + B: "Hello Fory!", // string + C: 98765, // int64 + } + + // Validate original data structure + assert.Equal(t, int32(12345), original.A, "Original A should be 12345") + assert.Equal(t, "Hello Fory!", original.B, "Original B should be 'Hello Fory!'") + assert.Equal(t, int64(98765), original.C, "Original C should be 98765") + + // 2. Serialize using generated code + f := fory.NewFory(true) + data, err := f.Marshal(original) + require.NoError(t, err, "Serialization should not fail") + require.NotEmpty(t, data, "Serialized data should not be empty") + assert.Greater(t, len(data), 0, "Serialized data should have positive length") + + // 3. Deserialize using generated code + var result *ValidationDemo + err = f.Unmarshal(data, &result) + require.NoError(t, err, "Deserialization should not fail") + require.NotNil(t, result, "Deserialized result should not be nil") + + // 4. Validate round-trip serialization + assert.Equal(t, original.A, result.A, "Field A should match after round-trip") + assert.Equal(t, original.B, result.B, "Field B should match after round-trip") + assert.Equal(t, original.C, result.C, "Field C should match after round-trip") + + // 5. Validate data integrity + assert.EqualValues(t, original, result, "Complete struct should match after round-trip") +} diff --git a/go/fory/tests/structs.go b/go/fory/tests/structs.go new file mode 100644 index 0000000000..170c255e49 --- /dev/null +++ b/go/fory/tests/structs.go @@ -0,0 +1,28 @@ +// 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 + +// ValidationDemo is a simple struct for testing code generation +// Contains only basic types since PR1 only supports basic types + +// fory:gen +type ValidationDemo struct { + A int32 `json:"a"` // int32 field + B string `json:"b"` // string field + C int64 `json:"c"` // int64 field (instead of array, as arrays are not supported yet) +} diff --git a/go/fory/type.go b/go/fory/type.go index dad56c7e17..8ce94417e4 100644 --- a/go/fory/type.go +++ b/go/fory/type.go @@ -19,13 +19,14 @@ package fory import ( "fmt" - "github.com/apache/fory/go/fory/meta" "hash/fnv" "reflect" "regexp" "strconv" "strings" "time" + + "github.com/apache/fory/go/fory/meta" ) type TypeId = int16 @@ -223,6 +224,73 @@ var ( genericSetType = reflect.TypeOf((*GenericSet)(nil)).Elem() ) +// Global type resolver shared by all Fory instances for generated serializers +var globalTypeResolver *typeResolver + +func init() { + // Initialize global type resolver after other init functions + initGlobalTypeResolver() +} + +func initGlobalTypeResolver() { + // Create a dummy fory instance just for initializing the global type resolver + r := &typeResolver{ + typeTagToSerializers: map[string]Serializer{}, + typeToSerializers: map[reflect.Type]Serializer{}, + typeIdToType: map[int16]reflect.Type{}, + typeToTypeInfo: map[reflect.Type]string{}, + typeInfoToType: map[string]reflect.Type{}, + dynamicStringToId: map[string]int16{}, + dynamicIdToString: map[int16]string{}, + + language: XLANG, + metaStringResolver: NewMetaStringResolver(), + requireRegistration: false, + + metaStrToStr: make(map[string]string), + metaStrToClass: make(map[string]reflect.Type), + hashToMetaString: make(map[uint64]string), + hashToClassInfo: make(map[uint64]TypeInfo), + + dynamicWrittenMetaStr: make([]string, 0), + typeIDToTypeInfo: make(map[int32]TypeInfo), + typeIDCounter: 300, + dynamicWriteStringID: 0, + + typesInfo: make(map[reflect.Type]TypeInfo), + nsTypeToTypeInfo: make(map[nsTypeKey]TypeInfo), + namedTypeToTypeInfo: make(map[namedTypeKey]TypeInfo), + + namespaceEncoder: meta.NewEncoder('.', '_'), + namespaceDecoder: meta.NewDecoder('.', '_'), + typeNameEncoder: meta.NewEncoder('$', '_'), + typeNameDecoder: meta.NewDecoder('$', '_'), + } + + // Initialize base type mappings - copy from newTypeResolver + for _, t := range []reflect.Type{ + boolType, + byteType, + int8Type, + int16Type, + int32Type, + intType, + int64Type, + float32Type, + float64Type, + stringType, + dateType, + timestampType, + interfaceType, + genericSetType, + } { + r.typeInfoToType[t.String()] = t + r.typeToTypeInfo[t] = t.String() + } + r.initialize() + globalTypeResolver = r +} + type TypeInfo struct { Type reflect.Type FullNameBytes []byte @@ -399,6 +467,39 @@ func (r *typeResolver) RegisterSerializer(type_ reflect.Type, s Serializer) erro return nil } +// RegisterGeneratedSerializer registers a generated serializer for a specific type. +// Generated serializers have priority over reflection-based serializers and can override existing ones. +func RegisterGeneratedSerializer(typ interface{}, s Serializer) error { + if typ == nil { + return fmt.Errorf("typ cannot be nil") + } + + reflectType := reflect.TypeOf(typ) + if reflectType.Kind() == reflect.Ptr { + reflectType = reflectType.Elem() + } + + // Use the global type resolver + if globalTypeResolver == nil { + return fmt.Errorf("global type resolver not initialized") + } + + // Allow overriding existing serializers by directly setting the map + // This gives generated serializers priority over reflection-based ones + globalTypeResolver.typeToSerializers[reflectType] = s + + // Handle typeId registration + typeId := s.TypeId() + if typeId != FORY_TYPE_TAG { + if typeId > NotSupportCrossLanguage { + // Allow overriding existing typeId mappings as well + globalTypeResolver.typeIdToType[typeId] = reflectType + } + } + + return nil +} + func (r *typeResolver) RegisterTypeTag(value reflect.Value, tag string) error { type_ := value.Type() if prev, ok := r.typeToSerializers[type_]; ok {