Implementierung der Editionen-Unterstützung

Anleitungen zur Implementierung der Edition-Unterstützung in Laufzeitumgebungen und Plugins.

Dieses Thema erklärt, wie Editionen in neuen Laufzeitumgebungen und Generatoren implementiert werden.

Übersicht

Edition 2023

Die erste veröffentlichte Edition ist Edition 2023, die darauf ausgelegt ist, proto2 und proto3 Syntax zu vereinheitlichen. Die Features, die wir hinzugefügt haben, um die Verhaltensunterschiede abzudecken, sind detailliert in Feature-Einstellungen für Editionen beschrieben.

Feature-Definition

Zusätzlich zur Unterstützung von Editionen und den globalen Features, die wir definiert haben, möchten Sie möglicherweise Ihre eigenen Features definieren, um die Infrastruktur zu nutzen. Dies ermöglicht es Ihnen, beliebige Features zu definieren, die von Ihren Generatoren und Laufzeitumgebungen verwendet werden können, um neue Verhaltensweisen zu steuern. Der erste Schritt ist die Beanspruchung einer Erweiterungsnummer für die FeatureSet Nachricht in descriptor.proto oberhalb von 9999. Sie können uns einen Pull-Request auf GitHub senden, und er wird in unserer nächsten Veröffentlichung enthalten sein (siehe z. B. #15439).

Sobald Sie Ihre Erweiterungsnummer haben, können Sie Ihr Features-Proto erstellen (ähnlich wie cpp_features.proto). Diese sehen typischerweise etwa so aus:

edition = "2023";

package foo;

import "google/protobuf/descriptor.proto";

extend google.protobuf.FeatureSet {
  MyFeatures features = <extension #>;
}

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: "Feature will be removed in 2025",
      edition_removed: EDITION_2025,
    },
    edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
    edition_defaults = { edition: EDITION_2024, value: "VALUE2" }
  ];
}

Hier haben wir ein neues Enum-Feature foo.feature_value definiert (derzeit werden nur Boolesche und Enum-Typen unterstützt). Zusätzlich zur Definition der Werte, die es annehmen kann, müssen Sie auch angeben, wie es verwendet werden kann:

  • Ziele (Targets) - gibt den Typ der Proto-Deskriptoren an, denen dieses Feature zugeordnet werden kann. Dies steuert, wo Benutzer das Feature explizit angeben können. Jeder Typ muss explizit aufgeführt werden.
  • Feature-Unterstützung (Feature support) - gibt die Lebensdauer dieses Features relativ zur Edition an. Sie müssen die Edition angeben, in der es eingeführt wurde, und es wird davor nicht zulässig sein. Sie können das Feature in späteren Editionen optional auslaufen lassen oder entfernen.
  • Editions-Standardwerte (Edition defaults) - gibt Änderungen am Standardwert des Features an. Dies muss jede unterstützte Edition abdecken, aber Sie können jede Edition auslassen, bei der sich der Standardwert nicht geändert hat. Beachten Sie, dass EDITION_PROTO2 und EDITION_PROTO3 hier angegeben werden können, um Standardwerte für die "Legacy"-Editionen bereitzustellen (siehe Legacy Editions).

Was ist ein Feature?

Features sind dazu gedacht, einen Mechanismus bereitzustellen, um schlechtes Verhalten im Laufe der Zeit an den Editionsgrenzen zu reduzieren. Während der Zeitplan für die tatsächliche Entfernung eines Features Jahre (oder Jahrzehnte) in der Zukunft liegen kann, sollte das angestrebte Ziel jedes Features die endgültige Entfernung sein. Wenn ein schlechtes Verhalten identifiziert wird, können Sie ein neues Feature einführen, das die Korrektur schützt. In der nächsten Edition (oder möglicherweise später) würden Sie den Standardwert umschalten und es den Benutzern gleichzeitig erlauben, ihr altes Verhalten beim Upgrade beizubehalten. Irgendwann in der Zukunft würden Sie das Feature als veraltet kennzeichnen, was bei Benutzern, die es überschreiben, eine benutzerdefinierte Warnung auslöst. In einer späteren Edition würden Sie es dann als entfernt kennzeichnen und Benutzern die Überschreibung nicht mehr gestatten (aber der Standardwert würde weiterhin gelten). Bis die Unterstützung für diese letzte Edition in einer breaking release eingestellt wird, bleibt das Feature für auf ältere Editionen beschränkte protos nutzbar, was ihnen Zeit zur Migration gibt.

