可以使用 create-deno-plugin 创建一个插件工程,方便对插件有个具象化的感知
首先是载入 dynamic library 见 deno/cli/ops/plugin.rs#L59,并找到模块中的符号 deno_plugin_init
,这是 deno 扩展机制和插件开发者约定的入口
然后调用模块中的 deno_plugin_init
方法,来注册模块希望暴露给 js 环境的方法,注册的步骤见 deno/cli/ops/plugin.rs#L71
模块中是以何种形式将自己的方法注册的呢,详见 webview_deno/src/lib.rs#L31,挑部分简单看下:
#[no_mangle] // 很重要,否则第2步就找不到符号了,因为 rust 支持重载之类的特性,所以编译后函数名会有变动
pub fn deno_plugin_init(interface: &mut dyn Interface) {
interface.register_op("webview_free", webview_free);
}
#[json_op]
fn webview_free(
json: Value,
_zero_copy: &mut [ZeroCopyBuf],
) -> Result<Value, AnyError> {
// ...
}
可以看到,就是很语义化的一条注册语句 register_op
register_op
的实现,详见 deno/cli/ops/plugin.rs#L103fn register_op(
&mut self,
name: &str,
dispatch_op_fn: plugin_api::DispatchOpFn,
) -> OpId {
// ...
let plugin_op_fn = move |state_rc: Rc<RefCell<OpState>>,
mut zero_copy: BufVec| {
// ...
let op = dispatch_op_fn(&mut interface, &mut zero_copy);
// ...
};
self
.state
.op_table
.register_op(name, metrics_op(Box::new(plugin_op_fn)))
}
dispatch_op_fn
是插件注册的方法,比如上文的 webview_free
,然后包装成另一个函数 plugin_op_fn
后注册,目前看来包装的目的应该是做一个出入参的调整,包装好后的函数被注册到了 state.op_table
中
如果进入 metrics_op
的实现查看的话,会发现它又包了一层 deno/cli/metrics.rs#L82 暂时看不知道原因
注册的步骤就到此为止了,流程可以大致梳理为:
op_table
中,以注册时 name
为键名,比如 "webview_free"
下面开始看下调用的方式,从 js 的调用开始入手:
Deno.openPlugin(pluginPath);
var { asyncOp: asyncOpId, syncOp: syncOpId } = Deno.core.ops();
export function syncOpWrapper(zeroCopy) {
return Deno.core.dispatch(syncOpId, zeroCopy);
}
openPlugin
的定义在 deno/cli/rt/40_plugins.js#L6
code.dispatch
的定义在 deno/core/core.js#L185,其内部又是使用的 send
,而 send
和 recv
的定义在 deno/core/bindings.rs#L139
send
的实现在 deno/bindings.rs#L385,其中的关键方法就是:
let op = OpTable::route_op(op_id, state.op_state.clone(), bufs);
可以看到和注册部分已经串联起来了,简单说就是注册是将方法添加到 hashmap 中,key 是方法名,value 是方法实现,调用的时候,再去 hashmap 里面根据 key 找实现,然后调用
可以看下 OpTable::route_op
的实现 deno/core/ops.rs#L86,其中关键调用是:
match op_fn {
Some(f) => (f)(state, bufs), // 这里
None => Op::NotFound,
}
可以看到是直接的函数调用
另外 js -> rust 的调用时,并没有对参数进行序列化操作,而是使用的 ZeroCopyBuf
,按照它的注释来看,它并没有做内存拷贝,而是将对应的原本由 v8 的 GC 控制的内存,绑定到 ZeroCopyBuf
实例的生命周期中,最坏情况下(rust 方法内没有显式地提前释放),就是这块内存直到相应的 rust
方法执行完毕后才会被释放
当然也有序列化的版本 json_op_sync,这样参数就会经过 JSON 的序列化和反序列化
上面只分析了同步调用的情况,异步调用的情况目前看来,可以将简单看成是将 js 的主线程和 rust 主线程绑定到了一起,剩下的异步操作,依赖了 rust 的 async 实现自动做调度
最初的学习是希望 native plugin 中可以访问到 v8::Isolate
从而拿到运行时的信息做简单的监控,目前看来插件 API 并没有将这块的访问开放出来,可能需要提个 issue 去问一问
目前看来可以通过自己定义调用签名的方式来完成调用,有待验证其他方法,因为 version 是在 read-only 区的,也就是 'static
lifetime 的,所以成功的结果不太有力:
extern "C" {
fn v8__V8__GetVersion() -> *const c_char;
}
pub fn gc_stats(_interface: &mut dyn Interface, _zero_copy: &mut [ZeroCopyBuf]) -> Op {
unsafe {
let c_str: &CStr = CStr::from_ptr(v8__V8__GetVersion());
println!("{:?}", c_str.to_str());
}
Op::Sync(Box::new([]))
}