不会Binder? 那就掌握 HIDL

4,051 阅读11分钟

介绍

HIDL( HAL interface definition language ),从字面上翻译,就是 HAL( hardware abstract layer )接口定义语言。在Android 8.0以上,HAL 基本上都用 HIDL 重新实现了。

大部分 HIDL 都是基于 Binder 通信的,这也称之为 Binder 式的 HIDL,然而还有一个叫做Passthrough 式的 HIDL,它不需要使用Binder通信,而是通过链接库的方式进行通信。本文只探讨 Binder 式的 HIDL,至于 Passthrough 式的 HIDL ,可以参考官方文档

我们不能狭隘地理解 HIDL 就是为 HAL 而生的,从更通用的意义上讲,它是为进程间的通信而生。我举个例子,在Android 8.0之前的版本,如果我们写了一个纯 native binder service,我们会发现,除了实现自定义的接口外,其它的都是样板代码,我当时就在想为何 Java 层有个 AIDL 可以帮我们自动生成样板代码,而 native 层却要自己手写呢?然而,HIDL 解决了这个问题。

HIDL除了解决了native样板代码的问题外,其实还解决了一个问题。如果我们想把这个native binder service在上层建立对应的接口,这个过程其实也有点小复杂的,其中有很多细节需要处理好,例如 Java 层的类型如何与 native 层建立对应关系。而现在有了 HIDL,从上层建立到 native 层的接口,这是一件轻而易举的事情。

说了这么多,你是否有种跃跃欲试的感觉,让我们通过一个例子来体会 HIDL 带来的快感。

如果想真正理解这篇文章,需要你对C++和Binder构架有一定的基础。

例子

本文使用的的例子,是使用 Binder 式的 HIDL,因此可能会与你在网上看到的文章大有不同。

创建helloworld项目

首先在 vendor 目录下,创建一个用于存放 hidl 项目的目录,命令如下

mkdir -p vendor/awesome/hardware/interfaces

awesome 一般是用公司名代替,harware/interfaces 可以清楚的表明这个目录下保存的是 hidl 项目。

现在让我们在这个目录下,建立一个 hidl 项目,就起名叫 helloworld,并创建它需要的目录

mkdir -p vendor/awesome/hardware/interfaces/helloworld/1.0

注意,这里创建了两层子目录,即 hello/1.0,注意这个 1.0 是代表这个 hidl 项目的版本号,是不应该被省略的。因为我们以后可以会建立1.1,2.0等等的版本,这样我们就可以通过版本号目录来区分。

现在需要在这个 1.0 目录下,创建一个 hidl 接口,这个接口是以 .hal 结尾的,它使用的是 hidl 语法,我把它命名为 IHello.hal,它的代码如下

package vendor.awesome.helloworld@1.0;

interface IHello
{
    // generates (string result) 表示返回的结果保存在 string 类型的 result 对象中
    hello(string name) generates (string result);
};

hidl 语法借鉴了 Java 和 C++ 的语法,因此这里可以看到很熟悉的语法,如何想学习更多的语法,请参考 官方文档

根目录映射

现在我们来理解 HIDL 中一个叫根目录映射的概念,它很重要。刚才创建的 IHello.hal 文件包名为 vendor.awesome.helloworld@1.0,这个包名可不是随便写的。HIDL 规定要可以通过包名找到项目下的 .hal 文件,在这里也就是 IHello.hal。

那么我们怎么把包名vendor.awesome.helloworld@1.0 和项目路径 vendor/awesome/hardware/interfaces/helloworld/1.0 建立映射关系呢?通过对比,我们可以发现,只要把vendor.awesomevendor/awesome/hardware/interfaces/ 建立映射关系即可。

那么怎么做到呢?我们需要在 vendor/awesome/hardware/interfaces/ 目录下创建一个 Android.bp 文件,在这个文件中创建这个映射关系。内容如下

hidl_package_root {
    name: "vendor.awesome",
    path: "vendor/awesome/hardware/interfaces",
}

编译helloworld项目

