跳到主要内容

Fory v1.2.0 发布

· 阅读需 10 分钟
杨朝坤
Apache Fory PMC Member

Apache Fory 团队很高兴宣布 1.2.0 版本正式发布。本版本包含来自 9 位贡献者的 38 个 PR,并继续改进各支持语言的跨语言运行时。请访问 Install 页面 获取各平台安装方式。

发布亮点

  • 扩展 Go、Rust、Kotlin、Scala、C# 和 JavaScript 的 gRPC 生成支持,其中 JavaScript 同时支持 Node.js 和浏览器 gRPC-Web。
  • 改进跨语言兼容性,细化 register-by-name API,支持兼容模式下的标量字段读取转换,并让 native serialization 默认启用兼容模式。
  • 强化 Java 平台支持,增加 Java 9/16 module-info 生成,并为 JDK 25 移除 sun.misc.Unsafe 使用。
  • 通过更多读取检查、deflater 泄漏修复以及更安全的 serializer/type-info 错误处理提升运行时安全性和鲁棒性。
  • 优化兼容模式和 row-format 性能,包括更快的兼容读取、紧凑 row layout 缓存以及内联 custom-codec dispatch。
  • 提升 Rust、C++ 和 service 生成质量,包括更好的标识符转义、名称冲突处理、嵌套容器引用处理和 map 代码生成。

Java 25+ 不再依赖 sun.misc.Unsafe

JDK 25 继续推动平台远离 sun.misc.Unsafe。Fory 1.2.0 为核心运行时增加了 Java 25 multi-release 实现,使应用在 JDK 25+ 上运行时,不再从 Fory 的有效 class graph 中解析 sun.misc.Unsafe

该实现仍保留 JDK 8-24 上的快速路径;但在 JDK 25+ 上会从 jar 的 versioned 区域加载替换类。这些替换类将字段访问和 memory-buffer 原语迁移到 JVM 支持的机制,例如 VarHandleMethodHandle、数组和 ByteBuffer。绕过构造器的对象分配也会被显式处理:在 JDK 25+ 上,过去依赖 Unsafe allocation 的类应提供可访问的无参构造器、使用 record 构造,或注册自定义 serializer。

这对 Java 用户是一个重要兼容性里程碑:应用运行在 Java 25 及之后版本时,Fory runtime 不再依赖已被终止弃用的 Unsafe API 路径。

兼容标量字段读取

兼容模式已经允许读写双方添加、删除和重排字段。Fory 1.2.0 将这个模型扩展到部分标量类型变更:当匹配到的顶层字段在 boolean、string、numeric 和 decimal 类型之间变化时,只要转换是无损的,reader 就可以完成反序列化。

例如,"123" 可以读取为整数字段,10 可以读取为 boolean 字段,boolean 可以读取为 1/0,数字或 decimal 可以读取为规范字符串;数字 widening 或 narrowing 只有在不丢失范围或精度时才会成功。无效字符串、越界值、有损 float/integer 转换以及开启引用跟踪的标量类型变更会在反序列化时报错。该转换只应用于匹配到的兼容字段,不应用于根值或集合元素。

下面示例展示 Rust、Python、Java 和 C++。兼容标量字段转换也支持 Fory 的所有兼容模式运行时:Java、Python、Rust、C++、Go、C#、Swift、Dart、JavaScript/TypeScript、Kotlin 和 Scala。

Java 和 Python 的 xlang 与 native serialization 默认都启用兼容模式,也可以显式配置:

Fory fory = Fory.builder()
.withXlang(true)
.withCompatible(true)
.build();
import pyfory

fory = pyfory.Fory(xlang=True, compatible=True)
native_fory = pyfory.Fory(xlang=False, compatible=True)

Rust 示例:

use fory::{Fory, ForyStruct};

#[derive(ForyStruct)]
struct MetricV1 {
value: i64,
}

#[derive(ForyStruct)]
struct MetricV2 {
value: String,
}

let mut writer = Fory::builder().xlang(true).compatible(true).build();
writer.register_by_name::<MetricV1>("example.Metric")?;

let mut reader = Fory::builder().xlang(true).compatible(true).build();
reader.register_by_name::<MetricV2>("example.Metric")?;

let bytes = writer.serialize(&MetricV1 { value: 42 })?;
let value: MetricV2 = reader.deserialize(&bytes)?;
assert_eq!(value.value, "42");

Python 示例:

from dataclasses import dataclass
import pyfory

@dataclass
class MetricV1:
value: pyfory.Int64

@dataclass
class MetricV2:
value: str

writer = pyfory.Fory(xlang=True, compatible=True)
writer.register(MetricV1, name="example.Metric")

reader = pyfory.Fory(xlang=True, compatible=True)
reader.register(MetricV2, name="example.Metric")

data = writer.dumps(MetricV1(42))
assert reader.loads(data).value == "42"

Java 示例:

public class MetricV1 {
public long value;
}

public class MetricV2 {
public String value;
}

Fory writer = Fory.builder().withXlang(true).withCompatible(true).build();
writer.register(MetricV1.class, "example", "Metric");

Fory reader = Fory.builder().withXlang(true).withCompatible(true).build();
reader.register(MetricV2.class, "example", "Metric");

MetricV1 source = new MetricV1();
source.value = 42L;
byte[] bytes = writer.serialize(source);
MetricV2 value = reader.deserialize(bytes, MetricV2.class);
assert value.value.equals("42");

C++ 示例:

#include <cassert>
#include <string>
#include "fory/serialization/fory.h"

