Erklärung von Erweiterungen

Beschreibt im Detail, was Erweiterungsdeklarationen sind, warum wir sie benötigen und wie wir sie verwenden.

Einführung

Diese Seite beschreibt im Detail, was Erweiterungsdeklarationen sind, warum wir sie benötigen und wie wir sie verwenden.

Wenn Sie eine Einführung in Erweiterungen benötigen, lesen Sie diesen Leitfaden zu Erweiterungen

Motivation

Erweiterungsdeklarationen zielen darauf ab, einen Mittelweg zwischen regulären Feldern und Erweiterungen zu finden. Wie Erweiterungen vermeiden sie eine Abhängigkeit vom Nachrichtentyp des Feldes, was zu einem schlankeren Build-Graph und kleineren Binärdateien in Umgebungen führt, in denen ungenutzte Nachrichten schwer oder unmöglich zu entfernen sind. Wie reguläre Felder erscheinen der Feldname/-nummer in der umschließenden Nachricht, was es einfacher macht, Konflikte zu vermeiden und eine praktische Liste der deklarierten Felder zu sehen.

Die Auflistung der belegten Erweiterungsnummern mit Erweiterungsdeklarationen erleichtert es Benutzern, eine verfügbare Erweiterungsnummer auszuwählen und Konflikte zu vermeiden.

Nutzung

Erweiterungsdeklarationen sind eine Option von Erweiterungsbereichen. Ähnlich wie Vorwärtsdeklarationen in C++ können Sie den Feldtyp, den Feldnamen und die Kardinalität (einzeln oder wiederholt) eines Erweiterungsfeldes deklarieren, ohne die `.proto`-Datei zu importieren, die die vollständige Erweiterungsdefinition enthält.

edition = "2023";

message Foo {
  extensions 4 to 1000 [
    declaration = {
      number: 4,
      full_name: ".my.package.event_annotations",
      type: ".logs.proto.ValidationAnnotations",
      repeated: true },
    declaration = {
      number: 999,
      full_name: ".foo.package.bar",
      type: "int32"}];
}

Diese Syntax hat die folgenden Semantiken

  • Mehrere declarations mit unterschiedlichen Erweiterungsnummern können in einem einzigen Erweiterungsbereich definiert werden, wenn die Größe des Bereichs dies zulässt.
  • Wenn für den Erweiterungsbereich eine Deklaration vorhanden ist, müssen alle Erweiterungen des Bereichs ebenfalls deklariert werden. Dies verhindert, dass nicht deklarierte Erweiterungen hinzugefügt werden, und erzwingt, dass neue Erweiterungen Deklarationen für den Bereich verwenden.
  • Der angegebene Nachrichtentyp (.logs.proto.ValidationAnnotations) muss nicht zuvor definiert oder importiert worden sein. Wir prüfen nur, ob es sich um einen gültigen Namen handelt, der potenziell in einer anderen .proto-Datei definiert werden könnte.
  • Wenn diese oder eine andere `.proto`-Datei eine Erweiterung dieser Nachricht (Foo) mit diesem Namen oder dieser Nummer definiert, erzwingen wir, dass die Nummer, der Typ und der vollständige Name der Erweiterung mit der hier vordeklarierten übereinstimmen.

Die Erweiterungsdeklarationen erwarten zwei Erweiterungsfelder mit unterschiedlichen Paketen

package my.package;
extend Foo {
  repeated logs.proto.ValidationAnnotations event_annotations = 4;
}
package foo.package;
extend Foo {
  optional int32 bar = 999;
}

Reservierte Deklarationen

Eine Erweiterungsdeklaration kann als reserved: true markiert werden, um anzuzeigen, dass sie nicht mehr aktiv verwendet wird und die Erweiterungsdefinition gelöscht wurde. Löschen Sie die Erweiterungsdeklaration nicht und ändern Sie nicht deren type oder full_name Wert.

Dieses reserved Tag ist getrennt vom reservierten Schlüsselwort für reguläre Felder und erfordert keine Aufteilung des Erweiterungsbereichs.

