当前位置: 首页 > 后端技术 > Node.js

Deno源码简析(三)JS与Rust交互

时间:2023-04-03 20:16:08 Node.js

今天开始分析JS与Rust是如何交互的。毕竟JS的性能在某些场景下还是不能胜任的,所以现在Rust登场的时候,Rust有媲美C/C++的性能,而且还自带强大的基础库,应用场景还是很多的。之前一直在讲的op,我个人认为是Deno上的一种插件机制,Deno上的所有功能基本上都是在这种插件机制的基础上工作的。从一开始send和recv的架构图可以看出,deno中JS和Rust的交互只能通过send和recv这两个方法。调用send的实际原理也很简单。根据opId调用对应的rust方法,如果是同步方法可以直接返回,如果是异步方法则需要使用recv接收返回值。直接从打开文件的op开始分析open/openAsync:exportfunctionopenSync(path:string,options:OpenOptions):number{constmode:number|undefined=options?.mode;returnsendSync("op_open",{path,options,mode});}导出函数open(path:string,options:OpenOptions):Promise{constmode:number|undefined=options?.mode;returnsendAsync("op_open",{path,options,mode,});}这里直接调用sendSync/sendAsync方法,然后跟进sendSync和sendAsync:exportfunctionsendSync(opName:string,args:object={},zeroCopy?:Uint8Array):好的{constopId=OPS_CACHE[opName];util.log("sendSync",opName,opId);constargsUi8=encode(args);constresUi8=core.dispatch(opId,argsUi8,zeroCopy);util.assert(resUi8!=null);constres=解码(resUi8);util.assert(res.promiseId==null);returnunwrapResponse(res);}导出异步函数sendAsync(opName:string,args:object={},zeroCopy?:Uint8Array):Promise{constopId=OPS_CACHE[操作名称];util.log("sendAsync",opName,opId);constpromiseId=nextPromiseId();args=Object.assign(args,{promiseId});constpromise=util.createResolvable();constargsUi8=encode(args);constbuf=核心。派遣(opId,argsUi8,zeroCopy);if(buf){//同步结果。constres=解码(buf);承诺。解决(res);}else{//异步result.promiseTable[promiseId]=promise;}constres=等待承诺;returnunwrapResponse(res);}sendSync比sendAsync更简单,直接从OPS_CACHE中获取对应的opId,然后将参数转成Uint8Array即可派发这个调用而sendAsync需要再创建一个promise,然后把promiseId附在参数上,然后分发这个调用,那么这个异步调用是怎么接收recv方法的结果的呢?转到core.js,deno在调用init时设置回调handleAsyncMsgFromRust:functioninit(){constshared=core.shared;断言(共享。byteLength>0);断言(sharedBytes==null);断言(shared32==null);sharedBytes=newUint8Array(共享);shared32=newInt32Array(共享);asyncHandlers=[];//调用者不应该调用core.recv,使用setAsyncHandler.recv(handleAsyncMsgFromRust);要做的是从SharedQueue中获取异步操作的结果并触发相应的异步处理程序:().asyncHandlers[opId](buf);}else{while(true){constopIdBuf=shift();如果(opIdBuf==null){中断;}assert(asyncHandlers[opIdBuf[0]]!=null);asyncHandlers[opIdBuf[0]](opIdBuf[1]);}}}SharedQueue本质上是JS和Rust的一块普通访问的内存,而SharedQueue也有自己的内存布局:一般来说,这块内存最多可以存放100个异步操作结果或者小于128*100bit(125kb)的内容。一旦超过这些设置,就会触发溢出,马上从Rust切换到JS运行,让JS及时处理这些内容,所以这个SharedQueue很重要,它可以影响整个应用的吞吐量然后return触发异步处理器,但是如何触发promise的resolve方法,所以继续深入,然后来到一开始初始化ops的地方:functiongetAsyncHandler(opName:string):(msg:Uint8Array)=>void{switch(opName){case"op_write":case"op_read":returndispatchMinimal.asyncMsgFromRust;默认值:返回dispatchJson.asyncMsgFromRust;}}//TODO(bartlomieju):临时解决方案,移动时必须固定//分派到单独的cratesexport函数initOps():void{OPS.ops_CACHE(=core);for(const[name,opId]ofObject.entries(OPS_CACHE)){core.setAsyncHandler(opId,getAsyncHandler(name));}core.setMacrotaskCallback(handleTimerMacrotask);}可以发现,除了op_write/op_read这两个ops使用第一个是dispatchMinimal.asyncMsgFromRust方法,其余都是使用dispatchJson.asyncMsgFromRust的响应回调。在dispatchJson.asyncMsgFromRust方法中,我们可以看到它专门处理了promise:util.assert(res.promiseId!=null);constpromise=promiseTable[res.promiseId!];util.assert(承诺!=null);删除promiseTable[res.promiseId!];promise.resolve(res);}根据我们之前传入的promiseId获取promise,然后直接resolved。那么还有一个小问题,dispatchMinimal.asyncMsgFromRust和dispatchJson.asyncMsgFromRust有什么区别?实际上,dispatchMinimal.asyncMsgFromRust专门负责io的读写。一般传入resourceid和一个buffer,等待rust处理,然后返回处理后的字节;而dispatchJson.asyncMsgFromRust的参数是通过JSON.stringify传递给rust然后解析并获取参数的。那么send和recv这两个方法是在哪里定义的呢?直接进入core/bingding.rs的initialize_context,这里初始化了deno的核心方法(都挂在了Deno.core的对象下),send和recv也在这里注入了JS世界:pubfninitialize_context<'s>(scope:&mutimplv8::ToLocal<'s>,)->v8::Local<'s,v8::Context>{...让mutrecv_tmpl=v8::FunctionTemplate::new(scope,接收);letrecv_val=recv_tmpl.get_function(scope,context).unwrap();core_val.set(context,v8::String::new(scope,"recv").unwrap().into(),recv_val.into(),);让mutsend_tmpl=v8::FunctionTemplate::new(scope,send);让send_val=send_tmpl.get_function(scope,context).unwrap();core_val.set(context,v8::String::new(scope,"send").unwrap().into(),send_val.into(),);...}画个手残图整理:插件写2020-06-28补充:deno插件当然是用Rust写的。和node使用C++相比,两者的入门难度其实是一样的。对于用惯了js的前端来说是一个门槛;但除了使用语言本身,当然还要看写作经验如何。这样,deno项目下其实就有了一个样例项目:test_plugin,所以我们就从这个样例项目入手,分析插件编写的各个步骤。另外,deno的插件接口还处于不稳定状态,所以目前还没有官方文档,现在讨论的内容以后可能会有变化。首先新建一个目录,执行cargoinit命令(还是很npm的风格),初始化项目:cargoinit这时候可以看到Cargo.toml文件(类似pacakage.json)[package]name="deno-plugin-test"version="0.1.0"authors=["popewu"]edition="2018"[lib]crate-type=["cdylib"][dependencies]serde={version="1.0.106",features=["derive"]}serde_derive="1.0.106"serde_json={version="1.0.52",features=["preserve_order"]}futures="0.3.4"deno_core={path="../deno/core"}关键是配置crate-type来决定我们编译的产品类型。deno插件需要一个动态库。那么依赖还需要配置futures库(写异步接口时需要),deno_core(指向本地代码库)。下面直接启动rust代码:#[no_mangle]pubfndeno_plugin_init(interface:&mutdynInterface){interface.register_op("testSync",test_sync_op);interface.register_op("testAsync",test_async_op);}声明deno_plugin_init方法,这个方法是整个插件的入口,方法名也是固定的。上面的no_mangle宏也是必须的,因为rust支持方法重载,没有no_mangle的方法名会被编译器改掉。然后调用interface.register_op注册我们的方法:fntest_sync_op(_interface:&mutdynInterface,data:&[u8],_:Option)->Op{...Op::Sync(..)}fntest_async_op(_interface:&mutdynInterface,data:&[u8],_:Option)->Op{...Op::Async(...)}方法接收的参数都是固定的,来自从方法签名可以看出rust从js接收的数据是一个u8数组,所以js传给rust的参数一般需要经过一层转换。最后打包,执行:cargobuild--release现在在target/release目录下就可以看到打包好的产品了。js端怎么用呢?上一节我们提到有sendSync和sendAsync方法可以帮助我们封装一些调用插件的逻辑,但是我们目前不能使用这两个非常友好的方法,因为只有一个地方可以初始化OPS_CACHE,而deno没有notexposedinterfacefor可以重新初始化,所以我们注册的方法在OPS_CACHE上是找不到的。另外,即使我们可以重新初始化OPS_CACHE,如果我们注册的方法覆盖了deno的内部方法,那肯定会引起一些新的问题。综上所述,还是要老老实实的写代码,实现相应的逻辑。constrid=Deno.openPlugin('./target/release/libdeno_plugin_test.dylib');const{testSync,testAsync}=Deno.core.ops();首先使用Deno.openPlugin方法加载组件,deno会调用deno_plugin_init方法让我们注册自己的op。然后我们可以调用Deno.core.ops()来获取新的OPS_CACHE。对于同步方法,我们可以直接调用Deno.core.dispatch方法。但是对于异步方法,我们还需要调用Deno.core.setAsyncHandler来设置相应的回调。当然,我们也可以使用官方例程封装调用逻辑,自己维护一张promise表,然后将promiseId作为附加参数传给rust。当callback响应时,根据promiseId找到对应的promise并resolve。最后,Deno.close(rid)释放资源。关于Deno.core.setAsyncHandler设置的回调,我觉得有一点需要注意。回调接收一个buf(u8数组)。这个数组一般指的是Deno.core.shared,也就是上一节。所谓ShareQueue,所以如果直接保存这个buf引用,很可能在下一个事件循环中发生变化;但在另一种情况下,这个buf是不会改变的,也就是我们异步方法返回的数据,如果超过了ShareQueue的大小,Deno会单独新建一个ArrayBuffer实例传给回调。此时,它不会改变。所以,建议我们直接复制一份比较靠谱。因为从setAynceHandler接收数据有一个不确定的地方,就是可能会复制到ShareQueue或者单独创建一个ArrayBuffer实例,所以如果是一些性能敏感的接口,可以使用zeroCopy参数,然后直接将数据传给RustWrite到zeroCopy的ArrayBuffer,然后自己处理。嗯,整体deno插件编写体验还是很流畅的。写插件的时候,最后需要返回一个u8数组。其实你不需要太在意Deno会做什么操作,你可以更专注于一个功能模块的实现。综上所述,在js和rust的交互方面,deno还是比较容易理解的,感觉对deno的未来多了一点信心。