4. 编写第一个HAL模块:创建模块目录、编写Android.mk与Android.bp
好,到了真正动手的时刻了。前面我们聊了HAL的设计理念、接口规范,现在要落地了。这一章,我们就从零开始,搭建一个完整的HAL模块骨架。
我个人习惯把HAL模块比作一个「三件套」:目录结构、构建脚本、接口实现。今天先搞定前两件,接口实现留到下一章细讲。
4.1 模块目录怎么搭?
Android源码里HAL模块的存放位置,其实有约定俗成的规矩。你打开hardware/目录看看,里面按厂商或芯片平台分了子目录。比如高通的是hardware/qcom/,我们自己的模块,我建议放在hardware/自己的公司名/模块名/下面。
举个例子,假设我们要做一个虚拟的温控传感器HAL,叫temperature。目录结构长这样:
hardware/
└── mycompany/
└── temperature/
├── 1.0/
│ ├── default/
│ │ ├── Android.mk
│ │ ├── Android.bp
│ │ ├── Temperature.cpp
│ │ └── Temperature.h
│ ├── Android.mk
│ └── Android.bp
├── include/
│ └── hardware/
│ └── temperature.h
└── module/
├── Android.mk
└── temperature_module.cpp
嗯,这里要注意:1.0/是HAL版本号目录,default/放具体实现,include/放对外暴露的头文件,module/放HAL模块的入口。这种分层,我在项目里用过很多次,清晰也好维护。
4.2 Android.mk 怎么写?
Android.mk 是 Android 传统构建系统使用的 Makefile 风格脚本。虽然现在 Google 主推 Android.bp,但很多老项目还在用 mk。我建议你两个都掌握,毕竟兼容性很重要。
先看一个最简的 Android.mk 示例,放在 hardware/mycompany/temperature/1.0/default/ 下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := temperature.default
LOCAL_MODULE_RELATIVE_PATH := hw
LOCAL_SRC_FILES := Temperature.cpp
LOCAL_SHARED_LIBRARIES := liblog libhardware libcutils
LOCAL_HEADER_LIBRARIES := libhardware_headers
include $(BUILD_SHARED_LIBRARY)
解释几个关键点:
- LOCAL_MODULE:模块名。命名规则是
模块名.类型,比如temperature.default表示这是 temperature 模块的 default 实现。 - LOCAL_MODULE_RELATIVE_PATH:指定安装路径为
/vendor/lib/hw/。这是 HAL 模块的标准路径,HAL 服务加载时会去这里找。 - LOCAL_SHARED_LIBRARIES:链接的共享库。libhardware 是必须的,它提供了 HAL 模块的加载框架。
hw/,结果模块被安装到了 /vendor/lib/hw/hw/ 下,HAL 服务死活找不到。记住,这里只写 hw,不要加多余的路径。
4.3 Android.bp 怎么写?
Android.bp 是 Soong 构建系统使用的 Blueprint 格式,语法更简洁,也是 Google 推荐的新标准。同一个模块,用 bp 写出来是这样的:
cc_library_shared {
name: "temperature.default",
relative_install_path: "hw",
srcs: ["Temperature.cpp"],
shared_libs: [
"liblog",
"libhardware",
"libcutils",
],
header_libs: ["libhardware_headers"],
vendor: true,
}
你看,bp 用 JSON 风格的键值对,比 mk 的变量赋值更直观。这里有个关键字段 vendor: true,表示这个模块是 vendor 分区的一部分,不是 system 分区。
为什么强调这个?因为 Android Treble 架构要求 HAL 模块必须放在 vendor 分区,system 分区只能放框架代码。如果你忘了写 vendor: true,模块会被编译到 system 分区,运行时可能因为 SELinux 策略被拒绝访问。
4.4 mk 和 bp 的对比
我整理了一个对比表格,方便你快速理解两者的差异:
| 对比项 | Android.mk | Android.bp |
|---|---|---|
| 语法风格 | Makefile 变量赋值 | JSON 键值对 |
| 条件判断 | 支持 ifeq/ifneq | 不支持(需用 Go 脚本) |
| 模块依赖 | 隐式依赖,按顺序解析 | 显式依赖,自动拓扑排序 |
| 编译速度 | 较慢,每次全量解析 | 较快,增量编译优化好 |
| Google 推荐度 | 逐渐淘汰 | 新项目首选 |
说白了,新项目我建议直接用 bp,但老项目维护时 mk 也得会看。两个都掌握,面试时也能多聊几句。
4.5 模块入口怎么写?
HAL 模块需要一个入口函数,让 HAL 服务能加载它。这个函数叫 HAL_MODULE_INFO_SYM,是一个结构体变量。放在 module/temperature_module.cpp 里:
#include <hardware/hardware.h>
#include <hardware/temperature.h>
static int temperature_open(const struct hw_module_t* module,
const char* id,
struct hw_device_t** device) {
// 这里先留空,下一章实现
return 0;
}
static struct hw_module_methods_t temperature_module_methods = {
.open = temperature_open,
};
struct hw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.module_api_version = TEMPERATURE_MODULE_API_VERSION_1_0,
.hal_api_version = HARDWARE_HAL_API_VERSION,
.id = TEMPERATURE_HARDWARE_MODULE_ID,
.name = "Temperature HAL Module",
.author = "My Company",
.methods = &temperature_module_methods,
};
这里 HAL_MODULE_INFO_SYM 是固定宏,展开后就是 HMI。HAL 服务通过 hw_get_module() 找到这个符号,然后调用 open 方法获取设备接口。
4.6 构建流程全景图
说了这么多,你可能有点晕。我画了一张流程图,把整个构建过程串起来:
从源码到最终被 HAL 服务加载,一共六步。每一步都有坑,但只要你按照上面的模板来,基本不会出大问题。
4.7 编译验证
写完了脚本,怎么验证对不对?我一般用这几个命令:
# 编译指定模块
mmm hardware/mycompany/temperature/1.0/default/
# 或者用 Soong 编译
m temperature.default
# 查看编译产物
ls out/target/product/你的设备/vendor/lib/hw/temperature.default.so
如果编译通过,你会看到 temperature.default.so 生成。如果报错,八成是路径写错了,或者依赖的库没找到。别慌,看编译日志,定位到具体行号,一般都能解决。
showcommands 参数,可以看到完整的编译命令,方便排查链接问题。比如 m temperature.default showcommands。