擴充套件宣告

詳細描述了什麼是擴充套件宣告,為什麼我們需要它們以及如何使用它們。

介紹

本頁詳細描述了什麼是擴充套件宣告,為什麼我們需要它們以及如何使用它們。

如果您需要擴充套件的介紹,請閱讀此擴充套件指南

動機

擴充套件宣告旨在在常規欄位和擴充套件之間取得一個折衷點。像擴充套件一樣,它們避免了對欄位訊息型別的依賴,因此在難以或不可能剝離未使用訊息的環境中,它們會產生更精簡的構建圖和更小的二進位制檔案。像常規欄位一樣,欄位名稱/編號出現在包含訊息中,這使得避免衝突並檢視宣告的欄位的便捷列表變得更容易。

使用擴充套件宣告列出已佔用的擴充套件編號,可以幫助使用者更容易地選擇可用的擴充套件編號並避免衝突。

用法

擴充套件宣告是擴充套件範圍的一個選項。與 C++ 中的前向宣告一樣,您可以在不匯入包含完整擴充套件定義的 .proto 檔案的情況下,宣告擴充套件欄位的欄位型別、欄位名稱和基數(單一或重複)。

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"}];
}

此語法具有以下語義

  • 如果範圍大小允許,可以在單個擴充套件範圍內定義多個具有不同擴充套件編號的 declaration
  • 如果擴充套件範圍有任何宣告,則該範圍的所有擴充套件也必須宣告。這可以防止新增未宣告的擴充套件,並強制任何新的擴充套件都使用該範圍的宣告。
  • 給定的訊息型別 (.logs.proto.ValidationAnnotations) 不需要事先定義或匯入。我們只檢查它是否是一個有效的名稱,可能在另一個 .proto 檔案中定義。
  • 當此或另一個 .proto 檔案使用此名稱或編號定義此訊息 (Foo) 的擴充套件時,我們將強制擴充套件的編號、型別和全名與此處前向宣告的匹配。

擴充套件宣告需要兩個具有不同包的擴充套件欄位

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

保留宣告

擴充套件宣告可以標記為 reserved: true,以表明它不再被積極使用,並且擴充套件定義已被刪除。不要刪除擴充套件宣告或編輯其 typefull_name

reserved 標籤與常規欄位的 reserved 關鍵字是分開的,並且不需要中斷擴充套件範圍

edition = "2023";

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

使用宣告中 reserved 編號的擴充套件欄位定義將無法編譯。

在 descriptor.proto 中的表示

擴充套件宣告在 descriptor.proto 中表示為 proto2.ExtensionRangeOptions 中的欄位。

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;
}

反射欄位查詢

擴充套件宣告不會從常規欄位查詢函式(如 Descriptor::FindFieldByName()Descriptor::FindFieldByNumber())返回。與擴充套件一樣,它們可以透過擴充套件查詢例程(如 DescriptorPool::FindExtensionByName())發現。這是一個明確的選擇,它反映了宣告不是定義且沒有足夠資訊返回完整 FieldDescriptor 的事實。

從 TextFormat 和 JSON 的角度來看,宣告的擴充套件仍然像常規擴充套件一樣。這也意味著將現有欄位遷移到宣告的擴充套件將需要首先遷移該欄位的任何反射使用。

使用擴充套件宣告分配編號

擴充套件使用欄位編號,就像普通欄位一樣,因此每個擴充套件都必須分配一個在其父訊息中唯一的編號,這一點很重要。我們建議使用擴充套件宣告來宣告父訊息中每個擴充套件的欄位編號和型別。擴充套件宣告充當所有父訊息擴充套件的登錄檔,並且 protoc 將強制確保沒有欄位編號衝突。當您新增新擴充套件時,通常透過將以前新增的擴充套件編號加一來選擇下一個可用編號。

提示: 對於 MessageSet 有一份特殊指南,它提供了一個指令碼來幫助選擇下一個可用編號。

無論何時刪除擴充套件,請務必將欄位編號標記為 reserved,以消除意外重用的風險。

此約定僅是建議——protobuf 團隊沒有能力或意願強迫任何人遵守它對每個可擴充套件訊息。如果您作為可擴充套件 proto 的所有者不想透過擴充套件宣告協調擴充套件編號,則可以選擇透過其他方式提供協調。但是要非常小心,因為意外重用擴充套件編號可能會導致嚴重問題。

解決這個問題的一種方法是完全避免擴充套件,而是使用google.protobuf.Any。對於前端儲存的 API 或客戶端關心 proto 內容但接收系統不關心的直通系統來說,這可能是一個不錯的選擇。

重用擴充套件編號的後果

擴充套件是在容器訊息外部定義的欄位;通常在單獨的 .proto 檔案中。這種定義的分散使得兩個開發人員很容易意外地為相同的擴充套件欄位編號建立不同的定義。

更改擴充套件定義的後果與擴充套件和標準欄位相同。重用欄位編號會在 proto 應如何從線格式解碼時引入歧義。protobuf 線格式精簡,沒有提供很好的方法來檢測使用一個定義編碼而使用另一個定義解碼的欄位。

這種歧義可能在短時間內顯現出來,例如客戶端使用一個擴充套件定義,而伺服器使用另一個進行通訊。

這種歧義也可能在更長的時間內顯現出來,例如儲存使用一個擴充套件定義編碼的資料,然後稍後使用第二個擴充套件定義檢索和解碼。如果第一個擴充套件定義在資料編碼和儲存後被刪除,則這種長期情況可能難以診斷。

這可能導致的結果是:

  1. 解析錯誤(最佳情況)。
  2. PII / SPII 洩露 – 如果 PII 或 SPII 使用一個擴充套件定義寫入,並使用另一個擴充套件定義讀取。
  3. 資料損壞 – 如果資料使用“錯誤”的定義讀取、修改並重新寫入。

資料定義模糊性幾乎肯定會至少讓某人花費時間進行除錯。它還可能導致資料洩露或損壞,需要數月才能清理。

使用提示

永遠不要刪除擴充套件宣告

刪除擴充套件宣告為未來意外重用打開了大門。如果擴充套件不再被處理且定義被刪除,則可以將擴充套件宣告標記為保留

永遠不要為新的擴充套件宣告使用 reserved 列表中的欄位名稱或編號

保留的數字過去可能已用於欄位或其他擴充套件。

不建議使用保留欄位的 full_name,因為在使用 textproto 時可能存在歧義。

永遠不要更改現有擴充套件宣告的型別

更改擴充套件欄位的型別可能導致資料損壞。

如果擴充套件欄位是列舉或訊息型別,並且該列舉或訊息型別正在重新命名,則更新宣告名稱是必需且安全的。為避免中斷,型別、擴充套件欄位定義和擴充套件宣告的更新都應在單個提交中進行。

重新命名擴充套件欄位時請謹慎

雖然重新命名擴充套件欄位對線格式來說沒問題,但它可能會破壞 JSON 和 TextFormat 解析。