跳到主要内容
版本:1.4.0

生成代码

本文介绍各目标语言的生成代码。

Fory IDL 生成的类型符合宿主语言习惯,可以直接用作领域对象。根据目标语言的不同,生成类型还包含字节转换辅助方法以及 Schema 模块或注册辅助方法。

生成的 Schema 模块根据 Schema 源文件命名,而不是根据包或命名空间命名。对于直接在语言包或命名空间中公开模块的目标,AddressbookForyModuleComplexPbForyModule 这类名称可让多个 IDL 文件面向同一个包或命名空间,而不会产生冲突的 ForyModule 类型。

参考 Schema

以下示例使用两个实际的 Schema:

  1. addressbook.fdl(显式类型 ID)
  2. auto_id.fdl(无显式类型 ID)

addressbook.fdl(节选)

package addressbook;

option go_package = "github.com/myorg/myrepo/gen/addressbook;addressbook";

message Person [id=100] {
string name = 1;
int32 id = 2;

enum PhoneType [id=101] {
PHONE_TYPE_MOBILE = 0;
PHONE_TYPE_HOME = 1;
PHONE_TYPE_WORK = 2;
}

message PhoneNumber [id=102] {
string number = 1;
PhoneType phone_type = 2;
}

list<PhoneNumber> phones = 7;
Animal pet = 8;
}

message Dog [id=104] {
string name = 1;
int32 bark_volume = 2;
}

message Cat [id=105] {
string name = 1;
int32 lives = 2;
}

union Animal [id=106] {
Dog dog = 1;
Cat cat = 2;
}

message AddressBook [id=103] {
list<Person> people = 1;
map<string, Person> people_by_name = 2;
}

auto_id.fdl(节选)

package auto_id;

enum Status {
UNKNOWN = 0;
OK = 1;
}

message Envelope {
string id = 1;

message Payload {
int32 value = 1;
}

union Detail {
Payload payload = 1;
string note = 2;
}

Payload payload = 2;
Detail detail = 3;
Status status = 4;
}

union Wrapper {
Envelope envelope = 1;
string raw = 2;
}

Java

输出布局

对于 package addressbook,Java 输出生成在:

  • <java_out>/addressbook/
  • 类型文件:AddressBook.javaPerson.javaDog.javaCat.javaAnimal.java
  • Schema 模块:AddressbookForyModule.java

对于没有 Java 包的 Schema,Schema 模块名称取自源文件的主文件名。例如,main.fdl 会生成 MainForyModule.java。Java 导入图不能混用默认包 Schema 与具名 Java 包。

类型生成

消息会生成带有 @ForyField、默认构造函数、getter/setter 和字节辅助方法的 Java 类:

public class Person {
public static enum PhoneType {
MOBILE,
HOME,
WORK;
}

public static class PhoneNumber {
@ForyField(id = 1)
private String number;

@ForyField(id = 2)
private PhoneType phoneType;

public byte[] toBytes() { ... }
public static PhoneNumber fromBytes(byte[] bytes) { ... }
}

@ForyField(id = 1)
private String name;

@ForyField(id = 8)
private Animal pet;

public byte[] toBytes() { ... }
public static Person fromBytes(byte[] bytes) { ... }
}

设置 evolving=false 的消息会使用 Java 固定 Schema 结构体编码生成。

联合类型会生成继承 org.apache.fory.type.union.Union 的类:

public final class Animal extends Union {
public enum AnimalCase {
DOG(1),
CAT(2);
public final int id;
AnimalCase(int id) { this.id = id; }
}

public static Animal ofDog(Dog v) { ... }
public AnimalCase getAnimalCase() { ... }
public int getAnimalCaseId() { ... }

public boolean hasDog() { ... }
public Dog getDog() { ... }
public void setDog(Dog v) { ... }
}

Schema 模块

每个 JVM Schema 都会生成一个 ForyModule。导入的 Schema 模块通过 fory.register(...) 安装,因此共享导入会由 Fory 实例去重。

public final class AddressbookForyModule implements org.apache.fory.ForyModule {
public static final AddressbookForyModule INSTANCE = new AddressbookForyModule();

static ThreadSafeFory getFory() { ... }

@Override
public void install(Fory fory) {
org.apache.fory.resolver.TypeResolver resolver = fory.getTypeResolver();
resolver.registerUnion(Animal.class, 106L, new org.apache.fory.serializer.UnionSerializer(resolver, Animal.class));
resolver.register(Person.class, 100L);
resolver.register(Person.PhoneType.class, 101L);
resolver.register(Person.PhoneNumber.class, 102L);
resolver.register(Dog.class, 104L);
resolver.register(Cat.class, 105L);
resolver.register(AddressBook.class, 103L);
}
}

对于没有显式 [id=...] 的 Schema,生成的注册代码使用计算得到的数字 ID(以下示例来自 auto_id.fdl):

resolver.register(Status.class, 1124725126L);
resolver.registerUnion(Wrapper.class, 1471345060L, new org.apache.fory.serializer.UnionSerializer(resolver, Wrapper.class));
resolver.register(Envelope.class, 3022445236L);
resolver.registerUnion(Envelope.Detail.class, 1609214087L, new org.apache.fory.serializer.UnionSerializer(resolver, Envelope.Detail.class));
resolver.register(Envelope.Payload.class, 2862577837L);

如果设置了 option enable_auto_type_id = false;,注册代码会使用符号名称:

resolver.register(Config.class, "myapp.models", "Config");
resolver.registerUnion(
Holder.class,
"myapp.models",
"Holder",
new org.apache.fory.serializer.UnionSerializer(resolver, Holder.class));

使用示例

Person person = new Person();
person.setName("Alice");
person.setPet(Animal.ofDog(new Dog()));

byte[] data = person.toBytes();
Person restored = Person.fromBytes(data);

gRPC 服务伴生代码

当 Schema 包含服务且编译器使用 --grpc 运行时,Java 生成器会在模型类型旁为每个服务生成一个 <ServiceName>Grpc.java 文件。

public final class AddressBookServiceGrpc {
public static final String SERVICE_NAME = "addressbook.AddressBookService";

public static AddressBookServiceStub newStub(io.grpc.Channel channel) { ... }
public static AddressBookServiceBlockingStub newBlockingStub(io.grpc.Channel channel) { ... }
public static AddressBookServiceFutureStub newFutureStub(io.grpc.Channel channel) { ... }

public abstract static class AddressBookServiceImplBase
implements io.grpc.BindableService {
public void lookup(Person request, io.grpc.stub.StreamObserver<AddressBook> responseObserver) { ... }
}
}

生成的 marshaller 使用 Schema 模块的 ThreadSafeFory 序列化每个请求或响应。它使用 grpc-java 的 MethodDescriptor.Marshaller API,因此编译这些文件的应用必须提供 grpc-java 依赖。这些依赖不会添加到 Fory Java 构件中。

Python

输出布局

每个 Schema 文件会生成一个 Python 模块,例如:

  • <python_out>/addressbook.py

类型生成

联合类型会生成 case 枚举和带有类型化辅助方法的 Union 子类:

class AnimalCase(Enum):
DOG = 1
CAT = 2

class Animal(Union):
@classmethod
def dog(cls, v: Dog) -> "Animal": ...

def case(self) -> AnimalCase: ...
def case_id(self) -> int: ...

