HIDL


HIDL

HAL 接口定義語言(簡稱 HIDL,發音為“hide-l”)是用於指定 HAL 和其用戶之間的接口的一種接口描述語言 (IDL)。HIDL 允許指定類型和方法調用(會匯集到接口和軟件包中)。從更廣泛的意義上來說,HIDL 是用於在可以獨立編譯的代碼庫之間進行通信的系統。

HIDL 旨在用於進程間通信 (IPC)。進程之間的通信經過 Binder 化。對於必須與進程相關聯的代碼庫,還可以使用直通模式(在 Java 中不受支持)。

HIDL 可指定數據結構和方法簽名,這些內容會整理歸類到接口(與類相似)中,而接口會匯集到軟件包中。盡管 HIDL 具有一系列不同的關鍵字,但 C++ 和 Java 程序員對 HIDL 的語法並不陌生。此外,HIDL 還使用 Java 樣式的注釋。

HIDL 設計

HIDL 的目標是,框架可以在無需重新構建 HAL 的情況下進行替換。HAL 將由供應商或 SOC 制造商構建,放置在設備的 /vendor 分區中,這樣一來,框架就可以在其自己的分區中通過 OTA 進行替換,而無需重新編譯 HAL。

HIDL 設計在以下方面之間保持了平衡:

  • 互操作性。在可以使用各種架構、工具鏈和編譯配置來編譯的進程之間創建可互操作的可靠接口。HIDL 接口是分版本的,發布后不得再進行更改。
  • 效率。HIDL 會嘗試盡可能減少復制操作的次數。HIDL 定義的數據以 C++ 標准布局數據結構傳遞至 C++ 代碼,無需解壓,可直接使用。此外,HIDL 還提供共享內存接口;由於 RPC 本身有點慢,因此 HIDL 支持兩種無需使用 RPC 調用的數據傳輸方法:共享內存和快速消息隊列 (FMQ)。
  • 直觀。通過僅針對 RPC 使用 in 參數,HIDL 避開了內存所有權這一棘手問題(請參閱 Android 接口定義語言 (AIDL));無法從方法高效返回的值將通過回調函數返回。無論是將數據傳遞到 HIDL 中以進行傳輸,還是從 HIDL 接收數據,都不會改變數據的所有權,也就是說,數據所有權始終屬於調用函數。數據僅需要在函數被調用期間保留,可在被調用的函數返回數據后立即清除。

使用直通模式

要將運行早期版本的 Android 的設備更新為使用 Android O,您可以將慣用的(和舊版)HAL 封裝在一個新 HIDL 接口中,該接口將在綁定式模式和同進程(直通)模式提供 HAL。這種封裝對於 HAL 和 Android 框架來說都是透明的。

直通模式僅適用於 C++ 客戶端和實現。運行早期版本的 Android 的設備沒有用 Java 編寫的 HAL,因此 Java HAL 自然而然經過 Binder 化。

編譯 .hal 文件時,除了用於 Binder 通信的標頭之外,hidl-gen 還會生成一個額外的直通標頭文件 BsFoo.h;此標頭定義了會被執行 dlopen 操作的函數。由於直通式 HAL 在它們被調用的同一進程中運行,因此在大多數情況下,直通方法由直接函數調用(同一線程)來調用。oneway 方法在各自的線程中運行,因為它們不需要等待 HAL 來處理它們(這意味着,在直通模式下使用 oneway 方法的所有 HAL 對於線程必須是安全的)。

如果有一個 IFoo.halBsFoo.h 會封裝 HIDL 生成的方法,以提供額外的功能(例如使 oneway 事務在其他線程中運行)。該文件類似於 BpFoo.h,不過,所需函數是直接調用的,並未使用 Binder 傳遞調用 IPC。未來,HAL 的實現可能提供多種實現結果,例如 FooFast HAL 和 FooAccurate HAL。在這種情況下,系統會針對每個額外的實現結果創建一個文件(例如 PTFooFast.cpp 和 PTFooAccurate.cpp)。

Binder 化直通式 HAL

您可以將支持直通模式的 HAL 實現 Binder 化。如果有一個 HAL 接口 a.b.c.d@M.N::IFoo,系統會創建兩個軟件包:

  • a.b.c.d@M.N::IFoo-impl。包含 HAL 的實現,並暴露函數 IFoo* HIDL_FETCH_IFoo(const char* name)。在舊版設備上,此軟件包經過 dlopen 處理,且實現使用 HIDL_FETCH_IFoo 進行了實例化。您可以使用 hidl-gen 和 -Lc++-impl 以及 -Landroidbp-impl 來生成基礎代碼。
  • a.b.c.d@M.N::IFoo-service。打開直通式 HAL,並將其自身注冊為 Binder 化服務,從而使同一 HAL 實現能夠同時以直通模式和 Binder 化模式使用。

如果有一個 IFoo,您可以調用 sp<IFoo> IFoo::getService(string name, bool getStub),以獲取對 IFoo 實例的訪問權限。如果 getStub 為 True,則 getService 會嘗試僅在直通模式下打開 HAL。如果 getStub 為 False,則 getService 會嘗試找到 Binder 化服務;如果未找到,則它會嘗試找到直通式服務。除了在 defaultPassthroughServiceImplementation 中,其余情況一律不得使用 getStub 參數。(搭載 Android O 的設備是完全 Binder 化的設備,因此不得在直通模式下打開服務。)

HIDL 語法

根據設計,HIDL 語言與 C 語言類似(但前者不使用 C 預處理器)。下面未描述的所有標點符號(用途明顯的 =和 | 除外)都是語法的一部分。

注意:有關 HIDL 代碼樣式的詳細信息,請參閱代碼樣式指南

  • /** */ 表示文檔注釋。此樣式只能應用於類型、方法、字段和枚舉值聲明。
  • /* */ 表示多行注釋。
  • // 表示注釋一直持續到行結束。除了 //,換行符與任何其他空白一樣。
  • 在以下示例語法中,從 // 到行結束的文本不是語法的一部分,而是對語法的注釋。
  • [empty] 表示該字詞可能為空。
  • ? 跟在文本或字詞后,表示它是可選的。
  • ... 表示包含零個或多個項、用指定的分隔符號分隔的序列。HIDL 中不含可變參數。
  • 逗號用於分隔序列元素。
  • 分號用於終止各個元素,包括最后的元素。
  • 大寫字母是非終止符。
  • italics 是一個令牌系列,如 integer 或 identifier(標准 C 解析規則)。
  • constexpr 是 C 樣式的常量表達式(例如 1 + 1 和 1L << 3)。
  • import_name 是軟件包或接口名稱,按 HIDL 版本編號中所述的方式加以限定。
  • 小寫 words 是文本令牌。

例如:

ROOT =
    PACKAGE IMPORTS PREAMBLE { ITEM ITEM ... }  // not for types.hal
  | PACKAGE IMPORTS ITEM ITEM...  // only for types.hal; no method definitions

ITEM =
    ANNOTATIONS? oneway? identifier(FIELD, FIELD ...) GENERATES?;
  |  safe_union identifier { UFIELD; UFIELD; ...};
  |  struct identifier { SFIELD; SFIELD; ...};  // Note - no forward declarations
  |  union identifier { UFIELD; UFIELD; ...};
  |  enum identifier: TYPE { ENUM_ENTRY, ENUM_ENTRY ... }; // TYPE = enum or scalar
  |  typedef TYPE identifier;

VERSION = integer.integer;

PACKAGE = package android.hardware.identifier[.identifier[...]]@VERSION;

PREAMBLE = interface identifier EXTENDS

EXTENDS = <empty> | extends import_name  // must be interface, not package

GENERATES = generates (FIELD, FIELD ...)

// allows the Binder interface to be used as a type
// (similar to typedef'ing the final identifier)
IMPORTS =
   [empty]
  |  IMPORTS import import_name;

TYPE =
  uint8_t | int8_t | uint16_t | int16_t | uint32_t | int32_t | uint64_t | int64_t |
 float | double | bool | string
|  identifier  // must be defined as a typedef, struct, union, enum or import
               // including those defined later in the file
|  memory
|  pointer
|  vec<TYPE>
|  bitfield<TYPE>  // TYPE is user-defined enum
|  fmq_sync<TYPE>
|  fmq_unsync<TYPE>
|  TYPE[SIZE]

FIELD =
   TYPE identifier

UFIELD =
   TYPE identifier
  |  safe_union identifier { FIELD; FIELD; ...} identifier;
  |  struct identifier { FIELD; FIELD; ...} identifier;
  |  union identifier { FIELD; FIELD; ...} identifier;

SFIELD =
   TYPE identifier
  |  safe_union identifier { FIELD; FIELD; ...};
  |  struct identifier { FIELD; FIELD; ...};
  |  union identifier { FIELD; FIELD; ...};
  |  safe_union identifier { FIELD; FIELD; ...} identifier;
  |  struct identifier { FIELD; FIELD; ...} identifier;
  |  union identifier { FIELD; FIELD; ...} identifier;

SIZE =  // Must be greater than zero
     constexpr

ANNOTATIONS =
     [empty]
  |  ANNOTATIONS ANNOTATION

ANNOTATION =
  |  @identifier
  |  @identifier(VALUE)
  |  @identifier(ANNO_ENTRY, ANNO_ENTRY  ...)

ANNO_ENTRY =
     identifier=VALUE

VALUE =
     "any text including \" and other escapes"
  |  constexpr
  |  {VALUE, VALUE ...}  // only in annotations

ENUM_ENTRY =
     identifier
  |  identifier = constexpr
 

術語

本部分使用的 HIDL 相關術語如下:

Binder 化 表示 HIDL 用於進程之間的遠程過程調用,並通過類似 Binder 的機制來實現。另請參閱“直通式”。
異步回調 由 HAL 用戶提供、傳遞給 HAL(通過 HIDL 方法)並由 HAL 調用以隨時返回數據的接口。
同步回調 將數據從服務器的 HIDL 方法實現返回到客戶端。不用於返回無效值或單個原始值的方法。
客戶端 調用特定接口的方法的進程。HAL 進程或框架進程可以是一個接口的客戶端和另一個接口的服務器。另請參閱“直通式”。
擴展 表示向另一接口添加方法和/或類型的接口。一個接口只能擴展另一個接口。可用於具有相同軟件包名稱的 Minor 版本遞增,也可用於在舊軟件包的基礎上構建的新軟件包(例如,供應商擴展)。
生成 表示將值返回給客戶端的接口方法。要返回一個非原始值或多個值,則會生成同步回調函數。
接口 方法和類型的集合。會轉換為 C++ 或 Java 中的類。接口中的所有方法均按同一方向調用:客戶端進程會調用由服務器進程實現的方法。
單向 應用到 HIDL 方法時,表示該方法既不返回任何值也不會造成阻塞。
軟件包 共用一個版本的接口和數據類型的集合。
直通式 HIDL 的一種模式,使用這種模式時,服務器是共享庫,由客戶端進行 dlopen 處理。在直通模式下,客戶端和服務器是相同的進程,但代碼庫不同。此模式僅用於將舊版代碼庫並入 HIDL 模型。另請參閱“Binder 化”。
服務器 實現接口的方法的進程。另請參閱“直通式”。
傳輸 在服務器和客戶端之間移動數據的 HIDL 基礎架構。
版本

軟件包的版本。由兩個整數組成:Major 版本和 Minor 版本。Minor 版本遞增可以添加(但不會更改)類型和方法。

 

 

接口和軟件包

HIDL 是圍繞接口進行編譯的,接口是面向對象的語言使用的一種用來定義行為的抽象類型。每個接口都是軟件包的一部分。

軟件包

軟件包名稱可以具有子級,例如 package.subpackage。已發布的 HIDL 軟件包的根目錄是 hardware/interfaces 或 vendor/vendorName(例如 Pixel 設備為 vendor/google)。軟件包名稱在根目錄下形成一個或多個子目錄;定義軟件包的所有文件都位於同一目錄下。例如,package android.hardware.example.extension.light@2.0 可以在 hardware/interfaces/example/extension/light/2.0 下找到。

下表列出了軟件包前綴和位置:

軟件包前綴 位置 接口類型
android.hardware.* hardware/interfaces/* HAL
android.frameworks.* frameworks/hardware/interfaces/* frameworks/ 相關
android.system.* system/hardware/interfaces/* system/ 相關
android.hidl.* system/libhidl/transport/* core

軟件包目錄中包含擴展名為 .hal 的文件。每個文件均必須包含一個指定文件所屬的軟件包和版本的 package 語句。文件 types.hal(如果存在)並不定義接口,而是定義軟件包中每個接口可以訪問的數據類型。

接口定義

除了 types.hal 之外,其他 .hal 文件均定義一個接口。接口通常定義如下:

interface IBar extends IFoo { // IFoo is another interface
    // embedded types
    struct MyStruct {/*...*/};

    // interface methods
    create(int32_t id) generates (MyStruct s);
    close();
};
 

不含顯式 extends 聲明的接口會從 android.hidl.base@1.0::IBase(類似於 Java 中的 java.lang.Object)隱式擴展。隱式導入的 IBase 接口聲明了多種不應也不能在用戶定義的接口中重新聲明或以其他方式使用的預留方法。這些方法包括:

  • ping
  • interfaceChain
  • interfaceDescriptor
  • notifySyspropsChanged
  • linkToDeath
  • unlinkToDeath
  • setHALInstrumentation
  • getDebugInfo
  • debug
  • getHashChain

導入

import 語句是用於訪問其他軟件包中的軟件包接口和類型的 HIDL 機制。import 語句本身涉及兩個實體:

  • 導入實體:可以是軟件包或接口;以及
  • 被導入實體:也可以是軟件包或接口。

導入實體由 import 語句的位置決定。當該語句位於軟件包的 types.hal 中時,導入的內容對整個軟件包是可見的;這是軟件包級導入。當該語句位於接口文件中時,導入實體是接口本身;這是接口級導入。

被導入實體由 import 關鍵字后面的值決定。該值不必是完全限定名稱;如果某個組成部分被刪除了,系統會自動使用當前軟件包中的信息填充該組成部分。 對於完全限定值,支持的導入情形有以下幾種:

  • 完整軟件包導入。如果該值是一個軟件包名稱和版本(語法見下文),則系統會將整個軟件包導入至導入實體中。
  • 部分導入。如果值為:
    • 一個接口,則系統會將該軟件包的 types.hal 和該接口導入至導入實體中。
    • 在 types.hal 中定義的 UDT,則系統僅會將該 UDT 導入至導入實體中(不導入 types.hal 中的其他類型)。
  • 僅類型導入。如果該值將上文所述的“部分導入”的語法與關鍵字 types 而不是接口名稱配合使用,則系統僅會導入指定軟件包的 types.hal 中的 UDT。

導入實體可以訪問以下各項的組合:

  • types.hal 中定義的被導入軟件包的常見 UDT;
  • 被導入的軟件包的接口(完整軟件包導入)或指定接口(部分導入),以便調用它們、向其傳遞句柄和/或從其繼承句柄。

導入語句使用完全限定類型名稱語法來提供被導入的軟件包或接口的名稱和版本:

import android.hardware.nfc@1.0;            // import a whole package
import android.hardware.example@1.0::IQuux; // import an interface and types.hal
import android.hardware.example@1.0::types; // import just types.hal
 

接口繼承

接口可以是之前定義的接口的擴展。擴展可以是以下三種類型中的一種:

  • 接口可以向其他接口添加功能,並按原樣納入其 API。
  • 軟件包可以向其他軟件包添加功能,並按原樣納入其 API。
  • 接口可以從軟件包或特定接口導入類型。

