Mojo 应用

Mojo 不仅可以在 Chromium 中使用,也可以在任何第三方程序中使用,因为它本身不依赖于 Chromium 中的业务逻辑部分。不过由于它的源码在 Chromium 中,在其他程序中使用可能没有那么方便。
Mojo 提供了不同层次的 API,外部可以根据自己的需要选择使用的层次,下面我们简单介绍每种 API 的使用方法,详细信息可以查看对应的 demo 程序。
目前 Mojo 支持 C++/Java/Js,这里只介绍 C++ 相关用法。

初始化 Mojo

初始化 Mojo 有两种方式,一种适用于静态链接 Mojo 的程序,一种适用于动态链接 Mojo 的程序。以下是静态链接时的初始化方法,动态链接时只需要把 mojo::core::Init() 替换为 MojoInitialize() 即可。

初始化接口的头文件为:
#include <mojo/core/embedder/embedder.h>
#include <mojo/core/embedder/scoped_ipc_support.h>
初始化方法如下:
int main(int argc, char** argv) {
  // 初始化CommandLine,DataPipe 依赖它
  base::CommandLine::Init(argc, argv);
  // 初始化 mojo
  mojo::core::Init();
  // 创建一个线程,用于Mojo内部收发数据
  base::Thread ipc_thread("ipc!");
  ipc_thread.StartWithOptions(
    base::Thread::Options(base::MessageLoop::TYPE_IO, 0));

  // 初始化 Mojo 的IPC支持,只有初始化后进程间的Mojo通信才能有效  
  // 这个对象要保证一直存活,否则IPC通信就会断开  
  mojo::core::ScopedIPCSupport ipc_support(
      ipc_thread.task_runner(),
      mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN);

  // ...
}

Mojo C API

Mojo C API 都比较简单,主要的头文件位于:
// SharedBuffer API
#include "mojo/public/c/system/buffer.h"
// DataPipe API
#include "mojo/public/c/system/data_pipe.h"
// MessagePipe API
#include "mojo/public/c/system/message_pipe.h"
以下是在单进程中使用 MessagePipe 发送和接收数据的方法:
// 使用 C 接口创建一条 MessagePipe
// MessagePipe 只是一对数字,只用于 ID 标识,并不对应任何系统资源
// 因此可以非常快速,不可能失败的,创建大量的 MessagePipe。
MojoHandle sender_handle, receiver_handle;
MojoResult result = MojoCreateMessagePipe(NULL, &sender_handle, &receiver_handle);
DCHECK_EQ(result, MOJO_RESULT_OK);
// 使用 C 接口发送一条消息
{
  // 创建一条 Message
  MojoMessageHandle message;
  result = MojoCreateMessage(nullptr, &message);
  DCHECK_EQ(result, MOJO_RESULT_OK);
  MojoAppendMessageDataOptions options;
  options.struct_size = sizeof(options);
  // 这个选项表示这条消息完整了,底层可以发送了
  options.flags = MOJO_APPEND_MESSAGE_DATA_FLAG_COMMIT_SIZE;
  void* buffer;
  uint32_t buffer_size;
  // 给 Message 填充数据
  result = MojoAppendMessageData(message, 6, nullptr, 0, &options, &buffer,&buffer_size);
  DCHECK_EQ(result, MOJO_RESULT_OK);
  memcpy(buffer, "hello", 6);
  LOG(INFO) << "send: " << (const char*)buffer;
  // 发送 Message
  result = MojoWriteMessage(sender_handle, message, nullptr);
  DCHECK_EQ(result, MOJO_RESULT_OK);
}
// 使用 C 接口接收一条消息
{
  MojoMessageHandle message;
  MojoResult result = MojoReadMessage(receiver_handle, nullptr, &message);
  DCHECK_EQ(result, MOJO_RESULT_OK);

  void* buffer = NULL;
  uint32_t num_bytes;
  result = MojoGetMessageData(message, nullptr, &buffer, &num_bytes, nullptr,nullptr);
  LOG(INFO) << "receive: " << (const char*)buffer;
}
其他关于 DataPipe 和 SharedBuffer 的使用方法都类似,由于实际项目中很少直接使用 C API,所以其使用方法在这里省略了。

Mojo C++ API

单进程

以下是在单进程中使用 MessagePipe 的方法,其中最重要的是要注意 mojo::MessagePipe pipe; 这一行(内部调用 Mojo 的 C API MojoCreateMessagePipe),它创建了一个 MessagePipe,本质上只是创建了一对随机数,对应 pipe 中的两个属性 handle0 和 handle1,这两个功能上没有任何区别,向其中的一个 handle 写的数据可以从另一个 handle 中读取出来。
#include "mojo/public/cpp/system/buffer.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "mojo/public/cpp/system/simple_watcher.h"
#include "mojo/public/cpp/system/wait.h"

// 使用C++接口创建一条 MessagePipe
mojo::MessagePipe pipe;
// 使用 C++ 接口发送一条消息
{
  const char kMessage[] = "HELLO";
  result =
      mojo::WriteMessageRaw(pipe.handle0.get(), kMessage, sizeof(kMessage),
                            nullptr, 0, MOJO_WRITE_MESSAGE_FLAG_NONE);
  DCHECK_EQ(result, MOJO_RESULT_OK);
  LOG(INFO) << "send: " << kMessage;
}
// 使用 C++ 接口接收一条消息
{
  std::vector<uint8_t> data;
  result = mojo::ReadMessageRaw(pipe.handle1.get(), &data, nullptr,
                                MOJO_READ_MESSAGE_FLAG_NONE);
  DCHECK_EQ(result, MOJO_RESULT_OK);
  LOG(INFO) << "receive msg: " << (char*)&data[0];
}

