【Rolldown源码】四、开始入手rolldown_binding包

503 阅读4分钟

[napi](napi - crates.io: Rust Package Registry): A framework for building compiled Node.js add-ons in Rust via Node-API.

初识 NAPI-RS - 知乎 (zhihu.com)

NAPI-RS 是怎么工作的: 从 NAPI 到 Build Script & FFI - 掘金 (juejin.cn)

Pasted image 20240522150348.png

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)返回了什么。

  1. There is no concept of a class in Rust. We use struct to represent a JavaScript Class.
  2. If you want to define a custom constructor, you can use #[napi(constructor)] on your constructor fn in the struct impl block.
#[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实例,

  1. 有的阶段会调用所有并发插件的钩子,这个时候会等所有的worker空闲时,执行对应的parallelPlugin。
  2. 有的只会调用单个,会从之前的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)
		}
	}
}

Pasted image 20240522223211.png