edition = "2023";

message Foo {
  extensions 4 to 1000 [
    declaration = {
      number: 500,
      full_name: ".my.package.event_annotations",
      type: ".logs.proto.ValidationAnnotations",
      reserved: true }];
}

Ein Erweiterungsfelddiagramm, das eine Nummer verwendet, die in der Deklaration reserviert ist, führt zu einem Kompilierungsfehler.

Darstellung in descriptor.proto

Die Erweiterungsdeklaration wird in descriptor.proto als Felder in proto2.ExtensionRangeOptions dargestellt

message ExtensionRangeOptions {
  message Declaration {
    optional int32 number = 1;
    optional string full_name = 2;
    optional string type = 3;
    optional bool reserved = 5;
    optional bool repeated = 6;
  }
  repeated Declaration declaration = 2;
}

Referenzfeldsuche

Erweiterungsdeklarationen werden nicht von den normalen Feldsuchfunktionen wie Descriptor::FindFieldByName() oder Descriptor::FindFieldByNumber() zurückgegeben. Wie Erweiterungen sind sie durch Erweiterungssuchroutinen wie DescriptorPool::FindExtensionByName() auffindbar. Dies ist eine explizite Entscheidung, die widerspiegelt, dass Deklarationen keine Definitionen sind und nicht genügend Informationen enthalten, um einen vollständigen FieldDescriptor zurückzugeben.

Deklarierte Erweiterungen verhalten sich weiterhin wie normale Erweiterungen aus Sicht von TextFormat und JSON. Dies bedeutet auch, dass die Migration eines vorhandenen Feldes zu einer deklarierten Erweiterung zunächst die Migration jeder reflektiven Nutzung dieses Feldes erfordert.

Verwenden Sie Erweiterungsdeklarationen, um Nummern zuzuweisen

Erweiterungen verwenden Feldnummern genauso wie normale Felder. Daher ist es wichtig, dass jeder Erweiterung eine Nummer zugewiesen wird, die innerhalb der übergeordneten Nachricht eindeutig ist. Wir empfehlen die Verwendung von Erweiterungsdeklarationen, um die Feldnummer und den Typ für jede Erweiterung in der übergeordneten Nachricht zu deklarieren. Die Erweiterungsdeklarationen dienen als Registrierung aller Erweiterungen der übergeordneten Nachricht, und protoc stellt sicher, dass es keine Konflikte bei den Feldnummern gibt. Wenn Sie eine neue Erweiterung hinzufügen, wählen Sie die nächste verfügbare Nummer, normalerweise durch einfaches Erhöhen der zuvor hinzugefügten Erweiterungsnummer um eins.

TIPP: Es gibt spezielle Anleitungen für MessageSet, die ein Skript enthalten, das bei der Auswahl der nächsten verfügbaren Nummer hilft.

Wenn Sie eine Erweiterung löschen, stellen Sie sicher, dass Sie die Feldnummer als reserviert markieren, um das Risiko einer versehentlichen Wiederverwendung zu beseitigen.

Diese Konvention ist nur eine Empfehlung – das Protobuf-Team hat weder die Fähigkeit noch den Wunsch, jemanden zur Einhaltung für jede erweiterbare Nachricht zu zwingen. Wenn Sie als Eigentümer eines erweiterbaren Protos die Erweiterungsnummern nicht über Erweiterungsdeklarationen koordinieren möchten, können Sie dies auf andere Weise tun. Seien Sie jedoch sehr vorsichtig, da versehentliche Wiederverwendung einer Erweiterungsnummer zu ernsthaften Problemen führen kann.

Eine Möglichkeit, das Problem zu umgehen, wäre, Erweiterungen ganz zu vermeiden und stattdessen google.protobuf.Any zu verwenden. Dies könnte eine gute Wahl für APIs sein, die als Schnittstelle zu Speichern dienen, oder für Durchleitungssysteme, bei denen der Client den Inhalt des Protos kennt, das empfangende System dies jedoch nicht.

