2、HAL接口定义语言(HIDL):基础语法、服务端与客户端实现、passthrough与binderized模式

HIDL,全称是HAL Interface Definition Language。说白了,它就是Android系统用来定义HAL接口的一种工具。你想想看,以前Android用HAL的方式,每个厂商自己搞一套,接口五花八门。Google后来受不了了,干脆统一了规矩——这就是HIDL诞生的原因。

我个人习惯把HIDL理解成「HAL世界的契约」。你定义好这个契约,服务端(HAL实现)和客户端(Framework)各守各的规矩,谁也别乱来。这样系统才能稳定,调试起来也省心。

2.1 HIDL基础语法

HIDL的语法其实挺像C++的,但更精简。我刚开始接触的时候,觉得它就是个「瘦身版C++头文件」。咱们直接看个例子:

package android.hardware.light@2.0;

interface ILight {
    enum Type : int32_t {
        BACKLIGHT = 0,
        KEYBOARD = 1,
        BUTTONS = 2,
        NOTIFICATIONS = 3
    };

    struct LightState {
        int32_t color;
        int32_t flashMode;
        int32_t brightness;
    };

    getSupportedTypes() generates (vec<Type> types);
    setLight(Type type, LightState state) generates (Status status);
};

这里有几个关键点:

  • package:定义包名和版本号。注意版本号是必须的,这是HIDL的硬性要求。
  • interface:接口定义,里面可以包含枚举、结构体和方法。
  • enum/struct:数据类型定义,支持嵌套。
  • generates:方法返回值的关键字。HIDL的方法可以有返回值,也可以没有。
我的小技巧:定义接口时,尽量把版本号写在package里,而不是文件名里。这样升级版本时,只需要改package声明,不用动文件结构。我在一个项目里吃过亏,文件名和package版本号不一致,编译报错查了半天。

HIDL支持的数据类型包括:

类型 说明 示例
int8_t ~ int64_t 有符号整数 int32_t count
uint8_t ~ uint64_t 无符号整数 uint8_t flags
float/double 浮点数 float temperature
string 字符串 string name
vec<T> 动态数组 vec<uint8_t> data
hidl_array<T, N> 固定大小数组 hidl_array<int32_t, 3> rgb

2.2 服务端与客户端实现

定义好接口文件后,接下来就是生成代码。HIDL编译器(hidl-gen)会帮你生成C++代码。嗯,这里要注意:生成的代码分两部分——服务端和客户端。

服务端实现:你写一个类继承生成的接口,然后实现里面的方法。我举个例子:

#include <android/hardware/light/2.0/ILight.h>

using namespace android::hardware::light::V2_0;

struct LightImpl : public ILight {
    Return<Status> setLight(Type type, const LightState& state) override {
        // 这里写你的HAL逻辑
        // 比如操作GPIO、写寄存器之类的
        return Status::SUCCESS;
    }

    Return<void> getSupportedTypes(
        std::function<void(const hidl_vec<Type>&)> _hidl_cb) override {
        // 回调方式返回数据
        _hidl_cb({Type::BACKLIGHT, Type::NOTIFICATIONS});
    }
};

客户端调用:Framework那边通过服务名获取接口,然后直接调用:

sp<ILight> service = ILight::getService();
if (service != nullptr) {
    LightState state = {0xFFFFFF, 0, 255};
    Status status = service->setLight(Type::BACKLIGHT, state);
}
我曾经踩过的坑:客户端调用getService()时,如果服务还没注册,会阻塞等待。默认超时时间是5秒。如果你的HAL初始化慢,Framework那边就可能卡住。我建议在服务端注册后,加一个「就绪标志」,客户端轮询这个标志再调用getService()。

2.3 Passthrough与Binderized模式

这是HIDL里最容易让人迷糊的地方。说白了,两种模式的区别就是「进程边界」的问题。

Binderized模式:HAL跑在独立进程里,通过Binder通信。好处是隔离性好,HAL挂了不会拖垮Framework。坏处是每次调用都有IPC开销。

Passthrough模式:HAL以共享库的形式加载到调用方进程里,直接函数调用。好处是零开销,性能好。坏处是HAL和调用方同进程,HAL崩了调用方也崩。

我画个图帮你理解:

HIDL 两种模式对比 Binderized 模式 Framework 进程 HAL 服务进程 Binder IPC hwservicemanager 注册/发现 Passthrough 模式 Framework 进程 HAL 共享库 (.so) 直接函数调用 dlopen 加载 动态链接 选择建议 性能敏感型HAL(如传感器、音频)→ Passthrough | 安全隔离型HAL(如指纹、DRM)→ Binderized

两种模式的选择,其实是个权衡。我个人的经验是:

  • 性能敏感型HAL(比如传感器、音频、相机预览)→ 用Passthrough。每次调用省掉IPC开销,延迟能降不少。
  • 安全隔离型HAL(比如指纹、DRM、TEE相关)→ 用Binderized。万一HAL崩了,不影响系统其他部分。
  • 调试阶段→ 先用Binderized。独立进程好调试,crash了不会带崩整个系统。

实际项目中的选择:我在做车载系统时,遇到过一个问题:音频HAL用Passthrough模式,结果音频服务偶尔crash,导致整个媒体播放器挂掉。后来改成Binderized模式,虽然延迟多了几百微秒,但稳定性大幅提升。所以,别一味追求性能,稳定性更重要。

最后说一句,HIDL的这两种模式,在Android 10之后其实有了变化。Google开始推AIDL(Android Interface Definition Language)来统一HAL和Framework的接口。但HIDL作为过渡方案,在现有系统里依然大量存在。你搞懂了HIDL,再去看AIDL,会发现很多概念是相通的。