现在 HIDL 接口已经定义好了,现在需要对这个 helloworld 项目进行编译。但是在编译前,我们还缺少一个 Android.mk 或 Android.bp 文件,目前 HIDL 已经不支持 Android.mk ,因此这里我们来生成一个 Android.bp 文件。

不会手写 Android.bp 文件也没有关系,系统有一个脚本可以自动为 hidl 项目生成 Android.bp 文件。脚本路径如下

hardware/interfaces/update-makefiles.sh

但是这个脚本只能为 hardware/interfaces 目录下的 hidl 项目生成 Android.bp 文件,而我们的项目在 vendor 目录下。不过没关系,我们可以在脚本中添加如下内容

do_makefiles_update \
  "vendor.awesome:vendor/awesome/hardware/interfaces" \
  "android.hidl:system/libhidl/transport"

注意,vendor.awesome:vendor/awesome/hardware/interfaces 就是我们刚才建立的根目录映射关系,不能写错了。而 android.hidl:system/libhidl/transport 是 HIDL 架构中的基本映射关系,这个应该是不能省略的。

现在让我们执行这个脚本,命令如下

./hardware/interfaces/update-makefiles.sh

成功执行这个脚本后, helloworld 项目的目录结构如下

vendor/awesome/hardware/interfaces/helloworld/
└── 1.0
    ├── Android.bp
    └── IHello.hal

现在已经成功生成了 Android.bp,让我们看下它的内容

hidl_interface {
    name: "vendor.awesome.helloworld@1.0",
    root: "vendor.awesome",
    product_specific: true,
    srcs: [
        "IHello.hal",
    ],
    interfaces: [
        "android.hidl.base@1.0",
    ],
    // 表示生成 Java  bp 接口
    gen_java: true,
}

这里的的内容非常直观,我也不过多解释了。现在万事俱备,只欠东风,让我们来编译这个项目,命令如下

mmm vendor/awesome/hardware/interfaces/helloworld/1.0/

编译成功后,会在 out 目录下生成 vendor.awesome.helloworld@1.0.so,并且还会在 out/soong/.intermediates/vendor/awesome/hardware/interfaces/helloworld/1.0/ 目录下生成与 Java 和 C++ 相关的文件

'vendor.awesome.helloworld@1.0'/
'vendor.awesome.helloworld@1.0-adapter'/
'vendor.awesome.helloworld@1.0-adapter_genc++'/
'vendor.awesome.helloworld@1.0-adapter-helper'/
'vendor.awesome.helloworld@1.0-adapter-helper_genc++'/
'vendor.awesome.helloworld@1.0-adapter-helper_genc++_headers'/
'vendor.awesome.helloworld@1.0_genc++'/
'vendor.awesome.helloworld@1.0_genc++_headers'/
'vendor.awesome.helloworld@1.0-vts.driver'/
'vendor.awesome.helloworld@1.0-vts.driver_genc++'/
'vendor.awesome.helloworld@1.0-vts.driver_genc++_headers'/
'vendor.awesome.helloworld@1.0-vts.profiler'/
'vendor.awesome.helloworld@1.0-vts.profiler_genc++'/
'vendor.awesome.helloworld@1.0-vts.profiler_genc++_headers'/
'vendor.awesome.helloworld@1.0-vts.spec'/
vendor.awesome.helloworld-V1.0-java/
vendor.awesome.helloworld-V1.0-java_gen_java/
vendor.awesome.helloworld-V1.0-java-shallow/

vendor.awesome.helloworld-V1.0-java/ 是jar包目录,如果我们在系统应用中,需要通过 Java 接口调用 HIDL,那么需要把这个 jar 包加入到 Android.mk 或 Android.bp 中,这个后面会讲到。

vendor.awesome.helloworld-V1.0-java_gen_java/ 目录会生成 Java 接口文件 IHello.java,通过这个 Java 接口文件,可以让系统应用程序调用 hidl 接口。

vendor.awesome.helloworld@1.0_genc++_headers 目录会生成 C++ 的头文件,如下

BnHwHello.h  
BpHwHello.h  
BsHello.h  
IHello.h  
IHello.h.d  
IHwHello.h