Flags, die optionale Verhaltensweisen steuern, die Sie nicht entfernen möchten, werden besser als benutzerdefinierte Optionen implementiert. Dies hängt damit zusammen, warum wir Features auf boolesche oder Enum-Typen beschränkt haben. Jedes Verhalten, das durch eine (relativ) unbegrenzte Anzahl von Werten gesteuert wird, passt wahrscheinlich nicht gut in das Editions-Framework, da es unrealistisch ist, so viele verschiedene Verhaltensweisen nach und nach abzuschaffen.

Eine Einschränkung hierbei sind Verhaltensweisen im Zusammenhang mit Wire-Grenzen. Die Verwendung sprachspezifischer Features zur Steuerung des Serialisierungs- oder Parsing-Verhaltens kann gefährlich sein, da eine andere Sprache auf der anderen Seite stehen könnte. Wire-Format-Änderungen sollten immer durch globale Features in descriptor.proto gesteuert werden, die von jeder Laufzeitumgebung einheitlich beachtet werden können.

Generatoren

Generatoren, die in C++ geschrieben sind, erhalten viel kostenlos, da sie die C++-Laufzeitumgebung verwenden. Sie müssen Feature-Auflösung nicht selbst handhaben, und wenn sie Feature-Erweiterungen benötigen, können sie diese in GetFeatureExtensions in ihrem CodeGenerator registrieren. Sie können im Allgemeinen GetResolvedSourceFeatures für den Zugriff auf aufgelöste Features eines Deskriptors im Codegen und GetUnresolvedSourceFeatures für den Zugriff auf ihre eigenen nicht aufgelösten Features verwenden.

Plugins, die in der gleichen Sprache wie die Laufzeitumgebung geschrieben sind, für die sie Code generieren, benötigen möglicherweise ein benutzerdefiniertes Bootstrapping für ihre Feature-Definitionen.

Explizite Unterstützung

Generatoren müssen genau angeben, welche Editionen sie unterstützen. Dies ermöglicht es Ihnen, die Unterstützung für eine Edition sicher nach ihrer Veröffentlichung zu einem Zeitpunkt Ihrer Wahl hinzuzufügen. Protoc lehnt alle Editionen-Protos ab, die an Generatoren gesendet werden, die nicht FEATURE_SUPPORTS_EDITIONS im Feld supported_features ihrer CodeGeneratorResponse enthalten. Zusätzlich haben wir die Felder minimum_edition und maximum_edition, um Ihr genaues Unterstützungsfenster anzugeben. Sobald Sie alle Code- und Feature-Änderungen für eine neue Edition definiert haben, können Sie maximum_edition erhöhen, um diese Unterstützung bekannt zu geben.

Codegen-Tests

Wir haben eine Reihe von Codegen-Tests, die verwendet werden können, um sicherzustellen, dass Edition 2023 keine unerwarteten funktionalen Änderungen hervorbringt. Diese waren in Sprachen wie C++ und Java sehr nützlich, wo ein erheblicher Teil der Funktionalität im gencode liegt. In Sprachen wie Python, wo der gencode im Grunde nur eine Sammlung von serialisierten Deskriptoren ist, sind diese nicht ganz so nützlich.

Diese Infrastruktur ist noch nicht wiederverwendbar, ist aber für eine zukünftige Veröffentlichung geplant. Zu diesem Zeitpunkt können Sie sie verwenden, um zu überprüfen, ob die Migration zu Editionen keine unerwarteten Codegen-Änderungen mit sich bringt.

Laufzeitumgebungen (Runtimes)

Laufzeitumgebungen ohne Reflection oder dynamische Nachrichten sollten nichts tun müssen, um Editionen zu implementieren. Diese Logik sollte vom Code-Generator gehandhabt werden.

Sprachen mit Reflection, aber ohne dynamische Nachrichten benötigen aufgelöste Features, können aber optional wählen, diese nur in ihrem Generator zu behandeln. Dies kann geschehen, indem sowohl aufgelöste als auch nicht aufgelöste Feature-Sets während des Codegens an die Laufzeitumgebung übergeben werden. Dies vermeidet die Reimplementierung der Feature-Auflösung in der Laufzeitumgebung, wobei der Hauptnachteil die Effizienz ist, da für jeden Deskriptor ein eindeutiges Feature-Set erstellt wird.