接口只能擴展一個其他接口(不支持多重繼承)。具有非零 Minor 版本號的軟件包中的每個接口必須擴展一個以前版本的軟件包中的接口。例如,如果 4.0 版本的軟件包 derivative 中的接口 IBar 是基於(擴展了)1.2 版本的軟件包 original 中的接口 IFoo,並且您又創建了 1.3 版本的軟件包 original,則 4.1 版本的 IBar 不能擴展 1.3 版本的 IFoo。相反,4.1 版本的 IBar 必須擴展 4.0 版本的 IBar,因為后者是與 1.2 版本的 IFoo 綁定的。 如果需要,5.0 版本的 IBar 可以擴展 1.3 版本的 IFoo

接口擴展並不意味着生成的代碼中存在代碼庫依賴關系或跨 HAL 包含關系,接口擴展只是在 HIDL 級別導入數據結構和方法定義。HAL 中的每個方法必須在相應 HAL 中實現。

供應商擴展

在某些情況下,供應商擴展會作為以下基礎對象的子類予以實現:代表其擴展的核心接口的基礎對象。同一對象會同時在基礎 HAL 名稱和版本下,以及擴展的(供應商)HAL 名稱和版本下注冊。

版本編號

軟件包分版本,且接口的版本和其軟件包的版本相同。版本用兩個整數表示:major.minor。

  • Major 版本不向后兼容。遞增 Major 版本號將會使 Minor 版本號重置為 0。
  • Minor 版本向后兼容。如果遞增 Minor 版本號,則意味着較新版本完全向后兼容之前的版本。您可以添加新的數據結構和方法,但不能更改現有的數據結構或方法簽名。

可同時在一台設備上提供 HAL 的多個 Major 或 Minor 版本。不過,Minor 版本應優先於 Major 版本,因為與之前的 Minor 版本接口一起使用的客戶端代碼也適用於同一接口的后續 Minor 版本。有關版本控制和供應商擴展的更多詳細信息,請參閱 HIDL 版本控制

接口布局總結

本部分總結了如何管理 HIDL 接口軟件包(如 hardware/interfaces)並整合了整個 HIDL 部分提供的信息。在閱讀之前,請務必先熟悉 HIDL 版本控制使用 hidl-gen 添加哈希中的哈希概念、關於在一般情況下使用 HIDL 的詳細信息以及以下定義:

術語 定義
應用二進制接口 (ABI) 應用編程接口 + 所需的任何二進制鏈接。
完全限定名稱 (fqName) 用於區分 hidl 類型的名稱。例如:android.hardware.foo@1.0::IFoo
軟件包 包含 HIDL 接口和類型的軟件包。例如:android.hardware.foo@1.0
軟件包根目錄 包含 HIDL 接口的根目錄軟件包。例如:HIDL 接口 android.hardware 在軟件包根目錄 android.hardware.foo@1.0 中。
軟件包根目錄路徑 軟件包根目錄映射到的 Android 源代碼樹中的位置。

有關更多定義,請參閱 HIDL 術語

每個文件都可以通過軟件包根目錄映射及其完全限定名稱找到

軟件包根目錄以參數 -r android.hardware:hardware/interfaces 的形式指定給 hidl-gen。例如,如果軟件包為 vendor.awesome.foo@1.0::IFoo 並且向 hidl-gen 發送了 -r vendor.awesome:some/device/independent/path/interfaces,那么接口文件應該位於 $ANDROID_BUILD_TOP/some/device/independent/path/interfaces/foo/1.0/IFoo.hal

在實踐中,建議稱為 awesome 的供應商或原始設備制造商 (OEM) 將其標准接口放在 vendor.awesome 中。在選擇了軟件包路徑之后,不能再更改該路徑,因為它已寫入接口的 ABI。

軟件包路徑映射不得重復

例如,如果您有 -rsome.package:$PATH_A 和 -rsome.package:$PATH_B,則 $PATH_A 必須等於 $PATH_B 才能實現一致的接口目錄(這也能讓接口版本控制起來更簡單)。

軟件包根目錄必須有版本控制文件

如果您創建一個軟件包路徑(如 -r vendor.awesome:vendor/awesome/interfaces),則還應創建文件 $ANDROID_BUILD_TOP/vendor/awesome/interfaces/current.txt,該文件應包含使用 hidl-gen(在使用 hidl-gen 添加哈希中廣泛進行了討論)中的 -Lhash 選項所創建接口的哈希。

注意:請謹慎管理所有更改!如果接口未凍結,則 供應商測試套件 (VTS) 將失敗,並且對接口進行的與 ABI 不兼容的更改將導致僅限框架的 OTA 失敗。

接口位於設備無關的位置

在實踐中,建議在分支之間共享接口。這樣可以最大限度地重復使用代碼,並在不同的設備和用例中對代碼進行最大程度的測試。

 

接口哈希

本文檔介紹了 HIDL 接口哈希,該哈希是一種旨在防止意外更改接口並確保接口更改經過全面審查的機制。這種機制是必需的,因為 HIDL 接口帶有版本編號,也就是說,接口一經發布便不得再更改,但不會影響應用二進制接口 (ABI) 的情況(例如更正備注)除外。

布局

每個軟件包根目錄(即映射到 hardware/interfaces 的 android.hardware 或映射到 vendor/foo/hardware/interfaces 的 vendor.foo)都必須包含一個列出所有已發布 HIDL 接口文件的 current.txt 文件。

# current.txt files support comments starting with a ‘#' character
# this file, for instance, would be vendor/foo/hardware/interfaces/current.txt

# Each line has a SHA-256 hash followed by the name of an interface.
# They have been shortened in this doc for brevity but they are
# 64 characters in length in an actual current.txt file.
d4ed2f0e...995f9ec4 vendor.awesome.foo@1.0::IFoo # comments can also go here

# types.hal files are also noted in types.hal files
c84da9f5...f8ea2648 vendor.awesome.foo@1.0::types

# Multiple hashes can be in the file for the same interface. This can be used
# to note how ABI sustaining changes were made to the interface.
# For instance, here is another hash for IFoo:

# Fixes type where "FooCallback" was misspelled in comment on "FooStruct"
822998d7...74d63b8c vendor.awesome.foo@1.0::IFoo
 

注意:為了便於跟蹤各個哈希的來源,Google 會將 HIDL current.txt 文件分為不同的部分:第一部分列出在 Android O 中發布的接口文件,第二部分列出在 Android O MR1 中發布的接口文件。我們強烈建議在您的 current.txt 文件中使用類似布局。

使用 hidl-gen 添加哈希

您可以手動將哈希添加到 current.txt 文件中,也可以使用 hidl-gen 添加。以下代碼段提供了可與 hidl-gen 搭配使用來管理 current.txt 文件的命令示例(哈希已縮短):

hidl-gen -L hash -r vendor.awesome:vendor/awesome/hardware/interfaces -r android.hardware:hardware/interfaces -r android.hidl:system/libhidl/transport vendor.awesome.nfc@1.0::types
9626fd18...f9d298a6 vendor.awesome.nfc@1.0::types
hidl-gen -L hash -r vendor.awesome:vendor/awesome/hardware/interfaces -r android.hardware:hardware/interfaces -r android.hidl:system/libhidl/transport vendor.awesome.nfc@1.0::INfc
07ac2dc9...11e3cf57 vendor.awesome.nfc@1.0::INfc
hidl-gen -L hash -r vendor.awesome:vendor/awesome/hardware/interfaces -r android.hardware:hardware/interfaces -r android.hidl:system/libhidl/transport vendor.awesome.nfc@1.0
9626fd18...f9d298a6 vendor.awesome.nfc@1.0::types
07ac2dc9...11e3cf57 vendor.awesome.nfc@1.0::INfc
f2fe5442...72655de6 vendor.awesome.nfc@1.0::INfcClientCallback
hidl-gen -L hash -r vendor.awesome:vendor/awesome/hardware/interfaces -r android.hardware:hardware/interfaces -r android.hidl:system/libhidl/transport vendor.awesome.nfc@1.0 >> vendor/awesome/hardware/interfaces/current.txt
 

警告:請勿更換之前發布的接口的哈希。如要更改此類接口,請向 current.txt 文件的末尾添加新的哈希。要了解詳情,請參閱 ABI 穩定性

hidl-gen 生成的每個接口定義庫都包含哈希,通過調用 IBase::getHashChain 可檢索這些哈希。hidl-gen編譯接口時,會檢查 HAL 軟件包根目錄中的 current.txt 文件,以查看 HAL 是否已被更改:

  • 如果沒有找到 HAL 的哈希,則接口會被視為未發布(處於開發階段),並且編譯會繼續進行。
  • 如果找到了相應哈希,則會對照當前接口對其進行檢查:
    • 如果接口與哈希匹配,則編譯會繼續進行。
    • 如果接口與哈希不匹配,則編譯會暫停,因為這意味着之前發布的接口會被更改。
      • 要在更改的同時不影響 ABI(請參閱 ABI 穩定性),請務必先修改 current.txt 文件,然后編譯才能繼續進行。
      • 所有其他更改都應在接口的 minor 或 major 版本升級中進行。

ABI 穩定性

要點:請仔細閱讀並理解本部分。

應用二進制接口 (ABI) 包括二進制關聯/調用規范/等等。如果 ABI/API 發生更改,則相應接口就不再適用於使用官方接口編譯的常規 system.img

確保接口帶有版本編號且 ABI 穩定至關重要,具體原因有如下幾個:

  • 可確保您的實現能夠通過供應商測試套件 (VTS) 測試,通過該測試后您將能夠正常進行僅限框架的 OTA。
  • 作為原始設備制造商 (OEM),您將能夠提供簡單易用且符合規定的板級支持包 (BSP)。
  • 有助於您跟蹤哪些接口可以發布。您可以將 current.txt 視為接口目錄的“地圖”,從中了解軟件包根目錄中提供的所有接口的歷史記錄和狀態。

對於在 current.txt 中已有條目的接口,為其添加新的哈希時,請務必僅為可保持 ABI 穩定性的接口添加哈希。請查看以下更改類型:

允許的更改
  • 更改備注(除非這會更改方法的含義)。
  • 更改參數的名稱。
  • 更改返回參數的名稱。
  • 更改注釋。
不允許的更改
  • 重新排列參數、方法等…
  • 重命名接口或將其移至新的軟件包。
  • 重命名軟件包。
  • 在接口的任意位置添加方法/結構體字段等等…
  • 會破壞 C++ vtable 的任何更改。
  • 等等…

服務和數據轉移

本部分介紹了如何注冊和發現服務,以及如何通過調用 .hal 文件內的接口中定義的方法將數據發送到服務。

注冊服務

HIDL 接口服務器(實現接口的對象)可注冊為已命名的服務。注冊的名稱不需要與接口或軟件包名稱相關。如果沒有指定名稱,則使用名稱“默認”;這應該用於不需要注冊同一接口的兩個實現的 HAL。例如,在每個接口中定義的服務注冊的 C++ 調用是:

status_t status = myFoo->registerAsService();
status_t anotherStatus = anotherFoo->registerAsService("another_foo_service");  // if needed
 

HIDL 接口的版本包含在接口本身中。版本自動與服務注冊關聯,並可通過每個 HIDL 接口上的方法調用 (android::hardware::IInterface::getInterfaceVersion()) 進行檢索。服務器對象不需要注冊,並可通過 HIDL 方法參數傳遞到其他進程,相應的接收進程會向服務器發送 HIDL 方法調用。

發現服務

客戶端代碼按名稱和版本請求指定的接口,並對所需的 HAL 類調用 getService

// C++
sp<V1_1::IFooService> service = V1_1::IFooService::getService();
sp<V1_1::IFooService> alternateService = V1_1::IFooService::getService("another_foo_service");
// Java
V1_1.IFooService service = V1_1.IFooService.getService(true /* retry */);
V1_1.IFooService alternateService = V1_1.IFooService.getService("another", true /* retry */);
 

每個版本的 HIDL 接口都會被視為單獨的接口。因此,IFooService 版本 1.1 和 IFooService 版本 2.2 都可以注冊為“foo_service”,並且兩個接口上的 getService("foo_service") 都可獲取該接口的已注冊服務。因此,在大多數情況下,注冊或發現服務均無需提供名稱參數(也就是說名稱為“默認”)。

供應商接口對象還會影響所返回接口的傳輸方法。對於軟件包 android.hardware.foo@1.0 中的接口 IFooIFoo::getService 返回的接口始終使用設備清單中針對 android.hardware.foo 聲明的傳輸方法(如果相應條目存在的話);如果該傳輸方法不存在,則返回 nullptr。

在某些情況下,即使沒有獲得相關服務,也可能需要立即繼續。例如,當客戶端希望自行管理服務通知或者在需要獲取所有 hwservice 並檢索它們的診斷程序(例如 atrace)中時,可能會發生這種情況。在這種情況下,可以使用其他 API,例如 C++ 中的 tryGetService 或 Java 中的 getService("instance-name", false)。Java 中提供的舊版 API getService 也必須與服務通知一起使用。使用此 API 不會避免以下競態條件:當客戶端使用某個非重試 API 請求服務器后,該服務器對自身進行了注冊。

服務終止通知

想要在服務終止時收到通知的客戶端會接收到框架傳送的終止通知。要接收通知,客戶端必須:

  1. 將 HIDL 類/接口 hidl_death_recipient(位於 C++ 代碼中,而非 HIDL 中)歸入子類。
  2. 替換其 serviceDied() 方法。
  3. 實例化 hidl_death_recipient 子類的對象。
  4. 在要監控的服務上調用 linkToDeath() 方法,並傳入 IDeathRecipient 的接口對象。請注意,此方法並不具備在其上調用它的終止接收方或代理的所有權。

偽代碼示例(C++ 和 Java 類似):

class IMyDeathReceiver : hidl_death_recipient {
  virtual void serviceDied(uint64_t cookie,
                           wp<IBase>& service) override {
    log("RIP service %d!", cookie);  // Cookie should be 42
  }
};
....
IMyDeathReceiver deathReceiver = new IMyDeathReceiver();
m_importantService->linkToDeath(deathReceiver, 42);
 

同一終止接收方可能已在多個不同的服務上注冊。

數據轉移

可通過調用 .hal 文件內的接口中定義的方法將數據發送到服務。具體方法有兩類:

  • 阻塞方法會等到服務器產生結果。
  • 單向方法僅朝一個方向發送數據且不阻塞。如果 RPC 調用中正在傳輸的數據量超過實現限制,則調用可能會阻塞或返回錯誤指示(具體行為尚不確定)。

不返回值但未聲明為 oneway 的方法仍會阻塞。

在 HIDL 接口中聲明的所有方法都是單向調用,要么從 HAL 發出,要么到 HAL。該接口沒有指定具體調用方向。需要從 HAL 發起調用的架構應該在 HAL 軟件包中提供兩個(或更多個)接口並從每個進程提供相應的接口。我們根據接口的調用方向來取名“客戶端”或“服務器”(即 HAL 可以是一個接口的服務器,也可以是另一個接口的客戶端)。

回調

“回調”一詞可以指代兩個不同的概念,可通過“同步回調”和“異步回調”進行區分。

“同步回調”用於返回數據的一些 HIDL 方法。返回多個值(或返回非基元類型的一個值)的 HIDL 方法會通過回調函數返回其結果。如果只返回一個值且該值是基元類型,則不使用回調且該值從方法中返回。服務器實現 HIDL 方法,而客戶端實現回調。

“異步回調”允許 HIDL 接口的服務器發起調用。通過第一個接口傳遞第二個接口的實例即可完成此操作。第一個接口的客戶端必須作為第二個接口的服務器。第一個接口的服務器可以在第二個接口對象上調用方法。例如,HAL 實現可以通過在由該進程創建和提供的接口對象上調用方法來將信息異步發送回正在使用它的進程。用於異步回調的接口中的方法可以是阻塞方法(並且可能將值返回到調用程序),也可以是 oneway 方法。要查看相關示例,請參閱 HIDL C++ 中的“異步回調”。