其中 IHello.h 是 C++ 客户端调用的接口,也就是服务端要实现的接口。

无论是生成的Java接口,还是C++头文件,都是基于Binder生成的样板代码。

理解HIDL项目的代码生成

我在官网上抠了一副图来理解下

代码生成

iFoo.hal 就是 HIDL 接口,对应于我们项目中的 IHello.hal。

成功编译 HIDL 项目后,它会生成一个 so 库,图中的是 android.hardware.samples.IFoo@1.0.so,在我们的例子中,它是 vendor.awesome.helloworld@1.0.so

然后 C++ 客户端和服务端,利用 iFoo.h 这个头文件进行 Binder 通信。这里的 iFoo.h 就是对应例子中的 IHello.h。

然而这个图中,没有列出关于 Java 方面的东西,不过没关系,我们在后面的例子中,使用 Java 客户端来与 C++ 服务端进行通信。

在服务端实现接口

IHello.hal 给我们定义了一个 HIDL 接口,并且生成了对应的 C++ 接口,也就是 IHello.h,因此我们先需要实现这个接口。

首先我们创建一个 default 目录,用于存入接口的实现。命令如下

mkdir -p vendor/awesome/hardware/interfaces/helloworld/1.0/defualt

不要问我为何目录名叫 default,因为源码中都是这样搞的,我只是跟随大流而已。

如果我们利用 IHello.h 一步一步去实现,当然没问题,但是这里也涉及到样板代码问题,因此太慢了。不过系统为我们提供了便利的命令,如下

hidl-gen -o vendor/awesome/hardware/interfaces/helloworld/1.0/defualt \
-L c++-impl -r vendor.awesome:vendor/awesome/hardware/interfaces/ \
-r android.hidl:system/libhidl/transport \
vendor.awesome.helloworld@1.0

-o 选项指定了生成代码的路径,-L c++-impl 表示用 c++ 来生成接口实现的样板代码,-r 选项指定了映射关系。

现在整个 helloworld 项目的目录结构如下

vendor/awesome/hardware/interfaces/helloworld/
└── 1.0
    ├── Android.bp
    ├── defualt
    │   ├── Hello.cpp
    │   └── Hello.h
    └── IHello.hal

让我来简单介绍下,Hello.h 是利用 IHello.h 生成服务端样板代码,Hello.cpp 定义了接口的实现。因此我们只需要在 Hello.cpp 中实现接口即可,hello 接口实现的代码如下

Return<void> Hello::hello(const hidl_string& name, hello_cb _hidl_cb) {
    std::string result = "hello world, ";
    result += name;
    _hidl_cb(result);
    return Void();
}

可能你会很疑惑,hello_cb 是个什么类型,它其实是一个回调,用于通知客户端这个接口返回的结果。然后你可能更疑惑,为何不用 return 直接返回?这是因为,在 HIDL 中,除了基本类型和 Void 返回类型,其它类型都需要通过回调返回。

我们可以把这个服务端的接口实现编译成一个 so 库,但是对于 Binder 式的 HIDL 来说,这个 so 库其实是不需要的,你可以跳过这一部分。但是对于 Passthrough 模式,这个库就很重要。目前网上大部分例子都会告诉你要编译这个库,所以这里就算一个答疑解惑。

通过如下命令可以生成一个 Android.bp 命令

hidl-gen -o vendor/awesome/hardware/interfaces/helloworld/1.0/defualt/ \
-L androidbp-impl -r vendor.awesome:vendor/awesome/hardware/interfaces/ \
-r android.hidl:system/libhidl/transport \
vendor.awesome.helloworld@1.0

这个命令就是在 default 目录下生成一个 Android.bp 文件,现在的目录结构如下

vendor/awesome/hardware/interfaces/helloworld/
└── 1.0
    ├── Android.bp
    ├── defualt
    │   ├── Android.bp
    │   ├── Hello.cpp
    │   └── Hello.h
    └── IHello.hal

现在用如下命令来编译

mmm vendor/awesome/hardware/interfaces/helloworld/1.0/defualt/

