介面版本管理

HIDL 要求每個以 HIDL 編寫的介面都必須有版本。HAL 介面發布後會凍結,因此必須在該介面的新版本中進行任何後續變更。雖然已發布的介面無法修改,但可以透過其他介面擴充。

HIDL 程式碼結構

HIDL 程式碼會以使用者定義的類型、介面和套件分類

  • 使用者定義的類型 (UDT)。HIDL 可讓您存取一組基本資料類型,這些類型可用於透過結構體、聯合體和列舉組合更複雜的類型。UDT 會傳遞至介面的各個方法,並可在套件層級 (適用於所有介面) 或介面的本機中定義。
  • 介面。介面是 HIDL 的基本構成要素,由 UDT 和方法宣告組成。介面也可以繼承其他介面。
  • 套件。整理相關的 HIDL 介面,以及這些介面運作的資料類型。套件會以名稱和版本來識別,並包含下列項目:
    • 名為 types.hal 的資料類型定義檔案。
    • 零個或多個介面,每個介面位於自己的 .hal 檔案中。

資料類型定義檔案 types.hal 只包含 UDT (所有套件層級 UDT 都會保留在單一檔案中)。套件中的所有介面都會使用目標語言的表示法。

版本管理理念

HIDL 套件 (例如 android.hardware.nfc) 在針對特定版本 (例如 1.0) 發布後即無法變更。您只能在其他套件中修改套件中的介面,或變更其 UDT。

在 HIDL 中,版本控制會套用至套件層級,而非介面層級,且套件中的所有介面和 UDT 都會共用相同的版本。套件版本會遵循語意版本設定,但不會包含修補程式級別和建構中繼資料元件。在特定套件中,如果是次要版本升級,表示套件的新版本與舊版向後相容;如果是主要版本升級,表示套件的新版本與舊版不相容。

從概念上來說,套件可以透過以下任一方式與其他套件建立關聯:

  • 完全不會
  • 套件層級的回溯相容性擴充功能。這會發生在套件的次要版本升級 (下一個遞增修訂版本) 中,新套件與舊套件的名稱和主要版本相同,但次要版本較高。從功能上來說,新套件是舊套件的超集,也就是:
    • 父項套件的頂層介面會顯示在新套件中,但介面可能會在 types.hal 中提供新方法、新的介面層級擴充功能和新的通用資料類型。
    • 您也可以將新的介面新增至新套件。
    • 父項套件的所有資料類型都會出現在新套件中,且可由舊套件中的 (可能已重新實作的) 方法處理。
    • 您也可以新增資料類型,讓升級版現有介面的新方法或新介面使用。
  • 介面層級回溯相容的擴充性。新套件也可以透過邏輯上分開的介面擴充原始套件,這些介面只提供額外功能,而非核心功能。為此,您可能會需要下列項目:
    • 新套件中的介面需要使用舊套件的資料類型。
    • 新套件中的介面可以擴充一或多個舊套件的介面。
  • 擴充原始的回溯不相容性。這是套件的大版本升級前版本,兩者之間不必有任何關聯。在這種情況下,您可以結合舊版套件的類型,以及舊版套件介面子集的繼承,來表達此類型。

介面結構

對於結構良好的介面,如果要新增不在原始設計範圍內的新類型功能,就必須修改 HIDL 介面。相反地,如果您可以或預期在介面兩側進行變更,以便在不變更介面本身的情況下引入新功能,則表示介面並未結構化。

Treble 支援單獨編譯的供應商和系統元件,其中裝置上的 vendor.imgsystem.img 可分別編譯。vendor.imgsystem.img 之間的所有互動都必須明確且完整定義,才能在多年後繼續運作。這包括許多 API 途徑,但主要途徑是 HIDL 用於 system.img/vendor.img 邊界上處理序間通訊的 IPC 機制。

需求條件

所有透過 HIDL 傳遞的資料都必須明確定義。為了確保實作和用戶端即使在個別編譯或獨立開發的情況下,也能繼續合作,資料必須遵守下列規定:

  • 可直接在 HIDL 中使用語義名稱和意義 (使用結構體、列舉等) 進行描述。
  • 可使用 ISO/IEC 7816 等公開標準來描述。
  • 可透過硬體標準或硬體的實體版面配置來描述。
  • 必要時可為不透明資料 (例如公開金鑰、ID 等)。