def is_dog(self) -> bool: ...
def dog_value(self) -> Dog: ...
def set_dog(self, v: Dog) -> None: ...

消息会生成 @pyfory.dataclass 类型,嵌套类型仍保持嵌套:

@pyfory.dataclass
class Person:
class PhoneType(IntEnum):
MOBILE = 0
HOME = 1
WORK = 2

@pyfory.dataclass
class PhoneNumber:
number: str = pyfory.field(id=1, default="")
phone_type: Person.PhoneType = pyfory.field(id=2, default=None)

name: str = pyfory.field(id=1, default="")
phones: List[Person.PhoneNumber] = pyfory.field(id=7, default_factory=list)
pet: Animal = pyfory.field(id=8, default=None)

def to_bytes(self) -> bytes: ...
@classmethod
def from_bytes(cls, data: bytes) -> "Person": ...

注册

生成的注册函数如下:

def register_addressbook_types(fory: pyfory.Fory):
fory.register_union(Animal, type_id=106, serializer=AnimalSerializer(fory))
fory.register_type(Person, type_id=100)
fory.register_type(Person.PhoneType, type_id=101)
fory.register_type(Person.PhoneNumber, type_id=102)
fory.register_type(Dog, type_id=104)
fory.register_type(Cat, type_id=105)
fory.register_type(AddressBook, type_id=103)

对于没有显式 [id=...] 的 Schema,生成的注册代码使用计算得到的数字 ID:

fory.register_type(Status, type_id=1124725126)
fory.register_union(Wrapper, type_id=1471345060, serializer=WrapperSerializer(fory))
fory.register_type(Envelope, type_id=3022445236)
fory.register_union(Envelope.Detail, type_id=1609214087, serializer=Envelope.DetailSerializer(fory))
fory.register_type(Envelope.Payload, type_id=2862577837)

如果设置了 option enable_auto_type_id = false;

fory.register_type(Config, name="myapp.models.Config")
fory.register_union(
Holder,
name="myapp.models.Holder",
serializer=HolderSerializer(fory),
)

使用示例

person = Person(name="Alice", pet=Animal.dog(Dog(name="Rex", bark_volume=10)))

data = person.to_bytes()
restored = Person.from_bytes(data)

gRPC 服务伴生代码

当 Schema 包含服务且编译器使用 --grpc 运行时,Python 生成器会生成名为 <module>_grpc.py 的伴生模块。模块名取自 Fory 包名,其中的点会替换为下划线;如果 Schema 没有包,则使用 generated。Python gRPC 输出默认使用 grpc.aio AsyncIO API。

import grpc
import grpc.aio


class AddressBookServiceStub:
def __init__(self, channel):
self.lookup = channel.unary_unary(
"/addressbook.AddressBookService/Lookup",
request_serializer=_serialize,
response_deserializer=_deserialize,
)


class AddressBookServiceServicer:
async def lookup(self, request, context):
await context.abort(grpc.StatusCode.UNIMPLEMENTED, "Method not implemented!")


def add_servicer(servicer, server): ...

Python gRPC 序列化器接收并返回完整的 bytes 载荷,因此生成的回调会直接调用模型模块的 _get_fory().serialize(...)_get_fory().deserialize(...)。使用生成伴生模块的应用必须安装 grpciopyfory 不会添加 gRPC 硬依赖。Python API 使用 snake_case 方法名,同时在 gRPC 路径中保留原始 IDL 方法名。

使用 --grpc --grpc-python-mode=sync 可生成同步 Python grpcio 伴生代码。同步模式保持相同的生成文件名和公开名称,但服务端方法使用普通 def 方法以及同步的 grpc.Channelgrpc.Server 实例。

Rust

输出布局

每个 Schema 会生成一个 Rust 模块文件,例如:

  • <rust_out>/addressbook.rs

使用 --grpc 且 Schema 包含服务时,Rust 还会生成:

  • <rust_out>/addressbook_service.rs
  • <rust_out>/addressbook_service_grpc.rs

类型生成

联合类型映射为带有 #[fory(id = ...)] Schema case 属性的 Rust 枚举。#[fory(unknown)] Unknown(::fory::UnknownCase) 标记由 Fory 提供的前向兼容载体。该标记只用于选择载体,不会向 Schema case 表添加条目;Schema case 仍可使用完整的 0..N ID 范围。生成的类型化联合必须至少包含一个非 Unknown case。编译器会将声明的第一个非 Unknown case 标记为 #[fory(default)],并基于该 case 生成 Default

#[derive(::fory::ForyUnion, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Animal {
#[fory(unknown)]
Unknown(::fory::UnknownCase),
#[fory(id = 0, default)]
Dog(self::Dog),
#[fory(id = 1)]
Cat(self::Cat),
}

impl ::std::default::Default for Animal {
fn default() -> Self {
Self::Dog(<self::Dog as ::fory::ForyDefault>::fory_default())
}
}

嵌套类型会生成嵌套模块:

pub mod person {
#[derive(ForyEnum, Debug, Clone, PartialEq, Default)]
#[repr(i32)]
pub enum PhoneType {
#[default]
Mobile = 0,
Home = 1,
Work = 2,
}

#[derive(ForyStruct, Debug, Clone, PartialEq, Default)]
pub struct PhoneNumber {
#[fory(id = 1)]
pub number: String,
#[fory(id = 2)]
pub phone_type: PhoneType,
}
}

消息派生 ForyStruct,并包含 to_bytes/from_bytes 辅助方法:

#[derive(ForyStruct, Debug, Clone, PartialEq, Default)]
pub struct Person {
#[fory(id = 1)]
pub name: String,
#[fory(id = 7)]
pub phones: Vec<person::PhoneNumber>,
#[fory(id = 8)]
pub pet: Animal,
}

注册

生成的注册函数如下:

pub fn register_types(fory: &mut Fory) -> Result<(), fory::Error> {
fory.register_union::<Animal>(106)?;
fory.register::<person::PhoneType>(101)?;
fory.register::<person::PhoneNumber>(102)?;
fory.register::<Person>(100)?;
fory.register::<Dog>(104)?;
fory.register::<Cat>(105)?;
fory.register::<AddressBook>(103)?;
Ok(())
}

对于没有显式 [id=...] 的 Schema,生成的注册代码使用计算得到的数字 ID:

fory.register::<Status>(1124725126)?;
fory.register_union::<Wrapper>(1471345060)?;
fory.register::<Envelope>(3022445236)?;
fory.register_union::<envelope::Detail>(1609214087)?;
fory.register::<envelope::Payload>(2862577837)?;

如果设置了 option enable_auto_type_id = false;

fory.register_by_name::<Config>("myapp.models.Config")?;
fory.register_union_by_name::<Holder>("myapp.models.Holder")?;

使用示例

let person = Person {
name: "Alice".into(),
pet: Animal::Dog(self::Dog::default()),
..Default::default()
};

let bytes = person.to_bytes()?;
let restored = Person::from_bytes(&bytes)?;

gRPC 服务伴生代码

当 Schema 包含服务且编译器使用 --grpc 运行时,Rust 生成器会生成一个服务 API 模块和一个 tonic 绑定模块。对于名为 addressbook 的 Schema 模块,这两个文件分别是 addressbook_service.rsaddressbook_service_grpc.rs

服务 API 模块包含异步 trait 和 gRPC 路径常量:

