RFC-0041:支持统一服务和设备

RFC-0041:支持统一服务和设备
状态已接受
区域
  • FIDL
说明

引入服务的概念:一组协议,其中可能包含一个或多个集合实例。

作者
提交日期(年-月-日)2019-04-08
审核日期(年-月-日)2019-04-23

摘要

引入服务的概念:一组协议,其中可能包含一个或多个集合实例。

设计初衷

目前,在组件框架中,服务被定义为单个协议,并且 /svc 下的进程命名空间中只能存在该协议的一个实例。这会导致我们无法描述更复杂的关系:

  • 以两种不同的形式表示的服务,具体取决于使用方。例如,当协议有两个不同的版本(例如 FontProviderFontProviderV2)时
  • 一项服务分为两部分,以便根据访问权限级别(例如常规访问权限与管理员访问权限,如 DirectoryDirectoryAdmin,其中后者提供特权访问权限)授予功能
  • 由许多不同协议组成的服务,供不同的使用方使用,例如,Power 用于功耗管理,Ethernet 用于网络堆栈
  • 具有多个实例的服务,例如提供 AudioRenderer 的多个音频设备,或公开 Printer 的多台打印机

这种灵活性有助于更清晰地表达服务,而无需使用服务中心等权宜解决方法。有了这种灵活性,我们可以将设备定义为服务。具体而言,我们计划将 /svc/$Protocol(即“每个进程命名空间仅使用一种协议”)演变为:

/svc/$Service/$Instance/$Member

而是引入了两个额外的间接引用:服务(例如打印机、以太网)和实例(例如默认、deskjet_by_desk、e80::d189:3247:5fb6:5808)。协议的路径将由以下部分组成:

  • $Service - 服务的完全限定类型,如 FIDL 中所声明
  • $Instance - 服务实例的名称,其中“default”是惯例用语,表示可用的首选(或唯一)实例
  • $Member - 服务成员名称,如 FIDL 中所声明,其中该成员的声明类型表示预期协议

设计

服务变种

我们先来考虑一下我们要支持的各种服务:

  • 单个唯一的协议:一个实例,一个协议:

    /svc/fuchsia.Scheduler/default/profile_provider
    
  • 多个协议的复合:一个实例,多个协议:

    /svc/fuchsia.Time/default/network
                          .../rough
    
  • 使用单一协议的服务的多个实例:多个实例,一种协议:

    /svc/fuchsia.hardware.Block/0/device
                            .../1/device
    
  • 多个实例,采用不同的协议组合:许多实例,许多协议:

    /svc/fuchsia.Wlan/ff:ee:dd:cc:bb:aa/device
                                    .../power
                  .../00:11:22:33:44:55/access_point
                                    .../power
    

语言

为了向 FIDL 引入服务的概念并支持各种变种,我们将对 FIDL 语言进行以下更改:

  1. 添加 service 关键字。
  2. 移除 Discoverable 属性。

借助 service 关键字,我们可以编写服务声明,并将一组协议定义为服务的成员。例如,我们可以声明不同类型的服务,如下所示:

  • 单个唯一的协议:一个实例,一个协议:

    service Scheduler {
      fuchsia.scheduler.ProfileProvider profile_provider;
    };
    
  • 多个协议的复合:一个实例,多个协议:

    service Time {
      fuchsia.time.Provider network;
      fuchsia.time.Provider rough;
    };
    
  • 使用单一协议的服务的多个实例:多个实例,一种协议:

    service Block {
      fuchsia.hardware.block.Device device;
    };
    
  • 多个实例,具有不同的协议组:许多实例,许多协议

    service Wlan {
      fuchsia.hardware.ethernet.Device device;
      fuchsia.wlan.AccessPoint access_point;
      fuchsia.hardware.Power power;
    };
    

服务声明可以有多个使用相同协议的成员,但每个成员声明都必须使用不同的标识符。请参阅上文中的“由多个协议组合而成”。

如果某个服务的实例可能包含与另一个实例不同的一组协议,则服务声明会声明任何实例中都可能存在的所有可能协议。请参阅上文中的“使用不同协议集的多个实例”。

服务声明不会提及服务的具体实例的名称或提供服务的组件的 URI,这将交由组件框架根据组件清单声明和运行时 API 的使用来决定。

语言绑定

