[napi](napi - crates.io: Rust Package Registry): A framework for building compiled
Node.jsadd-ons inRustvia Node-API.NAPI-RS 是怎么工作的: 从 NAPI 到 Build Script & FFI - 掘金 (juejin.cn)
rolldown-binding的build代码
"build-binding": "napi build -o=./src --manifest-path ../../crates/rolldown_binding/Cargo.toml --platform -p rolldown_binding --js binding.js --dts binding.d.ts"
运行上述命令后会生成三个文件,分别是
- binding.js
- binding.d.ts
- rolldown-binding.darwin-arm64.node
binding.js用来初始化对应平台Node的动态链接库,简略代码如下。
function requireNative() {
if (process.platform === 'darwin') {
return require('./rolldown-binding.darwin-universal.node')
}
}
nativeBinding = requireNative()
module.exports.Bundler = nativeBinding.Bundler
module.exports.ParallelJsPluginRegistry = nativeBinding.ParallelJsPluginRegistry
module.exports.registerPlugins = nativeBinding.registerPlugins
如果我们要使用rolldown来生成代码,只需要两步
// 第一步初始化
const bundler = new Bundler(
bindingInputOptions,
BindingOutputOptions,
new ParallelJsPluginRegistry(count),
)
// 生成
bundler.write();
先看看第一步初始化都做了什么,根据napi的文档,new Bundler等于调用了Bundler的,在这之前先看看第三个参数new ParallelJsPluginRegistry(count)返回了什么。
- There is no concept of a class in Rust. We use
structto represent a JavaScriptClass.- If you want to define a custom
constructor, you can use#[napi(constructor)]on your constructorfnin the structimplblock.
#[napi]
impl ParallelJsPluginRegistry {
#[napi(constructor)]
pub fn new(worker_count: u16) -> napi::Result<Self> {
if worker_count == 0 {
return Err(napi::Error::from_reason("worker count should be bigger than 0"));
}
let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed);
PLUGINS_MAP.insert(id, vec![]);
Ok(Self { id, worker_count })
}
// 这个方法等下Bundle初始化中会调用到, 整体最终返回的map是类似下面的数据
// {
// [index1]: [parallelJsPlugin1, parallelJsPlugin1, ...worker_count个],
// [index2]: [parallelJsPlugin2, parallelJsPlugin2, ...worker_count个]
// [index3]: [parallelJsPlugin3, parallelJsPlugin3, ...worker_count个]
// }
pub fn take_plugin_values(&self) -> PluginValues {
// 这里的会获取到对应ID的所有 PluginsInSingleWorker,
let plugins_list = PLUGINS_MAP.remove(&self.id).expect("plugin list already taken").1;
let mut map: FxHashMap<usize, Vec<BindingPluginOptions>> = FxHashMap::with_capacity_and_hasher(plugins_list[0].len(), BuildHasherDefault::default());
for plugins in plugins_list {
for plugin in plugins {
map.entry(plugin.index as usize).or_default().push(plugin.plugin);
}
}
map
}
}
返回了当前 ParallelJsPluginRegistry 的唯一ID和开启worker的线程数。
impl Bundler {
#[napi(constructor)]
pub fn new(
//
env: Env,
mut input_options: BindingInputOptions,
output_options: BindingOutputOptions,
parallel_plugins_registry: Option<ParallelJsPluginRegistry>,
) -> napi::Result<Self> {
// 【不重要】初始化自定义的跟踪,并在cleanup的时候销毁
try_init_custom_trace_subscriber(env);
// 【不重要】返回日志等级
let log_level = input_options.log_level.take().unwrap_or_else(|| "info".to_string());
// 获取线程数
#[cfg(not(target_family = "wasm"))]
let worker_count = parallel_plugins_registry.as_ref().map(|registry| registry.worker_count).unwrap_or_default();
// 拿到形如下的数据
// {
// [index1]: [parallelJsPlugin1, parallelJsPlugin1, ...worker_count个],
// [index2]: [parallelJsPlugin2, parallelJsPlugin2, ...worker_count个],
// [index3]: [parallelJsPlugin3, parallelJsPlugin3, ...worker_count个],
// }
#[cfg(not(target_family = "wasm"))]
let parallel_plugins_map = parallel_plugins_registry.map(|registry| registry.take_plugin_values());
// 如果有线程数就构造一个工作线程管理器
#[cfg(not(target_family = "wasm"))]
let worker_manager = if worker_count > 0 { Some(WorkerManager::new(worker_count)) } else { None };
// 这里返回了处理后的option和plugins
// plugins里面分普通插件结构体实例JsPlugin和并发插件结构体实例ParallelJsPlugin,每个ParallelJsPlugin中都关联了worker_manager且有worker_count数量个相同的并发插件。
let ret = normalize_binding_options(
input_options,
output_options,
#[cfg(not(target_family = "wasm"))]
parallel_plugins_map,
#[cfg(not(target_family = "wasm"))]
worker_manager,
)?;
Ok(Self {
cwd: ret.bundler_options.cwd.clone().unwrap_or_else(|| std::env::current_dir().unwrap()),
// tokio::sync::Mutex用来锁
// NativeBundler 是 rolldown(rust) 中的数据结构,所有的解析、编译等操作应该都在这里了
inner: Mutex::new(NativeBundler::with_plugins(ret.bundler_options, ret.plugins)),
log_level,
})
}
}
第一步到这就完成了,然后就等我们调用write,generate或scan的方法了,
以write为例,看看接下来都做了什么
impl Bundler {
#[napi]
#[tracing::instrument(level = "debug", skip_all)]
pub async fn write(&self) -> napi::Result<FinalBindingOutputs> {
self.write_impl().await
}
#[allow(clippy::significant_drop_tightening)]
pub async fn write_impl(&self) -> napi::Result<FinalBindingOutputs> {
// 尝试锁住inner并返回对应的Bundler,没有则返回错误信息
let mut bundler_core = self.inner.try_lock().map_err(|_| {
napi::Error::from_reason("Failed to lock the bundler. Is another operation in progress?")
})?;
// 又是进入到 rolldown(rust) 的方法,具体的到rolldown(rust) 中再看。
let outputs = Self::handle_result(bundler_core.write().await)?;
if !outputs.errors.is_empty() {
return Err(self.handle_errors(outputs.errors));
}
self.handle_warnings(outputs.warnings);
Ok(FinalBindingOutputs::new(outputs.assets))
}
}
看看其他的一些处理,比如plugin中的回调,以build_start举例。当rolldown进行到build_start阶段的时候会调用 PluginDriver 实例的 build_start 方法,build_start 方法会执行 JsPlugin实例或ParallelJsPlugin实例中的 build_start 方法。
impl PluginDriver {
pub async fn build_start(&self) -> HookNoopReturn {
let ret = {
#[cfg(not(target_arch = "wasm32"))]
{
block_on_spawn_all(self.plugins.iter().map(|(plugin, ctx)| plugin.build_start(ctx))).await
}
}
for r in ret {
r?;
}
Ok(())
}
}
普通插件结构体实例JsPlugin没有什么特殊的,执行到对应的阶段就调用插件中对应的钩子方法,列一下里面某一个钩子的方法
#[async_trait::async_trait]
impl Plugin for JsPlugin {
async fn build_start(
&self,
ctx: &rolldown_plugin::SharedPluginContext,
) -> rolldown_plugin::HookNoopReturn {
if let Some(cb) = &self.build_start {
cb.await_call(Arc::clone(ctx).into()).await?;
}
Ok(())
}
ParallelJsPlugin实例,
- 有的阶段会调用所有并发插件的钩子,这个时候会等所有的worker空闲时,执行对应的parallelPlugin。
- 有的只会调用单个,会从之前的worker_manager中拿到空闲的worker,执行对应的parallelPlugin。
&self.plugins[permit.worker_index() as usize],通过worker_index拿的对应的parallelPlugin
impl ParallelJsPlugin {
#[cfg(not(target_family = "wasm"))]
async fn run_single<'a, R, F: FnOnce(&'a JsPlugin) -> BoxFuture<R>>(&'a self, f: F) -> R {
let permit = self.worker_manager.acquire().await;
let plugin = &self.plugins[permit.worker_index() as usize];
f(plugin).await
}
#[cfg(not(target_family = "wasm"))]
async fn run_all<'a, R, E: std::fmt::Debug, F: FnMut(&'a JsPlugin) -> BoxFuture<Result<R, E>>>(
&'a self,
f: F,
) -> Result<Vec<R>, E> {
let _permit = self.worker_manager.acquire_all().await;
let results = future::join_all(self.plugins.iter().map(f)).await;
let mut ok_list: Vec<R> = Vec::with_capacity(results.len());
for result in results {
ok_list.push(result?);
}
Ok(ok_list)
}
}
impl Plugin for ParallelJsPlugin {
async fn build_start(
&self,
ctx: &rolldown_plugin::SharedPluginContext,
) -> rolldown_plugin::HookNoopReturn {
if self.first_plugin().build_start.is_some() {
self.run_all(|plugin| plugin.build_start(ctx)).await?;
}
Ok(())
}
async fn load(
&self,
ctx: &rolldown_plugin::SharedPluginContext,
args: &rolldown_plugin::HookLoadArgs,
) -> rolldown_plugin::HookLoadReturn {
if self.first_plugin().load.is_some() {
self.run_single(|plugin| plugin.load(ctx, args)).await
} else {
Ok(None)
}
}
}