#[::tonic::async_trait]
pub trait AddressBookService: ::std::marker::Send + ::std::marker::Sync + 'static {
async fn lookup(
&self,
request: ::tonic::Request<crate::addressbook::Person>,
) -> ::std::result::Result<
::tonic::Response<crate::addressbook::AddressBook>,
::tonic::Status,
>;
}

pub const ADDRESS_BOOK_SERVICE_SERVICE_NAME: &str = "addressbook.AddressBookService";
pub const ADDRESS_BOOK_SERVICE_LOOKUP_PATH: &str = "/addressbook.AddressBookService/Lookup";

tonic 绑定模块包含由 Fory 支持的 codec、载荷实现以及客户端/服务端包装器。它使用生成模型类型的 to_bytesfrom_bytes 辅助方法序列化每个请求或响应:

impl codec::ForyGrpcPayload for crate::addressbook::Person {
fn encode_fory_payload(&self) -> ::std::result::Result<::std::vec::Vec<u8>, ::fory::Error> {
self.to_bytes()
}

fn decode_fory_payload(payload: &[u8]) -> ::std::result::Result<Self, ::fory::Error> {
Self::from_bytes(payload)
}
}

编译生成 Rust 服务文件的应用必须提供 tonicbytes 依赖;Fory 的 Rust crate 不会将这些 gRPC 依赖作为硬依赖添加。

C++

输出布局

每个 Schema 文件会生成一个 C++ 头文件,例如:

  • <cpp_out>/addressbook.h

类型生成

消息会生成带有类型化访问器和字节辅助方法的 final 类:

class Person final {
public:
class PhoneNumber final {
public:
const std::string& number() const;
std::string* mutable_number();
template <class Arg, class... Args>
void set_number(Arg&& arg, Args&&... args);

fory::Result<std::vector<uint8_t>, fory::Error> to_bytes() const;
static fory::Result<PhoneNumber, fory::Error> from_bytes(const std::vector<uint8_t>& data);
};

const std::string& name() const;
std::string* mutable_name();
template <class Arg, class... Args>
void set_name(Arg&& arg, Args&&... args);

const Animal& pet() const;
Animal* mutable_pet();
};

可选消息字段会生成 has_xxxmutable_xxxclear_xxx API:

class Envelope final {
public:
bool has_payload() const { return payload_ != nullptr; }
const Envelope::Payload& payload() const { return *payload_; }
Envelope::Payload* mutable_payload() {
if (!payload_) {
payload_ = std::make_unique<Envelope::Payload>();
}
return payload_.get();
}
void clear_payload() { payload_.reset(); }

private:
std::unique_ptr<Envelope::Payload> payload_;
};

联合类型会生成 std::variant 包装器:

class Animal final {
public:
enum class AnimalCase : uint32_t {
DOG = 1,
CAT = 2,
};

static Animal dog(Dog v);
static Animal cat(Cat v);

AnimalCase animal_case() const noexcept;
uint32_t animal_case_id() const noexcept;

bool is_dog() const noexcept;
const Dog* as_dog() const noexcept;
Dog* as_dog() noexcept;
const Dog& dog() const;
Dog& dog();

template <class Visitor>
decltype(auto) visit(Visitor&& vis) const;

private:
std::variant<Dog, Cat> value_;
};

生成的头文件包含用于序列化元数据的 FORY_UNIONFORY_ENUMFORY_STRUCT 宏。字段和载荷配置嵌入生成的 FORY_STRUCT/FORY_UNION 条目中。

注册

生成的注册函数如下:

inline void register_types(fory::serialization::BaseFory& fory) {
fory.register_union<Animal>(106);
fory.register_enum<Person::PhoneType>(101);
fory.register_struct<Person::PhoneNumber>(102);
fory.register_struct<Person>(100);
fory.register_struct<Dog>(104);
fory.register_struct<Cat>(105);
fory.register_struct<AddressBook>(103);
}

对于没有显式 [id=...] 的 Schema,生成的注册代码使用计算得到的数字 ID:

fory.register_enum<Status>(1124725126);
fory.register_union<Wrapper>(1471345060);
fory.register_struct<Envelope>(3022445236);
fory.register_union<Envelope::Detail>(1609214087);
fory.register_struct<Envelope::Payload>(2862577837);

如果设置了 option enable_auto_type_id = false;

fory.register_struct<Config>("myapp.models.Config");
fory.register_union<Holder>("myapp.models.Holder");

使用示例

addressbook::Person person;
person.set_name("Alice");
*person.mutable_pet() = addressbook::Animal::dog(addressbook::Dog{});

auto bytes = person.to_bytes();
auto restored = addressbook::Person::from_bytes(bytes.value());

Go

输出布局

Go 输出路径取决于 Schema 选项和 --go_out

对于 addressbook.fdl,已配置 go_package,生成输出会遵循所配置的导入路径/包(例如位于 --go_out 根目录下)。

没有 go_package 时,输出使用指定的 --go_out 目录以及根据包名派生的文件名。

类型生成

嵌套类型默认使用下划线命名(Person_PhoneTypePerson_PhoneNumber):

type Person_PhoneType int32

const (
Person_PhoneTypeMobile Person_PhoneType = 0
Person_PhoneTypeHome Person_PhoneType = 1
Person_PhoneTypeWork Person_PhoneType = 2
)

type Person_PhoneNumber struct {
Number string `fory:"id=1"`
PhoneType Person_PhoneType `fory:"id=2"`
}

消息会生成带有 fory tag 和字节辅助方法的结构体:

type Person struct {
Name string `fory:"id=1"`
Id int32 `fory:"id=2"`
Phones []Person_PhoneNumber `fory:"id=7,type=list"`
Pet Animal `fory:"id=8"`
}

func (m *Person) ToBytes() ([]byte, error) { ... }
func (m *Person) FromBytes(data []byte) error { ... }

联合类型会生成带有构造函数、访问器和访问者 API 的类型化 case 结构体:

type AnimalCase uint32

type Animal struct {
case_ AnimalCase
value any
}

func DogAnimal(v *Dog) Animal { ... }
func CatAnimal(v *Cat) Animal { ... }

func (u Animal) Case() AnimalCase { ... }
func (u Animal) AsDog() (*Dog, bool) { ... }
func (u Animal) Visit(visitor AnimalVisitor) error { ... }

注册

生成的注册函数如下:

func RegisterTypes(f *fory.Fory) error {
if err := f.RegisterUnion(Animal{}, 106, fory.NewUnionSerializer(...)); err != nil {
return err
}
if err := f.RegisterEnum(Person_PhoneType(0), 101); err != nil {
return err
}
if err := f.RegisterStruct(Person_PhoneNumber{}, 102); err != nil {
return err
}
if err := f.RegisterStruct(Person{}, 100); err != nil {
return err
}
return nil
}

对于没有显式 [id=...] 的 Schema,生成的注册代码使用计算得到的数字 ID:

if err := f.RegisterEnum(Status(0), 1124725126); err != nil { ... }
if err := f.RegisterUnion(Wrapper{}, 1471345060, fory.NewUnionSerializer(...)); err != nil { ... }
if err := f.RegisterStruct(Envelope{}, 3022445236); err != nil { ... }
if err := f.RegisterUnion(Envelope_Detail{}, 1609214087, fory.NewUnionSerializer(...)); err != nil { ... }
if err := f.RegisterStruct(Envelope_Payload{}, 2862577837); err != nil { ... }