要簡化內存所有權,方法調用和回調只能接受 in 參數,並且不支持 out 或 inout 參數。

每事務限制

每事務限制不會強制限制在 HIDL 方法和回調中發送的數據量。但是,每事務調用 4KB 以上的數據便被視為過度調用。如果發生這種情況,建議重新設計給定 HIDL 接口的架構。另一個限制是可供 HIDL 基礎架構處理多個同時進行的事務的資源。由於多個線程或進程向一個進程發送調用或者接收進程未能快速處理多個 oneway 調用,因此多個事務可能會同時進行。默認情況下,所有並發事務可用的最大總空間為 1MB。

在設計良好的接口中,不應出現超出這些資源限制的情況;如果超出的話,則超出資源的調用可能會阻塞,直到資源可用或發出傳輸錯誤的信號。每當因正在進行的總事務導致出現超出每事務限制或溢出 HIDL 實現資源的情況時,系統都會記錄下來以方便調試。

方法實現

HIDL 生成以目標語言(C++ 或 Java)聲明必要類型、方法和回調的標頭文件。客戶端和服務器代碼的 HIDL 定義方法和回調的原型是相同的。HIDL 系統提供調用程序端(整理 IPC 傳輸的數據)的方法代理實現,並將代碼存根到被調用程序端(將數據傳遞到方法的開發者實現)。

函數的調用程序(HIDL 方法或回調)擁有對傳遞到該函數的數據結構的所有權,並在調用后保留所有權;被調用程序在所有情況下都無需釋放存儲。

  • 在 C++ 中,數據可能是只讀的(嘗試寫入可能會導致細分錯誤),並且在調用期間有效。客戶端可以深層復制數據,以在調用期間外傳播。
  • 在 Java 中,代碼會接收數據的本地副本(普通 Java 對象),代碼可以保留和修改此數據或允許垃圾回收器回收。

非 RPC 數據轉移

HIDL 在不使用 RPC 調用的情況下通過兩種方法來轉移數據:共享內存和快速消息隊列 (FMQ),只有 C++ 同時支持這兩種方法。

  • 共享內存。內置 HIDL 類型 memory 用於傳遞表示已分配的共享內存的對象。 可以在接收進程中使用,以映射共享內存。
  • 快速消息隊列 (FMQ)。HIDL 提供了一種可實現無等待消息傳遞的模板化消息隊列類型。它在直通式或綁定式模式下不使用內核或調度程序(設備間通信將不具有這些屬性)。通常,HAL 會設置其隊列的末尾,從而創建可以借助內置 HIDL 類型 MQDescriptorSync 或 MQDescriptorUnsync 的參數通過 RPC 傳遞的對象。接收進程可使用此對象設置隊列的另一端。
    • “已同步”隊列不能溢出,且只能有一個讀取器。
    • “未同步”隊列可以溢出,且可以有多個讀取器;每個讀取器必須及時讀取數據,否則數據就會丟失。
    兩種隊列都不能下溢(從空隊列進行讀取將會失敗),且都只能有一個寫入器。

有關 FMQ 的更多詳情,請參閱快速消息隊列 (FMQ)

快速消息隊列 (FMQ)

HIDL 的遠程過程調用 (RPC) 基礎架構使用 Binder 機制,這意味着調用涉及開銷、需要內核操作,並且可以觸發調度程序操作。不過,對於必須在開銷較小且無內核參與的進程之間傳輸數據的情況,則使用快速消息隊列 (FMQ) 系統。

FMQ 會創建具有所需屬性的消息隊列。MQDescriptorSync 或 MQDescriptorUnsync 對象可通過 HIDL RPC 調用發送,並可供接收進程用於訪問消息隊列。

僅 C++ 支持快速消息隊列。

MessageQueue 類型

Android 支持兩種隊列類型(稱為“風格”):

  • 未同步隊列:可以溢出,並且可以有多個讀取器;每個讀取器都必須及時讀取數據,否則數據將會丟失。
  • 已同步隊列:不能溢出,並且只能有一個讀取器。

這兩種隊列都不能下溢(從空隊列進行讀取將會失敗),並且只能有一個寫入器。

未同步

未同步隊列只有一個寫入器,但可以有任意多個讀取器。此類隊列有一個寫入位置;不過,每個讀取器都會跟蹤各自的獨立讀取位置。

對此類隊列執行寫入操作一定會成功(不會檢查是否出現溢出情況),但前提是寫入的內容不超出配置的隊列容量(如果寫入的內容超出隊列容量,則操作會立即失敗)。由於各個讀取器的讀取位置可能不同,因此每當新的寫入操作需要空間時,系統都允許數據離開隊列,而無需等待每個讀取器讀取每條數據。

讀取操作負責在數據離開隊列末尾之前對其進行檢索。如果讀取操作嘗試讀取的數據超出可用數據量,則該操作要么立即失敗(如果非阻塞),要么等到有足夠多的可用數據時(如果阻塞)。如果讀取操作嘗試讀取的數據超出隊列容量,則讀取一定會立即失敗。

如果某個讀取器的讀取速度無法跟上寫入器的寫入速度,則寫入的數據量和該讀取器尚未讀取的數據量加在一起會超出隊列容量,這會導致下一次讀取不會返回數據;相反,該讀取操作會將讀取器的讀取位置重置為等於最新的寫入位置,然后返回失敗。如果在發生溢出后但在下一次讀取之前,系統查看可供讀取的數據,則會顯示可供讀取的數據超出了隊列容量,這表示發生了溢出。(如果隊列溢出發生在系統查看可用數據和嘗試讀取這些數據之間,則溢出的唯一表征就是讀取操作失敗。)

已同步

已同步隊列有一個寫入器和一個讀取器,其中寫入器有一個寫入位置,讀取器有一個讀取位置。寫入的數據量不可能超出隊列可提供的空間;讀取的數據量不可能超出隊列當前存在的數據量。如果嘗試寫入的數據量超出可用空間或嘗試讀取的數據量超出現有數據量,則會立即返回失敗,或會阻塞到可以完成所需操作為止,具體取決於調用的是阻塞還是非阻塞寫入或讀取函數。如果嘗試讀取或嘗試寫入的數據量超出隊列容量,則讀取或寫入操作一定會立即失敗。

設置 FMQ

一個消息隊列需要多個 MessageQueue 對象:一個對象用作數據寫入目標位置,以及一個或多個對象用作數據讀取來源。沒有關於哪些對象用於寫入數據或讀取數據的顯式配置;用戶需負責確保沒有對象既用於讀取數據又用於寫入數據,也就是說最多只有一個寫入器,並且對於已同步隊列,最多只有一個讀取器。

創建第一個 MessageQueue 對象

通過單個調用創建並配置消息隊列:

#include <fmq/MessageQueue.h>
using android::hardware::kSynchronizedReadWrite;
using android::hardware::kUnsynchronizedWrite;
using android::hardware::MQDescriptorSync;
using android::hardware::MQDescriptorUnsync;
using android::hardware::MessageQueue;
....
// For a synchronized non-blocking FMQ
mFmqSynchronized =
  new (std::nothrow) MessageQueue<uint16_t, kSynchronizedReadWrite>
      (kNumElementsInQueue);
// For an unsynchronized FMQ that supports blocking
mFmqUnsynchronizedBlocking =
  new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
      (kNumElementsInQueue, true /* enable blocking operations */);
 
  • MessageQueue<T, flavor>(numElements) 初始化程序負責創建並初始化支持消息隊列功能的對象。
  • MessageQueue<T, flavor>(numElements, configureEventFlagWord) 初始化程序負責創建並初始化支持消息隊列功能和阻塞的對象。
  • flavor 可以是 kSynchronizedReadWrite(對於已同步隊列)或 kUnsynchronizedWrite(對於未同步隊列)。
  • uint16_t(在本示例中)可以是任意不涉及嵌套式緩沖區(無 string 或 vec 類型)、句柄或接口的 HIDL 定義的類型
  • kNumElementsInQueue 表示隊列的大小(以條目數表示);它用於確定將為隊列分配的共享內存緩沖區的大小。

創建第二個 MessageQueue 對象

使用從消息隊列的第一側獲取的 MQDescriptor 對象創建消息隊列的第二側。通過 HIDL RPC 調用將 MQDescriptor 對象發送到將容納消息隊列末端的進程。MQDescriptor 包含該隊列的相關信息,其中包括:

  • 用於映射緩沖區和寫入指針的信息。
  • 用於映射讀取指針的信息(如果隊列已同步)。
  • 用於映射事件標記字詞的信息(如果隊列是阻塞隊列)。
  • 對象類型 (<T, flavor>),其中包含 HIDL 定義的隊列元素類型和隊列風格(已同步或未同步)。

MQDescriptor 對象可用於構建 MessageQueue 對象:

MessageQueue<T, flavor>::MessageQueue(const MQDescriptor<T, flavor>& Desc, bool resetPointers)
 

resetPointers 參數表示是否在創建此 MessageQueue 對象時將讀取和寫入位置重置為 0。在未同步隊列中,讀取位置(在未同步隊列中,是每個 MessageQueue 對象的本地位置)在此對象創建過程中始終設為 0。通常,MQDescriptor 是在創建第一個消息隊列對象過程中初始化的。要對共享內存進行額外的控制,您可以手動設置 MQDescriptorMQDescriptor 是在 system/libhidl/base/include/hidl/MQDescriptor.h 中定義的),然后按照本部分所述內容創建每個 MessageQueue 對象。

阻塞隊列和事件標記

默認情況下,隊列不支持阻塞讀取/寫入。有兩種類型的阻塞讀取/寫入調用:

  • 短格式:有三個參數(數據指針、項數、超時)。支持阻塞針對單個隊列的各個讀取/寫入操作。在使用這種格式時,隊列將在內部處理事件標記和位掩碼,並且第一個消息隊列對象必須初始化為第二個參數為 true。例如:
    // For an unsynchronized FMQ that supports blocking
    mFmqUnsynchronizedBlocking =
      new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
          (kNumElementsInQueue, true /* enable blocking operations */);
     
                     
     
  • 長格式:有六個參數(包括事件標記和位掩碼)。支持在多個隊列之間使用共享 EventFlag 對象,並允許指定要使用的通知位掩碼。在這種情況下,必須為每個讀取和寫入調用提供事件標記和位掩碼。

對於長格式,可在每個 readBlocking() 和 writeBlocking() 調用中顯式提供 EventFlag。可以將其中一個隊列初始化為包含一個內部事件標記,如果是這樣,則必須使用 getEventFlagWord() 從相應隊列的 MessageQueue 對象中提取該標記,以用於在每個進程中創建與其他 FMQ 一起使用的 EventFlag 對象。或者,可以將 EventFlag 對象初始化為具有任何合適的共享內存。

一般來說,每個隊列都應只使用以下三項之一:非阻塞、短格式阻塞,或長格式阻塞。混合使用也不算是錯誤;但要獲得理想結果,則需要謹慎地進行編程。

使用 MessageQueue

MessageQueue 對象的公共 API 是:

size_t availableToWrite()  // Space available (number of elements).
size_t availableToRead()  // Number of elements available.
size_t getQuantumSize()  // Size of type T in bytes.
size_t getQuantumCount() // Number of items of type T that fit in the FMQ.
bool isValid() // Whether the FMQ is configured correctly.
const MQDescriptor<T, flavor>* getDesc()  // Return info to send to other process.

bool write(const T* data)  // Write one T to FMQ; true if successful.
bool write(const T* data, size_t count) // Write count T's; no partial writes.

bool read(T* data);  // read one T from FMQ; true if successful.
bool read(T* data, size_t count);  // Read count T's; no partial reads.

bool writeBlocking(const T* data, size_t count, int64_t timeOutNanos = 0);
bool readBlocking(T* data, size_t count, int64_t timeOutNanos = 0);

// Allows multiple queues to share a single event flag word
std::atomic<uint32_t>* getEventFlagWord();

bool writeBlocking(const T* data, size_t count, uint32_t readNotification,
uint32_t writeNotification, int64_t timeOutNanos = 0,
android::hardware::EventFlag* evFlag = nullptr); // Blocking write operation for count Ts.

bool readBlocking(T* data, size_t count, uint32_t readNotification,
uint32_t writeNotification, int64_t timeOutNanos = 0,
android::hardware::EventFlag* evFlag = nullptr) // Blocking read operation for count Ts;

//APIs to allow zero copy read/write operations
bool beginWrite(size_t nMessages, MemTransaction* memTx) const;
bool commitWrite(size_t nMessages);
bool beginRead(size_t nMessages, MemTransaction* memTx) const;
bool commitRead(size_t nMessages);
 

availableToWrite() 和 availableToRead() 可用於確定在一次操作中可傳輸的數據量。在未同步隊列中:

  • availableToWrite() 始終返回隊列容量。
  • 每個讀取器都有自己的讀取位置,並會針對 availableToRead() 進行自己的計算。
  • 如果是讀取速度緩慢的讀取器,隊列可以溢出,這可能會導致 availableToRead() 返回的值大於隊列的大小。發生溢出后進行的第一次讀取操作將會失敗,並且會導致相應讀取器的讀取位置被設為等於當前寫入指針,無論是否通過 availableToRead() 報告了溢出都是如此。

如果所有請求的數據都可以(並已)傳輸到隊列/從隊列傳出,則 read() 和 write() 方法會返回 true。這些方法不會阻塞;它們要么成功(並返回 true),要么立即返回失敗 (false)。

readBlocking() 和 writeBlocking() 方法會等到可以完成請求的操作,或等到超時(timeOutNanos 值為 0 表示永不超時)。

阻塞操作使用事件標記字詞來實現。默認情況下,每個隊列都會創建並使用自己的標記字詞來支持短格式的 readBlocking() 和 writeBlocking()。多個隊列可以共用一個字詞,這樣一來,進程就可以等待對任何隊列執行寫入或讀取操作。可以通過調用 getEventFlagWord() 獲得指向隊列事件標記字詞的指針,此類指針(或任何指向合適的共享內存位置的指針)可用於創建 EventFlag 對象,以傳遞到其他隊列的長格式 readBlocking()和 writeBlocking()readNotification 和 writeNotification 參數用於指示事件標記中的哪些位應該用於針對相應隊列發出讀取和寫入信號。readNotification 和 writeNotification 是 32 位的位掩碼。

readBlocking() 會等待 writeNotification 位;如果該參數為 0,則調用一定會失敗。如果 readNotification 值為 0,則調用不會失敗,但成功的讀取操作將不會設置任何通知位。在已同步隊列中,這意味着相應的 writeBlocking() 調用一定不會喚醒,除非已在其他位置對相應的位進行設置。在未同步隊列中,writeBlocking() 將不會等待(它應仍用於設置寫入通知位),而且對於讀取操作來說,不適合設置任何通知位。同樣,如果 readNotification 為 0,writeblocking() 將會失敗,並且成功的寫入操作會設置指定的 writeNotification 位。

要一次等待多個隊列,請使用 EventFlag 對象的 wait() 方法來等待通知的位掩碼。wait() 方法會返回一個狀態字詞以及導致系統設置喚醒的位。然后,該信息可用於驗證相應的隊列是否有足夠的控件或數據來完成所需的寫入/讀取操作,並執行非阻塞 write()/read()。要獲取操作后通知,請再次調用 EventFlag 的 wake() 方法。有關 EventFlag 抽象的定義,請參閱 system/libfmq/include/fmq/EventFlag.h

零復制操作

read/write/readBlocking/writeBlocking() API 會將指向輸入/輸出緩沖區的指針作為參數,並在內部使用 memcpy() 調用,以便在相應緩沖區和 FMQ 環形緩沖區之間復制數據。為了提高性能,Android 8.0 及更高版本包含一組 API,這些 API 可提供對環形緩沖區的直接指針訪問,這樣便無需使用 memcpy 調用。

