专栏名称: __ihhu
前段
目录
相关文章推荐
歸藏的AI工具箱  ·  终于有给设计师用的 Cursor 了 ·  昨天  
歸藏的AI工具箱  ·  终于有给设计师用的 Cursor 了 ·  昨天  
前端早读课  ·  【第3454期】如何用语音学习编程的 ·  昨天  
前端大全  ·  前端行情变了,差别真的挺大。。。 ·  2 天前  
前端大全  ·  Create React ... ·  6 天前  
前端大全  ·  React+AI 技术栈(2025 版) ·  4 天前  
51好读  ›  专栏  ›  __ihhu

襁褓中的 deno(一):运行时调用分析

__ihhu  · 掘金  · 前端  · 2018-06-05 01:48

正文

上一篇文章 中,我们简单地认识了一下 deno,聊了聊它的主要特性与不足。现在让我们的目光从宏观转向微观,从源码角度来了解一下,在 deno 中,TS 和 Go 是如何进行相互调用的。


v8worker2 是什么?

在阐述 deno 的内部调用机制之前,我们先来看一看 Ryan 大大的另外一个项目: v8worker2

为什么我们要关注这个项目呢? v8worker2 其实是一个 v8 的 Go 语言绑定 ,也就是说,通过调用 v8worker2 的方法,我们可以达到间接调用 v8 的效果。 很明显,这个就是利用 Go 来运行 JavaScript 代码的核心。

我们先来看看 v8worker2 是怎么用的:

// echo.go

import "github.com/ry/v8worker2"

func main() {
  //  注册 worker 实例,并注册一个接受 JS 信息的回调
  worker := v8worker2.New(func(msg []byte) []byte {
    println("In Go,", string(msg))
    return nil
  })

  utilCode := `...`
  jsCode := `
    // 在 JS 中注册接收 Go 信息的函数,并打印接收到的信息
    V8Worker2.recv(msg => {
      V8Worker2.print("In js, " + ab2str(msg));
    });
    // 从 JS 向 Go 中发送『from js』信息
    V8Worker2.send(str2ab("from js"));
  `

  // 加载 JS 工具函数代码,这里按下不表
  worker.Load("utils.js", utilCode)
  // 加载业务代码
  worker.Load("foo.js", jsCode)
  // 从 Go 中向 JS 中发送『from Go』信息
  worker.SendBytes([]byte("from Go"))
}

// 运行结果:
// In Go, from js
// In js, from Go

上面是一个最简单的 JS <—> Go 的信息交互代码示例,整体的思路非常清晰。先注册一个 worker ,然后在这个 worker 中加载运行相应的 JS 代码字符串,注意 worker.Load() 的第一个参数看似是文件名,其实这个参数只是用于异常等需要显示文件名的场景下,本身和代码并没有什么联系,最后是利用 worker.SendBytes() 函数向 JS 发送信息。

jsCode 中,我们会发现一个奇怪的变量 V8Worker2 , 这个变量是 JS 交互的核心,但是我们是在哪里定义这个变量的呢?让我们来简单看一下 v8worker2 的实现,答案就藏在其中。


v8worker2 实现浅析

v8worker2 非常得简洁,核心的文件只有 binding.cc worker.go 这两个,其中 binding.cc 更是重中之重。

binding.cc 中用 cpp 定义了供 JS 调用的 API(也就是上一节 JS 代码字符串中的 V8Worker2 )和供 Go 调用的 API,而 worker.go 文件中则是对 Go API 进行了简单地封装,也就变成了上一节 Go 代码中的 v8worker2.New worker.SendBytes 等等。我们来看看 binding.cc 中是怎么定义 JS 的 V8Worker2 的 :

// Go API v8worker2.New 的底层实现

worker* worker_new(int table_index) {
  worker* w = new (worker);

  Local<ObjectTemplate> global = ObjectTemplate::New(w->isolate);

  // 定义一个类型为 Object 的 JS 全局变量
  Local<ObjectTemplate> v8worker2 = ObjectTemplate::New(w->isolate);

  // 将上面的全局变量在 JS 中的名称设置为 V8Worker2
  global->Set(String::NewFromUtf8(w->isolate, "V8Worker2"), v8worker2);

  // 在 V8Worker2 变量上新增一个 key 为 print,value 为 Print(cpp 函数) 的属性
  v8worker2->Set(String::NewFromUtf8(w->isolate, "print"),
    FunctionTemplate::New(w->isolate, Print));

  // 在 V8Worker2 变量上新增一个 key 为 recv,value 为 Recv(cpp 函数) 的属性
  v8worker2->Set(String::NewFromUtf8(w->isolate, "recv"),
    FunctionTemplate::New(w->isolate, Recv));

  // 在 V8Worker2 变量上新增一个 key 为 send,value 为 Send(cpp 函数) 的属性
  v8worker2->Set(String::NewFromUtf8(w->isolate, "send"),
    FunctionTemplate::New(w->isolate, Send));

  context->Enter();
  return w;
}

从上面的代码中,我们可以看出,当我们在执行 worker := v8worker2.New() 这段 Go 代码的时候,在底层上我们其实是初始化了一个 v8 的 worker,然后在上面定义了 V8Worker2 变量与一系列函数,并把这些 cpp 函数与 JS 变量名关联起来,这也是我们的 JS 代码能直接使用 V8Worker2.send() 等函数的原因。

我们最后来看看 binding.cc 中主要定义了哪些函数和数据结构:

// worker 是 deno 的核心,包含独立的 v8 引擎和
// js 与 go 交互的相关信息
struct worker_s;
typedef struct worker_s worker;

// 信息交互的格式
struct buf_s {
  void* data;
  size_t len;
};
typedef struct buf_s buf;

/* 供 Go 调用的函数*/
// New 函数的底层实现
worker* worker_new(int table_index);

// worker.Load 函数的底层实现
int worker_load(worker* w, char* name_s, char* source_s);

// worker.SendBytes 函数的底层实现
int worker_send_bytes(worker* w, void* data, size_t len);

/* 供 JS 使用的函数*/
// V8Worker2.print 函数的实现
void Print(const FunctionCallbackInfo<Value>& args);

// V8Worker2.send 函数的实现
void Send(const FunctionCallbackInfo<Value>& args);

// V8Worker2.recv 函数的实现
void Recv(const FunctionCallbackInfo<Value>& args);

deno 中的信息交互分析

从上文中我们已经(假装)弄清了 JS 和 Go 是如何利用 v8worker2 来进行交互的,那么我们来结合 deno 中的实例来看一下。

我们来看一个很简单的例子 :

// os.ts
function readFileSync(filename: string): Uint8Array {
  const res = sendMsg("os", {
    command: pb.Msg.Command.READ_FILE_SYNC,
    readFileSyncFilename: filename
  });
  return res.readFileSyncData;
}

deno 实现了一个文件读取的方法,但是这里的实现只是简单地发送消息,然后拿到返回的消息,从中获取数据。那这里的消息是发送给谁的呢?很简单,这个消息是发送给 go 的。

// os.go

func InitOS() {
  Sub("os", func(buf []byte) []byte {
    msg := &Msg{}
    // 利用 protobuf 解码信息
    proto.Unmarshal(buf, msg)
    // 根据指令类型来进行处理函数的匹配
    switch msg.Command {
      case Msg_READ_FILE_SYNC:
        return ReadFileSync(msg.ReadFileSyncFilename)
     }
    return nil
  }) 
}

// 真正的读取文件内容的处理函数
func ReadFileSync(filename string) []byte {
  data, _err1 := afero.ReadFile(fs, filename)
  res := &Msg{
    Command: Msg_READ_FILE_SYNC_RES,
    ReadFileSyncData: data
  }
  // 利用 protobuf 来编码含有文件内容的 Msg
  out, _err2 := proto.Marshal(res)
  return out
}

这里的 InitOS 在后文中会提到,这里就闲话少表。联系上面的 os.ts 中的代码,我们能很明显地看出一条调用链:ts 发送一个信息,信息的类型为"os",指令为 READ_FILE_SYNC,还有个 filename 的参数。而在 os.go 中,我们会注册一个订阅函数,当接受到一个"os" 类型的信息时,就会执行 switch逻辑。当确认该条消息是关于文件读取时, 就会调用 ReadFileSync函数来进行真正的文件读取,并把读取到的内容封装成 Msg 返回给 ts。

整条调用关系通了,但是里面的细节却有点黑盒,我们这里再来分析一下 SendMsg Sub 函数,希望能对信息交互方式有更多的了解。首先来看 JS 的 SendMsg 方法:

// dispatch.ts
function sendMsg(channel: string, obj: pb.IMsg): null | pb.Msg {
  // 将 obj 对象和 channel 利用 protobuf 编码,并转换成 ArrayBuffer
  const payload = pb.Msg.fromObject(obj);
  const ui8Payload = pb.Msg.encode(msg).finish();
  const msg = pb.BaseMsg.fromObject({ channel, payload }); 
  const ui8 = pb.BaseMsg.encode(msg).finish();
  const ab = typedArrayToArrayBuffer(ui8);

  // 将信息发送给 Go 并获取结果
  const resBuf = V8Worker2.send(ab);
  // 将结果通过 protobuf 解码
  const res = pb.Msg.decode(new Uint8Array(resBuf));
  return res;
}

啊哈!我们看到了熟悉的 V8Worker2.send ,这菊稳了!其实整个 sendMsg 主要是在处理信息编码和解码,真正和 Go 的交互只有这么一句。顺带提一下,deno 中对信息的编码利用的是 protobuf ,这是 Google 推出的一种数据交换格式,和大家熟悉的 JSON、XML 什么的比较类似,不过主要面向的是复杂的跨语言、跨进程调用的场景,以后有机会可以展开再聊聊。







请到「今天看啥」查看全文


推荐文章
歸藏的AI工具箱  ·  终于有给设计师用的 Cursor 了
昨天
歸藏的AI工具箱  ·  终于有给设计师用的 Cursor 了
昨天
前端早读课  ·  【第3454期】如何用语音学习编程的
昨天
前端大全  ·  React+AI 技术栈(2025 版)
4 天前
热门精选  ·  小心,这些汉字有毒!
7 年前
中国金融四十人论坛  ·  缪延亮:汇率到底由何决定?
7 年前