Sprachen mit dynamischen Nachrichten müssen Editionen vollständig implementieren, da sie Deskriptoren zur Laufzeit erstellen müssen.

Syntax Reflection

Der erste Schritt bei der Implementierung von Editionen in einer Laufzeitumgebung mit Reflection ist die Entfernung aller direkten Überprüfungen des syntax-Schlüsselworts. All diese sollten zu feingranularen Feature-Helfern verschoben werden, die bei Bedarf weiterhin syntax verwenden können.

Die folgenden Feature-Helfer sollten auf Deskriptoren implementiert werden, mit sprachlich geeigneten Namen:

  • FieldDescriptor::has_presence - Ob ein Feld explizite Präsenz hat.
    • Wiederholte Felder haben niemals Präsenz.
    • Nachrichten-, Erweiterungs- und Oneof-Felder haben immer explizite Präsenz.
    • Alles andere hat Präsenz, iff field_presence ist nicht IMPLICIT.
  • FieldDescriptor::is_required - Ob ein Feld erforderlich ist.
  • FieldDescriptor::requires_utf8_validation - Ob ein Feld auf UTF-8-Gültigkeit geprüft werden soll.
  • FieldDescriptor::is_packed - Ob ein wiederholtes Feld eine gepackte Kodierung hat.
  • FieldDescriptor::is_delimited - Ob ein Nachrichtenfeld eine begrenzte Kodierung hat.
  • EnumDescriptor::is_closed - Ob ein Feld geschlossen ist.

Nachgelagerte Benutzer sollten zu diesen neuen Helfern migrieren, anstatt die Syntax direkt zu verwenden. Die folgende Klasse bestehender Deskriptor-APIs sollte idealerweise als veraltet markiert und schließlich entfernt werden, da sie Syntaxinformationen durchsickern lassen:

  • FileDescriptor syntax
  • Proto3 optional APIs
    • FieldDescriptor::has_optional_keyword
    • OneofDescriptor::is_synthetic
    • Descriptor::*real_oneof* - sollte in "oneof" umbenannt werden, und die vorhandenen "oneof"-Helfer sollten entfernt werden, da sie Informationen über synthetische Oneofs durchsickern lassen (die in Editionen nicht existieren).
  • Gruppentyp
    • Der Enum-Wert TYPE_GROUP sollte entfernt und durch den is_delimited-Helfer ersetzt werden.
  • Erforderliches Label
    • Der Enum-Wert LABEL_REQUIRED sollte entfernt und durch den is_required-Helfer ersetzt werden.

Es gibt viele Arten von Benutzercode, in denen diese Prüfungen existieren, aber nicht Editions-feindlich sind. Zum Beispiel ist Code, der proto3 optional aufgrund seiner synthetischen Oneof-Implementierung speziell behandeln muss, nicht Editions-feindlich, solange die Polarität etwas wie syntax == "proto3" ist (anstatt syntax != "proto2" zu prüfen).

Wenn es nicht möglich ist, diese APIs vollständig zu entfernen, sollten sie als veraltet markiert und entmutigt werden.

Feature-Sichtbarkeit

Wie in editions-feature-visibility erläutert, sollten Feature-Protos ein internes Detail jeder Protobuf-Implementierung bleiben. Die Verhaltensweisen, die sie steuern, sollten über Deskriptor-Methoden exponiert werden, aber die Protos selbst nicht. Insbesondere bedeutet dies, dass alle Optionen, die den Benutzern zur Verfügung gestellt werden, ihr Feld features entfernt haben müssen.

Der einzige Fall, in dem wir Features durchsickern lassen, ist bei der Serialisierung von Deskriptoren. Die resultierenden Deskriptor-Protos sollten eine getreue Darstellung der ursprünglichen Proto-Dateien sein und nicht aufgelöste Features innerhalb der Optionen enthalten.

Legacy Editions