使用以下公共 API 執行零復制 FMQ 操作:

bool beginWrite(size_t nMessages, MemTransaction* memTx) const;
bool commitWrite(size_t nMessages);

bool beginRead(size_t nMessages, MemTransaction* memTx) const;
bool commitRead(size_t nMessages);
 
  • beginWrite 方法負責提供用於訪問 FMQ 環形緩沖區的基址指針。在數據寫入之后,使用 commitWrite()提交數據。beginRead/commitRead 方法的運作方式與之相同。
  • beginRead/Write 方法會將要讀取/寫入的消息條數視為輸入,並會返回一個布爾值來指示是否可以執行讀取/寫入操作。如果可以執行讀取或寫入操作,則 memTx 結構體中會填入基址指針,這些指針可用於對環形緩沖區共享內存進行直接指針訪問。
  • MemRegion 結構體包含有關內存塊的詳細信息,其中包括基礎指針(內存塊的基址)和以 T 表示的長度(以 HIDL 定義的消息隊列類型表示的內存塊長度)。
  • MemTransaction 結構體包含兩個 MemRegion 結構體(first 和 second),因為對環形緩沖區執行讀取或寫入操作時可能需要繞回到隊列開頭。這意味着,要對 FMQ 環形緩沖區執行數據讀取/寫入操作,需要兩個基址指針。

從 MemRegion 結構體獲取基址和長度:

T* getAddress(); // gets the base address
size_t getLength(); // gets the length of the memory region in terms of T
size_t getLengthInBytes(); // gets the length of the memory region in bytes
 

獲取對 MemTransaction 對象內的第一個和第二個 MemRegion 的引用:

const MemRegion& getFirstRegion(); // get a reference to the first MemRegion
const MemRegion& getSecondRegion(); // get a reference to the second MemRegion
 

使用零復制 API 寫入 FMQ 的示例:

MessageQueueSync::MemTransaction tx;
if (mQueue->beginRead(dataLen, &tx)) {
    auto first = tx.getFirstRegion();
    auto second = tx.getSecondRegion();

    foo(first.getAddress(), first.getLength()); // method that performs the data write
    foo(second.getAddress(), second.getLength()); // method that performs the data write

    if(commitWrite(dataLen) == false) {
       // report error
    }
} else {
   // report error
}
 

以下輔助方法也是 MemTransaction 的一部分:

  • T* getSlot(size_t idx); 
    返回一個指針,該指針指向屬於此 MemTransaction 對象一部分的 MemRegions 內的槽位 idx。如果 MemTransaction 對象表示要讀取/寫入 N 個類型為 T 的項目的內存區域,則 idx 的有效范圍在 0 到 N-1 之間。
  • bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1); 
    將 nMessages 個類型為 T 的項目寫入到該對象描述的內存區域,從索引 startIdx 開始。此方法使用 memcpy(),但並非旨在用於零復制操作。如果 MemTransaction 對象表示要讀取/寫入 N 個類型為 T 的項目的內存區域,則 idx 的有效范圍在 0 到 N-1 之間。
  • bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1); 
    一種輔助方法,用於從該對象描述的內存區域讀取 nMessages 個類型為 T 的項目,從索引 startIdx 開始。此方法使用 memcpy(),但並非旨在用於零復制操作。

通過 HIDL 發送隊列

在創建側執行的操作:

  1. 創建消息隊列對象,如上所述。
  2. 使用 isValid() 驗證對象是否有效。
  3. 如果您要通過將 EventFlag 傳遞到長格式的 readBlocking()/writeBlocking() 來等待多個隊列,則可以從經過初始化的 MessageQueue 對象提取事件標記指針(使用 getEventFlagWord())以創建標記,然后使用該標記創建必需的 EventFlag 對象。
  4. 使用 MessageQueue getDesc() 方法獲取描述符對象。
  5. 在 .hal 文件中,為某個方法提供一個類型為 fmq_sync 或 fmq_unsync 的參數,其中 T 是 HIDL 定義的一種合適類型。使用此方法將 getDesc() 返回的對象發送到接收進程。

在接收側執行的操作:

    1. 使用描述符對象創建 MessageQueue 對象。務必使用相同的隊列風格和數據類型,否則將無法編譯模板。
    2. 如果您已提取事件標記,則在接收進程中從相應的 MessageQueue 對象提取該標記。
    3. 使用 MessageQueue 對象傳輸數據。

使用 Binder IPC

本頁介紹了 Android 8 中對 Binder 驅動程序進行的更改、提供了有關使用 Binder IPC 的詳細信息,並列出了必需的 SELinux 政策。

對 Binder 驅動程序進行的更改

從 Android 8 開始,Android 框架和 HAL 現在使用 Binder 互相通信。由於這種通信方式極大地增加了 Binder 流量,因此 Android 8 包含了幾項改進,旨在使 Binder IPC 速度很快。SoC 供應商和 OEM 應直接從 android-4.4、android-4.9 和更高版本內核/通用項目的相關分支進行合並。

多個 Binder 域(上下文)

通用 4.4 及更高版本,包括上游

為了在框架(獨立於設備)和供應商(特定於設備)代碼之間徹底拆分 Binder 流量,Android 8 引入了“Binder 上下文”的概念。每個 Binder 上下文都有自己的設備節點和上下文(服務)管理器。您只能通過上下文管理器所屬的設備節點對其進行訪問,並且在通過特定上下文傳遞 Binder 節點時,只能由另一個進程從相同的上下文訪問上下文管理器,從而確保這些域完全互相隔離。如需使用方法的詳細信息,請參閱 vndbinder 和 vndservicemanager

分散-集中

通用 4.4 及更高版本,包括上游

在之前的 Android 版本中,Binder 調用中的每條數據都會被復制 3 次:

  • 一次是在調用進程中將數據序列化為 Parcel
  • 一次是在內核驅動程序中將 Parcel 復制到目標進程
  • 一次是在目標進程中反序列化 Parcel

Android 8 使用分散-集中優化將副本數量從 3 減少到 1。數據保留其原始結構和內存布局,且 Binder 驅動程序會立即將數據復制到目標進程中,而不是先在 Parcel 中序列化數據。在目標進程中,這些數據的結構和內存布局保持不變,並且,在無需再次復制的情況下即可讀取這些數據。

精細鎖定

通用 4.4 及更高版本,包括上游

在之前的 Android 版本中,Binder 驅動程序使用全局鎖來防范對重要數據結構的並發訪問。雖然采用全局鎖時出現爭用的可能性極低,但主要的問題是,如果低優先級線程獲得該鎖,然后實現了搶占,則會導致同樣需要獲得該鎖的優先級較高的線程出現嚴重的延遲。這會導致平台卡頓。

原先嘗試解決此問題的方法是在保留全局鎖的同時禁止搶占。但是,這更像是一種臨時應對手段而非真正的解決方案,最終被上游拒絕並舍棄。后來嘗試的解決方法側重於提升鎖定的精細程度,自 2017 年 1 月以來,Pixel 設備上一直采用的是更加精細的鎖定。雖然這些更改大部分已公開,但后續版本中還會有一些重大的改進。

在確定了精細鎖定實現中的一些小問題后,我們使用不同的鎖定架構設計了一種改進的解決方案,並在所有通用內核分支中提交了更改。我們會繼續在大量不同的設備上測試這種實現;由於目前我們沒有發現這個方案存在什么突出問題,因此建議搭載 Android 8 的設備都使用這種實現。

注意:我們強烈建議為精細鎖定安排充足的測試時間。

實時優先級繼承

通用 4.4 和通用 4.9(即將推送到上游)

Binder 驅動程序一直支持 nice 優先級繼承。隨着 Android 中以實時優先級運行的進程日益增加,現在出現以下這種情形也屬正常:如果實時線程發出 Binder 調用,則處理該調用的進程中的線程同樣會以實時優先級運行。為了支持這些使用情景,Android 8 現在在 Binder 驅動程序中實現了實時優先級繼承。

除了事務級優先級繼承之外,“節點優先級繼承”允許節點(Binder 服務對象)指定執行對該節點的調用所需的最低優先級。之前版本的 Android 已經通過 nice 值支持節點優先級繼承,但 Android 8 增加了對實時調度政策節點繼承的支持。

注意:Android 性能團隊發現,實時優先級繼承會對框架 Binder 域 (/dev/binder) 造成負面影響,因此已針對該域停用實時優先級繼承。

userspace 更改

Android 8 包含在通用內核中使用當前 Binder 驅動程序所需的所有 userspace 更改,但有一個例外:針對 /dev/binder 停用實時優先級繼承的原始實現使用了 ioctl。由於后續開發將優先級繼承的控制方法改為了更加精細的方法(根據 Binder 模式,而非上下文),因此,ioctl 不存於 Android 通用分支中,而是提交到了我們的通用內核中

此項更改的影響是,所有節點均默認停用實時優先級繼承。Android 性能團隊發現,為 hwbinder 域中的所有節點啟用實時優先級繼承會有一定好處。要達到同樣的效果,請在用戶空間中擇優實施此更改

通用內核的 SHA

要獲取對 Binder 驅動程序所做的必要更改,請同步到下列 SHA(或更高版本):

使用 Binder IPC

一直以來,供應商進程都使用 Binder 進程間通信 (IPC) 技術進行通信。在 Android 8 中,/dev/binder 設備節點成為框架進程的專有節點,這意味着供應商進程無法再訪問此節點。供應商進程可以訪問 /dev/hwbinder,但必須將其 AIDL 接口轉為使用 HIDL。對於想要繼續在供應商進程之間使用 AIDL 接口的供應商,Android 會按以下方式支持 Binder IPC。

vndbinder

Android 8 支持供供應商服務使用的新 Binder 域,訪問此域需要使用 /dev/vndbinder(而非 /dev/binder)。添加 /dev/vndbinder 后,Android 現在擁有以下 3 個 IPC 域:

IPC 域 說明
/dev/binder 框架/應用進程之間的 IPC,使用 AIDL 接口
/dev/hwbinder 框架/供應商進程之間的 IPC,使用 HIDL 接口 
供應商進程之間的 IPC,使用 HIDL 接口
/dev/vndbinder 供應商/供應商進程之間的 IPC,使用 AIDL 接口

為了顯示 /dev/vndbinder,請確保內核配置項 CONFIG_ANDROID_BINDER_DEVICES 設為 "binder,hwbinder,vndbinder"(這是 Android 通用內核樹的默認設置)。

通常,供應商進程不直接打開 Binder 驅動程序,而是鏈接到打開 Binder 驅動程序的 libbinder 用戶空間庫。為 ::android::ProcessState() 添加方法可為 libbinder 選擇 Binder 驅動程序。供應商進程應該在調用 ProcessState,IPCThreadState 或發出任何普通 Binder 調用之前調用此方法。要使用該方法,請在供應商進程(客戶端和服務器)的 main() 后放置以下調用:

ProcessState::initWithDriver("/dev/vndbinder");
 

vndservicemanager

以前,Binder 服務通過 servicemanager 注冊,其他進程可從中檢索這些服務。在 Android 8 中,servicemanager 現在專供框架使用,而應用進程和供應商進程無法再對其進行訪問。

不過,供應商服務現在可以使用 vndservicemanager,這是一個使用 /dev/vndbinder(作為編譯基礎的源代碼與框架 servicemanager 的相同)而非 /dev/binder 的 servicemanager 的新實例。供應商進程無需更改即可與 vndservicemanager 通信;當供應商進程打開 /dev/vndbinder 時,服務查詢會自動轉至 vndservicemanager

vndservicemanager 二進制文件包含在 Android 的默認設備 Makefile 中。

SELinux 政策

想要使用 Binder 功能來相互通信的供應商進程需要滿足以下要求:

  1. 能夠訪問 /dev/vndbinder
  2. 將 Binder {transfer, call} 接入 vndservicemanager
  3. 針對想要通過供應商 Binder 接口調用供應商域 B 的任何供應商域 A 執行 binder_call(A, B) 操作。
  4. 有權在 vndservicemanager 中對服務執行 {add, find} 操作。

要滿足要求 1 和 2,請使用 vndbinder_use() 宏:

vndbinder_use(some_vendor_process_domain);
 

要滿足要求 3,需要通過 Binder 通信的供應商進程 A 和 B 的 binder_call(A, B) 可以保持不變,且不需要重命名。

要滿足要求 4,您必須按照處理服務名稱、服務標簽和規則的方式進行更改。

有關 SELinux 的詳細信息,請參閱 Android 中的安全增強型 Linux。有關 Android 8.0 中 SELinux 的詳細信息,請參閱 SELinux for Android 8.0

服務名稱

以前,供應商進程在 service_contexts 文件中注冊服務名稱並添加用於訪問該文件的相應規則。來自 device/google/marlin/sepolicy 的 service_contexts 文件示例:

AtCmdFwd                              u:object_r:atfwd_service:s0
cneservice                            u:object_r:cne_service:s0
qti.ims.connectionmanagerservice      u:object_r:imscm_service:s0
rcs                                   u:object_r:radio_service:s0
uce                                   u:object_r:uce_service:s0
vendor.qcom.PeripheralManager         u:object_r:per_mgr_service:s0
 

在 Android 8 中,vndservicemanager 會改為加載 vndservice_contexts 文件。遷移到 vndservicemanager(且已經在舊的 service_contexts 文件中)的供應商服務應該添加到新的 vndservice_contexts 文件中。

服務標簽

以前,服務標簽(例如 u:object_r:atfwd_service:s0)在 service.te 文件中定義。例如:

type atfwd_service,      service_manager_type;
 

在 Android 8 中,您必須將類型更改為 vndservice_manager_type,並將規則移至 vndservice.te 文件。例如:

type atfwd_service,      vndservice_manager_type;
 

Servicemanager 規則

以前,規則會授予域訪問權限,以向 servicemanager 添加服務或在其中查找服務。例如:

allow atfwd atfwd_service:service_manager find;
allow some_vendor_app atfwd_service:service_manager add;
 

在 Android 8 中,這樣的規則可繼續存在並使用相同的類。示例:

allow atfwd atfwd_service:service_manager find;
allow some_vendor_app atfwd_service:service_manager add;



HIDL 內存塊

HIDL MemoryBlock 是構建在 hidl_memoryHIDL @1.0::IAllocator 和 HIDL @1.0::IMapper 之上的抽象層,專為有多個內存塊共用單個內存堆的 HIDL 服務而設計。

性能提升

在應用中使用 MemoryBlock 可顯著減少 mmap/munmap 數量和用戶空間細分錯誤,從而提升性能。例如:

  • 對每個緩沖區分配使用一個 hidl_memory,則每次分配平均用時 238 us。
  • 使用 MemoryBlock 並共享單個 hidl_memory,則每次分配平均用時 2.82 us。

架構

HIDL MemoryBlock 架構包括一些有多個內存塊共用單個內存堆的 HIDL 服務:

HIDL MemoryBlock

圖 1. HIDL MemoryBlock 架構

常規用法

本部分提供了一個關於如何通過以下方式使用 MemoryBlock 的示例:先聲明 HAL,然后實現 HAL。

聲明 HAL

對於以下示例 IFoo HAL:

import android.hidl.memory.block@1.0::MemoryBlock;

interface IFoo {
    getSome() generates(MemoryBlock block);
    giveBack(MemoryBlock block);
};
 

Android.bp 如下所示:

