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 共享相同的版本。包版本遵循語義版本控制,沒有補丁級別和構建元數據組件。在給定的包中,次要版本凸起意味著包的新版本與舊包向後兼容,主要版本凸起意味著包的新版本與舊包不向後兼容。
從概念上講,一個包可以通過以下幾種方式之一與另一個包相關聯:
- 一點也不。
- 包級向後兼容的可擴展性。這發生在包的新次要版本升級(下一個增量修訂);新包與舊包具有相同的名稱和主要版本,但次要版本更高。在功能上,新包是舊包的超集,意思是:
- 父包的頂級接口存在於新包中,儘管這些接口可能有新方法、新接口本地 UDT(下面描述的接口級擴展)和
types.hal
中的新 UDT。 - 新接口也可以添加到新包中。
- 父包的所有數據類型都存在於新包中,並且可以由舊包中的(可能重新實現的)方法處理。
- 也可以添加新的數據類型以供升級現有接口的新方法或新接口使用。
- 父包的頂級接口存在於新包中,儘管這些接口可能有新方法、新接口本地 UDT(下面描述的接口級擴展)和
- 接口級向後兼容的可擴展性。新包還可以通過包含邏輯分離的接口來擴展原始包,這些接口僅提供附加功能,而不是核心功能。為此,可能需要以下內容:
- 新包中的接口需要使用舊包的數據類型。
- 新包中的接口可以擴展一個或多個舊包的接口。
- 擴展原來的向後不兼容。這是軟件包的主要版本升級,兩者之間不需要任何關聯。在某種程度上,它可以用舊版本包中的類型組合以及舊包接口子集的繼承來表示。
結構化接口
對於結構良好的界面,添加不屬於原始設計的新功能類型應該需要修改 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
代碼為system.img
上的組件提供了字符串消息或vec<uint8_t>
數據,則該數據無法由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/
下。包android.hardware.
[ name1
].[ name2
]… at version $m.$n
is under hardware/interfaces/name1/name2/
… /$m.$n/
;包android.hardware.camera
版本3.4
位於目錄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
是包的以點分隔的major.minor-version 格式(例如,1.0
)。 -
UDT
是 HIDL UDT 的點分隔名稱。由於 HIDL 支持嵌套 UDT,並且 HIDL 接口可以包含 UDT(一種嵌套聲明),因此使用點來訪問名稱。
例如,如果在包android.hardware.example
版本1.0
的通用類型文件中定義了以下嵌套聲明:
// 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
引用Bar
: Foo.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 編譯器會在所有導入的包中掃描匹配項。使用上面的例子,假設在android.hardware.nfc
包的1.1
版本中聲明了ExtendedNfcData
, 1.1
導入了1.0
(參見Package-Level Extensions ),並且定義只指定了 UDT 名稱:
struct ExtendedNfcData { NfcData base; // … additional members };
編譯器會查找任何名為NfcData
的 UDT,並在android.hardware.nfc
版本1.0
中找到一個,從而生成完全限定的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
),因此無法找到它。因此,規則 3 將其解析為android.hardware.foo@1.0::IFooCallback
,它是通過import android.hardware.foo@1.0;
的。 )。
類型.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
) -
types.hal
fromandroid.hardware.baz@1.0::types
(android.hardware.baz@1.0
中的接口沒有被導入) - 來自
android.hardware.qux@1.0
的IQux.hal
和types.hal
-
Quuz
fromandroid.hardware.quuz@1.0
(假設Quuz
定義在types.hal
中,整個types.hal
文件被解析,但Quuz
以外的類型不被導入)。
接口級版本控制
包中的每個接口都駐留在自己的文件中。接口所屬的包使用package
語句在接口頂部聲明。在包聲明之後,可以列出零個或多個接口級導入(部分或整個包)。例如:
package android.hardware.nfc@1.0;
在 HIDL 中,接口可以使用extends
關鍵字從其他接口繼承。對於擴展另一個接口的接口,它必須能夠通過import
語句訪問它。被擴展的接口(基本接口)的名稱遵循上面解釋的類型名稱限定規則。一個接口只能從一個接口繼承; HIDL 不支持多重繼承。
下面的 uprev 版本控制示例使用以下包:
// 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); }
更新規則
要定義一個包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
,這仍然不是有效的升級。
升級界面
將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
的版本(需要消除歧義,因為IQuux
用於聲明接口並從接口繼承)。由於聲明只是在聲明位置繼承所有包和版本屬性的名稱,因此消歧必須在基本接口的名稱中;我們也可以使用完全限定的 UDT,但這將是多餘的。
新接口IQuux
不會重新聲明從@1.0::IQuux
繼承的fromFooToBar()
方法;它只是列出了它添加的新方法fromBarToFoo()
。在 HIDL 中,繼承的方法可能不會在子接口中再次聲明,因此IQuux
接口無法顯式聲明fromFooToBar()
方法。
更新約定
有時接口名稱必須重命名擴展接口。我們建議枚舉擴展、結構和聯合與它們擴展的名稱相同,除非它們的差異足以保證使用新名稱。例子:
// 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
強制執行向後兼容規則。