如果设置了 option enable_auto_type_id = false;

if err := f.RegisterStructByName(Config{}, "myapp.models.Config"); err != nil { ... }
if err := f.RegisterUnionByName(Holder{}, "myapp.models.Holder", fory.NewUnionSerializer(...)); err != nil { ... }

go_nested_type_style 控制嵌套类型的命名方式:

option go_nested_type_style = "camelcase";

同时设置二者时,CLI 标志 --go_nested_type_style 会覆盖此 Schema 选项。

使用示例

person := &Person{
Name: "Alice",
Pet: DogAnimal(&Dog{Name: "Rex"}),
}

data, err := person.ToBytes()
if err != nil {
panic(err)
}
var restored Person
if err := restored.FromBytes(data); err != nil {
panic(err)
}

gRPC 服务伴生代码

当 Schema 包含服务且编译器使用 --grpc 运行时,Go 生成器会在模型文件旁生成一个 <module>_grpc.go 文件。伴生文件包含 grpc-go 客户端和服务端接口,以及由 Fory 支持的 CodecV2

type AddressBookServiceClient interface {
Lookup(ctx context.Context, in *Person, opts ...grpc.CallOption) (*AddressBook, error)
}

func NewAddressBookServiceClient(cc grpc.ClientConnInterface) AddressBookServiceClient { ... }

type CodecV2 struct{}

生成的 codec 与生成的 ToBytesFromBytes 辅助方法使用同一个包级线程安全 Fory 运行时。应用应将 CodecV2{} 传给 grpc-go 服务端选项,生成的客户端则会在每次调用时强制使用同一 codec:

server := grpc.NewServer(grpc.ForceServerCodecV2(addressbook.CodecV2{}))
addressbook.RegisterAddressBookServiceServer(server, service)

client := addressbook.NewAddressBookServiceClient(conn)

Go 方法名以 PascalCase 标识符导出,而 gRPC 方法路径会精确保留 Schema 中的服务名和方法名。更改服务名或方法名后,请重新生成通信两端的代码。

编译这些文件的应用必须提供 grpc-go 依赖;Fory Go 包不会将 gRPC 作为硬依赖添加。

C#

输出布局

每个 Schema 会生成一个 C# .cs 文件,例如:

  • <csharp_out>/addressbook/Addressbook.cs

C# 模型文件名使用规范化为 PascalCase 的源文件主文件名。例如,service.fdl 生成 Service.csorder-events.fdl 生成 OrderEvents.cs123-schema.fdl 生成 Schema123Schema.cs

类型生成

消息会生成带有 C# 属性和字节辅助方法的 [ForyStruct] 类:

[ForyStruct]
public sealed partial class Person
{
public string Name { get; set; } = string.Empty;
public int Id { get; set; }
public List<Person.PhoneNumber> Phones { get; set; } = new();
public Animal Pet { get; set; } = null!;

public byte[] ToBytes() { ... }
public static Person FromBytes(byte[] data) { ... }
}

联合类型会生成 [ForyUnion] 代数数据类型(ADT)。Unknown(UnknownCase) 是由 Fory 提供、使用 [ForyUnknownCase] 标记的前向兼容载体。该标记只用于选择载体,不会向 Schema case 表添加条目。Schema 定义的 case 使用非负 [ForyCase] ID。如果某个 case 需要非默认 Schema 编码,生成的 [ForyCase] 会携带 Type。已知 case 的记录名使用 PascalCase FDL case 名称;必要时载荷类型会以限定引用形式生成,以避免名称冲突。类型化联合必须至少包含一个非 Unknown case。

[ForyUnion]
public abstract partial record Animal
{
private Animal() {}

[ForyUnknownCase]
public sealed partial record Unknown(UnknownCase Value) : Animal;

[ForyCase(0)]
public sealed partial record Dog(global::addressbook.Dog Value) : Animal;

[ForyCase(1)]
public sealed partial record Cat(global::addressbook.Cat Value) : Animal;
}

模块安装

每个 Schema 都会生成一个模块类,该类先安装导入的模块,再注册本地 Schema 类型:

public static class AddressbookForyModule
{
public static void Install(Fory fory)
{
fory.Register<addressbook.Animal>((uint)106);
fory.Register<addressbook.Person>((uint)100);
// ...
}
}

C# 模型文件的基础名和模块类都使用规范化后的源文件主文件名。它们不使用 csharp_namespace,也不使用 gRPC 服务名。例如,service.fdl 生成 Service.csServiceForyModule,而 order-events.fdl 生成 OrderEvents.csOrderEventsForyModule。名为 Greeter 的 gRPC 服务会生成服务伴生文件 GreeterGrpc.cs,但不会改变 Schema 模块名称。要获得 GreeterForyModule,请将 Schema 文件命名为 greeter.fdlGreeter.fdl

此源文件命名规则允许多个 Schema 面向同一 C# 命名空间而不会发生冲突。生成器不会创建根据命名空间或服务派生的模块别名。

未提供显式类型 ID 时,生成的安装代码使用计算得到的数字 ID(与其他目标语言的行为一致)。

gRPC 服务伴生代码

当 Schema 包含服务且编译器使用 --grpc 运行时,C# 生成器会在 Schema 模型文件旁为每个服务生成一个 <ServiceName>Grpc.cs 文件。

public static partial class AddressBookService
{
public abstract partial class AddressBookServiceBase
{
public virtual Task<AddressBook> Lookup(
Person request,
grpc::ServerCallContext context) { ... }
}

public partial class AddressBookServiceClient
: grpc::ClientBase<AddressBookServiceClient>
{
public virtual AddressBook Lookup(Person request, grpc::CallOptions options) { ... }
public virtual grpc::AsyncUnaryCall<AddressBook> LookupAsync(
Person request,
grpc::CallOptions options) { ... }
}

public static grpc::ServerServiceDefinition BindService(
AddressBookServiceBase serviceImpl) { ... }

public static void BindService(
grpc::ServiceBinderBase serviceBinder,
AddressBookServiceBase? serviceImpl) { ... }
}

每个生成的方法描述符都使用由 Fory 支持的静态 Grpc.Core.Marshaller<T>,并复用 Schema 模块的 ThreadSafeFory。反序列化通过 PayloadAsReadOnlySequence() 读取 gRPC 消息体,并拒绝单个 Fory 帧之后的尾随字节。生成的服务伴生代码不使用 protobuf 解析器,也不会为每次 RPC 调用创建 Fory 实例。

流式 RPC 映射到标准 gRPC C# API:

IDL 形态服务端方法客户端方法
rpc A (Req) returns (Res)Task<Res> A(Req request, ServerCallContext context)A(...)AAsync(...)
rpc A (Req) returns (stream Res)Task A(Req request, IServerStreamWriter<Res> responseStream, ...)AsyncServerStreamingCall<Res> A(...)
rpc A (stream Req) returns (Res)Task<Res> A(IAsyncStreamReader<Req> requestStream, ...)AsyncClientStreamingCall<Req, Res> A(...)
rpc A (stream Req) returns (stream Res)Task A(IAsyncStreamReader<Req> requestStream, IServerStreamWriter<Res> ...)AsyncDuplexStreamingCall<Req, Res> A(...)