我们将修改语言绑定,以便更方便地连接到服务。具体而言,它们将变得更加注重服务,例如:

  • 连接到服务的“默认”实例,使用单个协议:一个实例,一个协议:

    • C++:
    Scheduler scheduler = Scheduler::Open();
    ProfileProviderPtr profile_provider;
    scheduler.profile_provider().Connect(profile_provider.NewRequest());
    
    • Rust:
    let scheduler = open_service::<Scheduler>();
    let profile_provider: ProfileProviderProxy = scheduler.profile_provider();
    
  • 连接到服务的“默认”实例,使用多种协议:一个实例,多种协议:

    • C++:
    Time time = Time::Open();
    ProviderPtr network;
    time.network().Connect(&network);
    ProviderPtr rough;
    time.rough().Connect(&rough);
    
    • Rust:
    let time = open_service::<Time>();
    let network = time.network();
    let rough = time.rough();
    
  • 使用单一协议连接到服务的多个实例:多个实例,一种协议:

    • C++:
    Block block_0 = Block::OpenInstance("0");
    DevicePtr device_0;
    block_0.device().Connect(&device_0);
    
    Block block_1 = Block::OpenInstance("1");
    DevicePtr device_1;
    block_1.device().Connect(&device_1);
    
    • Rust:
    let block_0 = open_service_instance::<Block>("0");
    let device_0 = block_0.device();
    let block_1 = open_service_instance::<Block>("1");
    let device_1 = block_1.device();
    
  • 使用多种协议连接到服务的多个实例:多个实例,多个协议:

    • C++:
    Wlan wlan_a = Wlan::OpenInstance("ff:ee:dd:cc:bb:aa");
    DevicePtr device;
    wlan_a.device().Connect(&device);
    Power power_a;
    wlan_a.power().Connect(&power_a);
    
    Wlan wlan_b = Wlan::OpenInstance("00:11:22:33:44:55");
    AccessPoint access_point;
    wlan_b.access_point().Connect(&access_point);
    Power power_b;
    wlan_b.power().Connect(&power_b);
    
    • Rust:
    let wlan_a = open_service_instance::<Wlan>("ff:ee:dd:cc:bb:aa");
    let device = wlan_a.device();
    let power_a = wlan_a.power();
    
    let wlan_b = open_service_instance::<Wlan>("00:11:22:33:44:55");
    let access_point = wlan_b.access_point();
    let power_b = wlan_b.power();
    

以下示例展示了建议的函数签名。

请注意,Open()OpenInstance() 方法还接受用于指定命名空间的可选参数。默认情况下,系统会使用进程的全局命名空间(可使用 fdio_ns_get_installed 检索)。

// Generated code.
namespace my_library {
class MyService final {
public:
  // Opens the "default" instance of the service.
  //
  // |ns| the namespace within which to open the service or nullptr to use
  // the process's "global" namespace as defined by |fdio_ns_get_installed()|.
  static MyService Open(fdio_ns_t* ns = nullptr) {
    return OpenInstance(fidl::kDefaultInstanceName, ns);
  }

  // Opens the specified instance of the service.
  //
  // |name| the name of the instance, must not be nullptr
  // |ns| the namespace within which to open the service or nullptr to use
  // the process's "global" namespace as defined by |fdio_ns_get_installed()|.
  static MyService OpenInstance(const std::string& instance_name,
                                fdio_ns_t* ns = nullptr);

  // Opens the instance of the service located within the specified directory.
  static MyService OpenAt(zxio_t* directory);
  static MyService OpenAt(fuchsia::io::DirectoryPtr directory);

  // Opens a directory of available service instances.
  //
  // |ns| the namespace within which to open the service or nullptr to use
  // the process's "global" namespace as defined by |fdio_ns_get_installed()|.
  static fidl::ServiceDirectory<MyService> OpenDirectory(fdio_ns_t* ns = nullptr) {
    return fidl::ServiceDirectory<MyService>::Open(ns);
  }

  // Gets a connector for service member "foo".
  fidl::ServiceConnector<MyService, MyProtocol> foo() const;

  // Gets a connector for service member "bar".
  fidl::ServiceConnector<MyService, MyProtocol> bar() const;

  /* more stuff like constructors, destructors, etc... */
}

以及绑定代码:

/// FIDL bindings code.
namespace fidl {
constexpr char[] kDefaultInstanceName = "default";

// Connects to a particular protocol offered by a service.
template <typename Service, typename Protocol>
class ServiceConnector final {
public:
   zx_status_t Connect(InterfaceRequest<Protocol> request);
};

// A directory of available service instances.
template <typename Service>
class ServiceDirectory final {
public:
  // Opens a directory of available service instances.
  //
  // |ns| the namespace within which to open the service or nullptr to use
  // the process's "global" namespace as defined by |fdio_ns_get_installed()|.
  static ServiceDirectory Open(fdio_ns_t* ns = nullptr);