hidl_interface {
    ...
    srcs: [
        "IFoo.hal",
    ],
    interfaces: [
        "android.hidl.memory.block@1.0",
        ...
};
 

實現 HAL

要實現示例 HAL,請執行以下操作:

  1. 獲取 hidl_memory(有關詳情,請參閱 HIDL C++)。

    #include <android/hidl/allocator/1.0/IAllocator.h>

    using ::android::hidl::allocator::V1_0::IAllocator;
    using ::android::hardware::hidl_memory;
    ...
      sp<IAllocator> allocator = IAllocator::getService("ashmem");
      allocator->allocate(2048, [&](bool success, const hidl_memory& mem)
      {
            if (!success) { /* error */ }
            // you can now use the hidl_memory object 'mem' or pass it
      }));
     
  2. 使用獲取的 hidl_memory 創建 HidlMemoryDealer

    #include <hidlmemory/HidlMemoryDealer.h>

    using ::android::hardware::HidlMemoryDealer
    /* The mem argument is acquired in the Step1, returned by the ashmemAllocator->allocate */
    sp<HidlMemoryDealer> memory_dealer = HidlMemoryDealer::getInstance(mem);
     
  3. 分配 MemoryBlock(使用 HIDL 定義的結構體)。

    示例 MemoryBlock

    struct MemoryBlock {
    IMemoryToken token;
    uint64_t size;
    uint64_t offset;
    };
     

    使用 MemoryDealer 分配 MemoryBlock 的示例:

    #include <android/hidl/memory/block/1.0/types.h>

    using ::android::hidl::memory::block::V1_0::MemoryBlock;

    Return<void> Foo::getSome(getSome_cb _hidl_cb) {
        MemoryBlock block = memory_dealer->allocate(1024);
        if(HidlMemoryDealer::isOk(block)){
            _hidl_cb(block);
        ...
     
  4. 解除 MemoryBlock 分配:

    Return<void> Foo::giveBack(const MemoryBlock& block) {
        memory_dealer->deallocate(block.offset);
    ...
     
  5. 操控數據:

    #include <hidlmemory/mapping.h>
    #include <android/hidl/memory/1.0/IMemory.h>

    using ::android::hidl::memory::V1_0::IMemory;

    sp<IMemory> memory = mapMemory(block);
    uint8_t* data =

    static_cast<uint8_t*>(static_cast<void*>(memory->getPointer()));
     
  6. 配置 Android.bp

    shared_libs: [
            "android.hidl.memory@1.0",

            "android.hidl.memory.block@1.0"

            "android.hidl.memory.token@1.0",
            "libhidlbase",
            "libhidlmemory",
     
  7. 查看流程,確定是否需要 lockMemory

    通常,MemoryBlock 使用引用計數來維護共享的 hidl_memory:當其中有 MemoryBlock 首次被映射時,系統會對該內存執行 mmap() 操作;如果沒有任何內容引用該內存,則系統會對其執行 munmap() 操作。為確保始終映射 hidl_memory,您可以使用 lockMemory,這是一種 RAII 樣式的對象,可使相應的 hidl_memory 在整個鎖定生命周期內保持映射狀態。示例:

    #include <hidlmemory/mapping.h>

    sp<RefBase> lockMemory(const sp<IMemoryToken> key);
     

擴展用法

本部分詳細介紹了 MemoryBlock 的擴展用法。

使用引用計數來管理 Memoryblock

在大多數情況下,要使用 MemoryBlock,最高效的方法是明確分配/解除分配。不過,在復雜應用中,使用引用計數進行垃圾回收可能會更好。要獲得 MemoryBlock 的引用計數,您可以將 MemoryBlock 與 binder 對象綁定,這有助於對引用進行計數,並在計數降至零時解除 MemoryBlock 分配。

聲明 HAL

聲明 HAL 時,請描述包含 MemoryBlock 和 IBase 的 HIDL 結構體:

import android.hidl.memory.block@1.0::MemoryBlock;

struct MemoryBlockAllocation {
    MemoryBlock block;
    IBase refcnt;
};
 

使用 MemoryBlockAllocation 替換 MemoryBlock 並移除相應方法,以返回 MemoryBlock。該內存塊將由引用計數功能通過 MemoryBlockAllocation 解除分配。示例:

interface IFoo {
    allocateSome() generates(MemoryBlockAllocation allocation);
};
 

實現 HAL

HAL 服務端實現示例:

class MemoryBlockRefCnt: public virtual IBase {
   MemoryBlockRefCnt(uint64_t offset, sp<MemoryDealer> dealer)
     : mOffset(offset), mDealer(dealer) {}
   ~MemoryBlockRefCnt() {
       mDealer->deallocate(mOffset);
   }
 private:
   uint64_t mOffset;
   sp<MemoryDealer> mDealer;
};

Return<void> Foo::allocateSome(allocateSome_cb _hidl_cb) {
    MemoryBlockAllocation allocation;
    allocation.block = memory_dealer->allocate(1024);
    if(HidlMemoryDealer::isOk(block)){
        allocation.refcnt= new MemoryBlockRefCnt(...);
        _hidl_cb(allocation);
 

HAL 客戶端實現示例:

ifoo->allocateSome([&](const MemoryBlockAllocation& allocation){
    ...
);
 

附加/檢索元數據

某些應用需要額外的數據才能與所分配的 MemoryBlock 綁定。您可以使用以下兩種方法來附加/檢索元數據:

  • 如果應用訪問元數據的頻率與訪問內存塊本身的頻率相同,請附加元數據並以結構體的形式傳遞所有元數據。示例:

    import android.hidl.memory.block@1.0::MemoryBlock;

    struct MemoryBlockWithMetaData{
        MemoryBlock block;
        MetaDataStruct metaData;
    };
     
  • 如果應用訪問元數據的頻率遠低於訪問內存塊的頻率,則使用接口被動傳遞元數據會更加高效。示例:

    import android.hidl.memory.block@1.0::MemoryBlock;

    struct MemoryBlockWithMetaData{
        MemoryBlock block;
        IMetaData metaData;
    };
     

    接下來,使用 Memory Dealer 將元數據和 MemoryBlock 綁定在一起。示例:

    MemoryBlockWithMetaData memory_block;
    memory_block.block = dealer->allocate(size);
    if(HidlMemoryDealer::isOk(block)){
        memory_block.metaData = new MetaData(...);

 

網絡堆棧配置工具

 

 

Android 操作系統中包含標准的 Linux 網絡實用程序,例如 ifconfigip 和 ip6tables。這些實用程序位於系統映像中,並支持對整個 Linux 網絡堆棧進行配置。在搭載 Android 7.x 及更低版本的設備上,供應商代碼可以直接調用此類二進制文件,這會導致出現以下問題:

  • 由於網絡實用程序在系統映像中更新,因此無法提供穩定的實現。
  • 網絡實用程序的范圍非常廣泛,因此難以在保證行為可預測的同時不斷改進系統映像。

在搭載 Android 8.0 及更高版本的設備上,供應商分區會在系統分區接收更新時保持不變。為了實現這一點,Android 8.0 不僅提供了定義穩定的版本化接口的功能,同時還使用了 SELinux 限制,以便在供應商映像與系統映像之間保持已知的良好相互依賴關系。

供應商可以使用平台提供的網絡配置實用程序來配置 Linux 網絡堆棧,但這些實用程序並未包含 HIDL 接口封裝容器。為定義這類接口,Android 8.0 中納入了 netutils-wrapper-1.0 工具。

Netutils 封裝容器

netutils 封裝容器實用程序提供了一部分未受系統分區更新影響的 Linux 網絡堆棧配置。Android 8.0 中包含版本 1.0 的封裝容器,借助它,您可以傳遞與所封裝的實用程序(安裝在系統分區的 /system/bin 中)相同的參數,如下所示:

u:object_r:system_file:s0           /system/bin/ip-wrapper-1.0 -> netutils-wrapper-1.0
u:object_r:system_file:s0           /system/bin/ip6tables-wrapper-1.0 -> netutils-wrapper-1.0
u:object_r:system_file:s0           /system/bin/iptables-wrapper-1.0 -> netutils-wrapper-1.0
u:object_r:system_file:s0           /system/bin/ndc-wrapper-1.0 -> netutils-wrapper-1.0
u:object_r:netutils_wrapper_exec:s0 /system/bin/netutils-wrapper-1.0
u:object_r:system_file:s0           /system/bin/tc-wrapper-1.0 -> netutils-wrapper-1.0
 

符號鏈接顯示由 netutils 封裝容器封裝的網絡實用程序,其中包括:

  • ip
  • iptables
  • ip6tables
  • ndc
  • tc

要在 Android 8.0 及更高版本中使用這些實用程序,供應商實現必須遵循以下規則:

  • 供應商進程不得直接執行 /system/bin/netutils-wrapper-1.0,否則會導致錯誤。
  • netutils-wrapper-1.0 封裝的所有實用程序必須使用其符號鏈接啟動。例如,將以前執行該操作的供應商代碼 (/system/bin/ip <FOO> <BAR>) 更改為 /system/bin/ip-wrapper-1.0 <FOO> <BAR>
  • 平台 SELinux 政策禁止執行不包含網域轉換的封裝容器。此規則不得更改,可在 Android 兼容性測試套件 (CTS) 中進行測試。
  • 平台 SELinux 政策還禁止直接執行來自供應商進程的實用程序(例如,/system/bin/ip <FOO> <BAR>)。此規則不得更改,可在 CTS 中進行測試。
  • 任何需要啟動封裝容器的供應商網域(進程)必須在 SELinux 政策中添加以下網域轉換規則:domain_auto_trans(VENDOR-DOMAIN-NAME, netutils_wrapper_exec, netutils_wrapper)
注意:要詳細了解 Android 8.0 及更高版本中的 SELinux,請參閱 在 Android 8.0 及更高版本中自定義 SEPolicy

Netutils 封裝容器過濾器

封裝的實用程序幾乎可用於配置 Linux 網絡堆棧的任何方面。不過,為了確保可以維護穩定的接口並允許對系統分區進行更新,只能執行某些命令行參數組合;其他命令將被拒絕。

供應商接口和鏈

封裝容器有一個概念稱為“供應商接口”。供應商接口通常是指由供應商代碼管理的接口,例如移動數據網絡接口。通常,其他類型的接口(如 WLAN)由 HAL 和框架管理。封裝容器按名稱(使用正則表達式)識別供應商接口,且允許供應商代碼對其執行多種操作。目前,供應商接口包括以下接口:

  • 名稱以“oem”后跟數字結尾的接口,例如 oem0 或 r_oem1234
  • 當前 SOC 和 OEM 實現使用的接口,如 rmnet_data[0-9]

通常由框架管理的接口的名稱(例如 wlan0)一律不是供應商接口。

封裝容器還有一個相似的概念稱為“供應商鏈”。供應商鏈在 iptables 命令中使用,也按名稱識別。目前,供應商鏈包括以下鏈:

  • 以 oem_ 開頭的鏈。
  • 當前 SOC 和 OEM 實現使用的鏈,例如以 nm_ 或 qcom_ 開頭的鏈。

允許執行的命令

下面列出了當前允許執行的命令。系統通過一組正則表達式對執行的命令行實施限制。有關詳情,請參閱 system/netd/netutils_wrappers/NetUtilsWrapper-1.0.cpp

ip

ip 命令用於配置 IP 地址、路由、IPsec 加密以及多種其他網絡參數。封裝容器允許執行以下命令:

  • 從供應商管理的接口添加和移除 IP 地址。
  • 配置 IPsec 加密。

iptables/ip6tables

iptables 和 ip6tables 命令用於配置防火牆、數據包處理、NAT 和其他按數據包處理。封裝容器允許執行以下命令:

  • 添加和刪除供應商鏈。
  • 在引用進入 (-i) 或離開 (-o) 供應商接口的數據包的任何鏈中添加和刪除規則。
  • 從任何其他鏈的任意一點跳轉到供應商鏈。

ndc

ndc 用於與在 Android 設備上執行大部分網絡配置的 netd 守護進程通信。封裝容器允許執行以下命令:

  • 創建和銷毀 OEM 網絡 (oemXX)。
  • 向 OEM 網絡添加供應商管理的接口。
  • 向 OEM 網絡添加路由。
  • 在全局范圍內和供應商接口上啟用或停用 IP 轉發。

tc

tc 命令用於配置供應商接口上的流量隊列和調整。

線程模型

標記為 oneway 的方法不會阻塞。對於未標記為 oneway 的方法,在服務器完成執行任務或調用同步回調(以先發生者為准)之前,客戶端的方法調用將一直處於阻塞狀態。服務器方法實現最多可以調用一個同步回調;多出的回調調用會被舍棄並記錄為錯誤。如果方法應通過回調返回值,但未調用其回調,系統會將這種情況記錄為錯誤,並作為傳輸錯誤報告給客戶端。

直通模式下的線程

在直通模式下,大多數調用都是同步的。不過,為確保 oneway 調用不會阻塞客戶端這一預期行為,系統會分別為每個進程創建線程。要了解詳情,請參閱 HIDL 概覽

綁定式 HAL 中的線程

為了處理傳入的 RPC 調用(包括從 HAL 到 HAL 用戶的異步回調)和終止通知,系統會為使用 HIDL 的每個進程關聯一個線程池。如果單個進程實現了多個 HIDL 接口和/或終止通知處理程序,則所有這些接口和/或處理程序會共享其線程池。當進程接收從客戶端傳入的方法調用時,它會從線程池中選擇一個空閑線程,並在該線程上執行調用。如果沒有空閑的線程,它將會阻塞,直到有可用線程為止。

如果服務器只有一個線程,則傳入服務器的調用將按順序完成。具有多個線程的服務器可以不按順序完成調用,即使客戶端只有一個線程也是如此。不過,對於特定的接口對象,oneway 調用會保證按順序進行(請參閱服務器線程模型)。對於托管了多個界面的多線程服務器,對不同界面的多項 oneway 調用可能會並行處理,也可能會與其他阻塞調用並行處理。

系統會在同一個 hwbinder 線程中發送多個嵌套調用。例如,如果進程 (A) 通過 hwbinder 線程對進程 (B) 進行同步調用,然后進程 (B) 對進程 (A) 進行同步回調,則系統會在 (A) 中的原始 hwbinder 線程(在原始調用中已被屏蔽)上執行該調用。這種優化使單個線程服務器能夠處理嵌套調用,但是對於需要在其他 IPC 調用序列中傳輸調用的情況,這種優化並不適用。例如,如果進程 (B) 進行了 binder/vndbinder 調用,並在此過程中調用了進程 (C),然后進程 (C) 回調進程 (A),則系統無法在進程 (A) 中的原始線程上處理該調用。

服務器線程模型

(直通模式除外)HIDL 接口的服務器實現位於不同於客戶端的進程中,並且需要一個或多個線程等待傳入的方法調用。這些線程構成服務器的線程池;服務器可以決定它希望在其線程池中運行多少線程,並且可以利用一個線程大小的線程池來按順序處理其接口上的所有調用。如果服務器的線程池中有多個線程,則服務器可以在其任何接口上接收同時傳入的調用(在 C++ 中,這意味着必須小心鎖定共享數據)。

傳入同一接口的單向調用會按順序進行處理。如果多線程客戶端在接口 IFoo 上調用 method1 和 method2,並在接口 IBar 上調用 method3,則 method1 和 method2 將始終按順序運行,但 method3 可以與 method1 和 method2 並行運行。

單一客戶端執行線程可能會通過以下兩種方式在具有多個線程的服務器上引發並行運行:

  • oneway 調用不會阻塞。如果執行 oneway 調用,然后調用非 oneway,則服務器可以同時執行 oneway 調用和非 oneway 調用。
  • 當系統從服務器調用回調時,通過同步回調傳回數據的服務器方法可以立即解除對客戶端的阻塞。

對於第二種方式,在調用回調之后執行的服務器函數中的任何代碼都可以並行運行,同時服務器會處理來自客戶端的后續調用。這包括服務器函數中的代碼,以及在函數結束時執行的自動析構函數中的代碼。如果服務器的線程池中有多個線程,那么即使僅從一個單一客戶端線程傳入調用,也會出現並行處理問題。(如果一個進程提供的任意 HAL 需要多個線程,則所有 HAL 都將具有多個線程,因為線程池是按進程共享的。)

當服務器調用所提供的回調時,transport 可以立即調用客戶端上已實現的回調,並解除對客戶端的阻塞。客戶端會繼續與服務器實現在調用回調之后所執行的任何任務(可能包括正在運行的析構函數)並行運行。回調后,只要服務器線程池中有足夠多的線程來處理傳入的調用,服務器函數中的代碼就不會再阻塞客戶端,但可以與來自客戶端的未來調用並行執行(除非服務器線程池中只有一個線程)。

除了同步回調外,來自單線程客戶端的 oneway 調用也可以由線程池中具有多個線程的服務器並行處理,但前提是要在不同的接口上執行這些 oneway 調用。同一接口上的 oneway 調用一律按順序處理。

注意:我們強烈建議服務器函數在調用回調函數后立即返回。

例如(在 C++ 中):

Return<void> someMethod(someMethod_cb _cb) {
    // Do some processing, then call callback with return data
    hidl_vec<uint32_t> vec = ...
    _cb(vec);
    // At this point, the client's callback will be called,
    // and the client will resume execution.
    ...
    return Void(); // is basically a no-op
};
 

客戶端線程模型

非阻塞調用(帶有 oneway 關鍵字標記的函數)與阻塞調用(未指定 oneway 關鍵字的函數)的客戶端線程模型有所不同。

阻塞調用

對於阻塞調用來說,除非發生以下情況之一,否則客戶端將一直處於阻塞狀態:

  • 出現傳輸錯誤;Return 對象包含可通過 Return::isOk() 檢索的錯誤狀態。
  • 服務器實現調用回調(如果有)。
  • 服務器實現返回值(如果沒有回調參數)。

如果成功的話,客戶端以參數形式傳遞的回調函數始終會在函數本身返回之前被服務器調用。回調是在進行函數調用的同一線程上執行,所以在函數調用期間,實現人員必須謹慎地持有鎖(並盡可能徹底避免持有鎖)。不含 generates 語句或 oneway 關鍵字的函數仍處於阻塞狀態;在服務器返回 Return<void> 對象之前,客戶端將一直處於阻塞狀態。

單向調用

如果某個函數標記有 oneway,則客戶端會立即返回,而不會等待服務器完成其函數調用。從表面(整體)上看,這意味着函數調用只用了一半的時間,因為它執行了一半的代碼,但是當編寫性能敏感型實現時,這會帶來一些調度方面的影響。通常,使用單向調用會導致調用程序繼續被調度,而使用正常的同步調用會使調度程序立即從調用程序轉移到被調用程序進程。這就是 binder 中的性能優化。對於必須在具有高優先級的目標進程中執行單向調用的服務,可以更改接收服務的調度策略。在 C++ 中,使用 libhidltransport 的 setMinSchedulerPolicy 方法,並在 sched.h 中定義調度程序優先級和策略可確保所有對服務的調用至少以設置的調度策略和優先級運行。

 

轉換 HAL 模塊

您可以通過轉換 hardware/libhardware/include/hardware 中的標頭將預裝的 HAL 模塊更新為 HIDL HAL 模塊。

使用 c2hal

c2hal 工具可處理大部分轉換工作,從而減少所需進行的手動更改次數。例如,要為 NFC HAL 生成 HIDL .hal文件,請使用以下命令:

make c2hal
c2hal -r android.hardware:hardware/interfaces -randroid.hidl:system/libhidl/transport -p android.hardware.nfc@1.0 hardware/libhardware/include/hardware/nfc.h
 

這些命令會在 hardware/interfaces/nfc/1.0/ 中添加文件。從 $ANDROID_BUILD_TOP 目錄運行 hardware/interfaces/update-makefiles.sh 還會向 HAL 添加所需的 makefile。在這里,您可以進行手動更改,以完全轉換 HAL。

c2hal 操作

當您運行 c2hal 時,標頭文件中的所有內容都會轉移到 .hal 文件。

c2hal 會識別在提供的標頭文件中包含函數指針的結構體,並將每個結構體轉換為單獨的接口文件。 例如,alloc_device_t 會轉換為 IAllocDevice HAL 模塊(位於文件 IAllocDevice.hal 中)。

所有其他數據類型都會復制到 types.hal 文件。 磅定義已移到枚舉中,不屬於 HIDL 的項或不可轉換的項(例如靜態函數聲明)會復制到標記有文字“NOTE”的備注中。

手動操作

c2hal 工具在遇到某些構造時不知該如何應對。例如,HIDL 沒有原始指針的概念;因此,當 c2hal 遇到標頭文件中的指針時,不知道應將指針解讀為數組還是對其他對象的引用。它同樣不理解無類型指針。

在轉換到 HIDL 期間,必須手動移除 int reserved[7] 等字段。應將返回值的名稱等項更新為更有意義的內容;例如,將方法的返回參數(例如,NFC 中的 write)從自動生成的 int32_t write_ret 轉換為 Status status(其中 Status 是包含可能的 NFC 狀態的新枚舉)。

實現 HAL

創建 .hal 文件以表示您的 HAL 后,您必須生成在 C++ 和 Java 中創建語言支持的 makefile(Make 或 Soong),除非 HAL 使用的功能在 Java 中不受支持。./hardware/interfaces/update-makefiles.sh 腳本可以為 hardware/interfaces 目錄中的 HAL 自動生成 makefile(對於其他位置的 HAL,只需更新腳本即可)。

如果 makefile 是最新版本,則表示您已准備好生成標頭文件和實現方法了。要詳細了解如何實現生成的接口,請參閱 HIDL C++(用於 C++ 實現)或 HIDL Java(用於 Java 實現)。

 

數據類型

本部分介紹了 HIDL 數據類型。如需了解實現詳情,請參閱 HIDL C++(如果是 C++ 實現)或 HIDL Java(如果是 Java 實現)。

與 C++ 的相似之處包括:

  • structs 使用 C++ 語法;unions 默認支持 C++ 語法。結構體和聯合都必須具有名稱;不支持匿名結構體和聯合。
  • HIDL 中允許使用 typedef(和在 C++ 中一樣)。
  • 允許使用 C++ 樣式的備注,並且此類備注會被復制到生成的標頭文件中。

與 Java 的相似之處包括:

  • 對於每個文件,HIDL 都會定義一個 Java 樣式的命名空間,並且這些命名空間必須以 android.hardware.開頭。生成的 C++ 命名空間為 ::android::hardware::…
  • 文件的所有定義都包含在一個 Java 樣式的 interface 封裝容器中。
  • HIDL 數組聲明遵循 Java 樣式,而非 C++ 樣式。例如:
    struct Point {
        int32_t x;
        int32_t y;
    };
    Point[3] triangle;   // sized array
     
  • 備注類似於 javadoc 格式。

數據表示法

采用標准布局(plain-old-data 類型要求的子集)的 struct 或 union 在生成的 C++ 代碼中具有一致的內存布局,這是依靠 struct 和 union 成員上的顯式對齊屬性實現的。

基本 HIDL 類型以及 enum 和 bitfield 類型(一律從基本類型派生而來)會映射到標准 C++ 類型,例如 cstdint中的 std::uint32_t

由於 Java 不支持無符號的類型,因此無符號的 HIDL 類型會映射到相應的有符號 Java 類型。結構體會映射到 Java 類;數組會映射到 Java 數組;Java 目前不支持聯合。字符串在內部以 UTF8 格式存儲。由於 Java 僅支持 UTF16 字符串,因此發送到或來自 Java 實現的字符串值會進行轉換;在重新轉換回來后,字符串值可能不會與原來的值完全相同,這是因為字符集並非總能順暢映射。

在 C++ 中通過 IPC 接收的數據會被標記為 const,並存儲在僅在函數調用期間存在的只讀內存中。在 Java 中通過 IPC 接收的數據已被復制到 Java 對象中,因此無需額外的復制操作即可保留下來(可以對其進行修改)。

注釋

可以將 Java 樣式的注釋添加到類型聲明中。注釋由 HIDL 編譯器的供應商測試套件 (VTS) 后端解析,但 HIDL 編譯器實際上並不理解任何此類經過解析的注釋。經過解析的 VTS 注釋將由 VTS 編譯器 (VTSC) 處理。

注釋使用 Java 語法:@annotation@annotation(value) 或 @annotation(id=value, id=value…),其中值可以是常量表達式、字符串或在 {} 中列出的一系列值,正如在 Java 中一樣。可以將多個名稱相同的注釋附加到同一項內容。

前向聲明

在 HIDL 中,結構體不能采用前向聲明,因此無法實現用戶定義的自指數據類型(例如,您不能在 HIDL 中描述關聯的列表,也不能描述樹)。大多數現有(Android 8.x 之前的)HAL 都對使用前向聲明有限制,這種限制可以通過重新排列數據結構聲明來移除。

由於存在這種限制,因此可以通過簡單的深層復制按值復制數據結構,而無需跟蹤可以在一個自指數據結構中出現多次的指針值。如果將同一項數據傳遞兩次(例如,使用兩個方法參數或使用兩個指向該數據的 vec<T>),則會生成並傳送兩個單獨的副本。

嵌套式聲明

HIDL 支持根據需要嵌套任意多層的聲明(有一種例外情況,請見下方的備注)。例如:

interface IFoo {
    uint32_t[3][4][5][6] multidimArray;

    vec<vec<vec<int8_t>>> multidimVector;

    vec<bool[4]> arrayVec;

    struct foo {
        struct bar {
            uint32_t val;
        };
        bar b;
    }
    struct baz {
        foo f;
        foo.bar fb; // HIDL uses dots to access nested type names
    }
   
 

例外情況是:接口類型只能嵌入到 vec<T> 中,並且只能嵌套一層(不能出現 vec<vec<IFoo>> 這樣的情況)。

原始指針語法

HIDL 語言不使用 *,並且不支持 C/C++ 原始指針的全面靈活性。要詳細了解 HIDL 如何封裝指針和數組/向量,請參閱 vec <T> 模板

接口

interface 關鍵字有以下兩種用途。

  • 打開 .hal 文件中接口的定義。
  • 可用作結構體/聯合字段、方法參數和返回項中的特殊類型。該關鍵字被視為一般接口,與 android.hidl.base@1.0::IBase 同義。

例如,IServiceManager 具有以下方法:

get(string fqName, string name) generates (interface service);
 

該方法可按名稱查找某個接口。此外,該方法與使用 android.hidl.base@1.0::IBase 替換接口完全一樣。

接口只能以兩種方式傳遞:作為頂級參數,或作為 vec<IMyInterface> 的成員。它們不能是嵌套式向量、結構體、數組或聯合的成員。

MQDescriptorSync 和 MQDescriptorUnsync

MQDescriptorSync 和 MQDescriptorUnsync 類型用於在 HIDL 接口內傳遞已同步或未同步的快速消息隊列 (FMQ) 描述符。要了解詳情,請參閱 HIDL C++(Java 中不支持 FMQ)。

memory 類型

memory 類型用於表示 HIDL 中未映射的共享內存。只有 C++ 支持該類型。可以在接收端使用這種類型的值來初始化 IMemory 對象,從而映射內存並使其可用。要了解詳情,請參閱 HIDL C++

警告:位於共享內存中的結構化數據所屬的類型必須符合以下條件:其格式在傳遞 memory 的接口版本的生命周期內絕不會改變。否則,HAL 可能會發生嚴重的兼容性問題。

pointer 類型

pointer 類型僅供 HIDL 內部使用。

bitfield <T> 類型模板

bitfield<T>(其中 T 是用戶定義的枚舉)表明該值是在 T 中定義的枚舉值的按位“或”值。在生成的代碼中,bitfield<T> 顯示為 T 的基礎類型。例如:

enum Flag : uint8_t {
    HAS_FOO = 1 << 0,
    HAS_BAR = 1 << 1,
    HAS_BAZ = 1 << 2
};
typedef bitfield<Flag> Flags;
setFlags(Flags flags) generates (bool success);
 

編譯器會按照處理 uint8_t 的相同方式處理 Flag 類型。

為什么不使用 (u)int8_t/(u)int16_t/(u)int32_t/(u)int64_t?使用 bitfield 可向讀取器提供額外的 HAL 信息,讀取器現在知道 setFlags 采用 Flag 的按位“或”值(即知道使用 int16_t 調用 setFlags 是無效的)。如果沒有 bitfield,則該信息僅通過文檔傳達。此外,VTS 實際上可以檢查標記的值是否為 Flag 的按位“或”值。

句柄基本類型

警告:任何類型的地址(即使是物理設備地址)都不能是原生句柄的一部分。在進程之間傳遞該信息很危險,會導致進程容易受到攻擊。在進程之間傳遞的任何值都必須先經過驗證,然后才能用於在進程內查找分配的內存。否則,錯誤的句柄可能會導致內存訪問錯誤或內存損壞。

HIDL 語義是按值復制,這意味着參數會被復制。所有大型數據或需要在進程之間共享的數據(例如同步柵欄)都是通過傳遞指向以下持久對象的文件描述符進行處理:針對共享內存的 ashmem、實際文件或可隱藏在文件描述符后的任何其他內容。Binder 驅動程序會將文件描述符復制到其他進程。

native_handle_t

Android 支持 native_handle_t(在 libcutils 中定義的一般句柄概念)。

typedef struct native_handle
{
  int version;        /* sizeof(native_handle_t) */
  int numFds;         /* number of file-descriptors at &data[0] */
  int numInts;        /* number of ints at &data[numFds] */
  int data[0];        /* numFds + numInts ints */
} native_handle_t;
 

原生句柄是整數和文件描述符的集合(按值傳遞)。單個文件描述符可存儲在沒有整數、包含單個文件描述符的原生句柄中。使用封裝有 handle 基本類型之原生句柄的傳遞句柄可確保相應的原生句柄直接包含在 HIDL 中。

native_handle_t 的大小可變,因此無法直接包含在結構體中。句柄字段會生成指向單獨分配的 native_handle_t 的指針。

在早期版本的 Android 中,原生句柄是使用 libcutils 中的相同函數創建的。在 Android 8.0 中,這些函數現在被復制到了 android::hardware::hidl 命名空間或移到了 NDK 中。HIDL 自動生成的代碼會自動對這些函數進行序列化和反序列化,而無需用戶編寫的代碼參與。

句柄和文件描述符所有權

當您調用傳遞(或返回)hidl_handle 對象(復合類型的頂級或一部分)的 HIDL 接口方法時,其中包含的文件描述符的所有權如下所述:

  • 將 hidl_handle 對象作為參數傳遞的調用程序會保留對其封裝的 native_handle_t 中包含的文件描述符的所有權;該調用程序在完成對這些文件描述符的操作后,必須將這些文件描述符關閉。
  • 通過將 hidl_handle 對象傳遞到 _cb 函數來返回該對象的進程會保留對該對象封裝的 native_handle_t中包含的文件描述符的所有權;該進程在完成對這些文件描述符的操作后,必須將這些文件描述符關閉。
  • 接收 hidl_handle 的傳輸擁有對相應對象封裝的 native_handle_t 中的文件描述符的所有權;接收器可在事務回調期間按原樣使用這些文件描述符,但如果想在回調完成后繼續使用這些文件描述符,則必須克隆原生句柄。事務完成時,傳輸將自動對文件描述符執行 close() 操作。

HIDL 不支持在 Java 中使用句柄(因為 Java 根本不支持句柄)。

有大小的數組

對於 HIDL 結構體中有大小的數組,其元素可以是結構體可包含的任何類型:

struct foo {
uint32_t[3] x; // array is contained in foo
};
 

字符串

字符串在 C++ 和 Java 中的顯示方式不同,但基礎傳輸存儲類型是 C++ 結構。如需了解詳情,請參閱 HIDL C++ 數據類型或 HIDL Java 數據類型

注意:通過 HIDL 接口將字符串傳遞到 Java 或從 Java 傳遞字符串(包括從 Java 傳遞到 Java)將會導致字符集轉換,而此項轉換可能無法精確保留原始編碼。

vec<T> 類型模板

vec<T> 模板表示包含 T 實例的可變大小的緩沖區。

T 可以是以下項之一:

  • 基本類型(例如 uint32_t)
  • 字符串
  • 用戶定義的枚舉
  • 用戶定義的結構體
  • 接口,或 interface 關鍵字(vec<IFoo>vec<interface> 僅在作為頂級參數時受支持)
  • 句柄
  • bitfield<U>
  • vec<U>,其中 U 可以是此列表中的任何一項,接口除外(例如,vec<vec<IFoo>> 不受支持)
  • U[](有大小的 U 數組),其中 U 可以是此列表中的任何一項,接口除外

用戶定義的類型

本部分介紹了用戶定義的類型。

枚舉

HIDL 不支持匿名枚舉。另一方面,HIDL 中的枚舉與 C++11 類似:

enum name : type { enumerator , enumerator = constexpr ,   }
 

基本枚舉是根據 HIDL 中的某個整數類型而定義的。如果沒有為基於整數類型的枚舉的第一個枚舉器指定值,則此值默認為 0。如果沒有為后面的枚舉器指定值,則此值默認為先前的值加 1。例如:

// RED == 0
// BLUE == 4 (GREEN + 1)
enum Color : uint32_t { RED, GREEN = 3, BLUE }
 

枚舉也可以從先前定義的枚舉繼承。如果沒有為子枚舉的第一個枚舉器指定值(在本例中為 FullSpectrumColor),則此值默認為父枚舉的最后一個枚舉器的值加 1。例如:

// ULTRAVIOLET == 5 (Color:BLUE + 1)
enum FullSpectrumColor : Color { ULTRAVIOLET }
 

警告:枚舉繼承的作用順序與大多數其他類型的集成是相反的。子枚舉值不可用作父枚舉值。這是因為子枚舉包含的值多於父枚舉。不過,父枚舉值可以安全地用作子枚舉值,因為根據定義,子枚舉值是父枚舉值的超集。在設計接口時請注意這一點,因為這意味着在以后的接口迭代中,引用父枚舉的類型無法引用子枚舉。

枚舉的值通過冒號語法(而不是像嵌套式類型一樣使用點語法)引用。語法是 Type:VALUE_NAME。如果在相同的枚舉類型或子類型中引用枚舉的值,則無需指定類型。示例:

enum Grayscale : uint32_t { BLACK = 0, WHITE = BLACK + 1 };
enum Color : Grayscale { RED = WHITE + 1 };
enum Unrelated : uint32_t { FOO = Color:RED + 1 };
 

從 Android 10 開始,枚舉具有可以在常量表達式中使用的 len 屬性。 MyEnum::len 是相應枚舉中條目的總數。這不同於值的總數,當值重復時,值的總數可能會較小。

結構體

HIDL 不支持匿名結構體。另一方面,HIDL 中的結構體與 C 非常類似。

HIDL 不支持完全包含在結構體內且長度可變的數據結構。這包括 C/C++ 中有時用作結構體最后一個字段且長度不定的數組(有時會看到其大小為 [0])。HIDL vec<T> 表示數據存儲在單獨的緩沖區中且大小動態變化的數組;此類實例由 struct 中的 vec<T> 的實例表示。

同樣,string 可包含在 struct 中(關聯的緩沖區是相互獨立的)。在生成的 C++ 代碼中,HIDL 句柄類型的實例通過指向實際原生句柄的指針來表示,因為基礎數據類型的實例的長度可變。

聯合

HIDL 不支持匿名聯合。另一方面,聯合與 C 類似。

聯合不能包含修正類型(指針、文件描述符、Binder 對象,等等)。它們不需要特殊字段或關聯的類型,只需通過 memcpy() 或等效函數即可復制。聯合不能直接包含(或通過其他數據結構包含)需要設置 Binder 偏移量(即句柄或 Binder 接口引用)的任何內容。例如:

union UnionType {
uint32_t a;
//  vec<uint32_t> r;  // Error: can't contain a vec<T>
uint8_t b;1
};
fun8(UnionType info); // Legal
 

聯合還可以在結構體中進行聲明。例如:

struct MyStruct {
    union MyUnion {
      uint32_t a;
      uint8_t b;
    }; // declares type but not member

    union MyUnion2 {
      uint32_t a;
      uint8_t b;
    } data; // declares type but not member
  }



Safe Union

HIDL 中的 safe_union 表示一種顯式標記聯合類型。它類似於 union,但 safe_union 會跟蹤基礎類型且與 Java 兼容。safe_union 類型適用於搭載 Android 10 及更高版本的新設備和升級設備。

語法

safe_union 在 HIDL 中的表示方式與 union 或 struct 完全相同。

safe_union MySafeUnion {
     TypeA a;
     TypeB b;
     ...
};
 

用法

在運行時中,safe_union 只是一種類型。默認情況下,它將是聯合中的第一個類型。例如,在上面的示例中,MySafeUnion 默認為 TypeA

在 C++ 和 Java 中,hidl-gen 會為 safe_union 生成一個自定義類或結構。該類包括每個成員的判別器(位於 hidl_discriminator 中),一個用於獲取當前判別器的方法 (getDiscriminator),以及每個成員的 setter 和 getter。每個 setter 和 getter 的名稱都與其成員完全一樣。例如,TypeA a 的 getter 名為“a”,它將返回 TypeA的內容。對應的 setter 也命名為“a”,它會采用 TypeA 的參數。在 safe_union 中設置值會更新 getDiscriminator 返回的判別器的值。從當前判別器以外的判別器訪問值會中止該程序。例如,如果在 MySafeUnion 實例上調用 getDiscriminator 會返回 hidl_discriminator::b,則嘗試檢索 a 會中止該程序。

Monostate

safe_union 始終有一個值,但如果希望它沒有值,請使用 android.hidl.safe_union@1.0::Monostate 作為占位符。例如,以下聯合可以是 noinit(空)或 foo

import android.hidl.safe_union@1.0::Monostate;

safe_union OptionalFoo {
     Monostate noinit;
     Foo foo;
};



版本編號

HIDL 要求每個使用 HIDL 編寫的接口均必須帶有版本編號。HAL 接口一經發布便會被凍結,如果要做任何進一步的更改,都只能在接口的新版本中進行。雖然無法對指定的已發布接口進行修改,但可通過其他接口對其進行擴展。

HIDL 代碼結構

HIDL 代碼按用戶定義的類型、接口和軟件包進行整理

  • 用戶定義的類型 (UDT)。HIDL 能夠提供對一組基本數據類型的訪問權限,這些數據類型可用於通過結構、聯合和枚舉組成更復雜的類型。UDT 會被傳遞到接口的方法。可以在軟件包級定義 UDT(針對所有接口的通用 UDT),也可以在本地針對某個接口定義 UDT。
  • 接口。作為 HIDL 的基本構造塊,接口由 UDT 和方法聲明組成。接口也可以繼承自其他接口。
  • 軟件包。整理相關 HIDL 接口及其操作的數據類型。軟件包通過名稱和版本進行標識,包括以下內容:
    • 稱為 types.hal 的數據類型定義文件。
    • 零個或多個接口,每個都位於各自的 .hal 文件中。

數據類型定義文件 types.hal 中僅包含 UDT(所有軟件包級 UDT 都保存在一個文件中)。采用目標語言的表示法可用於軟件包中的所有接口。

版本編號理念

針對指定版本(如 1.0)發布后,HIDL 軟件包(如 android.hardware.nfc)便不可再改變;您無法對其進行更改。如果要對已發布軟件包中的接口進行修改,或要對其 UDT 進行任何更改,都只能在另一個軟件包中進行。

在 HIDL 中,版本編號是在軟件包級而非接口級應用,並且軟件包中的所有接口和 UDT 共用同一個版本。軟件包版本遵循語義化版本編號規則,不含補丁程序級別和編譯元數據組成部分。在指定的軟件包中,minor 版本更新意味着新版本的軟件包向后兼容舊軟件包,而 major 版本更新意味着新版本的軟件包不向后兼容舊軟件包。

從概念上來講,軟件包可通過以下方式之一與另一個軟件包相關:

  • 完全不相關
  • 軟件包級向后兼容的可擴展性。軟件包的新 minor 版本升級(下一個遞增的修訂版本)中會出現這種情況;新軟件包擁有與舊軟件包一樣的名稱和 major 版本,但其 minor 版本會更高。從功能上來講,新軟件包是舊軟件包的超集,也就是說:
    • 父級軟件包的頂級接口會包含在新的軟件包中,不過這些接口可以在 types.hal 中有新的方法、新的接口本地 UDT(下文所述的接口級擴展)和新的 UDT。
    • 新接口也可以添加到新軟件包中。
    • 父級軟件包的所有數據類型均會包含在新軟件包中,並且可由來自舊軟件包中的方法(可能經過了重新實現)來處理。
    • 新數據類型也可以添加到新軟件包中,以供升級的現有接口的新方法使用,或供新接口使用。
  • 接口級向后兼容的可擴展性。新軟件包還可以擴展原始軟件包,方法是包含邏輯上獨立的接口,這些接口僅提供附加功能,並不提供核心功能。要實現這一目的,可能需要滿足以下條件:
    • 新軟件包中的接口需要依賴於舊軟件包的數據類型。
    • 新軟件包中的接口可以擴展一個或多個舊軟件包中的接口。
  • 擴展原始的向后不兼容性。這是軟件包的一種 major 版本升級,並且新舊兩個版本之間不需要存在任何關聯。如果存在關聯,則這種關聯可以通過以下方式來表示:組合舊版本軟件包中的類型,以及繼承舊軟件包中的部分接口。

構建接口

對於結構合理的接口,要添加不屬於原始設計的新類型的功能,應該需要修改 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/ 下。$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 的軟件包名稱,以及軟件包版本組成。完全限定名稱僅在聲明類型的實例時使用,在定義類型本身時不使用。例如,假設 1.0 版本的軟件包 android.hardware.nfc, 定義了一個名為 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 版本格式(例如,1.0)。
  • UDT 是 HIDL UDT 的點分隔名稱。由於 HIDL 支持嵌套式 UDT,並且 HIDL 接口可以包含 UDT(一種嵌套式聲明),因此用點訪問名稱。

例如,如果以下嵌套式聲明是在 1.0 版本的軟件包 android.hardware.example 內的通用類型文件中定義的:

// 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

在上述兩種情況下,只有在 Foo 的聲明范圍內才能使用 Bar 來引用 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 是一種枚舉類型,則該枚舉類型的每個值都會有一個完全限定名稱,這些名稱以該枚舉類型的完全限定名稱開頭,后跟一個冒號,然后是相應枚舉值的名稱。例如,假設 1.0 版本的軟件包 android.hardware.nfc,定義了一個枚舉類型 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
注意:不允許使用缺少版本但指定了存在軟件包的 UDT 名稱。

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。由於該名稱在當前軟件包中已存在(假設它已正確導入),因此它會用於聲明。

僅當以下條件之一為 true 時,才會導入當前軟件包中的名稱:

  • 使用 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 是在 1.1 版軟件包 android.hardware.nfc 中聲明的,則 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 是自動導入的)。
  • 使用規則 2 內插 IFooCallback 后得到 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; 導入)。

types.hal

每個 HIDL 軟件包都包含一個 types.hal 文件,該文件中包含參與相應軟件包的所有接口共享的 UDT。不論 UDT 是在 types.hal 中還是在接口聲明中聲明的,HIDL 類型始終是公開的,您可以在這些類型的定義范圍之外訪問它們。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.halandroid.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);
}
 

升級規則

要定義軟件包 package@major.minor,則 A 必須為 true,或 B 中的所有項必須為 true:

規則 A “是起始 minor 版本”:所有之前的 minor 版本(package@major.0package@major.1package@major.(minor-1))必須均未定義。
規則 B

以下各項均為 true:

  1. “以前的 minor 版本有效”:package@major.(minor-1) 必須已定義,並且遵循相同的規則 A(從 package@major.0 到 package@major.(minor-2) 均未定義)或規則 B(如果它是從 @major.(minor-2) 升級而來); 

    和 

  2. “繼承至少一個具有相同名稱的接口”:存在擴展 package@major.(minor-1)::IFoo 的接口 package@major.minor::IFoo(如果前一個軟件包具有接口); 

    和 

  3. “沒有具有不同名稱的繼承接口”:不得存在擴展 package@major.(minor-1)::IBaz 的 package@major.minor::IBar,其中 IBar 和 IBaz 是兩個不同的名稱。如果存在具有相同名稱的接口,則 package@major.minor::IBar 必須擴展 package@major.(minor-k)::IBar,以確保不存在 k 較小的 IBar。

由於規則 A:

  • 軟件包可以使用任何起始 minor 版本號(例如,android.hardware.biometrics.fingerprint 的起始版本號是 @2.1)。
  • android.hardware.foo@1.0 未定義”這項要求意味着目錄 hardware/interfaces/foo/1.0 甚至不應存在。

不過,規則 A 不會影響軟件包名稱相同但 major 版本不同的軟件包(例如,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::IBaseIBase 本身除外)。

必須同時遵循 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 中 1.0 版本的 android.hardware.example 的軟件包級 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() 方法。

要點:在 HIDL 中,每個從基類繼承的方法都必須在繼承類中顯式實現。如果方法實現需要回退到相關基類的方法實現,則回退必須位於實現中。

升級規范

有時接口名稱必須重新命名擴展接口。我們建議枚舉擴展、結構體和聯合采用與其擴展的內容相同的名稱,除非它們有足夠多的不同之處,有必要使用新名稱。例如:

// 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 的組合(按構成)來表示。

不過,有一種類型的關系經過嚴格定義,且必須強制執行,即軟件包級向后兼容的繼承。在這種情況下,父級軟件包是被繼承的軟件包,而子軟件包是擴展父級的軟件包。軟件包級向后兼容的繼承規則如下:

  1. 父級軟件包的所有頂級接口都會被子級軟件包中的接口繼承。
  2. 新接口也可以添加到新軟件包中(與其他軟件包中其他接口的關系不受限制)。
  3. 新數據類型也可以添加到新軟件包中,以供升級的現有接口的新方法使用,或供新接口使用。

這些規則可以使用 HIDL 接口級繼承和 UDT 構成來實現,但需要元級知識才能了解這些關系如何構成向后兼容的軟件包擴展。元級知識按以下方式推斷:

要點:對於  major.minor 版本的軟件包  package,如果存在  major.(minor-1) 版本的  package,則  package@major.minor 屬於 minor 版本升級,並且必須遵循向后兼容性規則。

如果軟件包符合這一要求,則 hidl-gen 會強制執行向后兼容性規則。

 

代碼樣式指南

HIDL 代碼樣式類似於 Android 框架中的 C++ 代碼,縮進 4 個空格,並且采用混用大小寫的文件名。軟件包聲明、導入和文檔字符串與 Java 中的類似,只有些微差別。

下面針對 IFoo.hal 和 types.hal 的示例展示了 HIDL 代碼樣式,並提供了指向每種樣式(IFooClientCallback.halIBar.hal 和 IBaz.hal 已省略)詳細信息的快速鏈接。

hardware/interfaces/foo/1.0/IFoo.hal

/*
 * (License Notice)
 */

package android.hardware.foo@1.0;

import android.hardware.bar@1.0::IBar;

import IBaz;
import IFooClientCallback;

/**
 * IFoo is an interface that…
 */
interface IFoo {

    /**
     * This is a multiline docstring.
     *
     * @return result 0 if successful, nonzero otherwise.
     */
     foo() generates (FooStatus result);

    /**
     * Restart controller by power cycle.
     *
     * @param bar callback interface that…
     * @return result 0 if successful, nonzero otherwise.
     */
    powerCycle(IBar bar) generates (FooStatus result);

    /** Single line docstring. */
    baz();

    /**
     * The bar function.
     *
     * @param clientCallback callback after function is called
     * @param baz related baz object
     * @param data input data blob
     */
    bar(IFooClientCallback clientCallback,
        IBaz baz,
        FooData data);

};
 
hardware/interfaces/foo/1.0/types.hal

/*
 * (License Notice)
 */

package android.hardware.foo@1.0;

/** Replied status. */
enum Status : int32_t {
    OK,
    /* invalid arguments */
    ERR_ARG,
    /* note, no transport related errors */
    ERR_UNKNOWN = -1,
};

struct ArgData {
    int32_t[20]  someArray;
    vec<uint8_t> data;
};
 

命名規范

函數名稱、變量名稱和文件名應該是描述性名稱;避免過度縮寫。將首字母縮略詞視為字詞(例如,請使用 INfc,而非 INFC)。

目錄結構和文件命名

目錄結構應如下所示:

  • ROOT-DIRECTORY
    • MODULE
      • SUBMODULE(可選,可以有多層)
        • VERSION
          • Android.mk
          • IINTERFACE_1.hal
          • IINTERFACE_2.hal
          • IINTERFACE_N.hal
          • types.hal(可選)

其中:

  • ROOT-DIRECTORY 為:
    • hardware/interfaces(如果是核心 HIDL 軟件包)。
    • vendor/VENDOR/interfaces(如果是供應商軟件包),其中 VENDOR 是指 SoC 供應商或原始設備制造商 (OEM)/原始設計制造商 (ODM)。
  • MODULE 應該是一個描述子系統的小寫字詞(例如 nfc)。如果需要多個字詞,請使用嵌套式 SUBMODULE。可以嵌套多層。
  • VERSION 應該與版本中所述的版本 (major.minor) 完全相同。
  • 接口名稱中所述,IINTERFACE_X 應該是含有 UpperCamelCase/PascalCase 的接口名稱(例如 INfc)。

例如:

  • hardware/interfaces
    • nfc
      • 1.0
        • Android.mk
        • INfc.hal
        • INfcClientCallback.hal
        • types.hal

注意:所有文件都必須采用不可執行的權限(在 Git 中)。

軟件包名稱

軟件包名稱必須采用以下完全限定名稱 (FQN) 格式(稱為 PACKAGE-NAME):

PACKAGE.MODULE[.SUBMODULE[.SUBMODULE[…]]]@VERSION
 

其中:

  • PACKAGE 是映射到 ROOT-DIRECTORY 的軟件包。具體來說,PACKAGE 是:
    • android.hardware(如果是核心 HIDL 軟件包)(映射到 hardware/interfaces)。
    • vendor.VENDOR.hardware(如果是供應商軟件包),其中 VENDOR 是指 SoC 供應商或 OEM/ODM(映射到 vendor/VENDOR/interfaces)。
  • MODULE[.SUBMODULE[.SUBMODULE[…]]]@VERSION 與目錄結構中所述結構內的文件夾名稱完全相同。
  • 軟件包名稱應為小寫。如果軟件包名稱包含多個字詞,則這些字詞應用作子模塊或以 snake_case 形式書寫。
  • 不允許使用空格。

軟件包聲明中始終使用 FQN。

版本

版本應具有以下格式:

MAJOR.MINOR
 

MAJOR 和 MINOR 版本都應該是一個整數。HIDL 使用語義化版本編號規則。

導入

導入采用以下 3 種格式之一:

  • 完整軟件包導入:import PACKAGE-NAME;
  • 部分導入:import PACKAGE-NAME::UDT;(或者,如果導入的類型在同一個軟件包中,則為 importUDT;
  • 僅類型導入:import PACKAGE-NAME::types;

PACKAGE-NAME 遵循軟件包名稱中的格式。當前軟件包的 types.hal(如果存在)是自動導入的(請勿對其進行顯式導入)。

完全限定名稱 (FQN)

僅在必要時對用戶定義的類型導入使用完全限定名稱。如果導入類型在同一個軟件包中,則省略 PACKAGE-NAME。FQN 不得含有空格。完全限定名稱示例:

android.hardware.nfc@1.0::INfcClientCallback
 

如果是在 android.hardware.nfc@1.0 下的另一個文件中,可以使用 INfcClientCallback 引用上述接口。否則,只能使用完全限定名稱。

對導入進行分組和排序

在軟件包聲明之后(在導入之前)添加一個空行。每個導入都應占用一行,且不應縮進。按以下順序對導入進行分組:

  1. 其他 android.hardware 軟件包(使用完全限定名稱)。
  2. 其他 vendor.VENDOR 軟件包(使用完全限定名稱)。
    • 每個供應商都應是一個組。
    • 按字母順序對供應商排序。
  3. 源自同一個軟件包中其他接口的導入(使用簡單名稱)。

在組與組之間添加一個空行。在每個組內,按字母順序對導入排序。例如:

import android.hardware.nfc@1.0::INfc;
import android.hardware.nfc@1.0::INfcClientCallback;

/* Importing the whole module. */
import vendor.barvendor.bar@3.1;

import vendor.foovendor.foo@2.2::IFooBar;
import vendor.foovendor.foo@2.2::IFooFoo;

import IBar;
import IFoo;
 

接口名稱

接口名稱必須以 I 開頭,后跟 UpperCamelCase/PascalCase 名稱。名稱為 IFoo 的接口必須在文件 IFoo.hal 中定義。此文件只能包含 IFoo 接口的定義(接口 INAME 應位於 INAME.hal 中)。

函數

對於函數名稱、參數和返回變量名稱,請使用 lowerCamelCase。例如:

open(INfcClientCallback clientCallback) generates (int32_t retVal);
oneway pingAlive(IFooCallback cb);
 

結構體/聯合字段名稱

對於結構體/聯合字段名稱,請使用 lowerCamelCase。例如:

struct FooReply {
    vec<uint8_t> replyData;
}
 

類型名稱

類型名稱指結構體/聯合定義、枚舉類型定義和 typedef。對於這些名稱,請使用 UpperCamelCase/PascalCase。例如:

enum NfcStatus : int32_t {
    /*...*/
};
struct NfcData {
    /*...*/
};
 

枚舉值

枚舉值應為 UPPER_CASE_WITH_UNDERSCORES。將枚舉值作為函數參數傳遞以及作為函數返回項返回時,請使用實際枚舉類型(而不是基礎整數類型)。例如:

enum NfcStatus : int32_t {
    HAL_NFC_STATUS_OK               = 0,
    HAL_NFC_STATUS_FAILED           = 1,
    HAL_NFC_STATUS_ERR_TRANSPORT    = 2,
    HAL_NFC_STATUS_ERR_CMD_TIMEOUT  = 3,
    HAL_NFC_STATUS_REFUSED          = 4
};
 

注意:枚舉類型的基礎類型是在冒號后顯式聲明的。因為它不依賴於編譯器,所以使用實際枚舉類型會更明晰。

對於枚舉值的完全限定名稱,請在枚舉類型名稱和枚舉值名稱之間使用冒號

PACKAGE-NAME::UDT[.UDT[.UDT[…]]:ENUM_VALUE_NAME
 

完全限定名稱內不得含有空格。僅在必要時使用完全限定名稱,其他情況下可以省略不必要的部分。例如:

android.hardware.foo@1.0::IFoo.IFooInternal.FooEnum:ENUM_OK
 

備注

對於單行備注,使用 ///* */ 和 /** */ 都可以。

// This is a single line comment
/* This is also single line comment */
/** This is documentation comment */
 
  • 使用 /* */ 進行備注。雖然 HIDL 支持使用 // 進行備注,但我們不建議這么做,因為它們不會出現在生成的輸出中。
  • 針對生成的文檔使用 /** */。此樣式只能應用於類型、方法、字段和枚舉值聲明。示例:
    /** Replied status */
    enum TeleportStatus {
        /** Object entirely teleported. */
        OK              = 0,
        /** Methods return this if teleportation is not completed. */
        ERROR_TELEPORT  = 1,
        /**
         * Teleportation could not be completed due to an object
         * obstructing the path.
         */
        ERROR_OBJECT    = 2,
        ...
    }
     
  • 另起一行使用 /** 開始書寫多行備注。每行都使用 * 開頭。用 */ 為備注結尾,並要單獨占一行且與星號對齊。例如:
    /**
     * My multi-line
     * comment
     */
     
  • 許可通知和變更日志的第一行應為 /*(一個星號),每行的開頭應使用 *,並且應將 */ 單獨放在最后一行(各行的星號應對齊)。例如:
    /*
     * Copyright (C) 2017 The Android Open Source Project
     * ...
     */

    /*
     * Changelog:
     * ...
     */
     

文件備注

每個文件的開頭都應為相應的許可通知。對於核心 HAL,該通知應為 development/docs/copyright-templates/c.txt 中的 AOSP Apache 許可。請務必更新年份,並使用 /* */ 樣式的多行備注(如上所述)。

您可以視需要在許可通知后空一行,后跟變更日志/版本編號信息。使用 /* */ 樣式的多行備注(如上所述),在變更日志后空一行,后跟軟件包聲明。

TODO 備注

TODO 備注應包含全部大寫的字符串 TODO,后跟一個冒號。例如:

// TODO: remove this code before foo is checked in.
 

只有在開發期間才允許使用 TODO 備注;TODO 備注不得存在於已發布的接口中。

接口/函數備注(文檔字符串)

對於多行和單行文檔字符串,請使用 /** */。對於文檔字符串,請勿使用 //

接口的文檔字符串應描述接口的一般機制、設計原理、目的等。函數的文檔字符串應針對特定函數(軟件包級文檔位於軟件包目錄下的 README 文件中)。

/**
 * IFooController is the controller for foos.
 */
interface IFooController {
    /**
     * Opens the controller.
     *
     * @return status HAL_FOO_OK if successful.
     */
    open() generates (FooStatus status);

    /** Close the controller. */
    close();
};
 

您必須為每個參數/返回值添加 @param 和 @return

  • 必須為每個參數添加 @param。其后應跟參數的名稱,然后是文檔字符串。
  • 必須為每個返回值添加 @return。其后應跟返回值的名稱,然后是文檔字符串。

例如:

/**
 * Explain what foo does.
 *
 * @param arg1 explain what arg1 is
 * @param arg2 explain what arg2 is
 * @return ret1 explain what ret1 is
 * @return ret2 explain what ret2 is
 */
foo(T arg1, T arg2) generates (S ret1, S ret2);
 

格式

一般格式規則包括:

  • 行長:每行文字最長不應超過 100 列。
  • 空格:各行不得包含尾隨空格;空行不得包含空格。
  • 空格與制表符:僅使用空格。
  • 縮進大小:數據塊縮進 4 個空格,換行縮進 8 個空格。
  • 大括號:(注釋值除外)大括號與前面的代碼在同一行,大括號與后面的分號占一整行。例如:
    interface INfc {
        close();
    };
     

軟件包聲明

軟件包聲明應位於文件頂部,在許可通知之后,應占一整行,並且不應縮進。聲明軟件包時需采用以下格式(有關名稱格式,請參閱軟件包名稱):

package PACKAGE-NAME;
 

例如:

package android.hardware.nfc@1.0;
 

函數聲明

函數名稱、參數、generates 和返回值應在同一行中(如果放得下)。例如:

interface IFoo {
    /** ... */
    easyMethod(int32_t data) generates (int32_t result);
};
 

如果一行中放不下,則嘗試按相同的縮進量放置參數和返回值,並突出 generate,以便讀取器快速看到參數和返回值。例如:

interface IFoo {
    suchALongMethodThatCannotFitInOneLine(int32_t theFirstVeryLongParameter,
                                          int32_t anotherVeryLongParameter);
    anEvenLongerMethodThatCannotFitInOneLine(int32_t theFirstLongParameter,
                                             int32_t anotherVeryLongParameter)
                                  generates (int32_t theFirstReturnValue,
                                             int32_t anotherReturnValue);
    superSuperSuperSuperSuperSuperSuperLongMethodThatYouWillHateToType(
            int32_t theFirstVeryLongParameter, // 8 spaces
            int32_t anotherVeryLongParameter
        ) generates (
            int32_t theFirstReturnValue,
            int32_t anotherReturnValue
        );
    /* method name is even shorter than 'generates' */
    foobar(AReallyReallyLongType aReallyReallyLongParameter,
           AReallyReallyLongType anotherReallyReallyLongParameter)
        generates (ASuperLongType aSuperLongReturnValue, // 4 spaces
                   ASuperLongType anotherSuperLongReturnValue);
}
 

其他細節:

  • 左括號始終與函數名稱在同一行。
  • 函數名稱和左括號之間不能有空格。
  • 括號和參數之間不能有空格,它們之間出現換行時除外。
  • 如果 generates 與前面的右括號在同一行,則前面加一個空格。如果 generates 與接下來的左括號在同一行,則后面加一個空格。
  • 將所有參數和返回值對齊(如果可能)。
  • 默認縮進 4 個空格。
  • 將換行的參數與上一行的第一個參數對齊,如果不能對齊,則這些參數縮進 8 個空格。

注釋

對於注釋,請采用以下格式:

@annotate(keyword = value, keyword = {value, value, value})
 

按字母順序對注釋進行排序,並在等號兩邊加空格。例如:

@callflow(key = value)
@entry
@exit
 

確保注釋占一整行。例如:

/* Good */
@entry
@exit

/* Bad */
@entry @exit
 

如果注釋在同一行中放不下,則縮進 8 個空格。例如:

@annotate(
        keyword = value,
        keyword = {
                value,
                value
        },
        keyword = value)
 

如果整個值數組在同一行中放不下,則在左大括號 { 后和數組內的每個逗號后換行。在最后一個值后緊跟着添加一個右括號。如果只有一個值,請勿使用大括號。

如果整個值數組可以放到同一行,則請勿在左大括號后和右大括號前加空格,並在每個逗號后加一個空格。例如:

/* Good */
@callflow(key = {"val", "val"})

/* Bad */
@callflow(key = { "val","val" })
 

注釋和函數聲明之間不得有空行。例如:

/* Good */
@entry
foo();

/* Bad */
@entry

foo();
 

枚舉聲明

對於枚舉聲明,請遵循以下規則:

  • 如果與其他軟件包共用枚舉聲明,則將聲明放在 types.hal 中,而不是嵌入到接口內。
  • 在冒號前后加空格,並在基礎類型后和左大括號前加空格。
  • 最后一個枚舉值可以有也可以沒有額外的逗號。

結構體聲明

對於結構體聲明,請遵循以下規則:

  • 如果與其他軟件包共用結構體聲明,則將聲明放在 types.hal 中,而不是嵌入到接口內。
  • 在結構體類型名稱后和左大括號前加空格。
  • 對齊字段名稱(可選)。例如:
    struct MyStruct {
        vec<uint8_t>   data;
        int32_t        someInt;
    }
     

數組聲明

請勿在以下內容之間加空格:

  • 元素類型和左方括號。
  • 左方括號和數組大小。
  • 數組大小和右方括號。
  • 右方括號和接下來的左方括號(如果存在多個維度)。

例如:

/* Good */
int32_t[5] array;

/* Good */
int32_t[5][6] multiDimArray;

/* Bad */
int32_t [ 5 ] [ 6 ] array;
 

矢量

請勿在以下內容之間加空格:

  • vec 和左尖括號。
  • 左尖括號和元素類型(例外情況:元素類型也是 vec)。
  • 元素類型和右尖括號(例外情況:元素類型也是 vec)。

例如:

/* Good */
vec<int32_t> array;

/* Good */
vec<vec<int32_t>> array;

/* Good */
vec< vec<int32_t> > array;

/* Bad */
vec < int32_t > array;

/* Bad */
vec < vec < int32_t > > array;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM