前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何实现 JS 运行时的 Inspector 能力

如何实现 JS 运行时的 Inspector 能力

作者头像
theanarkh
发布2022-12-06 09:37:22
1.3K0
发布2022-12-06 09:37:22
举报
文章被收录于专栏:原创分享

前言:无论什么语言,调试能力都是非常重要的,像 C、C++ 等语言,我们可以使用现成的工具去调试。JS 也不例外,我们可以通过浏览器来实现对 JS 的调试,但是 JS 运行时就不太一样了,因为 JS 运行时通常独立于浏览器运行,所以无法直接使用浏览器提供的能力,这时候就需要自己实现了。当然 JS 运行时不需要完全实现调试的功能,核心的能力都是由 V8 提供,JS 运行时只需要按照 V8 的规范实现一个 Inspector 代理就行。本文介绍以 V8 为基础,实现一个简单的 JS 运行时(严格来说不算,本文只是用它来代替一个描述),并基于这个 JS 运行时实现调试 JS 的能力。

浏览器或者其他工具通常提供了 Inspector 客户端,所以这部分我们不需要重新实现,而 V8 内部本身已经实现了调试具体的实现,我们只需要实现这个调试代理就行,这个代理的主要功能就是透传客户端和服务器之间通信的数据,通信的数据是基于 V8 提供的调试协议,具体可以参考 https://chromedevtools.github.io/devtools-protocol/v8/。下面来看一下具体的实现。

代码语言:javascript
复制
int main(int argc, char* argv[]) {

  std::thread t;
  setvbuf(stdout, nullptr, _IONBF, 0);
  setvbuf(stderr, nullptr, _IONBF, 0);
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  v8::V8::InitializeExternalStartupData(argv[0]);
  std::unique_ptr<Platform> platform = platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();
  Isolate::CreateParams create_params;
  create_params.array_buffer_allocator = ArrayBuffer::Allocator::NewDefaultAllocator();
  Isolate* isolate = Isolate::New(create_params);
  Isolate::Scope isolate_scope(isolate);
  HandleScope handle_scope(isolate);
  Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
  Local<Context> context = Context::New(isolate, nullptr, global);
  Context::Scope context_scope(context);
  // 创建一个和 V8 通信的客户端
  std::unique_ptr<V8InspectorClientImpl> client = std::make_unique<V8InspectorClientImpl>(platform, context);
  // 打开文件
  int fd = open(argv[1], 0, O_RDONLY);
  if (fd == -1) {
    std::cout<<"file not found";
    return errno;
  }
  // 新建一个线程用于透传调试数据
  t = std::thread(worker, client.get());
  // 执行 JS 代码
  {
    struct stat info;
    // 取得文件信息
    fstat(fd, &info);
    // 分配内存保存文件内容
    char *ptr = (char *)malloc(info.st_size + 1);
    // ptr[info.st_size] = '\0';
    read(fd, (void *)ptr, info.st_size);
    // 要执行的js代码
    Local<String> source = String::NewFromUtf8(isolate, ptr,
                        NewStringType::kNormal,
                        info.st_size).ToLocalChecked();
    ScriptOrigin origin(String::NewFromUtf8(isolate, "V8-Inspector", NewStringType::kNormal, strlen("V8-Inspector")).ToLocalChecked());

    // 编译
    Local<Script> script = Script::Compile(context, source, &origin).ToLocalChecked();
    // 解析完应该没用了,释放内存
    free(ptr);
    // 执行
    Local<Value> result = script->Run(context).ToLocalChecked();

  }

  t.join();
  // Dispose the isolate and tear down V8.
  isolate->Dispose();
  v8::V8::Dispose();
  v8::V8::ShutdownPlatform();
  delete create_params.array_buffer_allocator;
  return 0;
}

上面的代码大部分是使用 V8 执行 JS 的通用例子。主要关注的地方是创建了一个 V8InspectorClientImpl 对象和新建了一个线程(为什么需要新建线程在之前的文章已经分析过,就不再介绍)。在介绍 V8InspectorClientImpl 之前,先看看子线程的实现。

