第2章:HAL接口定义语言(HIDL)基础

HIDL,说白了就是Android用来定义硬件接口的“合同语言”。你写驱动,别人写框架,两边怎么约定数据格式、方法签名?靠的就是HIDL。我刚开始接触时也觉得它啰嗦,后来发现——没有这套规范,团队协作就是灾难。

2.1 HIDL语法与数据类型

HIDL的语法长得像C++,但更精简。每个接口文件以.hal结尾,里面定义方法和数据结构。

核心语法结构:

  • 接口定义:interface IMyDevice { ... }
  • 方法声明:returnType methodName(type param1, type param2);
  • 回调机制:oneway关键字用于异步调用

数据类型这块,我踩过不少坑。HIDL有自己的类型系统,不能直接用C++的int、float。

HIDL类型 对应C++类型 说明
int8_t int8_t 有符号8位整数
uint32_t uint32_t 无符号32位整数
float float 单精度浮点
string std::string UTF-8字符串
vec<T> std::vector<T> 动态数组
hidl_array<T, N> std::array<T, N> 固定长度数组

注意:HIDL不支持指针类型。所有数据必须通过值传递或引用传递。我见过有人试图传void*,编译直接报错。

来看一个完整的接口定义示例:

// IMySensor.hal
package vendor.example.sensor@1.0;

interface IMySensor {
    // 获取传感器数据
    getData() generates (vec<float> values);

    // 设置采样率(oneway表示异步调用)
    oneway setSamplingRate(uint32_t rateHz);

    // 回调接口
    notifyEvent(int32_t eventId, const hidl_string& message);
};

2.2 服务端与客户端接口定义

HIDL把接口分成了两端:服务端(实现方)和客户端(调用方)。这个设计理念很清晰——谁提供能力,谁使用能力。

服务端职责:

  • 实现接口中声明的所有方法
  • 注册到hwservicemanager,让客户端能找到你
  • 处理跨进程调用的生命周期

客户端职责:

  • 通过服务名获取服务端代理
  • 调用接口方法,传递参数
  • 处理返回结果或回调

我的经验:写客户端代码时,一定要做空指针检查。服务端可能没注册成功,或者进程挂了。我曾经因为没检查返回值,线上设备直接空指针崩溃。

服务端注册代码示例:

// 服务端注册
IMySensor* service = new MySensorImpl();
status_t status = service->registerAsService("default");
if (status != OK) {
    ALOGE("Failed to register IMySensor service");
    return -1;
}

客户端获取服务代码示例:

// 客户端获取服务
sp<IMySensor> sensor = IMySensor::getService("default");
if (sensor == nullptr) {
    ALOGE("Failed to get IMySensor service");
    return -1;
}

// 调用方法
vec<float> data;
sensor->getData([&](vec<float> result) {
    data = result;
});

2.3 包名与版本管理

包名不是随便起的。Android有严格的命名规范:

package vendor.公司名.产品名@版本号;

举个例子:vendor.google.sensors@1.0

版本号管理是HIDL里最容易出问题的地方。我来说说规则:

  • 主版本号:接口不兼容时递增(比如删了方法、改了参数类型)
  • 次版本号:向后兼容时递增(比如新增方法、新增枚举值)
  • 版本演进:@1.0 → @1.1 → @2.0

关键原则:客户端可以调用低版本服务端,但不能调用高版本。服务端必须兼容所有低版本客户端。

版本目录结构长这样:

vendor/example/sensor/
├── 1.0/
│   ├── IMySensor.hal
│   ├── types.hal
│   └── Android.bp
└── 1.1/
    ├── IMySensor.hal
    ├── types.hal
    └── Android.bp

2.4 @1.0与@1.1版本演进实战

假设我们有一个@1.0的接口,现在要升级到@1.1。怎么做?

@1.0原始接口:

// vendor/example/sensor/1.0/IMySensor.hal
package vendor.example.sensor@1.0;

interface IMySensor {
    getTemperature() generates (float temp);
    getHumidity() generates (float humidity);
};

@1.1扩展接口:

// vendor/example/sensor/1.1/IMySensor.hal
package vendor.example.sensor@1.1;

import vendor.example.sensor@1.0::IMySensor;

interface IMySensor extends @1.0::IMySensor {
    // 新增方法
    getPressure() generates (float pressure);

    // 新增枚举
    enum SensorStatus : int32_t {
        OK,
        ERROR,
        CALIBRATING
    };

    // 新增回调
    oneway onStatusChanged(SensorStatus status);
};

我曾经踩过的坑:在@1.1里修改了@1.0已有的方法签名。比如把getTemperature()的返回值从float改成double。结果所有@1.0的客户端全部编译失败。记住——版本升级只能加,不能改。

版本演进的核心流程,我用一张图来说明:

HIDL版本演进流程 @1.0 基础接口 getTemperature() @1.1 扩展接口 extends @1.0 + getPressure() @2.0 不兼容升级 全新接口定义 客户端兼容规则 @1.0客户端 → 可调用@1.0服务端 | @1.1客户端 → 可调用@1.0和@1.1服务端 服务端实现规则 @1.1服务端必须同时实现@1.0和@1.1的方法 | @1.0服务端只需实现@1.0方法 版本目录结构 vendor/example/sensor/1.0/IMySensor.hal → vendor/example/sensor/1.1/IMySensor.hal

版本管理还有个细节——types.hal。公共数据类型要放在types.hal里,不同版本可以共享或扩展。

// 1.0/types.hal
package vendor.example.sensor@1.0;

struct SensorData {
    float temperature;
    float humidity;
    int64_t timestamp;
};

// 1.1/types.hal
package vendor.example.sensor@1.1;

import vendor.example.sensor@1.0::SensorData;

struct ExtendedSensorData {
    SensorData base;
    float pressure;
    SensorStatus status;
};

我的建议:写HIDL接口时,一开始就把版本号规划好。哪怕只做@1.0,也要预留扩展点。比如枚举值留几个reserved,结构体里加个reserved字段。这样后面升级@1.1时,改动最小。

最后说一句——HIDL的版本管理不是摆设。Android系统升级时,vendor分区可能还是旧版本,system分区已经更新了。如果版本不兼容,设备直接变砖。嗯,这个坑我替你们踩过了。