编写新的 XPCOM 接口教程

高级概述

为了编写在原生代码(C++、Rust)和 JavaScript 上下文中都能工作的代码,需要一种机制来实现这一点。对于 Chrome 权限上下文,这就是 XPCOM 接口类。

此机制从一个 XPIDL 文件开始,用于定义接口的形状。在 构建系统 中,此文件会被处理,并自动生成 RustC++ 代码。

接下来,必须实现接口的方法和属性。这可以通过 JSM 模块或 C++ 接口类来完成。完成这些步骤后,必须将新文件添加到相应的 moz.build 文件中,以确保构建系统知道如何找到并处理它们。

通常,这些 XPCOM 组件会连接到 Services JavaScript 对象,以便能够方便地访问接口。例如,打开 浏览器控制台 并输入 Services. 以交互式访问这些组件。

从 C++ 中,可以使用 mozilla::components::ComponentName::Create() 通过 components.conf 中的 name 选项来访问组件。

虽然 Servicesmozilla::components 是访问组件的首选方法,但许多组件都是通过历史悠久(且有点晦涩)的 createInstance 机制访问的。如果可能,应避免使用这些机制的新用法。

let component = Cc["@mozilla.org/component-name;1"].createInstance(
  Ci.nsIComponentName
);
nsCOMPtr<nsIComponentName> component = do_CreateInstance(
  "@mozilla.org/component-name;1");

编写 XPIDL

首先确定一个名称。按照惯例,接口名前缀为 nsI(历史上是 Netscape)或 mozI,因为它们是在全局命名空间中定义的。虽然接口是全局的,但接口的实现可以在没有前缀的命名空间中定义。历史上,许多组件实现仍然使用 ns 前缀(注意 I 已被删除),但此约定不再需要。

本教程假设组件位于 path/to,名称为 ComponentName。接口名称将为 nsIComponentName,而实现将为 mozilla::ComponentName

首先,创建一个 XPIDL 文件

touch path/to/nsIComponentName.idl

并将其连接到 path/to/moz.build

XPIDL_SOURCES += [
    "nsIComponentName.idl",
]

接下来,编写初始 .idl 文件:path/to/nsIComponentName.idl

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// This is the base include which defines nsISupports. This class defines
// the QueryInterface method.
#include "nsISupports.idl"

// `scriptable` designates that this object will be used with JavaScript
// `uuid`       The example below uses a UUID with all Xs. Replace the Xs with
//              your own UUID generated with `mach gen-uuid`, `uuidgen`, or
//              https://mozilla.pettay.fi/uuidgen.html

/**
 * Make sure to document your interface.
 */
[scriptable, uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)]
interface nsIComponentName : nsISupports {

  // Fill out your definition here. This example attribute only returns a bool.

  /**
   * Make sure to document your attributes.
   */
  readonly attribute bool isAlive;
};

此定义仅包含一个属性 isAlive,它将演示我们最终是否正确完成了工作。有关此语法的更全面指南,请参阅 XPIDL 文档。

运行 ./mach build 后,XPIDL 解析器将读取此文件,如果语法错误,则会发出任何警告。然后它将为我们自动生成 C++(或 Rust)代码。对于此示例,生成的 nsIComponentName 类将位于

{obj-directory}/dist/include/nsIComponentName.h

查看此处自动生成的代码或在 SearchFox 上查看现有的 生成的 C++ 头文件 可能很有用。

编写 C++ 实现

现在我们有了接口的定义,但没有实现。接口可以使用 JSM 由 JavaScript 实现支持,但在此示例中,我们将使用 C++ 实现。

将 C++ 源文件添加到 path/to/moz.build

EXPORTS.mozilla += [
    "ComponentName.h",
]

UNIFIED_SOURCES += [
    "ComponentName.cpp",
]

现在编写头文件:path/to/ComponentName.h

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_nsComponentName_h__
#define mozilla_nsComponentName_h__

// This will pull in the header auto-generated by the .idl file:
// {obj-directory}/dist/include/nsIComponentName.h
#include "nsIComponentName.h"

