正文
在
上一篇文章
中,我们简单地认识了一下 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 什么的比较类似,不过主要面向的是复杂的跨语言、跨进程调用的场景,以后有机会可以展开再聊聊。