  // Gets the underlying directory.
  zxio_t* directory() const;

  // Gets a list of all available instances of the service.
  std::vector<std::string> ListInstances();

  // Opens an instance of the service.
  Service OpenInstance(const std::string& name);

  // Begins watching for services to be added or removed.
  //
  // Invokes the provided |callback| to report all currently available services
  // then reports incremental changes.  The callback must outlive the returned
  // |Watcher| object.
  //
  // The watch ends when the returned |Watcher| object is destroyed.
  [[nodiscard]] Watcher Watch(WatchCallback* callback,
                              async_dispatcher_t* dispatcher = nullptr);

  // Keeps watch.
  //
  // This object has RAII semantics.  The watch ends once the watcher has
  // been destroyed.
  class Watcher final {
  public:
    // Ends the watch.
    ~Watcher();
  };

  // Callback invoked when service instances are added or removed.
  class WatchCallback {
  public:
    virtual void OnInstanceAdded(std::string name) = 0;
    virtual void OnInstanceRemoved(std::string name) = 0;
    virtual void OnError(zx_status_t error) = 0;
  };
}

语言绑定将进一步扩展这些功能,提供便捷的方法来迭代服务的实例,并监控新实例的推出。

服务演变

为了改进服务,我们可以随着时间的推移向其添加新协议。为了保持源代码兼容性,请勿移除现有协议,否则源代码兼容性可能会被破坏,因为用户可能会依赖于通过语言绑定从服务生成的代码。

由于服务中的所有协议实际上都是可选的,因此它们可能会在运行时提供,也可能不会提供,并且应针对这种情况构建组件,这简化了我们在演进服务时面临的问题:

  • 您可以随时向服务添加协议成员
  • 应避免移除协议成员(以实现源代码兼容性)
  • 重命名协议成员涉及添加新的协议成员,并保留现有协议成员

为了演变服务本身,我们也制定了一组类似的限制。无法保证服务一定存在于组件的命名空间中,并且服务可以在命名空间内的多个不同位置显示,因此:

  • 您可以随时添加服务
  • 应避免移除服务(以实现源代码兼容性)
  • 重命名服务涉及复制服务并使用新名称,同时保留服务的原始副本(以实现源代码兼容性)

可能的扩展

我们希望 service 实例最终成为“第一类实例”,并允许成为消息的一部分,就像 protocol P 句柄可以作为 Prequest<P> 传递一样。例如,对于 service S,此值可能为 service_instance<S>。我们会确保尽快完成此延期,而不必等到今天。

我们会考虑(并计划)扩大成员类型,而不仅仅局限于允许使用协议的成员。例如,我们可能希望某项服务公开 VMO (handle<vmo>):

service DesignedService {
    ...
    handle<vmo>:readonly logo; // gif87a
};

实施策略

此提案应分阶段实现,以免破坏现有代码。

第 1 阶段
  1. 修改 component_manager,以便组件 v2 支持服务的新目录架构。
  2. 修改 appmgr 和 sysmgr,以便组件 v1 支持服务的新目录架构。
第 2 阶段
  1. 添加了对服务声明的支持。
  2. 修改语言绑定以生成服务。
第 3 阶段
  1. 对于具有 Discoverable 属性的所有协议,请创建适当的服务声明。> 注意:在此阶段,我们应验证服务的新旧目录架构之间是否可能存在名称冲突。
  2. 迁移所有源代码以使用服务。
第 4 阶段
  1. 从 FIDL 文件中移除所有 Discoverable 属性。
  2. 从 FIDL 和语言绑定中移除了对 Discoverable 的支持。
  3. 从 component_manager、appmgr 和 sysmgr 中移除了对旧目录架构的支持。

文档和示例

我们需要扩展 FIDL 教程,以介绍服务声明的使用以及它们如何与协议交互。然后,我们将介绍服务的不同结构:单例与多实例,以及如何使用语言绑定。

术语库

协议声明用于描述一组可能通过通道发送或接收的消息及其二进制表示法。

服务声明用于描述服务提供方作为单元提供的功能。它由服务名称和零个或多个命名成员协议组成,客户端使用这些协议与该 capability 进行交互。

同一协议可能会作为服务声明的成员出现多次,成员的名称表示协议的预期解释:

service Foo {
    fuchsia.io.File logs;
    fuchsia.io.File journal;
};

组件声明用于描述可执行软件的单元,包括组件的二进制文件的位置以及它打算使用公开提供给其他组件的功能(例如服务)。

此类信息通常会编码为软件包中的组件清单文件

// frobinator.cml
{
    "uses": [{ "service": "fuchsia.log.LogSink" }],
    "exposes": [{ "service": "fuchsia.frobinator.Frobber" }],
    "offers": [{
        "service": "fuchsia.log.LogSink",
        "from": "realm",
        "to": [ "#child" ]
    }],
    "program": { "binary": ... }
    "children": { "child": ... }
}

服务实例是指符合给定服务声明的 capability。在 Fuchsia 上,它表示为目录。其他系统可能使用不同的服务发现机制。

组件实例是具有自己专用沙盒的组件的特定实例。在运行时,它会通过在其传入命名空间中打开目录来使用其他组件提供的服务实例。反之,它会通过在传出目录中显示自己的服务实例,将这些实例公开给其他组件。组件管理器充当服务发现的代理。

