开始

今天开始分析JS与Rust是如何交互的,毕竟JS的性能在某些场景下还是不能胜任,这个时候就是Rust闪亮登场的时候,两者互相补足,无往不利!

op

之前一直说的op,我个人觉得就是deno上的一个插件机制,deno上所有的功能基本是都是在这个插件机制基础上工作的。

send和recv

在一开始的架构图我们就可以看到,在deno里面JS与Rust的交互只能通过send和recv这两个方法,调用send的实际原理也很简单根据opId去调用对应的rust方法,如果是同步的方法那就可以直接返回,但是如果是异步方法就需要用到recv去接收返回的值。

直接从打开文件open/openAsync这个op开始分析:

export function openSync(path: string, options: OpenOptions): number {  const mode: number | undefined = options?.mode;  return sendSync("op_open", { path, options, mode });}export function open(path: string, options: OpenOptions): Promise<number> {  const mode: number | undefined = options?.mode;  return sendAsync("op_open", {    path,    options,    mode,  });}

这里直接调用sendSync/sendAsync方法,然后再跟踪下去sendSync和sendAsync:

export function sendSync(  opName: string,  args: object = {},  zeroCopy?: Uint8Array): Ok {  const opId = OPS_CACHE[opName];  util.log("sendSync", opName, opId);  const argsUi8 = encode(args);  const resUi8 = core.dispatch(opId, argsUi8, zeroCopy);  util.assert(resUi8 != null);  const res = decode(resUi8);  util.assert(res.promiseId == null);  return unwrapResponse(res);}export async function sendAsync(  opName: string,  args: object = {},  zeroCopy?: Uint8Array): Promise<Ok> {  const opId = OPS_CACHE[opName];  util.log("sendAsync", opName, opId);  const promiseId = nextPromiseId();  args = Object.assign(args, { promiseId });  const promise = util.createResolvable<Ok>();  const argsUi8 = encode(args);  const buf = core.dispatch(opId, argsUi8, zeroCopy);  if (buf) {    // Sync result.    const res = decode(buf);    promise.resolve(res);  } else {    // Async result.    promiseTable[promiseId] = promise;  }  const res = await promise;  return unwrapResponse(res);}

sendSync相对sendAsync会简单一点,直接从OPS_CACHE拿到对应的opId,然后再把参数转成Uint8Array就可以分发这次调用下去。

而sendAsync则需要多创建一个promise,然后把promiseId附加到参数上,在分发这个次调用下去,那么这次异步调用怎么从recv方法接收结果回来的尼?

再去到core.js,deno在调用init的时候就设置了一个回调handleAsyncMsgFromRust:

function init() {    const shared = core.shared;    assert(shared.byteLength > 0);    assert(sharedBytes == null);    assert(shared32 == null);    sharedBytes = new Uint8Array(shared);    shared32 = new Int32Array(shared);    asyncHandlers = [];    // Callers should not call core.recv, use setAsyncHandler.    recv(handleAsyncMsgFromRust);  }

而handleAsyncMsgFromRust所做的就是从SharedQueue上取出异步操作结果然后触发相应的异步处理器:

function handleAsyncMsgFromRust(opId, buf) {    if (buf) {      // This is the overflow_response case of deno::Isolate::poll().      asyncHandlers[opId](buf);    } else {      while (true) {        const opIdBuf = shift();        if (opIdBuf == null) {          break;        }        assert(asyncHandlers[opIdBuf[0]] != null);        asyncHandlers[opIdBuf[0]](opIdBuf[1]);      }    }  }

SharedQueue本质上是一块JS与Rust都能共同访问的内存,而SharedQueue也有一套自身的内存布局:

总的来说这块内存最多能存100条异步操作结果或者少于128 * 100bit(125kb)大小的内容,一旦超过这些设定,就会触发overflow,立马从Rust切回到JS运行,让JS能够及时处理这些内容,所以这个SharedQueue是很重要的,可以影响整个应用的吞吐量。

再回到触发异步处理器,但是没到怎么触发promise的resolve方法,所以继续深入吧,再来到一开始初始化ops的地方:

function getAsyncHandler(opName: string): (msg: Uint8Array) => void {  switch (opName) {    case "op_write":    case "op_read":      return dispatchMinimal.asyncMsgFromRust;    default:      return dispatchJson.asyncMsgFromRust;  }}// TODO(bartlomieju): temporary solution, must be fixed when moving// dispatches to separate cratesexport function initOps(): void {  OPS_CACHE = core.ops();  for (const [name, opId] of Object.entries(OPS_CACHE)) {    core.setAsyncHandler(opId, getAsyncHandler(name));  }  core.setMacrotaskCallback(handleTimerMacrotask);}

可以发现,除了op_write/op_read这两个op使用的是dispatchMinimal.asyncMsgFromRust方法,其余的都是使用dispatchJson.asyncMsgFromRust响应回调。而在dispatchJson.asyncMsgFromRust这个方法里面我们就可以看到它专门对promise做了处理:

export function asyncMsgFromRust(resUi8: Uint8Array): void {  const res = decode(resUi8);  util.assert(res.promiseId != null);  const promise = promiseTable[res.promiseId!];  util.assert(promise != null);  delete promiseTable[res.promiseId!];  promise.resolve(res);}

根据我们之前传入的promiseId,来获取promise然后直接resolve。
那么还有一个小问题,dispatchMinimal.asyncMsgFromRust和dispatchJson.asyncMsgFromRust有啥区别尼?实际上dispatchMinimal.asyncMsgFromRust是专门处理io的读写的,一般都是传入资源id和一个buffer,等待rust的处理,然后返回处理后的字节数;而dispatchJson.asyncMsgFromRust参数都是经过JSON.stringify然后传给rust那边再解析取参。

那么还有send和recv这两个方法是在哪里定义的尼?
直接来到core/bingding.rs的initialize_context,在这里是deno初始化核心方法的地方(都是挂在Deno.core这个对象下),send和recv也是在这里注入到JS的世界里面:

pub fn initialize_context<'s>(  scope: &mut impl v8::ToLocal<'s>,) -> v8::Local<'s, v8::Context> {    ...    let mut recv_tmpl = v8::FunctionTemplate::new(scope, recv);      let recv_val = recv_tmpl.get_function(scope, context).unwrap();      core_val.set(        context,        v8::String::new(scope, "recv").unwrap().into(),        recv_val.into(),      );      let mut send_tmpl = v8::FunctionTemplate::new(scope, send);      let send_val = send_tmpl.get_function(scope, context).unwrap();      core_val.set(        context,        v8::String::new(scope, "send").unwrap().into(),        send_val.into(),      );      ...}

手残画张图整理一下:

插件编写

...待续

总结

整体下来,deno在js与rust的交互方面是挺好理解的,感觉对deno的未来又多加了几分信心了。