编译生成 C# 服务文件的应用必须提供 Grpc.Core.Api,以及选用的 .NET gRPC 托管或客户端包,例如 Grpc.AspNetCoreGrpc.Net.ClientApache.Fory 包不会将 gRPC 依赖作为硬依赖添加。

JavaScript/TypeScript

输出布局

每个 Schema 会生成一个 JavaScript/TypeScript .ts 文件,例如:

  • <javascript_out>/addressbook.ts

当 Schema 包含服务时,JavaScript 还可以生成服务伴生文件:

  • 使用 --grpc 时生成 <javascript_out>/addressbook_grpc.ts
  • 使用 --grpc-web 时生成 <javascript_out>/addressbook_grpc_web.ts

类型生成

消息会生成字段名采用 camelCase 的 export interface 声明:

export interface Person {
name: string;
id: number;
phones: PhoneNumber[];
pet?: Animal | null;
}

枚举会生成 export enum 声明:

export enum PhoneType {
MOBILE = 0,
HOME = 1,
WORK = 2,
}

联合类型会生成带有 case 枚举的可辨识联合:

export enum AnimalCase {
DOG = 1,
CAT = 2,
}

export type Animal =
{ case: AnimalCase.DOG; value: Dog } | { case: AnimalCase.CAT; value: Cat };

Schema 辅助方法

每个生成的模型文件都会导出用于自定义 Fory 实例的注册辅助方法和根类型序列化辅助方法。公开 API 如下:

import type Fory, { Serializer } from "@apache-fory/core";

export function registerAddressbookTypes(fory: Fory): {
person: {
serialize: (value: Person | null) => Uint8Array;
deserialize: (bytes: Uint8Array) => Person;
serializer: Serializer;
};
};
export const serializePerson: (value: Person | null) => Uint8Array;
export const deserializePerson: (bytes: Uint8Array) => Person;

导入的 Schema 模块会由 registerXxxTypes(fory) 自动注册。对于生成的默认序列化路径,请使用 serializeXdeserializeX。当应用自行管理 Fory 实例时,请调用 registerXxxTypes(fory)。生成的 gRPC 伴生代码会自动导入这些辅助方法。

gRPC 服务伴生代码

当 Schema 包含服务且编译器使用 --grpc 运行时,JavaScript 生成器会生成名为 <module>_grpc.ts 的 Node.js 伴生文件。该文件包含服务描述符、处理器接口、客户端类以及适用于 @grpc/grpc-js 的服务端注册辅助方法。

export interface GreeterHandlers extends grpc.UntypedServiceImplementation {
sayHello: grpc.handleUnaryCall<HelloRequest, HelloReply>;
}

export function addGreeterService(
server: grpc.Server,
handlers: GreeterHandlers,
): void { ... }

export class GreeterClient extends grpc.Client { ... }

编译器使用 --grpc-web 运行时,JavaScript 生成器会生成名为 <module>_grpc_web.ts 的浏览器伴生文件。该文件包含适用于 grpc-web 的回调客户端;包含一元 RPC 的服务还会获得 Promise 客户端:

export class GreeterWebClient { ... }

export class GreeterWebPromiseClient { ... }

Node.js 服务伴生代码会导入 @grpc/grpc-js;浏览器伴生代码会导入 grpc-web。请将相应包添加到编译或运行生成文件的应用中。

Swift

输出布局

每个 Schema 会生成一个 Swift .swift 文件,例如:

  • <swift_out>/addressbook/addressbook.swift

类型生成

生成器使用拆分的模型宏和稳定的字段/case ID 创建 Swift 模型。类型化联合必须包含 @ForyUnknownCase case unknown(UnknownCase) 以及至少一个非 unknown case;unknown(UnknownCase) 只是由 Fory 提供的前向兼容载体。该标记只用于选择载体,不会向 Schema case 表添加条目。

当包/命名空间非空时,命名空间的组织形式由 swift_namespace_style 控制:

  • enum(默认):使用嵌套枚举作为命名空间包装器。
  • flatten:为顶层类型名称添加根据包名派生的前缀(例如 Demo_Foo_User)。

当包/命名空间为空时,不应用枚举包装器或扁平化前缀。

对于采用默认 enum 风格的非空包:

public enum Addressbook {
@ForyUnion
public enum Animal {
@ForyUnknownCase
case unknown(UnknownCase)
@ForyCase(id: 0)
case dog(Addressbook.Dog)
@ForyCase(id: 1)
case cat(Addressbook.Cat)
}

@ForyStruct
public struct Person: Equatable {
@ForyField(id: 1)
public var name: String = ""
@ForyField(id: 8)
public var pet: Addressbook.Animal = .foryDefault()
}
}

对于采用 flatten 风格的非空包:

@ForyStruct
public struct Addressbook_Person: Equatable { ... }

同时设置二者时,CLI 标志 --swift_namespace_style 会覆盖 Schema 选项 swift_namespace_style

联合类型会生成为带有关联载荷值的标签 Swift 枚举。带有 ref/weak_ref 字段的消息会生成为 final class 模型,以保留引用语义。列表/映射字段中的定长或带标签整数编码会生成为 Swift 字段类型提示,例如 @ListField(element: .encoding(.fixed))@MapField(value: .encoding(.tagged))。对于非空的定长整数列表元素,Swift 会将该字段归类为相应的 Fory 基元紧凑数组类型;定长整数集合仍然使用 Fory 集合。

模块安装

每个 Schema 都包含一个负责传递安装导入模块的 ForyModule 所有者:

public enum ForyModule {
public static func install(_ fory: Fory) throws {
try ComplexPb.ForyModule.install(fory)
fory.register(Addressbook.Person.self, id: 100)
fory.register(Addressbook.Animal.self, id: 106)
}
}

对于采用 flatten 风格的非空包,辅助类型也会添加前缀(例如 Addressbook_ForyModule)。

对于没有显式 [id=...] 的 Schema,安装代码使用计算得到的数字 ID。如果设置了 option enable_auto_type_id = false;,生成代码会使用基于名称的注册 API。

Dart

输出布局

每个 Schema 会生成两个 Dart 文件:一个包含注解类型和 IDL 模块所有者的主 .dart 文件,以及一个包含生成序列化器和元数据的 .fory.dart part 文件。

  • <dart_out>/package/package.dart
  • <dart_out>/package/package.fory.dart

类型生成

消息会生成带有 @ForyStruct 注解的 final class 声明,每个字段都带有 @ForyField

()
final class Person {
Person();

(id: 1)
String name = '';

(id: 2, type: Int32Type())
int id = 0;

(id: 7)
List<Person_PhoneNumber> phones = <Person_PhoneNumber>[];

(id: 8)
Animal pet = Animal._empty();
}

枚举会生成带有 rawValue getter 和 fromRawValue 工厂方法的 Dart enum 声明:

enum Person_PhoneType {
mobile,
home,
work;

int get rawValue => switch (this) {
Person_PhoneType.mobile => 0,
Person_PhoneType.home => 1,
Person_PhoneType.work => 2,
};

static Person_PhoneType fromRawValue(int value) => switch (value) {
0 => Person_PhoneType.mobile,
1 => Person_PhoneType.home,
2 => Person_PhoneType.work,
_ => throw StateError('Unknown Person_PhoneType raw value $value.'),
};
}

