HIDL 要求每個以 HIDL 編寫的介面都必須有版本。HAL 介面發布後會凍結,因此必須在該介面的新版本中進行任何後續變更。雖然已發布的介面無法修改,但可以透過其他介面擴充。
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.img
和 system.img
可分別編譯。vendor.img
和 system.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
。在套件或介面層級,您必須透過 Foo
:Foo.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
會在本機上查詢,並找到上方的 typedef
。NfcData
也會在本機上查詢,但由於未在本機上定義,因此會使用規則 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.nfc
的 1.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.hal
和types.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.0 、package@major.1 、…、package@major.(minor-1) ) 均不得定義。 |
---|
規則 B | 符合下列所有條件:
|
---|
根據規則 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.hal
中 android.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::IFoo
中 foo
方法的功能。
套件層級版本管理
HIDL 版本管理會在套件層級執行;套件發布後即無法變更 (其介面和 UDT 集合無法變更)。套件可以透過多種方式彼此關聯,所有這些方式都可以透過介面層級繼承和透過組合建構 UDT 來表達。
不過,系統會嚴格定義並強制執行一種關係:套件層級向後相容的繼承。在這個情境中,父項套件是繼承的套件,而子項套件則是擴充父項的套件。套件層級的回溯相容繼承規則如下:
- 子套件中的介面會繼承父項套件的所有頂層介面。
- 您也可以新增介面,並將其加入新套件 (不受限於與其他套件中其他介面的關係)。
- 您也可以新增資料類型,讓更新版現有介面的新方法或新介面使用。
這些規則可透過使用 HIDL 介面層級繼承和 UDT 組合來實作,但需要超級層級知識,才能瞭解這些關係構成了向後相容的套件擴充功能。系統會根據下列方式推斷這項知識:
如果套件符合這項規定,hidl-gen
就會強制執行回溯相容性規則。