多进程

一个 MessagePipe 中有一对 handle,分别是 handle0 和 handle1,向其中一个 handle 写的数据可以从另外一个 handle 读出来,这是前面已经说过的,如果把其中的一个 handle 发送到另外一个进程,这一对 handle 之间依然能够相互收发数据。Mojo 提供了多种方法来发送 handle 到其他的进程,其中最简单的是使用 Invitation。
要在多个进程间使用 Mojo,必须先通过 Invitation 将这些进程“连接”起来,这需要一个进程发送 Invitation,另一个进程接收 Invitation,发送 Invitation 的方法如下:
// 创建一条系统级的IPC通信通道
// 在linux上是 domain socket, Windows 是 named pipe,MacOS是Mach Port,该通道用于支持跨进程的消息通信
mojo::PlatformChannel channel;
LOG(INFO) << "local: "
          << channel.local_endpoint().platform_handle().GetFD().get()
          << " remote: "
          << channel.remote_endpoint().platform_handle().GetFD().get();

mojo::OutgoingInvitation invitation;
// 创建1个Message Pipe用来和其他进程通信
// 这里的 pipe 就相当于单进程中的pipe.handle0
// handle1 会被存储在invitation中,随后被发送出去
// 可以多次调用,以便Attach多个MessagePipe到Invitation中
mojo::ScopedMessagePipeHandle pipe =
    invitation.AttachMessagePipe("my raw pipe");
LOG(INFO) << "pipe: " << pipe->value();

base::LaunchOptions options;
base::CommandLine command_line(
    base::CommandLine::ForCurrentProcess()->GetProgram());
// 将PlatformChannel中的RemoteEndpoint的fd作为参数传递给子进程
// 在posix中,fd会被复制到新的随机的fd,fd号改变
// 在windows中,fd被复制后会直接进行传递,fd号不变
channel.PrepareToPassRemoteEndpoint(&options, &command_line);
// 启动新进程
base::Process child_process = base::LaunchProcess(command_line, options);
channel.RemoteProcessLaunchAttempted();

// 发送Invitation
mojo::OutgoingInvitation::Send(
    std::move(invitation), child_process.Handle(),
    channel.TakeLocalEndpoint(),
    base::BindRepeating(
        [](const std::string& error) { LOG(ERROR) << error; }));
在新进程中接收 Invitation 的方法如下:
// Accept an invitation.
mojo::IncomingInvitation invitation = mojo::IncomingInvitation::Accept(
    mojo::PlatformChannel::RecoverPassedEndpointFromCommandLine(
        *base::CommandLine::ForCurrentProcess()));
// 取出 Invitation 中的pipe
mojo::ScopedMessagePipeHandle pipe =
    invitation.ExtractMessagePipe("my raw pipe");
LOG(INFO) << "pipe: " << pipe->value();
这样就实现了将 pipe 中的一个 handle 发送到其他进程了,这两个进程可以开始使用 pipe 进行收发数据了。

以上只是将 handle 从一个进程发送到另一个进程的一种方法,这种方法一般用在新进程创建的时候,如果两个进程已经通过 Invitation 连接起来了,那么可以通过已经建立起来的 MessagePipe 来发送新的 MessagePipe 的 handle 到接收进程,发送端的代码如下:
const std::string kMessage("MessagePipe\0", 12);
mojo::ScopedMessagePipeHandle client;
mojo::ScopedMessagePipeHandle server;
// 创建一个新的MessagePipe(的一对handle)
// 也可以使用 mojo::MessagePipe 来创建,这两者是一样的
result = mojo::CreateMessagePipe(nullptr, &client, &server);
DCHECK_EQ(result, MOJO_RESULT_OK);

// 使用已经连接起来的pipe将 'client' 发送到对方
result = mojo::WriteMessageRaw(pipe.get(), kMessage.c_str(),
                                kMessage.length(), &client->value(), 1,
                                MOJO_WRITE_MESSAGE_FLAG_NONE);
DCHECK_EQ(result, MOJO_RESULT_OK);
// 后面就可以使用servier来和对端通信了
接收端的代码如下:
std::vector<uint8_t> data;
// 用来存储接收到的 handle
std::vector<mojo::ScopedHandle> handles;
result = mojo::ReadMessageRaw(pipe.get(), &data, &handles,
                              MOJO_READ_MESSAGE_FLAG_NONE);
DCHECK_EQ(result, MOJO_RESULT_OK);

// 将接收到的handle转换为MessagePipeHandle
// 后续就可以使用这个handle和对端通信了
mojo::ScopedMessagePipeHandle client =
    mojo::ScopedMessagePipeHandle::From(std::move(handles[0]));
以上演示了使用已经建立起来的 pipe 来发送新的 MessageaPipe 的 handle 到其他进程的方法,其实这个 handle 不仅可以是 MessagePipe 的 handle,也可以是 DataPipe 或者 SharedBuffer 或者是系统中的其他 handle,比如 socket fd,窗口句柄等。Mojo 提供了 PlatformHandle 类来包装这些 Native 的 handle,以便这些 handle 可以使用 MessagePipe 进行发送。Mojo 内部会使用所在系统的相应 API 来实现 handle 的复制和发送(在 Linux 上是 sendmsg,在 windows 上是 DuplicateHandle)。