联合类型会生成带有 @ForyUnion 注解的类,其中包含工厂构造函数、case 枚举和自定义序列化器:

enum AnimalCase {
dog,
cat;

int get id => switch (this) {
AnimalCase.dog => 1,
AnimalCase.cat => 2,
};
}

()
final class Animal {
final AnimalCase _case;
final Object? _value;

const Animal._(this._case, this._value);

factory Animal.dog(Dog value) => Animal._(AnimalCase.dog, value);
factory Animal.cat(Cat value) => Animal._(AnimalCase.cat, value);

bool get isDog => _case == AnimalCase.dog;
Dog get dogValue => _value as Dog;
// ...
}

嵌套类型使用扁平下划线命名(例如 Person_PhoneNumberPerson_PhoneType)。

list<T> 字段生成有序集合载体并使用 Fory 列表协议。array<T> 字段生成密集的一维布尔或数值载体,并使用专用的密集数组协议。生成代码不能只因为某种语言拥有优化的类列表载体就选择 array<T>;Schema 类型由 IDL 决定。

IDL SchemaDart 生成载体说明
list<int32>List<int>列表协议,varint 元素编码
list<fixed int32>List<int>列表协议,定长元素段
array<bool>BoolList每个布尔值一个字节
array<int8>Int8List密集有符号字节
array<int16>Int16List密集小端序 int16
array<int32>Int32List密集小端序 int32
array<int64>Int64List密集小端序 int64
array<uint8>Uint8List密集无符号字节
array<uint16>Uint16List密集小端序 uint16
array<uint32>Uint32List密集小端序 uint32
array<uint64>Uint64List密集小端序 uint64
array<float16>Float16List密集 binary16 存储
array<bfloat16>Bfloat16List密集 bfloat16 存储
array<float32>Float32List密集小端序 float32
array<float64>Float64List密集小端序 float64

使用 ArrayType(element: BoolType()) 的生成 Dart 字段必须使用 BoolList;普通 List<bool> 仍是 list<bool> 的生成及手写载体。

列表元素或映射值的引用跟踪使用容器语法糖注解:

(element: DeclaredType(ref: true))
(id: 3)
List<Node> children = <Node>[];

(value: DeclaredType(ref: true))
(id: 2)
Map<String, Node> byName = <String, Node>{};

模块安装

每个生成的 Dart IDL 库都包含一个根据输入文件命名的模块所有者,例如 addressbook.dart 对应 AddressbookForyModule。该模块先安装导入的模块,再使用默认 IDL 标识注册每个本地 Schema 类型:

abstract final class AddressbookForyModule {
static void install(Fory fory) {
complex_pb.ComplexPbForyModule.install(fory);
_registerType(fory, Person);
_registerType(fory, Dog);
}

static Fory getFory() { ... }

static void _registerType(Fory fory, Type type) {
if (type == Person) {
registerGeneratedStruct(fory, _personForySchema, id: 100, namespace: null, typeName: null);
return;
}
// ... other types
}
}

使用示例

import 'package:fory/fory.dart';
import 'generated/addressbook/addressbook.dart';

void main() {
final fory = Fory();
AddressbookForyModule.install(fory);

final person = Person()
..name = 'Alice'
..id = 1;

final bytes = fory.serialize(person);
final roundTrip = fory.deserialize<Person>(bytes);
}

gRPC 服务伴生代码

当 Schema 包含服务且编译器使用 --grpc 运行时,Dart 生成器会在模型类型旁为每个 Schema 生成一个 <module>_grpc.dart 文件。该文件面向 package:grpc。请求和响应序列化使用伴生代码自动获取的 Fory 运行时,该运行时会在首次使用时注册 Schema 类型,因此无需手动注册;应用也可以选择在首次调用前通过 Schema 模块的 install(...) 注入自定义 Fory

生成器支持全部四种 RPC 模式:一元、服务端流式、客户端流式和双向流式。客户端类继承 Client;服务基类继承 Service,并使用 $addMethod 自行注册每个方法。

class GreeterClient extends Client {
// Single response: ResponseFuture. Streaming response: ResponseStream.
ResponseFuture<HelloReply> sayHello(HelloRequest request, {CallOptions? options}) { ... }
ResponseStream<HelloReply> sayHellos(HelloRequest request, {CallOptions? options}) { ... }
ResponseFuture<HelloReply> collectHellos(Stream<HelloRequest> request, {CallOptions? options}) { ... }
ResponseStream<HelloReply> chatHellos(Stream<HelloRequest> request, {CallOptions? options}) { ... }
}

abstract class GreeterServiceBase extends Service {
Future<HelloReply> sayHello(ServiceCall call, HelloRequest request);
Stream<HelloReply> sayHellos(ServiceCall call, HelloRequest request);
Future<HelloReply> collectHellos(ServiceCall call, Stream<HelloRequest> request);
Stream<HelloReply> chatHellos(ServiceCall call, Stream<HelloRequest> request);
}

单响应客户端方法返回 ResponseFuture<R>(客户端流式调用使用 .single 适配流式调用);流式响应方法返回 ResponseStream<R>。在服务端,实现会覆盖抽象方法:单个请求以 Q 形式接收,客户端流式请求以 Stream<Q> 形式接收;单响应返回 Future,流式响应返回 Stream。编译这些文件的应用必须提供 grpc 依赖;Fory Dart 运行时不会添加该依赖。gRPC 路径使用原始 IDL 方法名。

Kotlin

Kotlin 目标仅生成 Kotlin 源代码。编译器不会生成 Java 文件。

输出布局

对于带有 package addressbook 的源文件 addressbook.fdl,Kotlin 输出生成在:

  • <kotlin_out>/addressbook/
  • 类型文件:AddressBook.ktPerson.ktDog.ktCat.ktAnimal.kt
  • Schema 模块:AddressbookForyModule.kt

Schema 模块名称取自源文件的主文件名。同一 Kotlin 包中的 Schema 必须生成不同的文件名;写入文件前,编译器会拒绝重复的 Kotlin 生成文件路径。

如果存在 option kotlin_package = "...";,输出路径和 Kotlin 包会使用该选项;否则 Kotlin 使用 FDL 包。Kotlin 导入图不能混用默认包 Schema 与具名 Kotlin 包。注册仍使用 FDL 包,以保持跨语言类型名称稳定。

类型生成

消息默认生成 Kotlin data class 声明:

@ForyStruct
public data class Person(
@field:ForyField(id = 1)
public val name: String,

@field:ForyField(id = 7)
public val phones: List<PersonPhoneNumber>,

@field:ForyField(id = 8)
public val pet: Animal,
) {
public fun toBytes(): ByteArray = AddressbookForyModule.getFory().serialize(this)

public companion object {
public fun fromBytes(bytes: ByteArray): Person =
AddressbookForyModule.getFory().deserialize(bytes, Person::class.java)
}
}

对于编译器检测到参与构造循环的消息,生成器会生成普通可变类,使生成的序列化器可以在读取反向引用前发布该实例:

@ForyStruct
public class Node() {
@ForyField(id = 1)
public var id: String = ""

@Ref
@ForyField(id = 2)
public var parent: Node? = null
}

生成的 Kotlin IDL 源代码使用 Kotlin ? 表示可空性,而不是 Fory @Nullable;这也适用于因编译器检测到构造循环而生成的可变类。