如果使用不透明資料,則必須由 HIDL 介面的一方讀取。舉例來說,如果 vendor.img 程式碼將字串訊息或 vec<uint8_t> 資料傳送至 system.img 上的元件,system.img 本身無法剖析該資料,只能將資料傳回 vendor.img 進行解讀。將值從 vendor.img 傳遞至 system.img 上的供應商程式碼或其他裝置時,必須明確說明資料格式和解讀方式,且仍是介面的一部分

規範

您應該可以只使用 .hal 檔案編寫 HAL 的實作或用戶端 (也就是說,您不需要查看 Android 來源或公開標準)。建議您明確指定所需的行為。像是「實作可能會執行 A 或 B」這類的陳述式,會鼓勵實作與用戶端相互連結。

HIDL 程式碼版面配置

HIDL 包含核心和供應商套件。

核心 HIDL 介面是 Google 指定的介面。這些包裝函式所屬的套件會以 android.hardware. 開頭,並以子系統命名,可能會使用巢狀命名層級。舉例來說,NFC 套件名稱為 android.hardware.nfc,相機套件名稱為 android.hardware.camera。一般來說,核心套件的名稱為 android.hardware.[name1].[name2]…. HIDL 套件除了名稱外,還包含版本。舉例來說,android.hardware.camera 套件可能為 3.4 版本;這點很重要,因為套件版本會影響其在來源樹狀結構中的擺放位置。

所有核心套件都會放在建構系統的 hardware/interfaces/ 下方。版本 $m.$n 的套件 android.hardware.[name1].[name2]… 位於 hardware/interfaces/name1/name2//$m.$n/ 下;版本 3.4 的套件 android.hardware.camera 位於目錄 hardware/interfaces/camera/3.4/.。套件前置字串 android.hardware. 和路徑 hardware/interfaces/ 之間有硬式編碼對應。

非核心 (供應商) 套件是由 SoC 供應商或 ODM 產生的。非核心套件的前置字串為 vendor.$(VENDOR).hardware.,其中 $(VENDOR) 是指 SoC 供應商或 OEM/ODM。這會對應至樹狀結構中的路徑 vendor/$(VENDOR)/interfaces (這項對應項目也已硬式編碼)。

完整的使用者定義型別名稱

在 HIDL 中,每個 UDT 都有一個完整合格名稱,其中包含 UDT 名稱、定義 UDT 的套件名稱和套件版本。只有在宣告類型例項時才會使用完全修飾的名稱,而非在定義類型本身時使用。舉例來說,假設套件 android.hardware.nfc, 版本 1.0 定義了名為 NfcData 的結構體。在宣告位置 (無論是在 types.hal 中還是介面宣告中),宣告只會指出:

struct NfcData {
    vec<uint8_t> data;
};

在宣告此類型的例項時 (無論是在資料結構中或做為方法參數),請使用完整的類型名稱:

android.hardware.nfc@1.0::NfcData

一般語法為 PACKAGE@VERSION::UDT,其中:

  • PACKAGE 是 HIDL 套件的點分隔名稱 (例如android.hardware.nfc)。
  • VERSION 是套件的點分隔主要.次版本格式 (例如 1.0)。
  • UDT 是 HIDL UDT 的點分隔名稱。由於 HIDL 支援巢狀 UDT,且 HIDL 介面可包含 UDT (一種巢狀宣告),因此會使用點號存取名稱。

舉例來說,如果在 android.hardware.example 版本 1.0 套件的 common_types 檔案中定義了以下巢狀宣告:

// types.hal
package android.hardware.example@1.0;
struct Foo {
    struct Bar {
        // …
    };
    Bar cheers;
};

Bar 的完整名稱為 android.hardware.example@1.0::Foo.Bar。如果除了位於上述套件中的巢狀宣告之外,巢狀宣告還位於名為 IQuux 的介面中:

// IQuux.hal
package android.hardware.example@1.0;
interface IQuux {
    struct Foo {
        struct Bar {
            // …
        };
        Bar cheers;
    };
    doSomething(Foo f) generates (Foo.Bar fb);
};

Bar 的完整名稱為 android.hardware.example@1.0::IQuux.Foo.Bar

在兩種情況下,Bar 只能在 Foo 宣告的範圍內稱為 Bar。在套件或介面層級,您必須透過 FooFoo.Bar 參照 Bar,如同上述 doSomething 方法的宣告一樣。或者,您也可以更詳細地宣告方法,如下所示:

// IQuux.hal
doSomething(android.hardware.example@1.0::IQuux.Foo f) generates (android.hardware.example@1.0::IQuux.Foo.Bar fb);

