Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ci/run_ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
;;
Expand Down
146 changes: 143 additions & 3 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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–1.15:
Comment thread
chaokunyang marked this conversation as resolved.
Outdated

```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 <file.go>` 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 <your 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

Expand Down
164 changes: 164 additions & 0 deletions go/fory/cmd/fory/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading