凌云的博客

行胜于言

Protobuf 版本概述[译]

分类:proto| 发布时间:2025-01-13 08:47:00

原文: Protobuf Editions Overview

Protobuf Editions 功能概述。

Protobuf Editions 取代了我们用于 Protocol Buffers 的 proto2 和 proto3 名称。 您无需在 proto 定义文件的顶部添加 syntax = "proto2"syntax = "proto3",而是使用版本号(例如 edition = "2023")来指定文件将具有的默认行为。 版本使语言能够随着时间的推移逐步发展。

Editions 代表一组功能,每个功能都有一个默认值(行为),而不是旧版本所具有的硬编码行为。 功能是文件、消息、字段、枚举等的选项,用于指定 protoc、代码生成器和 protobuf 运行时的行为。 当您的需求与所选版本的默认行为不匹配时,您可以在这些不同级别(文件、消息、字段等)明确覆盖行为。 您还可以覆盖您的覆盖。本主题后面关于词法作用域的部分将对此进行更详细的介绍。

截至目前(2025-01-13),最后一个发布版本是 2023。

功能的生命周期

Editions 为功能的生命周期提供了基本增量。 功能具有预期的生命周期:引入它、更改其默认行为、弃用它,然后删除它。例如:

  1. 版本 2031 创建 feature.amazing_new_feature,默认值为 false。此值保持与所有早期版本相同的行为。 也就是说,它默认为无影响。
  2. 开发人员将其 .proto 文件更新为 edition ="2031"
  3. 在后续版本(例如版本 2033)将 feature.amazing_new_feature 的默认值从 false 切换为 true。 这是所有 proto 的期望行为,也是 protobuf 团队创建该功能的原因。
    使用 Prototiller 工具将早期版本的 proto 文件迁移到版本 2033 会根据需要添加显式 feature.amazing_new_feature = false 条目以继续保留以前的行为。 当开发人员希望将新行为应用于他们的 .proto 文件时,他们会删除这些新添加的设置。
  4. 在某个时候,feature.amazing_new_feature 在某个版本中被标记为弃用,并在后续版本中删除。
    删除某个功能时,该行为的代码生成器和支持该行为的运行时库也可能会删除。不过,时间安排会很宽松。 按照生命周期早期步骤中的示例,弃用可能发生在 2034 版,但直到大约两年后的 2036 版才会被删除。 删除功能总是会引发主要版本升级。

由于此生命周期,任何未使用弃用功能的 .proto 文件都可以从一个版本升级到下一个版本,无需操作。 您将有足够的时间来升级您的代码,包括 Google 迁移的整个窗口期以及弃用期。

前面的生命周期示例对功能使用了布尔值,但功能也可以使用枚举。 例如,features.field_presence 具有值 LEGACY_REQUIREDEXPLICITIMPLICIT

迁移到 Protobuf Editions

Editions 不会破坏现有二进制文件,也不会更改消息的二进制、文本或 JSON 序列化格式。 第一版的破坏性尽可能小。 第一版建立了基线,并将 proto2 和 proto3 定义合并为新的单一定义格式。

当后续版本发布时,功能的默认行为可能会发生变化。 您可以让 Prototiller 对 .proto 文件进行无操作转换,也可以选择接受部分或全部新行为。 计划大约每年发布一次版本。

转换 Proto2 到 Editions

本节展示了一个 proto2 文件,以及运行 Prototiller 工具将定义文件更改为使用 Protobuf Editions 语法后它可能是什么样子。

Proto2 语法

// proto2 file
syntax = "proto2";

package com.example;

message Player {
  // in proto2, optional fields have explicit presence
  optional string name = 1 [default = "N/A"];
  // proto2 still supports the problematic "required" field rule
  required int32 id = 2;
  // in proto2 this is not packed by default
  repeated int32 scores = 3;

  enum Handed {
    HANDED_UNSPECIFIED = 0;
    HANDED_LEFT = 1;
    HANDED_RIGHT = 2;
    HANDED_AMBIDEXTROUS = 3;
  }

  // in proto2 enums are closed
  optional Handed handed = 4;

  reserved "gender";
}

Editions 语法

// Edition version of proto2 file
edition = "2023";

package com.example;

option features.utf8_validation = NONE;

message Player {
  // fields have explicit presence, so no explicit setting needed
  string name = 1 [default = "N/A"];
  // to match the proto2 behavior, LEGACY_REQUIRED is set at the field level
  int32 id = 2 [features.field_presence = LEGACY_REQUIRED];
  // to match the proto2 behavior, EXPANDED is set at the field level
  repeated int32 scores = 3 [features.repeated_field_encoding = EXPANDED];

  enum Handed {
    // this overrides the default edition 2023 behavior, which is OPEN
    option features.enum_type = CLOSED;
    HANDED_UNSPECIFIED = 0;
    HANDED_LEFT = 1;
    HANDED_RIGHT = 2;
    HANDED_AMBIDEXTROUS = 3;
  }

  Handed handed = 4;

  reserved gender;
}

转换 Proto3 到 Editions

本节展示了一个 proto3 文件,以及运行 Prototiller 工具将定义文件更改为使用 Protobuf Editions 语法后它可能是什么样子。

Proto3 语法

// proto3 file
syntax = "proto3";

package com.example;

message Player {
  // in proto3, optional fields have explicit presence
  optional string name = 1 [default = "N/A"];
  // in proto3 no specified field rule defaults to implicit presence
  int32 id = 2;
  // in proto3 this is packed by default
  repeated int32 scores = 3;

  enum Handed {
    HANDED_UNSPECIFIED = 0;
    HANDED_LEFT = 1;
    HANDED_RIGHT = 2;
    HANDED_AMBIDEXTROUS = 3;
  }

  // in proto3 enums are open
  optional Handed handed = 4;

  reserved "gender";
}

Editions 语法

// Editions version of proto3 file
edition = "2023";

package com.example;

message Player {
  // fields have explicit presence, so no explicit setting needed
  string name = 1 [default = "N/A"];
  // to match the proto3 behavior, IMPLICIT is set at the field level
  int32 id = 2 [features.field_presence = IMPLICIT];
  // PACKED is the default state, and is provided just for illustration
  repeated int32 scores = 3 [features.repeated_field_encoding = PACKED];

  enum Handed {
    HANDED_UNSPECIFIED = 0;
    HANDED_LEFT = 1;
    HANDED_RIGHT = 2;
    HANDED_AMBIDEXTROUS = 3;
  }

  Handed handed = 4;

  reserved gender;
}

词法作用域

Editions 语法支持词汇作用域,每个功能都有一个允许的目标列表。 例如,在第一版中,只能在文件级别或最低粒度级别指定功能。 词汇作用域的实现使您能够在整个文件中设置功能的默认行为,然后在消息、字段、枚举、枚举值、oneof、服务或方法级别覆盖该行为。 当在同一范围(字段、枚举值)内未进行任何设置时,将应用在更高级别(文件、消息)进行的设置。 任何未明确设置的功能都符合 .proto 文件使用的版本中定义的行为。

以下代码示例显示了在文件、字段和枚举级别设置的一些功能。 设置位于突出显示的行中:

edition = "2023";

option features.enum_type = CLOSED;

message Person {
  string name = 1;
  int32 id = 2 [features.field_presence = IMPLICIT];

  enum Pay_Type {
    PAY_TYPE_UNSPECIFIED = 1;
    PAY_TYPE_SALARY = 2;
    PAY_TYPE_HOURLY = 3;
  }

  enum Employment {
    option features.enum_type = OPEN;
    EMPLOYMENT_UNSPECIFIED = 0;
    EMPLOYMENT_FULLTIME = 1;
    EMPLOYMENT_PARTTIME = 2;
  }
  Employment employment = 4;
}

在前面的例子中,存在功能设置为 IMPLICIT;如果未设置,则默认为 EXPLICIT。 Pay_Type 枚举将处于 CLOSED 状态,因为它应用了文件级设置。 但是,Employment 枚举将处于 OPEN 状态,因为它是在枚举内设置的。

Prototiller

目前,所有版本格式的转换均由 Protobuf 团队处理。

当转换为自助服务模式时,我们将提供迁移指南和迁移工具,以简化版本之间的迁移。 该工具名为 Prototiller,可让您:

  • 将 proto2 和 proto3 定义文件大规模转换为新版本语法
  • 将文件从一个版本迁移到另一个版本
  • 以其他方式操作 proto 文件

向后兼容性

我们正在开发 Protobuf Editions,力求对现有系统影响最小。 例如,您可以将 proto2 和 proto3 的定义文件导入到 editions 版本的定义文件中,反之亦然:

// file myproject/foo.proto
syntax = "proto2";

enum Employment {
  EMPLOYMENT_UNSPECIFIED = 0;
  EMPLOYMENT_FULLTIME = 1;
  EMPLOYMENT_PARTTIME = 2;
}
// file myproject/edition.proto
edition = "2023";

import "myproject/foo.proto";

尽管从 proto2 或 proto3 迁移到 editions 版本时生成的代码可能会有所不同,但其底层的数据传输格式保持不变。 因此,您仍然可以使用 editions 语法定义的 proto 文件来读取和写入 proto2 或者 proto3 格式的数据文件或数据流。

语法变化

与 proto2 和 proto3 相比,editions 版本有一些语法上的变化。

语法说明: 不再使用 syntax 元素,而是使用 edition 元素:

syntax = "proto2";
syntax = "proto3";
edition = "2028";

保留名称: 保留字段名和枚举值名时,不再需要使用引号:

reserved foo, bar;

群组语法: proto2 中可用的群组语法在 editions 中已被移除。群组使用的特殊传输格式仍然可以通过使用 DELIMITED 消息编码来获得。

Required 标签: 仅在 proto2 中可用的 required 标签在 editions 中不可用。 底层功能仍然可用(但不推荐),可以通过使用 features.field_presence=LEGACY_REQUIRED 来实现。