完整的列舉值

如果 UDT 是列舉型別,則列舉型別的每個值都會有完整的修飾名稱,開頭為列舉型別的完整修飾名稱,後面接上冒號,再接上列舉值的名稱。舉例來說,假設套件 android.hardware.nfc, 版本 1.0 定義了列舉類型 NfcStatus

enum NfcStatus {
    STATUS_OK,
    STATUS_FAILED
};

在參照 STATUS_OK 時,完整合格名稱如下:

android.hardware.nfc@1.0::NfcStatus:STATUS_OK

一般語法為 PACKAGE@VERSION::UDT:VALUE,其中:

  • PACKAGE@VERSION::UDT 是列舉型別的完整名稱。
  • VALUE 是值的名稱。

自動推論規則

不需要指定完整的 UDT 名稱。UDT 名稱可安全地省略下列項目:

  • 套件,例如 @1.0::IFoo.Type
  • 套件和版本,例如 IFoo.Type

HIDL 會嘗試使用自動干擾規則完成名稱 (規則數字越小,優先順序越高)。

規則 1

如果未提供套件和版本,系統會嘗試查詢本機名稱。例子:

interface Nfc {
    typedef string NfcErrorMessage;
    send(NfcData d) generates (@1.0::NfcStatus s, NfcErrorMessage m);
};

NfcErrorMessage 會在本機上查詢,並找到上方的 typedefNfcData 也會在本機上查詢,但由於未在本機上定義,因此會使用規則 2 和 3。@1.0::NfcStatus 提供版本,因此規則 1 不適用。

規則 2

如果規則 1 失敗,且完整合格名稱缺少元件 (套件、版本或套件和版本),系統會自動填入目前套件中的資訊。接著,HIDL 編譯器會在目前的檔案 (以及所有匯入項目) 中尋找自動填入的完整名稱。以上述範例為例,假設 ExtendedNfcData 的宣告是在與 NfcData 相同的版本 (1.0) 中,以相同的套件 (android.hardware.nfc) 建立,如下所示:

struct ExtendedNfcData {
    NfcData base;
    // … additional members
};

HIDL 編譯器會從目前的套件填入套件名稱和版本名稱,產生完整的 UDT 名稱 android.hardware.nfc@1.0::NfcData。由於名稱已存在於目前套件中 (假設已正確匯入),因此會用於宣告。

只有在下列任一情況為真時,系統才會匯入目前套件中的名稱:

  • 並透過 import 陳述式明確匯入。
  • 這是在目前套件的 types.hal 中定義

如果 NfcData 僅以版本號碼限定,則會採用相同的程序:

struct ExtendedNfcData {
    // autofill the current package name (android.hardware.nfc)
    @1.0::NfcData base;
    // … additional members
};

規則 3

如果規則 2 無法產生相符項目 (在目前套件中未定義 UDT),HIDL 編譯器會掃描所有匯入套件中的相符項目。以上述範例為例,假設 ExtendedNfcData 是在套件 android.hardware.nfc1.1 版本中宣告,1.1 會如預期匯入 1.0 (請參閱「套件層級擴充功能」),且定義只指定 UDT 名稱:

struct ExtendedNfcData {
    NfcData base;
    // … additional members
};

編譯器會尋找任何名為 NfcData 的 UDT,並在 1.0 版本的 android.hardware.nfc 中找到一個,進而產生 android.hardware.nfc@1.0::NfcData 的完整修飾 UDT。如果系統針對特定部分限定的 UDT 找到多個相符項目,HIDL 編譯器就會擲回錯誤。

範例

根據規則 2,系統會優先使用目前套件中定義的匯入型別,而非其他套件中的匯入型別:

// hardware/interfaces/foo/1.0/types.hal
package android.hardware.foo@1.0;
struct S {};

// hardware/interfaces/foo/1.0/IFooCallback.hal
package android.hardware.foo@1.0;
interface IFooCallback {};

// hardware/interfaces/bar/1.0/types.hal
package android.hardware.bar@1.0;
typedef string S;

// hardware/interfaces/bar/1.0/IFooCallback.hal
package android.hardware.bar@1.0;
interface IFooCallback {};

