Mojo C++ Bindings API

Bindings API 是使用 Mojo 的重点,在项目中会大量使用。

Mojom

Mojo 在 Binding 层中引入了 Mojom 这种 IDL 语言,用它来定义接口。接口定义文件的后缀一般为 .mojom,一个简单的接口定义如下:

// mojom/test.mojom

// 定义接口所属的"module",在生成的 C++ 代码中对应到命名空间
module demo.mojom;

// 定义一个接口,在 C++ 中对应一个抽象类
interface Test {
  // 定义一个 Hello 方法,该方法没有返回值
  Hello(string who);
  // 定义一个 Hi 方法,返回值为 string 类型
  // 返回值会对应到 C++ 中的回调方法
  Hi() => (string who);
};

定义好 mojom 文件后,需要在合适的 BUILD.gn 文件中添加一下代码,以便在编译时将 mojom 文件生成源代码文件:

# for mojo
import("//mojo/public/tools/bindings/mojom.gni")

mojom("mojom_test") {
  sources = [
    "mojom/test.mojom",
  ]
}

这样定义之后,就可以在其他地方依赖 mojom_test 这个 target 了。

生成的代码类似下面这样:

// out/Default/gen/demo/mojom/test.mojom.h

namespace demo {
namespace mojom {
class Test;
// 注意这些重命名,在71版本中大量使用,需要熟悉
// 记住 xxxPtr,xxxPtrInfo,xxxRequest
// 这些命名在最新的版本中已经被改了
using TestPtr = mojo::InterfacePtr<Test>;
using TestPtrInfo = mojo::InterfacePtrInfo<Test>;
using TestRequest = mojo::InterfaceRequest<Test>;
...
// 相应的Associated版本
using TestAssociatedPtr =
    mojo::AssociatedInterfacePtr<Test>;
using TestAssociatedPtrInfo =
    mojo::AssociatedInterfacePtrInfo<Test>;
using TestAssociatedRequest =
    mojo::AssociatedInterfaceRequest<Test>;


class TestProxy;

template <typename ImplRefTraits>
class TestStub;

class TestRequestValidator;
class TestResponseValidator;

// 这个就是生成的接口类了
class  Test
    : public TestInterfaceBase {
 public:
  static const char Name_[];
  static constexpr uint32_t Version_ = 0;
  static constexpr bool PassesAssociatedKinds_ = false;
  static constexpr bool HasSyncMethods_ = false;

  using Proxy_ = TestProxy;

  template <typename ImplRefTraits>
  using Stub_ = TestStub<ImplRefTraits>;

  using RequestValidator_ = TestRequestValidator;
  using ResponseValidator_ = TestResponseValidator;
  enum MethodMinVersions : uint32_t {
    kHelloMinVersion = 0,
    kHiMinVersion = 0,
  };
  virtual ~Test() {}

  // Hello 方法
  virtual void Hello(const std::string& who) = 0;

  // Hi 方法,返回值是回调形式
  using HiCallback = base::OnceCallback<void(const std::string&)>;
  virtual void Hi(HiCallback callback) = 0;
};
...
} // namespace mojom
} // namespace demo

Bindings

可以看到 Hello 方法和 Hi 方法都是纯虚函数,需要自己实现具体的功能,我们简单实现一下这个 Test 接口:

class TestImpl : public demo::mojom::Test {
 public:
  void Hello(const std::string& who) override {
    who_ = who;
    LOG(INFO) << "Test1 run: Hello " << who_;
  }

  void Hi(HiCallback callback) override {
    LOG(INFO) << "Test1 run: Hi " << who_;
    std::move(callback).Run(who_);
  }

 private:
  std::string who_;
};

可以看到,实现一个 Mojo 接口类和实现一个普通的 C++接口并没有什么区别,接下来就是使用这个类了。

Mojo 接口的使用是依赖 MessagePipe 的,我们知道一个 MessagePipe 有一对 handle,这一对 handle 套到接口类中就是一个 handle 对应 Test 接口,一个 handle 对应 Test 的实现(的一个实例)。MessagePipe 和 Interface 的关系如下图:

要使用一个接口必须有一个 MessagePipe,我们可以创建一个 MessagePipe,就像下面这样:

// 这些是mojo生成的,放到这里方面理解
using TestPtr = mojo::InterfacePtr<Test>;
using TestPtrInfo = mojo::InterfacePtrInfo<Test>;
using TestRequest = mojo::InterfaceRequest<Test>;

// 创建一个MessagePipe
mojo::MessagePipe pipe;
// 创建 TestPtr,TestPtr见mojo生成的代码
// 把 pipe 的handle0传给了TestPtr
// 下面的写法也可以分两步写:
// demo::mojom::TestPtr test;
// test.Bind(...)
demo::mojom::TestPtr test(demo::mojom::TestPtrInfo(std::move(pipe.handle0),0));
// 接下来就可以调用Test接口提供的方法了
// 这些调用会被序列化后使用handle0发送出去
test->Hello("World!");
test->Hi(base::BindOnce([](const std::string& who) {
          LOG(INFO) << "Test1 response: Hi " << who;
        }));