枚举会生成带有稳定 Fory 枚举 ID 的 Kotlin 枚举类。联合类型会生成带有 @ForyUnion 的密封类;由 Fory 提供的 Unknown(UnknownCase) 载体使用 @ForyUnknownCase 标记。该标记只用于选择载体,不会向 Schema case 表添加条目。Schema 定义的 case 可以使用 0..N 的 case ID,并持有单个 value 属性。类型化联合必须至少包含一个非 Unknown case。

package addressbook

import org.apache.fory.annotation.ForyCase
import org.apache.fory.annotation.ForyUnion
import org.apache.fory.annotation.ForyUnknownCase
import org.apache.fory.type.union.UnknownCase

@ForyUnion
public sealed class Animal {
@ForyUnknownCase
public data class Unknown(public val value: UnknownCase) : Animal()

@ForyCase(id = 0)
public data class Dog(public val value: addressbook.Dog) : Animal()
}

当 Schema case 名称和载荷类型具有相同的简单名称时,带包的 Kotlin 输出会保留 Schema case 名称,并限定载荷类型。如果某种目标输出模式无法为冲突表达合法限定名,编译器会在生成的 case 类名称后添加 Case

Kotlin int32int64uint32uint64 字段默认使用 xlang varint 编码,因此生成的 Kotlin 在默认情况下不会生成 @VarInt。只有 Schema 请求非默认编码时,才会生成 @Fixed@Taggedduration 映射为 kotlin.time.Duration,编码时会拒绝无限时长。密集 array<float16>array<bfloat16> 使用 Java 核心的 Float16ArrayBFloat16Array 载体。生成的 Kotlin IDL 对 array<int8> 使用 @ArrayType ByteArray,包括嵌套位置。

Schema 模块

生成的 Schema 模块会注册 Schema 类型,并根据目标类名解析 KSP 生成的序列化器。包所持有的辅助 Fory 实例使用 ForyKotlin.builder().withXlang(true) 并安装 Schema 模块,因此消息的 toBytes/fromBytes 辅助方法无需调用方管理 Fory 配置即可工作。对于 addressbook.fdl

public object AddressbookForyModule : ForyModule {
private val fory: ThreadSafeFory by lazy {
ForyKotlin.builder()
.withXlang(true)
.withRefTracking(true)
.withModule(this)
.buildThreadSafeFory()
}

internal fun getFory(): ThreadSafeFory = fory

override fun install(fory: Fory) {
KotlinSerializers.registerType(fory, Person::class.java, 100L)
KotlinSerializers.registerSerializer(fory, Person::class.java)
KotlinSerializers.registerUnion(fory, Animal::class.java, 106L)
}
}

registerUnion 会查找生成的 <Target>_ForySerializer;调用方无需传入序列化器实例。

gRPC 服务伴生代码

当 Schema 包含服务且编译器使用 --grpc 运行时,Kotlin 生成器会在模型类型旁为每个服务生成一个 <ServiceName>GrpcKt.kt 文件。该文件包含 grpc-kotlin 协程伴生对象,而不是 Java *Grpc.java 源代码。

public object AddressBookServiceGrpcKt {
public const val SERVICE_NAME: String = "addressbook.AddressBookService"

@JvmStatic
public val serviceDescriptor: io.grpc.ServiceDescriptor
get() = serviceDescriptorValue

@JvmStatic
public val lookupMethod: io.grpc.MethodDescriptor<Person, AddressBook>
get() = lookupMethodValue

public abstract class AddressBookServiceCoroutineImplBase(
coroutineContext: kotlin.coroutines.CoroutineContext =
kotlin.coroutines.EmptyCoroutineContext,
) : io.grpc.kotlin.AbstractCoroutineServerImpl(coroutineContext) {
public open suspend fun lookup(request: Person): AddressBook =
throw io.grpc.StatusException(
io.grpc.Status.UNIMPLEMENTED.withDescription(
"Method addressbook.AddressBookService/Lookup is unimplemented",
),
)
}

public class AddressBookServiceCoroutineStub @JvmOverloads constructor(
channel: io.grpc.Channel,
callOptions: io.grpc.CallOptions = io.grpc.CallOptions.DEFAULT,
) : io.grpc.kotlin.AbstractCoroutineStub<AddressBookServiceCoroutineStub>(
channel,
callOptions,
) {
public suspend fun lookup(
request: Person,
headers: io.grpc.Metadata = io.grpc.Metadata(),
): AddressBook =
io.grpc.kotlin.ClientCalls.unaryRpc(
channel,
lookupMethod,
request,
callOptions,
headers,
)
}
}

流式 RPC 使用 kotlinx.coroutines.flow.Flow

IDL 形态服务端方法客户端方法
rpc A (Req) returns (Res)suspend fun a(request: Req): Ressuspend fun a(request: Req): Res
rpc A (Req) returns (stream Res)fun a(request: Req): Flow<Res>fun a(request: Req): Flow<Res>
rpc A (stream Req) returns (Res)suspend fun a(requests: Flow<Req>): Ressuspend fun a(requests: Flow<Req>): Res
rpc A (stream Req) returns (stream Res)fun a(requests: Flow<Req>): Flow<Res>fun a(requests: Flow<Req>): Flow<Res>

每个方法描述符都使用由 Fory 支持的 io.grpc.MethodDescriptor.Marshaller,并复用生成 Schema 模块的 ThreadSafeFory。生成的服务伴生代码不会调用 protobuf 解析器,不会公开 KSP 序列化器类名,也不会为每次调用创建 Fory 实例。

编译生成 Kotlin 服务文件的应用必须提供 grpc-java、grpc-kotlin 和 kotlinx-coroutines-core 依赖。Fory Kotlin 构件不会将这些 gRPC 依赖作为硬依赖添加。

Scala

Scala 目标仅生成 Scala 3 源代码。fory-scala 构件仍支持 Scala 2.13 和 Scala 3,但生成的 IDL 源代码和宏派生需要 Scala 3。

输出布局

对于 package addressbook,Scala 输出生成在:

  • <scala_out>/addressbook/
  • 类型文件:AddressBook.scalaPerson.scalaDog.scalaCat.scalaAnimal.scala
  • Schema 模块:AddressbookForyModule.scala

对于没有 Scala 包的 Schema,Schema 模块名称取自源文件的主文件名。例如,main.fdl 会生成 MainForyModule.scala。Scala 导入图不能混用默认包 Schema 与具名 Scala 包。

类型生成

不在编译器检测到的构造循环中的消息会生成 case 类:

import org.apache.fory.annotation.{ForyField, ForyStruct}
import org.apache.fory.scala.ForySerializer

@ForyStruct
final case class Person(
@ForyField(id = 1) name: String,
@ForyField(id = 3) email: Option[String],
@ForyField(id = 7) phones: List[Person.PhoneNumber],
@ForyField(id = 8) pet: Animal
) derives ForySerializer {
def toBytes(): Array[Byte] =
AddressbookForyModule.getFory.serialize(this)
}

object Person {
def fromBytes(bytes: Array[Byte]): Person =
AddressbookForyModule.getFory.deserialize(bytes).asInstanceOf[Person]
}

处于循环构造周期中的消息会生成带有可变序列化字段的普通类,使读取过程可以在读取反向引用前注册对象:

import org.apache.fory.annotation.{ForyField, ForyStruct, Ref}
import org.apache.fory.scala.ForySerializer