编译的结果如下

out/target/product/sdm660_64/vendor/lib64/hw/vendor.awesome.helloworld@1.0-impl.so

创建服务

实现服务端接口,其实就是创建了一个服务端的 Binder,现在我们要创建一个 service 来注册这个 Binder。主流的方式就是把这个 service 放到 default 目录下,因此我们在这个目录下创建 service.cpp 文件,内容如下

#define LOG_TAG "HelloService"
#include <hidl/HidlTransportSupport.h> // configureRpcThreadpool joinRpcThreadpool
#include <android/log.h> // ALOGD
#include "Hello.h"

// libhwbinder:
using ::android::hardware::configureRpcThreadpool;
using ::android::hardware::joinRpcThreadpool;

using ::vendor::awesome::helloworld::V1_0::IHello;
using ::vendor::awesome::helloworld::V1_0::implementation::Hello;

using ::android::sp;
using ::android::OK;
using ::android::status_t;
int main()
{
    sp<IHello> service = nullptr;
    status_t status;

    service = new Hello();
    if (service == nullptr)
    {
        ALOGD("Failed to create Hello service.");
        return 1;
    }

    configureRpcThreadpool(1, true /* callerWillJoin */);

    // 注册 binder 
    status = service->registerAsService();
    
    if (status != OK)
    {
        ALOGE("Failed to register Hello service.");
        return 1;
    }

    ALOGD("Hello service started successfully.");

    joinRpcThreadpool();
    return 0;
}

从代码中可以看到,注册 Binder 的方式是,创建一个 Hello 对象,然后调用它的 registerAsService() 即可,其它的代码都是 Binder 机制的代码,照着写就可以了。

Binder 机制应该是所有 Android 系统开发人员必须掌握的一项技能,无论你是 AP 还是 BSP。

现在 service 文件已经有了,我们需要把它编译成一个 bin,因此在 default 目录下的 Android.bp 文件进行配置。这次就没有命令直接生成了,但是我们可以参考其实项目,内容如下

cc_binary {
    name: "vendor.awesome.helloworld@1.0-service",
    relative_install_path: "hw",
    proprietary: true,
    srcs: [
        "service.cpp",
        "Hello.cpp",
    ],
    shared_libs: [
        "libhidlbase",
        "libhidltransport",
        "libutils",
        "liblog",
        "vendor.awesome.helloworld@1.0",
    ],
}

现在用如下命令执行编译

 mmm vendor/awesome/hardware/interfaces/helloworld/1.0/defualt

编译生成的结果如下

out/target/product/sdm660_64/vendor/bin/hw/vendor.awesome.helloworld@1.0-service

这个service是个“死”的,它不会自动运行起来,当然如果只是为了测试,你可以通过 adb shell 手动拉起来这个服务。

但是要在正式版本中可用的话,我们可以定义一个rc文件,把这个service拉起来, 我们在 default 目录下创建一个文件,名为 vendor.awesome.helloworld@1.0-service.rc,内容如下

service helloworld /vendor/bin/hw/vendor.awesome.helloworld@1.0-service
class hal
user system
group system

等会我们会把这个 rc 文件加入到 Android.bp 中,不过现在我们还要解决另外一个问题,那就是客端可能找不到 service 注册的 Binder,那是因为有个叫 VINTF 的东西限制了。现在我们在 default 目录下创建一个文件 vendor.awesome.helloworld@1.0-service.xml,内容如下

<manifest version="1.0" type="device">
    <hal format="hidl">
        <name>vendor.awesome.helloworld</name>
        <transport>hwbinder</transport>
        <version>1.0</version>
        <interface>
            <name>IHello</name>
            <instance>default</instance>
        </interface>
    </hal>
</manifest>

如果想了解更多VINTF的东西,请参考官网

现在让我们把 rc 文件和 vintf 文件加入到 Android.bp 文件中