Wie in legacy-syntax-editions weiter erläutert, ist eine gute Möglichkeit, frühzeitig eine Abdeckung Ihrer Editionen-Implementierung zu erreichen, die Vereinheitlichung von proto2, proto3 und Editionen. Dies migriert proto2 und proto3 im Grunde unter der Haube zu Editionen und lässt alle in Syntax Reflection implementierten Helfer ausschließlich Features verwenden (anstatt auf Syntax zu verzweigen). Dies kann durch Einfügen einer Feature-Inferenz-Phase in die Feature-Auflösung erfolgen, bei der verschiedene Aspekte der Proto-Datei Aufschluss darüber geben können, welche Features geeignet sind. Diese Features können dann in die Features des übergeordneten Elements zusammengeführt werden, um den aufgelösten Feature-Satz zu erhalten.

Obwohl wir bereits angemessene Standardwerte für proto2/proto3 bereitstellen, sind für Edition 2023 die folgenden zusätzlichen Inferenzen erforderlich:

  • required - Wir inferieren LEGACY_REQUIRED Präsenz, wenn ein Feld LABEL_REQUIRED hat.
  • groups - Wir inferieren DELIMITED Nachrichten-Encoding, wenn ein Feld TYPE_GROUP hat.
  • packed - Wir inferieren PACKED Encoding, wenn die Option packed auf true gesetzt ist.
  • expanded - Wir inferieren EXPANDED Encoding, wenn ein proto3-Feld packed explizit auf false gesetzt hat.

Konformitätstests

Editions-spezifische Konformitätstests wurden hinzugefügt, müssen aber opt-in erfolgen. Ein Flag --maximum_edition 2023 kann an den Runner übergeben werden, um diese zu aktivieren. Sie müssen Ihr Test-Binary so konfigurieren, dass es die folgenden neuen Nachrichtentypen verarbeitet:

  • protobuf_test_messages.editions.proto2.TestAllTypesProto2 - Identisch mit der alten proto2-Nachricht, aber transformiert zu Edition 2023.
  • protobuf_test_messages.editions.proto3.TestAllTypesProto3 - Identisch mit der alten proto3-Nachricht, aber transformiert zu Edition 2023.
  • protobuf_test_messages.editions.TestAllTypesEdition2023 - Wird verwendet, um Edition-2023-spezifische Testfälle abzudecken.

Feature-Auflösung

Editionen verwenden lexikalische Geltungsbereiche, um Features zu definieren, was bedeutet, dass jeder Nicht-C++-Code, der Editionen-Unterstützung implementieren muss, unseren Algorithmus zur Feature-Auflösung neu implementieren muss. Der Großteil der Arbeit wird jedoch von protoc selbst erledigt, das so konfiguriert werden kann, dass eine Zwischennachricht FeatureSetDefaults ausgegeben wird. Diese Nachricht enthält eine "Kompilierung" eines Satzes von Feature-Definitionsdateien, die die Standard-Feature-Werte in jeder Edition darlegen.

Zum Beispiel würde die obige Feature-Definition zu folgenden Standardwerten zwischen proto2 und Edition 2025 kompiliert werden (in Textformatnotation):

defaults {
  edition: EDITION_PROTO2
  overridable_features { [foo.features] {} }
  fixed_features {
    // Global feature defaults…
    [foo.features] { feature_value: VALUE1 }
  }
}
defaults {
  edition: EDITION_PROTO3
  overridable_features { [foo.features] {} }
  fixed_features {
    // Global feature defaults…
    [foo.features] { feature_value: VALUE1 }
  }
}
defaults {
  edition: EDITION_2023
  overridable_features {
    // Global feature defaults…
    [foo.features] { feature_value: VALUE1 }
  }
}
defaults {
  edition: EDITION_2024
  overridable_features {
    // Global feature defaults…
    [foo.features] { feature_value: VALUE2 }
  }
}
defaults {
  edition: EDITION_2025
  overridable_features {
    // Global feature defaults…
  }
  fixed_features { [foo.features] { feature_value: VALUE2 } }
}
minimum_edition: EDITION_PROTO2
maximum_edition: EDITION_2025

Globale Feature-Standardwerte sind zur Kompaktheit weggelassen, aber sie wären ebenfalls vorhanden. Dieses Objekt enthält eine geordnete Liste jeder Edition mit einem eindeutigen Satz von Standardwerten (einige Editionen werden möglicherweise nicht vorhanden sein) innerhalb des angegebenen Bereichs. Jeder Satz von Standardwerten ist in überschreibbare und feste Features unterteilt. Die ersteren sind unterstützte Features für die Edition, die vom Benutzer frei überschrieben werden können. Die festen Features sind solche, die noch nicht eingeführt oder entfernt wurden und vom Benutzer nicht überschrieben werden können.