// hardware/interfaces/bar/1.0/IBar.hal
package android.hardware.bar@1.0;
import android.hardware.foo@1.0;
interface IBar {
    baz1(S s); // android.hardware.bar@1.0::S
    baz2(IFooCallback s); // android.hardware.foo@1.0::IFooCallback
};
  • S 會以 android.hardware.bar@1.0::S 做為插補,並在 bar/1.0/types.hal 中找到 (因為 types.hal 會自動匯入)。
  • IFooCallback 會使用規則 2 以 android.hardware.bar@1.0::IFooCallback 做為插補,但由於 bar/1.0/IFooCallback.hal 並未自動匯入 (types.hal 則是自動匯入),因此無法找到 IFooCallback。因此,規則 3 會將其解析為 android.hardware.foo@1.0::IFooCallback,並透過 import android.hardware.foo@1.0; 匯入。

types.hal

每個 HIDL 套件都包含一個 types.hal 檔案,其中包含會在參與該套件的所有介面之間共用的 UDT。HIDL 類型一律為公開類型;無論 UDT 是在 types.hal 中宣告,還是在介面宣告中宣告,這些類型皆可在定義範圍之外存取。types.hal 並非用來描述套件的公開 API,而是用來代管套件中所有介面使用的 UDT。由於 HIDL 的特性,所有 UDT 都是介面的一部分。

types.hal 由 UDT 和 import 陳述式組成。由於 types.hal 可供套件的每個介面使用 (這是隱含匯入項目),因此這些 import 陳述式在定義上屬於套件層級。types.hal 中的 UDT 也可以納入匯入的 UDT 和介面。

例如,針對 IFoo.hal

package android.hardware.foo@1.0;
// whole package import
import android.hardware.bar@1.0;
// types only import
import android.hardware.baz@1.0::types;
// partial imports
import android.hardware.qux@1.0::IQux.Quux;
// partial imports
import android.hardware.quuz@1.0::Quuz;

匯入以下內容:

  • android.hidl.base@1.0::IBase (隱含)
  • android.hardware.foo@1.0::types (隱含)
  • android.hardware.bar@1.0 中的所有內容 (包括所有介面和其 types.hal)
  • android.hardware.baz@1.0::types 中的 types.hal (android.hardware.baz@1.0 中的介面不會匯入)
  • android.hardware.qux@1.0 中的 IQux.haltypes.hal
  • android.hardware.quuz@1.0 中的 Quuz (假設 Quuz 是在 types.hal 中定義,整個 types.hal 檔案都會剖析,但除了 Quuz 以外的類型不會匯入)。

介面層級版本管理

套件中的每個介面都位於專屬檔案中。介面所屬的套件會透過 package 陳述式在介面頂端宣告。在套件宣告之後,可能會列出零個或多個介面層級匯入項目 (部分或整個套件)。例如:

package android.hardware.nfc@1.0;

在 HIDL 中,介面可以使用 extends 關鍵字繼承其他介面。如要讓介面擴充其他介面,則必須透過 import 陳述式存取該介面。要擴充的介面名稱 (基礎介面) 必須遵循上述類型名稱限定條件規則。介面只能繼承一個介面;HIDL 不支援多重繼承。

下方的版本升級範例使用以下套件:

// types.hal
package android.hardware.example@1.0
struct Foo {
    struct Bar {
        vec<uint32_t> val;
    };
};

// IQuux.hal
package android.hardware.example@1.0
interface IQuux {
    fromFooToBar(Foo f) generates (Foo.Bar b);
}

Uprev 規則

如要定義套件 package@major.minor,必須滿足 A 或 B 的所有條件:

規則 A 「是否為起始次要版本」:所有先前的次要版本 (package@major.0package@major.1、…、package@major.(minor-1)) 均不得定義。
規則 B

符合下列所有條件:

  1. 「先前次要版本有效」:package@major.(minor-1) 必須定義並遵循相同的規則 A (package@major.0package@major.(minor-2) 都未定義) 或規則 B (如果是 @major.(minor-2) 的升級版本);

    AND

  2. 「至少繼承一個同名的介面」:存在一個可延伸 package@major.(minor-1)::IFoo 的介面 package@major.minor::IFoo (如果先前的套件有介面);

    AND

  3. 「No inherited interface with a different name」:package@major.minor::IBar 不得擴充 package@major.(minor-1)::IBaz,其中 IBarIBaz 是兩個不同的名稱。如果有同名的介面,package@major.minor::IBar 必須擴充 package@major.(minor-k)::IBar,以便確保沒有 IBar 具有較小的 k。

根據規則 A:

  • 套件可從任何次要版本號碼開始 (例如 android.hardware.biometrics.fingerprint@2.1 開始)。
  • 要求「android.hardware.foo@1.0 未定義」表示目錄 hardware/interfaces/foo/1.0 不應存在。

