3. HIDL接口定义语言:.hal文件语法、数据类型详解、包名与版本管理

好,我们正式进入HIDL的核心——接口定义语言。说白了,.hal文件就是HIDL世界的“头文件”加“协议描述”。你写好了它,C++和Java两端才能愉快地通信。我刚开始接触HIDL时,觉得这玩意儿不就是个IDL吗?跟AIDL能有多大区别?结果一上手就踩了坑——包名写错、版本号对不上、数据类型用混了,编译直接报错。嗯,今天我们就把这些坑一个个填平。

3.1 .hal文件的基本骨架

一个标准的.hal文件,长什么样?我给你看个最简例子:

package android.hardware.light@2.0;

interface ILight {
    getLights() generates (vec<LightState> types);
    setLight(LightState type, LightState state) generates (Status status);
};

看到没?就三部分:包声明、接口声明、方法声明。但这里面的门道可不少。

核心原则:.hal文件只描述接口契约,不包含实现。你写的每一个方法签名,都是驱动层和框架层之间的“握手协议”。

3.2 包名与版本管理——最容易翻车的地方

包名的格式是固定的:

package <公司域名倒写>.<硬件模块名>@<主版本号>.<次版本号>;

举个例子:package vendor.example.led@1.0;

这里有几个硬性规则,我当年可是吃过亏的:

  • 域名倒写必须全小写——别问我为什么,Google就是这么规定的。你写成Vendor.Example,编译直接挂。
  • 模块名用单数——light不是lights,camera不是cameras。我见过有人写sensors,结果生成代码时命名空间全乱了。
  • 版本号用@分隔——主版本号不兼容时递增,次版本号向后兼容时递增。这个跟语义化版本号一个道理。

我曾经踩过的坑:有一次我把包名写成了android.hardware.light@2.0,但实际HAL实现里用的是@2.1的接口。结果服务死活注册不上,日志里就一句“interface not found”。查了两天才发现是版本号不匹配。记住:客户端和服务端的包名+版本号必须完全一致,一个字符都不能差。

3.3 数据类型详解——HIDL的“类型系统”

HIDL的数据类型,我把它分成三类:基础类型、容器类型、自定义类型。咱们一个一个看。

3.3.1 基础类型

HIDL类型 C++对应类型 Java对应类型 说明
int8_t int8_t byte 有符号8位
uint8_t uint8_t short 无符号8位
int32_t int32_t int 最常用
uint64_t uint64_t long 大数场景
float float float 单精度
double double double 双精度
bool bool boolean 布尔值
string hidl_string String UTF-8编码

这里有个细节:string在HIDL里是UTF-8编码的,不是UTF-16。我见过有人从Java层传了个中文String过来,C++端用std::string接,结果乱码了。为什么?因为Java的String是UTF-16,HIDL帮你做了转换,但如果你在C++端又手动转了一次,就出问题了。

3.3.2 容器类型

HIDL提供了三种容器:

  • vec<T>——动态数组。对应C++的hidl_vec<T>,Java的ArrayList<T>。
  • hidl_array<T, N>——固定大小数组。这个在C++端是模板类,Java端直接生成数组。
  • Memory——共享内存的抽象。用于传递大块数据,比如相机帧。

我的建议:能用vec就别用固定数组。为什么?因为HIDL的固定数组在跨进程传递时,大小是硬编码的。你想想看,如果以后要支持更多数量的传感器,改数组大小就得改接口版本号,多麻烦。用vec就灵活多了。

3.3.3 自定义类型——enum、struct、union

这才是HIDL的精髓。你可以定义自己的数据类型:

enum Status : int32_t {
    SUCCESS = 0,
    ERROR_GENERIC = -1,
    ERROR_NOT_SUPPORTED = -2,
};

struct LightState {
    uint8_t brightness;
    uint32_t color;
    string name;
};

union Data {
    int32_t intVal;
    float floatVal;
};

注意几个要点:

  • enum必须指定底层类型——默认是int32_t,但你可以改成uint8_t节省空间。
  • struct里不能有指针——HIDL是跨进程的,指针传不过去。用vec或string代替。
  • union不常用——因为跨进程传递时,接收方不知道当前是哪个成员有效。我一般用struct加一个discriminator字段来模拟。

3.4 方法声明——输入、输出与回调

HIDL的方法声明有三种形式:

形式 语法 说明
同步调用 methodName(T arg) generates (R result); 最常用,客户端阻塞等待
单向调用 oneway methodName(T arg); 不等待返回,适合通知类场景
回调模式 methodName(T arg) generates (R result) callback; 服务端主动回调客户端

我个人习惯:能用同步调用就别用oneway。为什么?因为oneway没有返回值,你没法知道服务端到底处理了没有。调试的时候特别痛苦。我有个项目,用oneway发了一个关灯指令,结果灯没关,查了半天发现是服务端进程挂了,但客户端完全不知道。

注意:oneway方法不能有generates子句。你想啊,都不等返回了,还生成什么结果?这个语法错误编译期就能发现,但架不住有人手滑写错。

3.5 知识结构总览

下面这张图,把HIDL接口定义的核心要素串起来了。你写.hal文件时,脑子里要有这个结构:

HIDL接口定义语言知识结构图 HIDL接口定义语言核心结构 .hal 文件 包名与版本 接口与方法 数据类型 域名倒写 模块名@版本 同步调用 单向调用 回调模式 基础类型 容器类型 自定义类型 关键设计原则 1. 包名+版本号唯一标识一个接口,客户端与服务端必须完全匹配 2. 数据类型选择影响跨进程传输性能,优先使用基础类型 3. 方法签名中的 generates 子句定义返回值,oneway 不能有 generates 4. 自定义类型(struct/enum)应放在独立的 .hal 文件中以便复用

3.6 实战建议——写.hal文件时的几个习惯

最后,分享几个我个人的习惯:

  1. 每个接口单独一个文件——别把所有接口塞到一个.hal里。我见过一个项目,一个文件里写了十几个接口,编译慢不说,改一个接口所有人都得重新生成代码。
  2. 枚举值从0开始——虽然HIDL允许你指定任意值,但0通常表示“成功”或“正常”。这个习惯跟C/C++的bool兼容,也方便调试。
  3. 方法参数不超过5个——参数多了,用struct包起来。为什么?因为HIDL的IPC传输有大小限制,参数太多容易超过阈值,而且代码可读性也差。
  4. 版本号从1.0开始——别从0.x开始。Google的惯例就是1.0起跳,你非要搞个0.1,别人看了会觉得你不专业。

一个小技巧:写.hal文件时,先用hidl-gen -L check命令检查语法。这个工具会告诉你包名对不对、类型有没有拼错、版本号格式是否合法。我每次写完都会跑一遍,省得编译时再被报错打断思路。