@ForyStruct
final class Node() derives ForySerializer {
@ForyField(id = 1)
var id: String = ""

@Ref
@ForyField(id = 2)
var parent: Option[Node] = None
}

枚举会生成带有稳定 Fory ID 的 Scala 3 枚举:

import org.apache.fory.annotation.ForyEnumId

enum PhoneType {
@ForyEnumId(0)
case Mobile

@ForyEnumId(1)
case Home

@ForyEnumId(2)
case Work
}

联合类型会生成 Scala 3 代数数据类型(ADT)枚举。Unknown(UnknownCase) 是由 Fory 提供、使用 @ForyUnknownCase 标记的前向兼容载体。该标记只用于选择载体,不会添加 Schema 条目,因此该载体不在 Schema case 表中。Schema 定义的 case 使用非负 @ForyCase ID。类型化联合必须至少包含一个非 Unknown case。

package addressbook

import org.apache.fory.annotation.{ForyCase, ForyUnion, ForyUnknownCase}
import org.apache.fory.scala.ForySerializer
import org.apache.fory.`type`.union.UnknownCase

@ForyUnion
enum Animal derives ForySerializer {
@ForyUnknownCase
case Unknown(value: UnknownCase)

@ForyCase(id = 0)
case Dog(value: _root_.addressbook.Dog)

@ForyCase(id = 1)
case Cat(value: _root_.addressbook.Cat)
}

当 Schema case 名称和载荷类型具有相同的简单名称时,带包的 Scala 输出会保留 Schema case 名称,并限定载荷类型。如果某种目标输出模式无法为冲突表达合法限定名,编译器会在生成的 case 名称后添加 Case

optional T 字段会生成 Option[T]。顶层消息引用在字段或构造函数参数上使用 @Ref。嵌套元素/值引用使用 List[Node @Ref] 这类类型使用注解。

Schema 模块

生成的 Schema 模块会注册 Schema 序列化器、枚举、结构体和联合类型。包所持有的辅助 Fory 实例使用 ForyScala.builder().withXlang(true) 并安装 Schema 模块,因此消息的 toBytes/fromBytes 辅助方法无需调用方管理 Fory 配置即可工作:

object AddressbookForyModule extends org.apache.fory.ForyModule {
private lazy val fory: ThreadSafeFory =
ForyScala.builder()
.withXlang(true)
.withRefTracking(true)
.withModule(this)
.buildThreadSafeFory()

private[addressbook] def getFory: ThreadSafeFory = fory

override def install(fory: Fory): Unit = {
ScalaSerializers.registerEnum(fory, classOf[Person.PhoneType], 101L)
ForySerializer.register(fory, classOf[Person.PhoneNumber], 102L)
ForySerializer.register(fory, classOf[Person], 100L)
ForySerializer.register(fory, classOf[Animal], 106L)
}
}

gRPC 服务伴生代码

当 Schema 包含服务且编译器使用 --grpc 运行时,Scala 生成器会为每个本地服务定义生成一个 <ServiceName>Grpc.scala 伴生文件。该文件与生成的模型和 Schema 模块位于同一个 Scala 包中。

对于如下服务:

service AddressBookService {
rpc Lookup (Person) returns (AddressBook);
rpc Watch (Person) returns (stream AddressBook);
rpc Upload (stream Person) returns (AddressBook);
rpc Chat (stream Person) returns (stream AddressBook);
}

生成的伴生文件包含:

  • SERVICE_NAME 和 grpc-java 方法描述符
  • 用于服务端实现的 AddressBookServiceImplBase
  • 用于客户端调用的 AddressBookServiceClient
  • 由 Fory 支持、用于请求和响应载荷的 grpc-java marshaller

生成的 Scala 客户端为每个方法保留 grpc-java API,同时为能够通过直接 Scala 句柄保留所需生命周期控制的 RPC 形态添加符合 Scala 习惯的便捷方法:

RPC 形态Scala 便捷方法grpc-java 风格方法
rpc Lookup (Person) returns (AddressBook)lookup(request): RpcFuture[Resp]异步观察者、阻塞调用和 ListenableFuture
rpc Watch (Person) returns (stream AddressBook)watch(request): RpcIterator[Resp]异步观察者和阻塞迭代器
rpc Upload (stream Person) returns (AddressBook)请求 StreamObserver
rpc Chat (stream Person) returns (stream AddressBook)请求和响应 StreamObserver

一元客户端便捷方法返回 org.apache.fory.scala.rpc.RpcFuture

val client = AddressBookServiceGrpc.newClient(channel)
val call = client.lookup(person)
call.asFuture.foreach(handleAddressBook)(scala.concurrent.ExecutionContext.global)

服务端流式客户端便捷方法返回 org.apache.fory.scala.rpc.RpcIterator

val stream = client.watch(person)
try {
while (stream.hasNext) {
handleAddressBook(stream.next())
}
} finally {
stream.close()
}

如果客户端在消费完整个流之前停止,请关闭或取消 RpcIterator。生成的适配器会取消底层 gRPC 调用,避免服务端继续写入客户端不再读取的响应流。

客户端流式和双向流式方法使用 grpc-java StreamObserver API:

val requestStream = client.upload(
new io.grpc.stub.StreamObserver[AddressBook] {
override def onNext(value: AddressBook): Unit = handleAddressBook(value)
override def onError(t: Throwable): Unit = handleError(t)
override def onCompleted(): Unit = ()
}
)
requestStream.onNext(person)
requestStream.onCompleted()

服务端实现与 grpc-java 对应。一元方法可以覆盖 Scala 生成的直接请求到响应方法,但流式方法需要覆盖基于观察者的方法,并按照 grpc-java 生命周期规则调用 onNextonErroronCompleted

编译生成 Scala gRPC 伴生文件的应用必须提供 grpc-java 依赖,例如 grpc-apigrpc-stub,以及 grpc-netty-shaded 这类传输实现。fory-scala 构件不会将 grpc-java 作为硬依赖添加。

跨语言说明

类型 ID 行为

  • 显式 [id=...] 值会由生成的模块安装代码或注册辅助方法直接使用。
  • 省略类型 ID 时,生成代码会使用计算得到的数字 ID(参见 auto_id.* 输出)。
  • 如果设置了 option enable_auto_type_id = false;,生成的模块安装代码或注册辅助方法会使用基于名称的 API,而不是数字 ID。

嵌套类型形态

语言嵌套类型形式
JavaPerson.PhoneNumber
PythonPerson.PhoneNumber
Rustperson::PhoneNumber
C++Person::PhoneNumber
GoPerson_PhoneNumber(默认)
C#Person.PhoneNumber
JavaScript/TypeScriptPerson.PhoneNumber
SwiftPerson.PhoneNumber
DartPerson_PhoneNumber
KotlinPersonPhoneNumber
ScalaPerson.PhoneNumber

字节辅助方法命名

语言辅助方法
JavatoBytes / fromBytes
KotlintoBytes / fromBytes
ScalatoBytes / fromBytes
Pythonto_bytes / from_bytes
Rustto_bytes / from_bytes
C++to_bytes / from_bytes
GoToBytes / FromBytes
C#ToBytes / FromBytes
JavaScript/TypeScript(通过 fory.serialize()
SwifttoBytes / fromBytes
Dart(通过 fory.serialize()