不過,規則 A 不會影響套件名稱相同但主要版本不同的套件 (例如,android.hardware.camera.device 同時定義了 @1.0@3.2@3.2 不需要與 @1.0 互動)。因此,@3.2::IExtFoo 可以擴充 @1.0::IFoo

如果套件名稱不同,package@major.minor::IBar 可以從名稱不同的介面延伸 (例如 android.hardware.bar@1.0::IBar 可以延伸 android.hardware.baz@2.2::IBaz)。如果介面未明確使用 extend 關鍵字宣告超類型,則會延伸 android.hidl.base@1.0::IBase (IBase 本身除外)。

必須同時遵循 B.2 和 B.3。舉例來說,即使 android.hardware.foo@1.1::IFoo 擴充 android.hardware.foo@1.0::IFoo 以通過規則 B.2,如果 android.hardware.foo@1.1::IExtBar 擴充 android.hardware.foo@1.0::IBar,這仍不是有效的升級版本。

Uprev 介面

如要將 android.hardware.example@1.0 (如上所定義) 升級至 @1.1

// types.hal
package android.hardware.example@1.1;
import android.hardware.example@1.0;

// IQuux.hal
package android.hardware.example@1.1
interface IQuux extends @1.0::IQuux {
    fromBarToFoo(Foo.Bar b) generates (Foo f);
}

這是 types.halandroid.hardware.example 版本 1.0 的套件層級 import。雖然套件 1.1 版本中未新增任何新的 UDT,但仍需要參照 1.0 版本中的 UDT,因此 types.hal 中會進行套件層級匯入。(您也可以在 IQuux.hal 中使用介面層級匯入功能,達到相同的效果)。

IQuux 宣告的 extends @1.0::IQuux 中,我們指定了要繼承的 IQuux 版本 (需要進行區分,因為 IQuux 用於宣告介面,並從介面繼承)。由於宣告只是名稱,會繼承宣告位置的所有套件和版本屬性,因此必須在基本介面名稱中加入不重複的名稱;我們也可以使用完全符合規定的 UDT,但這樣會造成重複。

新的介面 IQuux 不會重新宣告從 @1.0::IQuux 繼承的 fromFooToBar() 方法;它只會列出新增的 fromBarToFoo() 新方法。在 HIDL 中,繼承的方法不能在子介面中再次宣告,因此 IQuux 介面無法明確宣告 fromFooToBar() 方法。

Uprev 慣例

有時介面名稱必須重新命名擴充介面。建議您將列舉擴充功能、結構體和聯集的名稱與其擴充的項目保持一致,除非這些名稱有明顯差異,才需要使用新名稱。例如:

// in parent hal file
enum Brightness : uint32_t { NONE, WHITE };

// in child hal file extending the existing set with additional similar values
enum Brightness : @1.0::Brightness { AUTOMATIC };

// extending the existing set with values that require a new, more descriptive name:
enum Color : @1.0::Brightness { HW_GREEN, RAINBOW };

如果方法可以使用新的語意名稱 (例如 fooWithLocation),則建議使用該名稱。否則,應以類似的方式命名擴充項目。舉例來說,如果沒有更好的替代名稱,@1.1::IFoo 中的 foo_1_1 方法可以取代 @1.0::IFoofoo 方法的功能。

套件層級版本管理

HIDL 版本管理會在套件層級執行;套件發布後即無法變更 (其介面和 UDT 集合無法變更)。套件可以透過多種方式彼此關聯,所有這些方式都可以透過介面層級繼承和透過組合建構 UDT 來表達。

不過,系統會嚴格定義並強制執行一種關係:套件層級向後相容的繼承。在這個情境中,父項套件是繼承的套件,而子項套件則是擴充父項的套件。套件層級的回溯相容繼承規則如下:

  1. 子套件中的介面會繼承父項套件的所有頂層介面。
  2. 您也可以新增介面,並將其加入新套件 (不受限於與其他套件中其他介面的關係)。
  3. 您也可以新增資料類型,讓更新版現有介面的新方法或新介面使用。

這些規則可透過使用 HIDL 介面層級繼承和 UDT 組合來實作,但需要超級層級知識,才能瞭解這些關係構成了向後相容的套件擴充功能。系統會根據下列方式推斷這項知識:

如果套件符合這項規定,hidl-gen 就會強制執行回溯相容性規則。