  • 组件实例通常(但不总是)与进程一一对应。
  • 组件运行程序通常可以在同一进程中运行多个组件实例,每个实例都有自己的传入命名空间。

惯用方式使用服务

向后兼容性

此提案将废弃 Discoverable 属性,并最终将其从 FIDL 中移除。

线格格式没有任何变化。

如果您要引入新的数据类型或语言功能,请考虑您希望用户对 FIDL 定义进行哪些更改,以免破坏生成的代码的用户。如果您的功能对生成的语言绑定施加了任何新的源代码兼容性限制,请在此处列出这些限制。

性能

连接到服务的默认实例或先验已知的实例时,这对 IPC 性能应该没有影响。

如需连接到实例 ID 未知的其他实例,用户需要先列出服务的目录并找到实例,然后才能连接。

对 build 和二进制文件大小的影响微乎其微,因为服务定义必须由后端针对特定语言绑定生成。

安全

通过此提案,我们可以将服务拆分为具有不同访问权限的单独协议,从而实现更精细的访问权限控制。

此提案对安全性没有其他影响。

测试

编译器中的单元测试,以及对兼容性测试套件的更改,用于检查服务中包含的协议是否可连接。

缺点、替代方案和未知情况

本文将探讨以下问题:

问题 1:为什么服务声明属于 FIDL?

响应

  • 我们使用 FIDL 来描述 Fuchsia 的系统 API,包括组件交换的协议。
  • 相同的协议可以通过多种方式使用,具体取决于具体情况。将这些协议的各种用途表示为服务,有助于开发者更轻松地根据具体情况访问正确的一组协议。
  • FIDL 已提供可轻松扩展的语言绑定,以便为开发者提供一致且便捷的方式来访问这些服务。

讨论

  • [ianloic] 但组件清单呢?为什么不使用 FIDL 来描述这些内容?
  • [jeffbrown] 组件清单介绍的概念远远超出了 IPC 问题
  • [abdulla] 在组件清单中描述服务会导致这些服务的说明重复
  • [ianloic] 我们能否根据清单生成组件的框架?
  • [drees] 在 FIDL 中放置服务声明会强制采用特定结构,这在其他平台上是否有意义?
  • [jeffbrown] 我们希望将服务声明设为组件外部,因为它们需要在组件之间共享,这是服务交换的协定点
  • [ianloic] overnet 的服务声明可能类似
  • [pascallouis] 根据我们目前知道的需要,从简单开始是否合适?我们日后可以根据需要进行调整。
  • [pascallouis] FIDL 首先是 Fuchsia,因此,根据我们目前掌握的信息,引入仅在该上下文中才有意义的功能是合理的,但这些功能最终可能会推广到其他上下文
  • [dustingreen] 单独的文件怎么样?
  • [pascallouis] 这些文件非常小且孤独,如果我们将其保留在 FIDL 中,则可以进行静态类型检查,如果需要,稍后再移动这些文件似乎风险较低

问题 2:协议、服务和组件之间有什么区别?

响应

  • 协议声明用于描述可能通过通道发送或接收的一组消息及其二进制表示法。
  • 服务声明用于描述服务提供方作为单元提供的功能。它由服务名称和零个或多个命名成员协议组成,客户端使用这些协议与该 capability 进行交互。
    • 同一协议可以作为服务声明的成员出现多次;成员的名称表示协议的预期解释。
      • 例如 service Foo { fuchsia.io.File logs; fuchsia.io.File journal; };
  • 组件声明用于描述可执行软件的单元,包括组件的二进制文件的位置以及它打算使用公开提供给其他组件的功能(例如服务)。