// The implementation can be namespaced, while the XPCOM interface is globally namespaced.
namespace mozilla {

// Notice how the class name does not need to be prefixed, as it is defined in the
// `mozilla` namespace.
class ComponentName final : public nsIComponentName {
  // This first macro includes the necessary information to use the base nsISupports.
  // This includes the QueryInterface method.
  NS_DECL_ISUPPORTS

  // This second macro includes the declarations for the attributes. There is
  // no need to duplicate these declarations.
  //
  // In our case it includes a declaration for the isAlive attribute:
  //   GetIsAlive(bool *aIsAlive)
  NS_DECL_NSICOMPONENTNAME

 public:
  ComponentName() = default;

 private:
  // A private destructor must be declared.
  ~ComponentName() = default;
};

}  // namespace mozilla

#endif

现在编写定义:path/to/ComponentName.cpp

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ComponentName.h"

namespace mozilla {

// Use the macro to inject all of the definitions for nsISupports.
NS_IMPL_ISUPPORTS(ComponentName, nsIComponentName)

// This is the actual implementation of the `isAlive` attribute. Note that the
// method name is somewhat different than the attribute. We specified "read-only"
// in the attribute, so only a getter, not a setter was defined for us. Here
// the name was adjusted to be `GetIsAlive`.
//
// Another common detail of implementing an XPIDL interface is that the return values
// are passed as out parameters. The methods are treated as fallible, and the return
// value is an `nsresult`. See the XPIDL documentation for the full nitty gritty
// details.
//
// A common way to know the exact function signature for a method implementation is
// to copy and paste from existing examples, or inspecting the generated file
// directly: {obj-directory}/dist/include/nsIComponentName.h
NS_IMETHODIMP
ComponentName::GetIsAlive(bool* aIsAlive) {
  *aIsAlive = true;
  return NS_OK;
}

} // namespace: mozilla

注册组件

此时,组件应该已正确编写,但尚未在组件系统中注册。为此,我们需要创建或修改 components.conf

touch path/to/components.conf

现在更新 moz.build 以指向它。

XPCOM_MANIFESTS += [
    "components.conf",
]

阅读 定义 XPCOM 组件 可能很有用,但以下配置足以将我们的组件连接到 Services 对象。还应将服务添加到 tools/lint/eslint/eslint-plugin-mozilla/lib/services.json。最简单的方法是从 <objdir>/xpcom/components/services.json 中复制。

Classes = [
    {
        # This CID is the ID for component entries, and needs a separate UUID from
        # the .idl file. Replace the Xs with a uuid from `mach gen-uuid`,
        # `uuidgen`, or https://mozilla.pettay.fi/uuidgen.html
        'cid': '{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}',
        'interfaces': ['nsIComponentName'],

        # A contract ID is a human-readable identifier for an _implementation_ of
        # an XPCOM interface.
        #
        # "@mozilla.org/process/environment;1"
        #  ^^^^^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^ ^
        #  |            |       |           |
        #  |            |       |           The version number, usually just 1.
        #  |            |       Component name
        #  |            Module
        #  Domain
        #
        # This design goes back to a time when XPCOM was intended to be a generalized
        # solution for the Gecko Runtime Environment (GRE). At this point most (if
        # not all) of mozilla-central has an @mozilla domain.
        'contract_ids': ['@mozilla.org/component-name;1'],

        # This is the name of the C++ type that implements the interface.
        'type': 'mozilla::ComponentName',

        # The header file to pull in for the implementation of the interface.
        'headers': ['path/to/ComponentName.h'],

        # In order to hook up this interface to the `Services` object, we can
        # provide the "js_name" parameter. This is an ergonomic way to access
        # the component.
        'js_name': 'componentName',
    },
]

此时,完整的 moz.build 文件应如下所示

# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

XPIDL_SOURCES += [
    "nsIComponentName.idl",
]

XPCOM_MANIFESTS += [
    "components.conf",
]

EXPORTS.mozilla += [
    "ComponentName.h",
]

UNIFIED_SOURCES += [
    "ComponentName.cpp",
]

这完成了使用 C++ 的基本 XPCOM 接口的实现。可以通过 浏览器控制台 或其他 Chrome 上下文访问该组件。

console.log(Services.componentName.isAlive);
> true