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 模块的加载框架。
避坑指南:我曾经把 LOCAL_MODULE_RELATIVE_PATH 写成了 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 方法获取设备接口。

核心要点:HAL_MODULE_INFO_SYM 是模块的身份证,必须定义且正确填充。tag 字段必须是 HARDWARE_MODULE_TAG,否则加载时会校验失败。

4.6 构建流程全景图

说了这么多,你可能有点晕。我画了一张流程图,把整个构建过程串起来:

HAL 模块构建与加载流程 C++ 源码文件 Android.mk / Android.bp Soong / Make 编译 temperature.default.so /vendor/lib/hw/ hw_get_module() 加载 关键说明 模块名必须与 id 字段一致 relative_install_path 写 hw vendor: true 不能少

从源码到最终被 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。