    • 此类信息通常会编码为软件包中的组件清单文件。示例:

      // frobinator.cml
      {
          "uses": [{ "service": "fuchsia.log.LogSink" }],
          "exposes": [{ "service": "fuchsia.frobinator.Frobber" }],
          "offers": [{ "service": "fuchsia.log.LogSink",
                       "from": "realm", "to": [ "#child" ]}],
          "program": { "binary": ... }
          "children": { "child": ... }
      }
      
  • 服务实例是指符合给定服务声明的 capability。在 Fuchsia 上,它表示为目录。其他系统可能使用不同的服务发现机制。

  • 组件实例是具有自己专用沙盒的组件的特定实例。在运行时,它会通过在其传入命名空间中打开目录来使用其他组件提供的服务实例。反之,它会通过在传出目录中显示自己的服务实例,将其公开给其他组件。组件管理器充当服务发现的代理。

    • 组件实例通常(但不总是)与进程一一对应。
    • 组件运行程序通常可以在同一进程中运行多个组件实例,每个实例都有自己的传入命名空间。

讨论

  • [ianloic] 我们应针对选择协议组合与服务声明提供哪些指导?
  • [abdulla] 协议组成表示协议本身高度相关,而服务表示正在共同提供一组功能(可能不相关)
  • [pascallouis] 通过单个通道组合多路复用协议,因此对消息排序有影响,而服务的各个协议具有不同的通道
  • [jeffbrown] 可以在不同位置委托,不相关,组合无法为您提供此功能,服务允许在运行时进行“发现”,例如列出可用的协议

问题 3:为服务实例提出的扁平拓扑是否足够表达性?

响应

  • 扁平拓扑结构易于使用,因为无需递归遍历路径即可找到所有实例。这会影响易用性和性能。
  • 如果相关信息编码在实例名称中,例如 /svc/fuchsia.Ethernet/rack.5,port.9/packet_receiver,则扁平拓扑的表达能力与分层拓扑一样强。
  • 您可以使用 Open()Open(namespace)OpenAt(directory) 从不同位置访问服务。换句话说,并非所有服务都需要来自进程全局命名空间中的 `/svc"。这样,您就可以根据需要创建任意服务拓扑。

问题 4:我们应该如何随着时间的推移扩展服务?

响应

  • 我们可以向现有服务声明添加新成员。添加新成员不会破坏源代码或二进制兼容性,因为每个成员实际上都是可选的(尝试连接到该协议是一项可能会失败的操作)。
  • 我们可以从服务声明中移除现有成员。移除(或重命名)现有成员可能会破坏源代码和二进制文件兼容性,并且可能需要制定周密的迁移计划来减少不利影响。
  • 服务文档应明确说明服务的预期使用或实现方式,尤其是在这种使用方式不明显的情况下,例如说明服务的哪些功能已废弃并计划移除。
  • 预期的版本控制模式:随着协议的演变,向服务添加新成员。通过协议枚举(列出目录),客户端可以发现支持的内容。示例:

    • 在版本 1 中...

      service Fonts {
          FontProvider provider;
      };
      
      protocol FontProvider {
          GimmeDaFont(string font_name) -> (fuchsia.mem.Buffer ttf);
      };
      
    • 在版本 2 中,增量更新...

      service Fonts {
          FontProvider provider;
          FontProvider2 provider2;
      };
      
      protocol FontProvider2 {
          compose FontProvider;
          GetDefaultFontByFamily(string family) -> (string family);
      };
      
    • 在版本 3 中,我们对...

      service Fonts {
          [Deprecated]
          FontProvider provider;
          [Deprecated]
          FontProvider provider2;
          TypefaceChooser typeface_chooser;
      }
      
      protocol TypefaceChooser {
          GetTypeface(TypefaceCriteria criteria);
      };
      
      table TypefaceCriteria {
          1: Family family;
          2: Style style;
          3: int weight;
      };
      

问题 5:如果组件实例希望公开与单个底层逻辑资源相关的多个服务,该如何表达?

响应

  • 组件会定义通过其组件清单公开的多个服务。示例:

    // frobinator.cml
    {
        ...
        "exposes": [
            { "service": "fuchsia.frobinator.Fooer" },
            { "service": "fuchsia.frobinator.Barer" },
        ],
        ...
    }
    
  • 然后,该组件会在单个底层资源之上实现这些服务,但这些服务的用户无需了解这一事实。