WebUSB在Chromium里的实现
WebUSB API 是一套W3C规范 https:// wicg.github.io/webusb ,目前还处于草案阶段,但在大部分桌面浏览器中都已实现。
WebUSB API 接口提供了从网页查找和连接 USB 设备的属性和方法,类似在Web端操作硬件的API 还有如 WebSerial API,我们研究 WebUSB API 在 Chromium中的实现,可以顺带理解Chromium的架构,以及我们如何扩展浏览器功能。
JS API 示例
按照MDN上的示例 https:// developer.mozilla.org/e n-US/docs/Web/API/WebUSB_API ,我们打开 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\
http://
v8_usb.cc
,这些自动生成的源码加入到构建中,实际也是在 RenderFrameImpl::DidClearWindowObject() 的调用路径上。
那么 USB.requestDevice 的具体实现在哪里?IDL只是定义了接口,肯定是要另外编码实现的,我们猜测是 http:// 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)将 http:// usb.cc (具体功能实现)和 http:// 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\ http:// 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);