using namespace fory::serialization;

struct MetricV1 {
int64_t value;
};
FORY_STRUCT(MetricV1, value);

struct MetricV2 {
std::string value;
};
FORY_STRUCT(MetricV2, value);

auto writer = Fory::builder().xlang(true).compatible(true).build();
auto reader = Fory::builder().xlang(true).compatible(true).build();

writer.register_struct<MetricV1>("example", "Metric");
reader.register_struct<MetricV2>("example", "Metric");

auto bytes = writer.serialize(MetricV1{42}).value();
auto value = reader.deserialize<MetricV2>(bytes).value();
assert(value.value == "42");

反方向同样适用,例如将 String 字段值 "42" 读取为 int64,前提是字符串满足 Fory 严格的有限十进制语法,并且目标范围可以精确表示该值。

生成 gRPC 支持

Fory 1.2.0 扩展了 compiler 生成的 gRPC service companion。生成的 service 使用标准 gRPC transport、channel、deadline、metadata、interceptor、status code 和 streaming shape,但 request/response 对象使用 Fory 编码,而不是 protobuf message bytes。当 RPC 两端都由同一份 Fory IDL、protobuf IDL 或 FlatBuffers IDL 生成,并且你希望使用 gRPC 运维语义与 Fory payload 编码时,可以使用这种模式。

生成 gRPC 现在覆盖 Java、Python、Go、Rust、C#、Scala、Kotlin 和 JavaScript/TypeScript。JavaScript 同时支持 Node.js gRPC 和浏览器 gRPC-Web client 生成。

下面示例使用同一个 schema:

package demo.greeter;

message HelloRequest {
string name = 1;
}

message HelloReply {
string reply = 1;
}

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

Rust 生成基于 tonic 的 service API 和 binding module:

foryc service.fdl --rust_out=./generated/rust --grpc
use demo_greeter::{HelloReply, HelloRequest};
use demo_greeter_service::Greeter;
use demo_greeter_service_grpc::greeter_client::GreeterClient;
use demo_greeter_service_grpc::greeter_server::GreeterServer;

tonic::transport::Server::builder()
.add_service(GreeterServer::new(MyGreeter::default()))
.serve(addr)
.await?;

let mut client = GreeterClient::connect("http://[::1]:50051").await?;
let reply = client.say_hello(HelloRequest { name: "Fory".into() }).await?;

Go 生成 grpc-go interface 和 Fory-backed CodecV2

foryc service.fdl --go_out=./generated/go --grpc
server := grpc.NewServer(
grpc.ForceServerCodecV2(greeter.CodecV2{}),
)
greeter.RegisterGreeterServer(server, greeterService{})

client := greeter.NewGreeterClient(conn)
reply, err := client.SayHello(ctx, &greeter.HelloRequest{Name: "Fory"})

JavaScript 同时支持 Node.js gRPC 和浏览器 gRPC-Web:

foryc service.fdl --javascript_out=./generated/javascript --grpc --grpc-web
import * as grpc from "@grpc/grpc-js";
import { addGreeterService, createGreeterClient } from "./service_grpc";
import { createGreeterWebPromiseClient } from "./service_grpc_web";

addGreeterService(server, greeterHandlers);

const nodeClient = createGreeterClient(
"localhost:50051",
grpc.credentials.createInsecure(),
);
nodeClient.sayHello({ name: "Fory" }, callback);

const webClient = createGreeterWebPromiseClient("https://api.example.com");
const reply = await webClient.sayHello({ name: "Fory" });

浏览器 gRPC-Web 遵循 gRPC-Web transport 限制:支持 unary 和 server-streaming 方法;client-streaming 和 bidirectional streaming 仍属于 Node.js/native gRPC 形态。

Kotlin 生成 grpc-kotlin coroutine companion:

foryc service.fdl --kotlin_out=./generated/kotlin --grpc
class GreeterService : GreeterGrpcKt.GreeterCoroutineImplBase() {
override suspend fun sayHello(request: HelloRequest): HelloReply =
HelloReply(reply = "Hello, ${request.name}")
}

val server = ServerBuilder
.forPort(50051)
.addService(GreeterService())
.build()
.start()

val stub = GreeterGrpcKt.GreeterCoroutineStub(channel)
val reply = stub.sayHello(HelloRequest(name = "Fory"))

Scala 生成 Scala 3 grpc-java companion,并提供更符合 Scala 使用习惯的 client handle:

foryc service.fdl --scala_out=./generated/scala --grpc
final class GreeterService extends GreeterGrpc.GreeterImplBase {
override def sayHello(request: HelloRequest): HelloReply =
HelloReply(s"Hello, ${request.name}")
}

val server = ServerBuilder
.forPort(50051)
.addService(new GreeterService)
.build()
.start()

val client = GreeterGrpc.newClient(channel)
val reply = Await.result(client.sayHello(HelloRequest("Fory")).asFuture, 30.seconds)

生成的 gRPC companion 不会让核心 Fory 语言包强依赖 gRPC。应用按实际使用的 transport 添加依赖:Java 和 Scala 使用 grpc-java,Python 使用 grpcio,Go 使用 grpc-go,Rust 使用 tonic/bytes,C# 使用 .NET gRPC 包,JavaScript 使用 @grpc/grpc-jsgrpc-web,Kotlin 使用 grpc-java/grpc-kotlin。

功能特性

Bug 修复

其他改进

新贡献者

完整变更日志https://github.com/apache/fory/compare/v1.1.0...v1.2.0