Реализация поддержки редакций

Инструкции по реализации поддержки редакций в средах выполнения и плагинах.

В этой теме объясняется, как реализовать редакции в новых средах выполнения и генераторах.

Обзор

Редакция 2023

Первой выпущенной редакцией является Редакция 2023, которая предназначена для объединения синтаксисов proto2 и proto3. Функции, которые мы добавили для покрытия различий в поведении, подробно описаны в разделе Настройки функций для редакций.

Определение функции

В дополнение к поддержке редакций и глобальных функций, которые мы определили, вам может понадобиться определить свои собственные функции, чтобы использовать инфраструктуру. Это позволит вам определять произвольные функции, которые могут использоваться вашими генераторами и средами выполнения для управления новыми поведениями. Первый шаг — запросить номер расширения для сообщения FeatureSet в descriptor.proto выше 9999. Вы можете отправить pull-request нам в GitHub, и он будет включен в наш следующий релиз (см., например, #15439).

Как только у вас будет номер расширения, вы можете создать свой features proto (аналогично cpp_features.proto). Они обычно выглядят примерно так:

edition = "2023";

package foo;

import "google/protobuf/descriptor.proto";

extend google.protobuf.FeatureSet {
  MyFeatures features = <номер расширения>;
}

message MyFeatures {
  enum FeatureValue {
    FEATURE_VALUE_UNKNOWN = 0;
    VALUE1 = 1;
    VALUE2 = 2;
  }

  FeatureValue feature_value = 1 [
    targets = TARGET_TYPE_FIELD,
    targets = TARGET_TYPE_FILE,
    feature_support = {
      edition_introduced: EDITION_2023,
      edition_deprecated: EDITION_2024,
      deprecation_warning: "Функция будет удалена в 2025",
      edition_removed: EDITION_2025,
    },
    edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
    edition_defaults = { edition: EDITION_2024, value: "VALUE2" }
  ];
}

Здесь мы определили новую функцию перечисления foo.feature_value (в настоящее время поддерживаются только логические и перечислимые типы). В дополнение к определению значений, которые она может принимать, вам также нужно указать, как ее можно использовать:

  • Цели (Targets) - указывает типы дескрипторов proto, к которым может быть прикреплена эта функция. Это контролирует, где пользователи могут явно указать функцию. Каждый тип должен быть явно перечислен.
  • Поддержка функции (Feature support) - указывает срок жизни этой функции относительно редакции. Вы должны указать редакцию, в которой она была введена, и она не будет разрешена до этого. Вы можете дополнительно устареть или удалить функцию в более поздних редакциях.
  • Значения по умолчанию для редакций (Edition defaults) - указывает любые изменения значения по умолчанию для функции. Это должно охватывать каждую поддерживаемую редакцию, но вы можете опустить любую редакцию, где значение по умолчанию не изменилось. Обратите внимание, что EDITION_PROTO2 и EDITION_PROTO3 могут быть указаны здесь для предоставления значений по умолчанию для "устаревших" редакций (см. Устаревшие редакции).

Что такое функция?

Функции предназначены для предоставления механизма постепенного отказа от плохого поведения со временем, на границах редакций. Хотя сроки фактического удаления функции могут составлять годы (или десятилетия) в будущем, желаемой целью любой функции должно быть eventual removal (окончательное удаление). Когда идентифицируется плохое поведение, вы можете ввести новую функцию, которая защищает исправление. В следующей редакции (или, возможно, позже) вы переключили бы значение по умолчанию, все еще позволяя пользователям сохранять свое старое поведение при обновлении. В какой-то момент в будущем вы пометили бы функцию как устаревшую, что вызвало бы пользовательское предупреждение для любых пользователей, переопределяющих ее. В более поздней редакции вы затем пометили бы ее как удаленную, предотвращая дальнейшее переопределение пользователями (но значение по умолчанию все равно будет применяться). До тех пор, пока поддержка этой последней редакции не будет прекращена в breaking release, функция останется usable (используемой) для protobuf, застрявших на старых редакциях, давая им время на миграцию.

Флаги, управляющие опциональными поведениями, которые вы не собираетесь удалять, лучше реализовывать как пользовательские опции. Это связано с причиной, по которой мы ограничили функции либо логическими, либо перечислимыми типами. Любое поведение, управляемое (относительно) неограниченным количеством значений, вероятно, не очень подходит для framework редакций, поскольку нереалистично eventually turn down (со временем отключить) так много различных поведений.

Одно предостережение к этому — поведения, связанные с wire boundaries (границами бинарного формата). Использование специфичных для языка функций для управления поведением сериализации или разбора может быть опасным, поскольку с другой стороны может быть любой другой язык. Изменения wire-format (бинарного формата) всегда должны контролироваться глобальными функциями в descriptor.proto, которые могут единообразно соблюдаться каждой средой выполнения.

Генераторы

Генераторы, написанные на C++, получают многое бесплатно, потому что они используют среду выполнения C++. Им не нужно самим обрабатывать Разрешение функций, и если им нужны какие-либо расширения функций, они могут зарегистрировать их в GetFeatureExtensions в своем CodeGenerator. Они обычно могут использовать GetResolvedSourceFeatures для доступа к разрешенным функциям для дескриптора в codegen и GetUnresolvedSourceFeatures для доступа к их собственным неразрешенным функциям.

Плагины, написанные на том же языке, что и среда выполнения, для которой они генерируют код, могут требовать некоторой пользовательской bootstrapping (начальной загрузки) для их определений функций.

Явная поддержка

Генераторы должны точно указывать, какие редакции они поддерживают. Это позволяет вам безопасно добавлять поддержку редакции после ее выпуска, по вашему собственному расписанию. Protoc будет отклонять любые protobuf редакций, отправленные генераторам, которые не включают FEATURE_SUPPORTS_EDITIONS в поле supported_features их CodeGeneratorResponse. Кроме того, у нас есть поля minimum_edition и maximum_edition для указания вашего точного окна поддержки. После того как вы определили весь код и изменения функций для новой редакции, вы можете увеличить maximum_edition, чтобы сообщить об этой поддержке.

Тесты кодогенерации

У нас есть набор тестов кодогенерации, которые можно использовать для фиксации того, что Редакция 2023 не производит непредвиденных функциональных изменений. Они были очень полезны в языках, таких как C++ и Java, где значительная часть функциональности находится в gencode. С другой стороны, в языках, таких как Python, где gencode — это в основном просто коллекция сериализованных дескрипторов, они не так полезны.

Эта инфраструктура еще не является переиспользуемой, но планируется быть таковой в будущем релизе. В тот момент вы сможете использовать их для проверки того, что миграция на редакции не имеет никаких непредвиденных изменений кодогенерации.

Среды выполнения

Среды выполнения без рефлексии или динамических сообщений не должны нуждаться в каких-либо действиях для реализации редакций. Вся эта логика должна обрабатываться генератором кода.

Языки с рефлексией, но без динамических сообщений нуждаются в разрешенных функциях, но могут дополнительно выбрать обработку только в их генераторе. Это может быть сделано путем передачи как разрешенных, так и неразрешенных наборов функций в среду выполнения во время кодогенерации. Это позволяет избежать повторной реализации Разрешения функций в среде выполнения с основным недостатком эффективности, поскольку это создаст уникальный набор функций для каждого дескриптора.

Языки с динамическими сообщениями должны полностью реализовывать редакции, потому что им нужно иметь возможность строить дескрипторы во время выполнения.

Рефлексия синтаксиса

Первый шаг в реализации редакций в среде выполнения с рефлексией — это удалить все прямые проверки ключевого слова syntax. Все они должны быть перемещены в более детальные вспомогательные функции, которые могут продолжать использовать syntax, если это необходимо.

Следующие вспомогательные функции должны быть реализованы для дескрипторов, с соответствующими языку названиями:

  • FieldDescriptor::has_presence - Имеет ли поле явное присутствие
    • Повторяющиеся поля никогда не имеют присутствия
    • Поля сообщений, расширений и oneof всегда имеют явное присутствие
    • Все остальное имеет присутствие тогда и только тогда, когда field_presence не IMPLICIT
  • FieldDescriptor::is_required - Является ли поле обязательным
  • FieldDescriptor::requires_utf8_validation - Должно ли поле проверяться на валидность utf8
  • FieldDescriptor::is_packed - Имеет ли повторяющееся поле упакованное (packed) кодирование
  • FieldDescriptor::is_delimited - Имеет ли поле сообщения разделенное (delimited) кодирование
  • EnumDescriptor::is_closed - Является ли перечисление закрытым

{{% alert title="Примечание" color="note" %}} В большинстве языков функция кодирования сообщения все еще сигнализируется с помощью TYPE_GROUP, а обязательные поля все еще имеют установленный LABEL_REQUIRED. Это не идеально и было сделано, чтобы облегчить миграции downstream. В конечном итоге эти следует перенести на соответствующие вспомогательные функции и TYPE_MESSAGE/LABEL_OPTIONAL.{{% /alert %}}

Пользователи downstream должны перейти на эти новые вспомогательные функции вместо использования синтаксиса напрямую. Следующий класс существующих API дескрипторов в идеале следует устареть и в конечном итоге удалить, поскольку они раскрывают информацию о синтаксисе:

  • FileDescriptor syntax
  • API proto3 optional
    • FieldDescriptor::has_optional_keyword
    • OneofDescriptor::is_synthetic
    • Descriptor::*real_oneof* - следует переименовать в просто "oneof", а существующие вспомогательные функции "oneof" следует удалить, поскольку они раскрывают информацию о синтетических oneof (которых не существует в редакциях).
  • Тип Group
    • Значение перечисления TYPE_GROUP следует удалить, заменив его на вспомогательную функцию is_delimited.
  • Метка Required
    • Значение перечисления LABEL_REQUIRED следует удалить, заменив его на вспомогательную функцию is_required.

Существует много классов пользовательского кода, где эти проверки существуют, но не являются враждебными к редакциям. Например, код, который должен обрабатывать proto3 optional особо из-за его синтетической реализации oneof, не будет враждебен к редакциям, пока полярность выглядит как syntax == "proto3" (а не syntax != "proto2").

Если невозможно полностью удалить эти API, их следует устареть и не поощрять.

Видимость функций

Как обсуждалось в editions-feature-visibility, feature protos должны оставаться внутренней деталью любой реализации Protobuf. Поведения, которые они контролируют, должны быть доступны через методы дескриптора, но сами protos не должны. Примечательно, что это означает, что любые опции, которые предоставляются пользователям, должны иметь свои поля features удалены.

Единственный случай, когда мы разрешаем функциям просачиваться наружу, — это при сериализации дескрипторов. Полученные descriptor protos должны быть точным представлением исходных proto файлов и должны содержать неразрешенные функции внутри опций.

Устаревшие редакции

Как более подробно обсуждается в legacy-syntax-editions, отличный способ получить ранний охват вашей реализации редакций — это унифицировать proto2, proto3 и редакции. Это эффективно мигрирует proto2 и proto3 в редакции под капотом и заставляет все вспомогательные функции, реализованные в Рефлексии синтаксиса, использовать функции исключительно (вместо ветвления на синтаксисе). Это можно сделать, вставив фазу feature inference (вывода функций) в Разрешение функций, где различные аспекты proto файла могут информировать о том, какие функции уместны. Эти функции затем могут быть объединены с функциями родителя, чтобы получить разрешенный набор функций.

Хотя мы предоставляем разумные значения по умолчанию для proto2/proto3 уже для редакции 2023, требуются следующие дополнительные выводы:

  • required - мы выводим LEGACY_REQUIRED присутствие, когда поле имеет LABEL_REQUIRED
  • groups - мы выводим DELIMITED кодирование сообщения, когда поле имеет TYPE_GROUP
  • packed - мы выводим PACKED кодирование, когда опция packed истинна
  • expanded - мы выводим EXPANDED кодирование, когда поле proto3 имеет packed явно установленным в false

Тесты на соответствие

Были добавлены специфичные для редакций тесты на соответствие, но для них нужно явно opted-in (дать согласие). Флаг --maximum_edition 2023 может быть передан runner (исполнителю), чтобы включить их. Вам нужно будет настроить ваш testee binary (тестируемый бинарный файл) для обработки следующих новых типов сообщений:

  • protobuf_test_messages.editions.proto2.TestAllTypesProto2 - Идентично старому proto2 сообщению, но преобразованному в редакцию 2023
  • protobuf_test_messages.editions.proto3.TestAllTypesProto3 - Идентично старому proto3 сообщению, но преобразованному в редакцию 2023
  • protobuf_test_messages.editions.TestAllTypesEdition2023 - Используется для покрытия специфичных для редакции-2023 тестовых случаев

Разрешение функций

Редакции используют лексическую область видимости для определения функций, что означает, что любой не-C++ код, которому необходимо реализовать поддержку редакций, должен будет повторно реализовать наш алгоритм разрешения функций. Однако основная часть работы выполняется самим protoc, который можно настроить на вывод промежуточного сообщения FeatureSetDefaults. Это сообщение содержит "компиляцию" набора файлов определений функций, излагая значения функций по умолчанию в каждой редакции.

Например, определение функции выше скомпилировалось бы в следующие значения по умолчанию между proto2 и редакцией 2025 (в нотации text-format):

defaults {
  edition: EDITION_PROTO2
  overridable_features { [foo.features] {} }
  fixed_features {
    // Глобальные значения функций по умолчанию…
    [foo.features] { feature_value: VALUE1 }
  }
}
defaults {
  edition: EDITION_PROTO3
  overridable_features { [foo.features] {} }
  fixed_features {
    // Глобальные значения функций по умолчанию…
    [foo.features] { feature_value: VALUE1 }
  }
}
defaults {
  edition: EDITION_2023
  overridable_features {
    // Глобальные значения функций по умолчанию…
    [foo.features] { feature_value: VALUE1 }
  }
}
defaults {
  edition: EDITION_2024
  overridable_features {
    // Глобальные значения функций по умолчанию…
    [foo.features] { feature_value: VALUE2 }
  }
}
defaults {
  edition: EDITION_2025
  overridable_features {
    // Глобальные значения функций по умолчанию…
  }
  fixed_features { [foo.features] { feature_value: VALUE2 } }
}
minimum_edition: EDITION_PROTO2
maximum_edition: EDITION_2025

Глобальные значения функций по умолчанию опущены для компактности, но они также были бы присутствуют. Этот объект содержит упорядоченный список каждой редакции с уникальным набором значений по умолчанию (некоторые редакции могут в итоге отсутствовать) в указанном диапазоне. Каждый набор значений по умолчанию разделен на переопределяемые и фиксированные функции. Первые — это поддерживаемые функции для редакции, которые могут быть свободно переопределены пользователями. Фиксированные функции — это те, которые еще не были введены или были удалены, и не могут быть переопределены пользователями.

Мы предоставляем правило Bazel для компиляции этих промежуточных объектов:

load("@com_google_protobuf//editions:defaults.bzl", "compile_edition_defaults")

compile_edition_defaults(
    name = "my_defaults",
    srcs = ["//some/path:lang_features_proto"],
    maximum_edition = "PROTO2",
    minimum_edition = "2024",
)

Выходной FeatureSetDefaults может быть встроен в строковый литерал в виде сырых данных (raw string literal) на том языке, в котором вам нужно выполнить разрешение функций. Мы также предоставляем макрос embed_edition_defaults для этого:

embed_edition_defaults(
    name = "embed_my_defaults",
    defaults = ":my_defaults",
    output = "my_defaults.h",
    placeholder = "DEFAULTS_DATA",
    template = "my_defaults.h.template",
)

В качестве альтернативы, вы можете вызвать protoc напрямую (вне Bazel), чтобы сгенерировать эти данные:

protoc --edition_defaults_out=defaults.binpb --edition_defaults_minimum=PROTO2 --edition_defaults_maximum=2023 <файлы функций...>

Как только сообщение defaults подключено и разобрано вашим кодом, разрешение функций для дескриптора файла в данной редакции следует простому алгоритму:

  1. Проверить, что редакция находится в соответствующем диапазоне [minimum_edition, maximum_edition]
  2. Выполнить бинарный поиск по упорядоченному полю defaults для самой высокой записи, меньшей или равной редакции
  3. Объединить overridable_features в fixed_features из выбранных значений по умолчанию
  4. Объединить любые явные функции, установленные в дескрипторе (поле features в опциях файла)

Оттуда вы можете рекурсивно разрешать функции для всех других дескрипторов:

  1. Инициализировать набором функций родительского дескриптора
  2. Объединить любые явные функции, установленные в дескрипторе (поле features в опциях)

Для определения "родительского" дескриптора вы можете обратиться к нашей реализации на C++. В большинстве случаев это straightforward (просто), но расширения немного удивительны, потому что их родителем является enclosing scope (охватывающая область), а не extendee. Oneof также нужно рассматривать как родителя их полей.

Тесты на соответствие

В будущем релизе мы планируем добавить тесты на соответствие для проверки разрешения функций кросс-языково. До тех пор наши регулярные тесты на соответствие дают частичное покрытие, и наши примеры модульных тестов на наследование могут быть портированы для предоставления более комплексного покрытия.

Примеры

Ниже приведены некоторые реальные примеры того, как мы реализовали поддержку редакций в наших средах выполнения и плагинах.

Java

  • #14138 - Начальная загрузка компилятора с C++ gencode для Java features proto
  • #14377 - Использование функций в генераторах кода Java, Kotlin и Java Lite, включая тесты кодогенерации
  • #15210 - Использование функций в полных средах выполнения Java, охватывающее начальную загрузку функций Java, разрешение функций и устаревшие редакции, вместе с модульными тестами и тестированием на соответствие

Чистый Python

  • #14546 - Настройка тестов кодогенерации заранее
  • #14547 - Полностью реализует редакции за один раз, вместе с модульными тестами и тестированием на соответствие

𝛍pb

  • #14638 - Первый проход реализации редакций, охватывающий разрешение функций и устаревшие редакции
  • #14667 - Добавлено более полная обработка метки/типа поля, поддержка code generator (генератора кода) upb и некоторые тесты
  • #14678 - Подключает upb к среде выполнения Python, с большим количеством модульных тестов и тестов на соответствие

Ruby

  • #16132 - Подключение upb/Java ко всем четырем средам выполнения Ruby для полной поддержки редакций