代码语言:javascript
复制
void worker(V8InspectorClientImpl* client) {
    struct sockaddr_in server_addr;
    int connfd;
    struct sockaddr_in clent_addr; 
    socklen_t len = sizeof(clent_addr);
    size_t count;
    int server_fd = socket(AF_INET, SOCK_DGRAM, 0);

    if (server_fd < 0) { 
        perror("create socket error"); 
        goto EXIT;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family       = AF_INET;
    server_addr.sin_port         = htons(8888);
    server_addr.sin_addr.s_addr  = htonl(INADDR_ANY);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { 
        perror("bind address error"); 
        goto EXIT;
    }
    char buf[BUF_LEN];
    while(1)
    {
        struct sockaddr_in server; 
        socklen_t server_len = sizeof(server);
        memset(&server, 0, sizeof(server));
        server.sin_family       = AF_INET;
        server.sin_port         = htons(5555);
        server.sin_addr.s_addr  = htonl(INADDR_ANY);

        memset(buf, 0, BUF_LEN);
        count = recvfrom(server_fd, buf, BUF_LEN, 0, (struct sockaddr*)&clent_addr, &len);
        if(count == -1)
        {
            continue;
        }
        int client_port = htons(clent_addr.sin_port);
        // From V8 inspector
        if (client_port == 6666) {
            sendto(server_fd, buf, count, 0, (struct sockaddr*)&server, server_len); 
        } else { 
            // From Inspector client, such as Chrome Dev Tools
            client->onMessage(buf, count);
        }
    }

    close(server_fd);
EXIT:
    return;
}

子线程的逻辑也很简单,就是启动一个 UDP server 来传递调试客户端和服务器之间的数据。理论上来说,我们可以使用 TCP、UDP 甚至 Unix 域来实现数据的通信,因为重点是有一个数据通道完成数据透传到能力,具体的用什么协议去实现这个通道并不重要。但是因为调试客户端是基于 websocket 协议通信的,所以我们需要有一个 websocket 的服务器,为了实现的简单,这个 websocket 服务器我们使用 JS 实现,然后 JS 里再通过 UDP 把数据传递给子线程,子线程再传递给 V8,反过来, V8 的数据也是通过同样的方式传给客户端。架构如下。

接下来看一下 V8 Inspector 部分的实现。

代码语言:javascript
复制
V8InspectorClientImpl::V8InspectorClientImpl(const std::unique_ptr<v8::Platform> &platform, const v8::Local<v8::Context> &context) {
    platform_ = platform.get();
    context_ = context;
    isolate_ = context_->GetIsolate();
    // V8 Inspector 通过 channel 返回数据给客户端
    channel_.reset(new V8InspectorChannelImp(isolate_));
    inspector_ = v8_inspector::V8Inspector::create(isolate_, this);
    // 通过 session 发送数据给 V8 Inspector
    session_ = inspector_->connect(kContextGroupId, channel_.get(), v8_inspector::StringView());
    std::string contextName = "NoInspector";
    v8_inspector::V8ContextInfo v8info(context, kContextGroupId, convertToStringView(contextName));
    inspector_->contextCreated(v8info);
    terminated_ = true;
    run_nested_loop_ = false;
}

V8InspectorClientImpl 是一个负责和 V8 Inspector 通信的对象,它主要封装了 channel 和 session 对象,这两个对象是具体和 V8 通信的。介绍完整体和基础的数据结构后,接下来看看细节。刚才介绍中说到当收到客户端数据时,子线程会调用 onMessage 通知 Inspector。

代码语言:javascript
复制
void V8InspectorClientImpl::onMessage(char *buf, size_t count) {
    std::lock_guard<std::mutex> guard(mutex_);
    requests_.push_back(std::string(buf, 0, count));
    if (requests_.size() == 1) {
        isolate_->RequestInterrupt([](v8::Isolate* isolate, void* data) {
            V8InspectorClientImpl *client = static_cast<V8InspectorClientImpl *>(data);
            client->dispatchProtocolMessage();
        }, this); 
    }
    condition_variable_.notify_one();
}

onMessage 首先把数据插入主线程的任务队列,然后通过 RequestInterrupt 注册一个任务,等待 V8 处理这个 RequestInterrupt 任务时,就会通过 dispatchProtocolMessage 处理 requests_ 里面的任务。这里其实是一个非常关键的地方,在不同的 JS 运行时中,这个通知的方式不一样,比如在 Node.js 里,Node.js 除了调用 RequestInterrupt 还会通过线程间通信机制 async 通知主线程,因为这时候主线程可能阻塞在事件驱动模块中,也可能正在执行 JS,所以需要两种方式通知主线程,保证客户端的数据可以被处理。在本文这个简单的 JS 运行时中,目前只会在一个 while 循环中不断执行 JS,所以这里通过 RequestInterrupt 就可以了。V8 会在执行 JS 的时候,处理 RequestInterrupt 的任务,接下来看一下 dispatchProtocolMessage。

代码语言:javascript
复制
void V8InspectorClientImpl::dispatchProtocolMessage() {
    std::vector<std::string>::iterator it;
    std::vector<std::string> queues;
    {
        std::lock_guard<std::mutex> guard(mutex_);
        requests_.swap(queues);
    }
    for(it = queues.begin(); it != queues.end(); it++)
    {
        // 传给 V8 Inspector
        session_->dispatchProtocolMessage(convertToStringView(*it));
    }
}

dispatchProtocolMessage 很简单,通过 session 把数据传给 V8。V8 处理完后会通过 channel 通知客户端。

代码语言:javascript
复制
V8InspectorChannelImp::V8InspectorChannelImp(v8::Isolate *isolate) {
    isolate_ = isolate;
    client_fd_ = socket(AF_INET, SOCK_DGRAM, 0);
    // 用于和子线程通信的 UDP client socket
    struct sockaddr_in client_addr;
    memset(&client_addr, 0, sizeof(client_addr));
    client_addr.sin_family       = AF_INET;
    client_addr.sin_port         = htons(6666);
    client_addr.sin_addr.s_addr  = htonl(INADDR_ANY);
    if (bind(client_fd_, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0) { 
        perror("bind address error"); 
    }
    // 子线程监听的端口
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr_.sin_family       = AF_INET;
    server_addr_.sin_port         = htons(8888);
    server_addr_.sin_addr.s_addr  = htonl(INADDR_ANY);
}

void V8InspectorChannelImp::sendResponse(int callId, std::unique_ptr<v8_inspector::StringBuffer> message) {
    const std::string response = convertToString(isolate_, message->string());
    send(response.c_str(), response.length());
}

// 发送给子线程
void V8InspectorChannelImp::send(const char* buf, int size) {
    sendto(client_fd_, buf, size, 0, (struct sockaddr*)&server_addr_, sizeof(server_addr_)); 
}

V8 处理完数据后或者有新的事件触发时会通过 channel 通知 V8 使用者, V8 使用者接着通过 UDP 把数据透传给子线程,子线程再发送到客户端。这里也是实现的一个关键的地方,根据前面的分析,子线程是不断地阻塞在 recvfrom 等待数据的,所以唯一能唤醒子线程的就是给它发送数据,这里是为了实现上的简单。在 Node.js 里,子线程会跑一个事件循环,子线程除了可以在收到数据时被唤醒,还可以通过线程间通信机制 async 去唤醒。所以子线程收到数据时会根据发送发的端口进行不同的处理,如果是来自客户端,则转发给 V8 Inspector,如果数据是来自 V8 Inspector,则转发给客户端。

代码语言:javascript
复制
int client_port = htons(clent_addr.sin_port);
 // From V8 inspector
 if (client_port == 6666) {
     sendto(server_fd, buf, count, 0, (struct sockaddr*)&server, server_len); 
 } else { 
     // From Inspector client, such as Chrome Dev Tools
     client->onMessage(buf, count);
 }

至此,大概的流程就介绍完了,但是还有一个非常关键的地方,那就是断点调试。刚才介绍的场景没有断点的场景,比如我们的代码正在正常地运行,然后通过客户端发送获取 CPU Profile 的请求。断点的实现在之前的文章里已经介绍过了,所以就不多介绍了,直接看代码。

代码语言:javascript
复制
void V8InspectorClientImpl::runMessageLoopOnPause(int contextGroupId) {
    if (run_nested_loop_) {
        return;
    }
    std::cout<<"runMessageLoopOnPause"<<std::endl;
    terminated_ = false;
    run_nested_loop_ = true;
    while (!terminated_) {
        {
            std::unique_lock<std::mutex> lock(mutex_);
            condition_variable_.wait(lock);
        }
        dispatchProtocolMessage();
    }
    terminated_ = true;
    run_nested_loop_ = false;
}

当 V8 执行到一个断点时,就会执行 runMessageLoopOnPause 进入停住的状态。这里是通过条件变量来实现停住的状态,也就是阻塞线程。当客户端点击继续执行时,就会在刚才分析的 onMessage 中唤醒线程,接着通过 dispatchProtocolMessage 通知 V8 继续执行。从而实现断点的功能。实现了和 V8 Inspector 通信部分后,再看一下 JS 层。

代码语言:javascript
复制
const { WebSocketServer } = require('ws');
const dgram = require('dgram');

const udpClient = dgram.createSocket('udp4');
udpClient.bind(5555);

const wsServer = new WebSocketServer({
    port: 9229,
});

wsServer.on('connection', (socket) => {
  udpClient.on('message', (data) => {
    socket.send(data.toString());
  });
  socket.on('message', (data) => {
    udpClient.send(data, 8888);
  });
});

JS 层主要是符合透传客户端和 V8 Inspector 的数据。最终实现的功能如下。

通过 Chrome Dev Tools 就可以对我们的 JS 运行时进行调试。

时间关系,就介绍到这里,目前只是实现一个简单可用的版本来体验一下 V8 Inspector 的实现,有兴趣可以参考代码 和之前的文章

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档