Wir stellen eine Bazel-Regel zum Kompilieren dieser Zwischenobjekte bereit.

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",
)

Das ausgegebene FeatureSetDefaults kann in ein rohes String-Literal in der Sprache eingebettet werden, in der Sie Feature-Auflösung durchführen müssen. Wir stellen auch ein embed_edition_defaults-Makro zur Verfügung, um dies zu tun.

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

Alternativ können Sie protoc direkt (außerhalb von Bazel) aufrufen, um diese Daten zu generieren:

protoc --edition_defaults_out=defaults.binpb --edition_defaults_minimum=PROTO2 --edition_defaults_maximum=2023 <feature files...>

Sobald die Standardnachricht verbunden und von Ihrem Code geparst wurde, folgt die Feature-Auflösung für einen Datei-Deskriptor einer bestimmten Edition einem einfachen Algorithmus:

  1. Validieren Sie, dass die Edition im entsprechenden Bereich [minimum_edition, maximum_edition] liegt.
  2. Binäre Suche im geordneten Feld defaults nach dem höchsten Eintrag, der kleiner oder gleich der Edition ist.
  3. Führen Sie overridable_features in fixed_features aus den ausgewählten Standardwerten zusammen.
  4. Führen Sie alle expliziten Features zusammen, die auf dem Deskriptor gesetzt sind (das Feld features in den Dateioptionen).

Von dort aus können Sie rekursiv Features für alle anderen Deskriptoren auflösen:

  1. Initialisieren Sie mit dem Feature-Satz des übergeordneten Deskriptors.
  2. Führen Sie alle expliziten Features zusammen, die auf dem Deskriptor gesetzt sind (das Feld features in den Optionen).

Für die Bestimmung des "übergeordneten" Deskriptors können Sie sich auf unsere C++-Implementierung beziehen. Dies ist in den meisten Fällen unkompliziert, aber Erweiterungen sind etwas überraschend, da ihr übergeordnetes Element der umschließende Geltungsbereich und nicht der Extendee ist. Oneofs müssen ebenfalls als übergeordnete Elemente ihrer Felder betrachtet werden.

Konformitätstests

In einer zukünftigen Veröffentlichung planen wir, Konformitätstests hinzuzufügen, um die Feature-Auflösung sprachübergreifend zu verifizieren. Bis dahin bieten unsere regulären Konformitätstests eine teilweise Abdeckung, und unsere Unit-Tests für Vererbung als Beispiel können portiert werden, um eine umfassendere Abdeckung zu bieten.

Beispiele

Im Folgenden finden Sie einige reale Beispiele, wie wir Editionen-Unterstützung in unseren Laufzeitumgebungen und Plugins implementiert haben.

Java

  • #14138 - Bootstrapping des Compilers mit C++-gencode für Java-Features-Proto
  • #14377 - Verwendung von Features in Java-, Kotlin- und Java Lite-Code-Generatoren, einschließlich Codegen-Tests
  • #15210 - Verwendung von Features in Java-Full-Runtimes, die Java-Features-Bootstrap, Feature-Auflösung und Legacy-Editionen abdecken, zusammen mit Unit-Tests und Konformitätstests

Pure Python

  • #14546 - Einrichtung von Codegen-Tests im Voraus
  • #14547 - Vollständige Implementierung von Editionen in einem Zug, zusammen mit Unit-Tests und Konformitätstests

𝛍pb

  • #14638 - Erster Durchlauf der Editionen-Implementierung, der Feature-Auflösung und Legacy-Editionen abdeckt
  • #14667 - Hinzugefügte vollständigere Behandlung von Feld-Labels/Typen, Unterstützung für den Code-Generator von upb und einige Tests
  • #14678 - Verbindet upb mit der Python-Laufzeitumgebung, mit weiteren Unit-Tests und Konformitätstests

Ruby

  • #16132 - Verbindet upb/Java mit allen vier Ruby-Laufzeitumgebungen für vollständige Editionen-Unterstützung