// 使用一个Test接口的实例和pipe的handle1构造一个Binding对象
// 上面的Hello和Hi的调用会被mojo转发到这个对象中
// 这些代码可以放在接口调用之后进行,因为mojo会缓存之前的调用
// 只要TestImpl对象和Binding对象不被销毁,对Test接口的调用就会正常进行
TestImpl test_impl;
mojo::Binding<demo::mojom::Test> test(&test_impl,
  demo::mojom::TestRequest(std::move(pipe.handle1)));

以上是在单进程中使用 Mojo 接口的方法,如果把 handle1 发送到其他进程,则可以实现跨进程调用远程接口了。

在实际使用中,经常把 Binding 对象放到接口的实现类中,像下面这样:

class TestImpl : public demo::mojom::Test {
 public:
  TestImpl(demo::mojom::TestRequest request)
    : binding_(this,std::move(request)) {

  void Hello(const std::string& who) override {
    who_ = who;
    LOG(INFO) << "Test1 run: Hello " << who_;
  }

  void Hi(HiCallback callback) override {
    LOG(INFO) << "Test1 run: Hi " << who_;
    std::move(callback).Run(who_);
  }

 private:
  std::string who_;
  mojo::Binding<demo::mojom::Test> binding_;
};

在使用的时候就可以比较简单的使用:

auto TestImpl = new TestImpl(demo::mojom::TestRequest(std::move(pipe.handle1)))

只要保证这个对象不被销毁,则接口都可以正常调用。

Mojo 接口的参数不仅可以是简单类型,也可以是 handle 类型的,也就是说可以使用 Mojo 接口来传递 MojoHandle。

mojo::BindingSet

以上是 Binding 的基本使用,一个接口的调用实际需要有一个 MessagePipe 支撑,那如果我想在多个地方调用同一个接口怎么办呢?

根据接口是否有状态(也就是对接口的调用会改变接口的实现对象,Test 接口就是有状态的,因为 Hello 的调用会更新 who_ 属性)有不同的方法:

  • 接口有状态: 每次需要调用的时候都重新走一遍以上流程;
  • 接口无状态: 让 TestPtr 全局可访问,所有对 Test 接口的调用都使用同一个 TestPtr 对象,或者使用 mojo::BindingSet,它允许一个 TestImpl 实例接收来自多个 TestPtr 的调用。

mojo::BindingSet 的使用非常简单,只要把 mojo::Binding 替换为 mojo::BindingSet,需要 Binding 的时候调用 BindingSet 的 AddBinding 方法即可。

使用 BindingSet 时接口和 MessagePipe 的关系如下,可以看到接收端只需要一个接口实例即可服务于多个接口调用。

Associated Interface

每一个 Binding 层中的接口(的实例)都唯一对应一个 MessagePipe,不能在同一个 MessagePipe 中使用两个接口,如果必须要在同一个 MessagePipe 中使用多个接口(因为这样可以保证顺序),则需要使用 Associated Interface,这种关联接口可以用于多个接口之间有调用顺序依赖的情况,它和普通的 Interface 的关系如下:

这里的 MasterInterface 就是前文所述的接口,可以看到 Master Interface 和多个 Associated Interfaces 共用一个 MessagePipe。

要使用关联接口需要修改接口可以使用在定义接口时使用 associated 关键字:

// mojom/test3.mojom

interface Api {
    PrintApi(string data);
};

interface Api2 {
    PrintApi2(string data);
};

// Test3 依赖 Api 和 Api2 接口,不使用关联接口
interface Test3 {
    GetApi(Api& api);
    SetApi2(Api2 api2);
};

// Test3 依赖 Api 和 Api2 接口,使用关联接口
interface Test32 {
    GetApi(associated Api& api);
    SetApi2(associated Api2 api2);
};

mojo::MakeRequest

mojo::MakeRequest() 可以简化 MessagePipe 的创建,只是一个非常简单的包装而已:

// 创建新的MessagePipe用于Api接口的调用
mojo::MessagePipe api_pipe;
Remote<Api> api(PendingRemote<Api>(std::move(api_pipe.handle0),0));
test3->GetApi(PendingReceiver<Api>(std::move(api_pipe.handle1)));
//以上代码可以简化为:
Remote<Api> api;
test3->GetApi(mojo::MakeRequest(&api));
// 使用MakeRequest结果和上面一样,可以更简单,在更新的版本中Remote中添加了BindNew*方法,用来取代MakeRequest

新版本改动

在 Chromium 的新版本中,所有的 xxxPtr,xxxPtrInfo 等类都进行了改名,简单来讲新旧版本的类名的对应关系如下:

// 在新版本中这些类被重命名,这里模拟新版本
template<class T> using Remote = mojo::InterfacePtr<T>;
template<class T> using PendingRemote = mojo::InterfacePtrInfo<T>;
template<class T> using Receiver = mojo::Binding<T>;
template<class T> using PendingReceiver = mojo::InterfaceRequest<T>;

// 以下定义用于模拟新版本的关联接口
template<class T> using AssociatedRemote = mojo::AssociatedInterfacePtr<T>;
template<class T> using PendingAssociatedRemote = mojo::AssociatedInterfacePtrInfo<T>;
template<class T> using AssociatedReceiver = mojo::AssociatedBinding<T>;
template<class T> using PendingAssociatedReceiver = mojo::AssociatedInterfaceRequest<T>;