Konsequenzen der Wiederverwendung einer Erweiterungsnummer

Eine Erweiterung ist ein Feld, das außerhalb der Container-Nachricht definiert ist; normalerweise in einer separaten .proto-Datei. Diese Verteilung von Definitionen macht es einfach, dass zwei Entwickler versehentlich unterschiedliche Definitionen für dieselbe Erweiterungsfeldnummer erstellen.

Die Konsequenzen einer Änderung einer Erweiterungsdefinition sind für Erweiterungen und Standardfelder dieselben. Die Wiederverwendung einer Feldnummer führt zu Mehrdeutigkeit bei der Dekodierung eines Protos aus dem Wire-Format. Das Protobuf-Wire-Format ist schlank und bietet keine gute Möglichkeit, Felder zu erkennen, die mit einer Definition kodiert und mit einer anderen dekodiert wurden.

Diese Mehrdeutigkeit kann sich in einem kurzen Zeitraum manifestieren, z. B. wenn ein Client eine Erweiterungsdefinition und ein Server eine andere verwendet und miteinander kommuniziert.

Diese Mehrdeutigkeit kann sich auch über einen längeren Zeitraum manifestieren, z. B. wenn Daten gespeichert werden, die mit einer Erweiterungsdefinition kodiert wurden, und später mit der zweiten Erweiterungsdefinition abgerufen und dekodiert werden. Dieser langfristige Fall kann schwer zu diagnostizieren sein, wenn die erste Erweiterungsdefinition nach der Kodierung und Speicherung der Daten gelöscht wurde.

Das Ergebnis davon kann sein

  1. Ein Parse-Fehler (bestes Szenario).
  2. Ausgeleitete PII / SPII – wenn PII oder SPII mit einer Erweiterungsdefinition geschrieben und mit einer anderen Erweiterungsdefinition gelesen werden.
  3. Datenbeschädigung – wenn Daten mit der „falschen“ Definition gelesen, geändert und neu geschrieben werden.

Daten-Definitions-Mehrdeutigkeit wird mit ziemlicher Sicherheit zumindest Zeit für die Fehlersuche kosten. Sie könnte auch zu Datenlecks oder Beschädigungen führen, deren Behebung Monate dauern kann.

Nutzungstipps

Löschen Sie niemals eine Erweiterungsdeklaration

Das Löschen einer Erweiterungsdeklaration öffnet die Tür für versehentliche Wiederverwendung in der Zukunft. Wenn die Erweiterung nicht mehr verarbeitet wird und die Definition gelöscht wird, kann die Erweiterungsdeklaration als reserviert markiert werden.

Verwenden Sie niemals einen Feldnamen oder eine Nummer aus der reservierten Liste für eine neue Erweiterungsdeklaration

Reservierte Nummern wurden möglicherweise in der Vergangenheit für Felder oder andere Erweiterungen verwendet.

Die Verwendung des full_name eines reservierten Feldes wird aufgrund der möglichen Mehrdeutigkeit bei der Verwendung von Textproto nicht empfohlen.

Ändern Sie niemals den Typ einer vorhandenen Erweiterungsdeklaration

Die Änderung des Typs eines Erweiterungsfeldes kann zu Datenbeschädigung führen.

Wenn das Erweiterungsfeld vom Typ Enum oder Nachricht ist und dieser Enum- oder Nachrichtentyp umbenannt wird, ist die Aktualisierung des Deklarationsnamens erforderlich und sicher. Um Fehler zu vermeiden, sollten die Aktualisierung des Typs, der Erweiterungsfelddiagramm und die Erweiterungsdeklaration in einer einzigen Übernahme erfolgen.

Seien Sie vorsichtig beim Umbenennen eines Erweiterungsfeldes

Während die Umbenennung eines Erweiterungsfeldes für das Wire-Format in Ordnung ist, kann sie die JSON- und TextFormat-Analyse stören.