cc_binary {
    name: "vendor.awesome.helloworld@1.0-service",
    relative_install_path: "hw",
    init_rc: ["vendor.awesome.helloworld@1.0-service.rc"],
    vintf_fragments: ["vendor.awesome.helloworld@1.0-service.xml"],
    proprietary: true,
    srcs: [
        "service.cpp",
        "Hello.cpp",
    ],
    shared_libs: [
        "libhidlbase",
        "libhidltransport",
        "libutils",
        "liblog",
        "vendor.awesome.helloworld@1.0",
    ],
}

再用上面的命令编译 service,这些文件会下如下目录生成

out/target/product/sdm660_64/vendor/etc/vintf/manifest/vendor.awesome.helloworld@1.0-service.xml
out/target/product/sdm660_64/vendor/etc/init/vendor.awesome.helloworld@1.0-service.rc

Java客户端

现在服务端已经完全实现了,那么如何在客户端调用呢,这里我们选择 Java 客户端来实现。

如果你的应用想使用这个 HIDL 接口,那么它必须是系统应用。

首先我在 Android.mk 或 Android.bp 引入 jar 包。

Android.mk 内容如下

LOCAL_STATIC_JAVA_LIBRARIES += vendor.awesome.helloworld-V1.0-java

Android.bp 内容如下

static_libs: [ "vendor.awesome.helloworld-V1.0-java", ],

然后在 Java 文件中导入接口类,并获取 Binder 服务,然后调用接口

// 导入接口类
import vendor.awesome.helloworld.V1_0.IHello;

try {
    // 获取服务
    IHello service = IHello.getService();
    if (service != null) {
        Toast.makeText(this, "Congratulations!", Toast.LENGTH_SHORT).show();
        // 调用接口
        service.hello("David");
    }
} catch (RemoteException e) {
    e.printStackTrace();
}

其它问题

如果想整个都完美跑起来,我们还需要解决其它一些问题。以下内容仅供参考。

添加编译项

HIDL 库和 service 需要加入到编译项中,在 device/qcom/sdm660_64/sdm660_64.mk 添加如下代码

PRODUCT_PACKAGES += \
       vendor.awesome.helloworld@1.0 \
       vendor.awesome.helloworld@1.0-service

VINTF compatibility

首先在编译时会遇到 VINTF compatibility 报错,我们需要增加相应的修改,在 vendor/qcom/opensource/core-utils/vendor_framework_compatibility_matrix.xml 修改如下

添加的内容如下

    <hal format="hidl" optional="true">
        <name>vendor.awesome.helloworld</name>
        <transport>hwbinder</transport>
        <version>1.0</version>
        <interface>
            <name>IHello</name>
            <instance>default</instance>
        </interface>
    </hal>

selinux

服务端进程想自动跑起来,那就涉及到了 selinux 的问题的,我们要给这个进程打个标签。 但是 selinux 的东西不是一两句话能说清楚的,因此我只提供文件路径以及修改,供大家参考。

创建一个文件 device/qcom/sepolicy/vendor/common/helloworld.te,添加如下内容

# helloworld service
type helloworld, domain;
type helloworld_exec, exec_type, file_type, vendor_file_type;

init_daemon_domain(helloworld);

这里是定义了一个 helloworld 域,并且当 init 进程拉起 service 时,把域转换为 helloworld 。

在 device/qcom/sepolicy/vendor/common/file_contexts 中修改如下

# helloworld
/vendor/bin/hw/vendor\.awesome\.helloworld@1\.0-service u:object_r:helloworld_exec:s0

这里是为service bin定义一个 security context。

感想

本文只对 HIDL 做了基本的介绍,既然 HIDL 是一个语言,那就还有很多东西要学习,建议大家以本文的例子为参考,再到官网去学习更多的东西。

从本文的例子可以看出,我们可以轻松地实现 Java 层到 native 层的 Binder 通信。但是我们不要太高兴,因为这些东西都是建立在 Binder 机制之上的,我们只有搞清楚的了 Binder 机制,我们才能以不变应万变。

参考

关于 VINTF 介绍

www.cnblogs.com/codeking100…

source.android.google.cn/devices/arc…

关于 selinux

blog.csdn.net/innost/arti…

source.android.google.cn/security/se…