添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

WebUSB在Chromium里的实现

WebUSB API 是一套W3C规范 wicg.github.io/webusb ,目前还处于草案阶段,但在大部分桌面浏览器中都已实现。

WebUSB API 接口提供了从网页查找和连接 USB 设备的属性和方法,类似在Web端操作硬件的API 还有如 WebSerial API,我们研究 WebUSB API 在 Chromium中的实现,可以顺带理解Chromium的架构,以及我们如何扩展浏览器功能。

JS API 示例

按照MDN上的示例 developer.mozilla.org/e ,我们打开 Chrome的 Devtool,在控制台输入

navigator.usb
  .requestDevice({ filters: [{ }] })
  .then((device) => {
    console.log(device.productName);
    console.log(device.manufacturerName);
  .catch((error) => {
    console.error(error);

回车执行后,将弹出一个设备选择框

隐私保护和安全性是浏览器API设计的重要考量点,几乎所有的涉及操作本机资源,或可能操作系统窗口的 API,都会要求用户的显式确认,就如上面确认对话框一样。

我们选择一个设备,点击连接,在控制台输出USB的信息

Logitech Wireless Headset
Logitech

完整的API大家可以查看MDN的页面和示例,我们只以上面的代码为例,研究下在Chromium里的实现

添加 JS API

首先我们注意到的是,navigator上有一个属性 usb,应该是一个对象,其又具有 requestDevice 方法。

要在 DOM上绑定对象与方法,通常比较直接的是在 content::RenderFrameImpl 里实现,比如在content::RenderFrameImpl::DidClearWindowObject() 这个调用里添加绑定代码。我们可能需要写一堆繁琐的v8绑定代码,将对象在C++世界和JS世界里相互转换,这个工作比较无趣又易出错。

实际上,在Blink[third_paty/blink] 里, 提供了 Web IDL的写法,根据IDL的定义生成 v8绑定代码,这样简单清晰。WebUSB的 blink端代码在 third_party/blink/renderer/modules/webusb,我们在这里可以看到很多 idl文件,其实都是 WebUSB 的规范定义,我们现在只看下面两个

  • third_party/blink/renderer/modules/webusb/usb.idl
[
    Exposed(DedicatedWorker WebUSBOnDedicatedWorkers,
            ServiceWorker WebUSBOnServiceWorkers,
            Window WebUSB),
    SecureContext
] interface USB : EventTarget {
    attribute EventHandler onconnect;
    attribute EventHandler ondisconnect;
    [CallWith=ScriptState, RaisesException, MeasureAs=UsbGetDevices] Promise<sequence<USBDevice>> getDevices();
    [CallWith=ScriptState, RaisesException, Exposed=Window, MeasureAs=UsbRequestDevice] Promise<sequence<USBDevice>> requestDevice(USBDeviceRequestOptions options);
};
  • third_party/blink/renderer/modules/webusb/navigator_usb.idl
[
    Exposed=Window,
    ImplementedAs=USB,
    SecureContext
] partial interface Navigator {
    [SameObject, RuntimeEnabled=WebUSB] readonly attribute USB usb;
};

从字面也能知道,在 Navigator 上定义了一个 USB 对象
这些idl文件在编译时会生成 v8绑定代码,比如usb.idl对应的生成为gen\third_party\blink\renderer\bindings\modules\v8\ v8_usb.cc ,这些自动生成的源码加入到构建中,实际也是在 RenderFrameImpl::DidClearWindowObject() 的调用路径上。

那么 USB.requestDevice 的具体实现在哪里?IDL只是定义了接口,肯定是要另外编码实现的,我们猜测是 usb.cc/usb.h , 我们看下这两个文件,果然发现 usb.h 里有如下方法定义

  // USB.idl
  ScriptPromise requestDevice(ScriptState*,
                              const USBDeviceRequestOptions*,
                              ExceptionState&);

此时,你可能会有个疑问,IDL自动生成的C++代码,应该生成的是个接口类,requestDevice 被定义成 virtual, 具体实现由继承类完成
但我们在 usb.h 里看到的 requestDevice 方法似乎不是继承实现?
哪这个 requestDevice 是怎么被调用到的呢?

实际上,生成的 v8代码并不是以C++继承的方式与 usb.cc 关联,我们看下v8_usb.cc,看看是在哪里调用USB::requestDevice , 唯一能找到 的是这一行

USB* blink_receiver = V8USB::ToWrappableUnsafe(v8_receiver);
auto&& return_value = blink_receiver->requestDevice(script_state, arg1_options, exception_state); 

显然,是通过一些辅助代码(Wrap/Unwrap)将 usb.cc (具体功能实现)和 v8_usb.cc (v8绑定)关联了起来,我们回头再看下 class USB 的定义,发现有一行

DEFINE_WRAPPERTYPEINFO();

具体细节我们目前还没有搞清楚,但暂且只需要知道这么个关系就行了,最终,JS世界的 navigator.usb.requestDevice 的调用是走到 C++世界的 USB::requestDevice() 里,那么我们就继续看 USB::requestDevice的实现

Renderer里的实现

忽略掉一些边界检查代码,我们直接看到这一行

service_->GetPermission(std::move(filters),
                          resolver->WrapCallbackInScriptScope(WTF::BindOnce(
                              &USB::OnGetPermission, WrapPersistent(this))));

这是一个典型的异步调用,在 Chromium 的 base里,大部分C++代码都是这种异步调用的,主要是为了保证浏览器的响应,几乎不允许阻塞调用,另外,Chromium代码里,几乎不用锁,信号量等同步代码,一个调用如果不是立即能完成的,就需要带上一个回调,将任务发送到另一个线程来完成,而一些不能共享的资源,通过 std::move() 转移走所有权给任务线程,有点像 rust的所有权转移。

不过这种回调方式确实看起来写起来都比较费劲,最好是Chromium能开发个语法糖编译脚本,将同步写法转为异步写法。

USB::requestDevice 运行在 Renderer进程里,根据 Chrouium多进程架构的划分,通常具体业务都是丢到Browser进程或其他专用进程完成,显然 USB::requestDevice 是把具体工作丢给了service_->GetPermission,service_完成工作后调用 USB::OnGetPermission, OnGetPermission的功能很简单,就是 Promise.resolve。

我们看下 service_ 的定义

HeapMojoRemote<mojom::blink::WebUsbService> service_;

Mojom 是Chromium里用到的 RPC方案,可以是不同进程间,也可以是不同线程间。简单点说,就用mojom文件定义接口,自动生成对应语言的 Server/Client代码, Server代码作为业务实现端,还要具体实现接口。

对于这些,我们暂且都只了解个大概就行了,当前我们主要目的是把整个的流程撸清楚,细节暂时放一边。

我们搜索下代码,找出定义 WebUsbService 的mojom文件,发现 third_party\blink\public\mojom\usb\web_usb_service.mojom。

interface WebUsbService {
  // |result| is the device that user grants permission to use.
  GetPermission(array<device.mojom.UsbDeviceFilter> device_filters)
      => (device.mojom.UsbDeviceInfo? result);
};

那我们看看具体 GetPermission 做了什么,很显然,我们要在c++代码里搜索下 GetPermission

Browser 端的实现

搜索发现broswer段的service 实现在 src\content\browser\usb\ web_usb_service_impl.cc [.h]

#include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h" // mojom 生成的接口文件
// 接口实现
void GetPermission(
      std::vector<device::mojom::UsbDeviceFilterPtr> device_filters,
      GetPermissionCallback callback) override;
void WebUsbServiceImpl::GetPermission(
    std::vector<device::mojom::UsbDeviceFilterPtr> device_filters,
    GetPermissionCallback callback) {
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (!delegate ||
      !delegate->CanRequestDevicePermission(GetBrowserContext(), origin_)) {
    std::move(callback).Run(nullptr);