bun_native_plugin/lib.rs
1//! > ⚠️ Note: This is an advanced and experimental API recommended only for plugin developers who are familiar with systems programming and the C ABI. Use with caution.
2//!
3//! # Bun Native Plugins
4//!
5//! This crate provides a Rustified wrapper over the Bun's native bundler plugin C API.
6//!
7//! Some advantages to _native_ bundler plugins as opposed to regular ones implemented in JS:
8//!
9//! - Native plugins take full advantage of Bun's parallelized bundler pipeline and run on multiple threads at the same time
10//! - Unlike JS, native plugins don't need to do the UTF-8 <-> UTF-16 source code string conversions
11//!
12//! What are native bundler plugins exactly? Precisely, they are NAPI modules which expose a C ABI function which implement a plugin lifecycle hook.
13//!
14//! The currently supported lifecycle hooks are:
15//!
16//! - `onBeforeParse` (called immediately before a file is parsed, allows you to modify the source code of the file)
17//!
18//! ## Getting started
19//!
20//! Since native bundler plugins are NAPI modules, the easiest way to get started is to create a new [napi-rs](https://github.com/napi-rs/napi-rs) project:
21//!
22//! ```bash
23//! bun add -g @napi-rs/cli
24//! napi new
25//! ```
26//!
27//! Then install this crate:
28//!
29//! ```bash
30//! cargo add bun-native-plugin
31//! ```
32//!
33//! Now, inside the `lib.rs` file, expose a C ABI function which has the same function signature as the plugin lifecycle hook that you want to implement.
34//!
35//! For example, implementing `onBeforeParse`:
36//!
37//! ```rust
38//! use bun_native_plugin::{OnBeforeParse};
39//!
40//! /// This is necessary for napi-rs to compile this into a proper NAPI module
41//! #[napi]
42//! pub fn register_bun_plugin() {}
43//!
44//! /// Use `no_mangle` so that we can reference this symbol by name later
45//! /// when registering this native plugin in JS.
46//! ///
47//! /// Here we'll create a dummy plugin which replaces all occurrences of
48//! /// `foo` with `bar`
49//! #[no_mangle]
50//! pub extern "C" fn on_before_parse_plugin_impl(
51//! args: *const bun_native_plugin::sys::OnBeforeParseArguments,
52//! result: *mut bun_native_plugin::sys::OnBeforeParseResult,
53//! ) {
54//! let args = unsafe { &*args };
55//! let result = unsafe { &mut *result };
56//!
57//! // This returns a handle which is a safe wrapper over the raw
58//! // C API.
59//! let mut handle = OnBeforeParse::from_raw(args, result) {
60//! Ok(handle) => handle,
61//! Err(_) => {
62//! // `OnBeforeParse::from_raw` handles error logging
63//! // so it fine to return here.
64//! return;
65//! }
66//! };
67//!
68//! let input_source_code = match handle.input_source_code() {
69//! Ok(source_str) => source_str,
70//! Err(_) => {
71//! // If we encounter an error, we must log it so that
72//! // Bun knows this plugin failed.
73//! handle.log_error("Failed to fetch source code!");
74//! return;
75//! }
76//! };
77//!
78//! let loader = handle.output_loader();
79//! let output_source_code = source_str.replace("foo", "bar");
80//! handle.set_output_source_code(output_source_code, loader);
81//! }
82//! ```
83//!
84//! Then compile this NAPI module. If you using napi-rs, the `package.json` should have a `build` script you can run:
85//!
86//! ```bash
87//! bun run build
88//! ```
89//!
90//! This will produce a `.node` file in the project directory.
91//!
92//! With the compiled NAPI module, you can now register the plugin from JS:
93//!
94//! ```js
95//! const result = await Bun.build({
96//! entrypoints: ["index.ts"],
97//! plugins: [
98//! {
99//! name: "replace-foo-with-bar",
100//! setup(build) {
101//! const napiModule = require("path/to/napi_module.node");
102//!
103//! // Register the `onBeforeParse` hook to run on all `.ts` files.
104//! // We tell it to use function we implemented inside of our `lib.rs` code.
105//! build.onBeforeParse(
106//! { filter: /\.ts/ },
107//! { napiModule, symbol: "on_before_parse_plugin_impl" },
108//! );
109//! },
110//! },
111//! ],
112//! });
113//! ```
114//!
115//! ## Very important information
116//!
117//! ### Error handling and panics
118//!
119//! It is highly recommended to avoid panicking as this will crash the runtime. Instead, you must handle errors and log them:
120//!
121//! ```rust
122//! let input_source_code = match handle.input_source_code() {
123//! Ok(source_str) => source_str,
124//! Err(_) => {
125//! // If we encounter an error, we must log it so that
126//! // Bun knows this plugin failed.
127//! handle.log_error("Failed to fetch source code!");
128//! return;
129//! }
130//! };
131//! ```
132//!
133//! ### Passing state to and from JS: `External`
134//!
135//! One way to communicate data from your plugin and JS and vice versa is through the NAPI's [External](https://napi.rs/docs/concepts/external) type.
136//!
137//! An External in NAPI is like an opaque pointer to data that can be passed to and from JS. Inside your NAPI module, you can retrieve
138//! the pointer and modify the data.
139//!
140//! As an example that extends our getting started example above, let's say you wanted to count the number of `foo`'s that the native plugin encounters.
141//!
142//! You would expose a NAPI module function which creates this state. Recall that state in native plugins must be threadsafe. This usually means
143//! that your state must be `Sync`:
144//!
145//! ```rust
146//! struct PluginState {
147//! foo_count: std::sync::atomic::AtomicU32,
148//! }
149//!
150//! #[napi]
151//! pub fn create_plugin_state() -> External<PluginState> {
152//! let external = External::new(PluginState {
153//! foo_count: 0,
154//! });
155//!
156//! external
157//! }
158//!
159//!
160//! #[napi]
161//! pub fn get_foo_count(plugin_state: External<PluginState>) -> u32 {
162//! let plugin_state: &PluginState = &plugin_state;
163//! plugin_state.foo_count.load(std::sync::atomic::Ordering::Relaxed)
164//! }
165//! ```
166//!
167//! When you register your plugin from Javascript, you call the napi module function to create the external and then pass it:
168//!
169//! ```js
170//! const napiModule = require("path/to/napi_module.node");
171//! const pluginState = napiModule.createPluginState();
172//!
173//! const result = await Bun.build({
174//! entrypoints: ["index.ts"],
175//! plugins: [
176//! {
177//! name: "replace-foo-with-bar",
178//! setup(build) {
179//! build.onBeforeParse(
180//! { filter: /\.ts/ },
181//! {
182//! napiModule,
183//! symbol: "on_before_parse_plugin_impl",
184//! // pass our NAPI external which contains our plugin state here
185//! external: pluginState,
186//! },
187//! );
188//! },
189//! },
190//! ],
191//! });
192//!
193//! console.log("Total `foo`s encountered: ", pluginState.getFooCount());
194//! ```
195//!
196//! Finally, from the native implementation of your plugin, you can extract the external:
197//!
198//! ```rust
199//! pub extern "C" fn on_before_parse_plugin_impl(
200//! args: *const bun_native_plugin::sys::OnBeforeParseArguments,
201//! result: *mut bun_native_plugin::sys::OnBeforeParseResult,
202//! ) {
203//! let args = unsafe { &*args };
204//! let result = unsafe { &mut *result };
205//!
206//! let mut handle = OnBeforeParse::from_raw(args, result) {
207//! Ok(handle) => handle,
208//! Err(_) => {
209//! // `OnBeforeParse::from_raw` handles error logging
210//! // so it fine to return here.
211//! return;
212//! }
213//! };
214//!
215//! let plugin_state: &PluginState =
216//! // This operation is only safe if you pass in an external when registering the plugin.
217//! // If you don't, this could lead to a segfault or access of undefined memory.
218//! match unsafe { handle.external().and_then(|state| state.ok_or(Error::Unknown)) } {
219//! Ok(state) => state,
220//! Err(_) => {
221//! handle.log_error("Failed to get external!");
222//! return;
223//! }
224//! };
225//!
226//!
227//! // Fetch our source code again
228//! let input_source_code = match handle.input_source_code() {
229//! Ok(source_str) => source_str,
230//! Err(_) => {
231//! handle.log_error("Failed to fetch source code!");
232//! return;
233//! }
234//! };
235//!
236//! // Count the number of `foo`s and add it to our state
237//! let foo_count = source_code.matches("foo").count() as u32;
238//! plugin_state.foo_count.fetch_add(foo_count, std::sync::atomic::Ordering::Relaxed);
239//! }
240//! ```
241//!
242//! ### Concurrency
243//!
244//! Your `extern "C"` plugin function can be called _on any thread_ at _any time_ and _multiple times at once_.
245//!
246//! Therefore, you must design any state management to be threadsafe
247#![allow(non_upper_case_globals)]
248#![allow(non_camel_case_types)]
249#![allow(non_snake_case)]
250pub use anyhow;
251pub use bun_macro::bun;
252
253pub mod sys;
254
255#[repr(transparent)]
256pub struct BunPluginName(*const c_char);
257
258impl BunPluginName {
259 pub const fn new(ptr: *const c_char) -> Self {
260 Self(ptr)
261 }
262}
263
264#[macro_export]
265macro_rules! define_bun_plugin {
266 ($name:expr) => {
267 pub static BUN_PLUGIN_NAME_STRING: &str = concat!($name, "\0");
268
269 #[no_mangle]
270 pub static BUN_PLUGIN_NAME: bun_native_plugin::BunPluginName =
271 bun_native_plugin::BunPluginName::new(BUN_PLUGIN_NAME_STRING.as_ptr() as *const _);
272
273 #[napi]
274 fn bun_plugin_register() {}
275 };
276}
277
278unsafe impl Sync for BunPluginName {}
279
280use std::{
281 any::TypeId,
282 borrow::Cow,
283 cell::UnsafeCell,
284 ffi::{c_char, c_void},
285 marker::PhantomData,
286 str::Utf8Error,
287 sync::PoisonError,
288};
289
290#[repr(C)]
291pub struct TaggedObject<T> {
292 type_id: TypeId,
293 pub(crate) object: Option<T>,
294}
295
296struct SourceCodeContext {
297 source_ptr: *mut u8,
298 source_len: usize,
299 source_cap: usize,
300}
301
302extern "C" fn free_plugin_source_code_context(ctx: *mut c_void) {
303 // SAFETY: The ctx pointer is a pointer to the `SourceCodeContext` struct we allocated.
304 unsafe {
305 drop(Box::from_raw(ctx as *mut SourceCodeContext));
306 }
307}
308
309impl Drop for SourceCodeContext {
310 fn drop(&mut self) {
311 if !self.source_ptr.is_null() {
312 // SAFETY: These fields come from a `String` that we allocated.
313 unsafe {
314 drop(String::from_raw_parts(
315 self.source_ptr,
316 self.source_len,
317 self.source_cap,
318 ));
319 }
320 }
321 }
322}
323
324pub type BunLogLevel = sys::BunLogLevel;
325pub type BunLoader = sys::BunLoader;
326
327fn get_from_raw_str<'a>(ptr: *const u8, len: usize) -> PluginResult<Cow<'a, str>> {
328 let slice: &'a [u8] = unsafe { std::slice::from_raw_parts(ptr, len) };
329
330 // Windows allows invalid UTF-16 strings in the filesystem. These get converted to WTF-8 in Zig.
331 // Meaning the string may contain invalid UTF-8, we'll have to use the safe checked version.
332 #[cfg(target_os = "windows")]
333 {
334 std::str::from_utf8(slice)
335 .map(Into::into)
336 .or_else(|_| Ok(String::from_utf8_lossy(slice)))
337 }
338
339 #[cfg(not(target_os = "windows"))]
340 {
341 // SAFETY: The source code comes from Zig, which uses UTF-8, so this should be safe.
342
343 std::str::from_utf8(slice)
344 .map(Into::into)
345 .or_else(|_| Ok(String::from_utf8_lossy(slice)))
346 }
347}
348
349#[derive(Debug, Clone)]
350pub enum Error {
351 Utf8(Utf8Error),
352 IncompatiblePluginVersion,
353 ExternalTypeMismatch,
354 Unknown,
355 LockPoisoned,
356}
357
358pub type PluginResult<T> = std::result::Result<T, Error>;
359pub type Result<T> = anyhow::Result<T>;
360
361impl std::fmt::Display for Error {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 write!(f, "{:?}", self)
364 }
365}
366
367impl std::error::Error for Error {
368 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
369 None
370 }
371
372 fn description(&self) -> &str {
373 "description() is deprecated; use Display"
374 }
375
376 fn cause(&self) -> Option<&dyn std::error::Error> {
377 self.source()
378 }
379}
380
381impl From<Utf8Error> for Error {
382 fn from(value: Utf8Error) -> Self {
383 Self::Utf8(value)
384 }
385}
386
387impl<Guard> From<PoisonError<Guard>> for Error {
388 fn from(_: PoisonError<Guard>) -> Self {
389 Self::LockPoisoned
390 }
391}
392
393/// A safe handle for the arguments + result struct for the
394/// `OnBeforeParse` bundler lifecycle hook.
395///
396/// This struct acts as a safe wrapper around the raw C API structs
397/// (`sys::OnBeforeParseArguments`/`sys::OnBeforeParseResult`) needed to
398/// implement the `OnBeforeParse` bundler lifecycle hook.
399///
400/// To initialize this struct, see the `from_raw` method.
401pub struct OnBeforeParse<'a> {
402 pub args_raw: *mut sys::OnBeforeParseArguments,
403 result_raw: *mut sys::OnBeforeParseResult,
404 compilation_context: *mut SourceCodeContext,
405 __phantom: PhantomData<&'a ()>,
406}
407
408impl<'a> OnBeforeParse<'a> {
409 /// Initialize this struct from references to their raw counterparts.
410 ///
411 /// This function will do a versioning check to ensure that the plugin
412 /// is compatible with the current version of Bun. If the plugin is not
413 /// compatible, it will log an error and return an error result.
414 ///
415 /// # Example
416 /// ```rust
417 /// extern "C" fn on_before_parse_impl(args: *const sys::OnBeforeParseArguments, result: *mut sys::OnBeforeParseResult) {
418 /// let args = unsafe { &*args };
419 /// let result = unsafe { &mut *result };
420 /// let handle = match OnBeforeParse::from_raw(args, result) {
421 /// Ok(handle) => handle,
422 /// Err(()) => return,
423 /// };
424 /// }
425 /// ```
426 pub fn from_raw(
427 args: *mut sys::OnBeforeParseArguments,
428 result: *mut sys::OnBeforeParseResult,
429 ) -> PluginResult<Self> {
430 if unsafe { (*args).__struct_size } < std::mem::size_of::<sys::OnBeforeParseArguments>()
431 || unsafe { (*result).__struct_size } < std::mem::size_of::<sys::OnBeforeParseResult>()
432 {
433 let message = "This plugin is not compatible with the current version of Bun.";
434 let mut log_options = sys::BunLogOptions {
435 __struct_size: std::mem::size_of::<sys::BunLogOptions>(),
436 message_ptr: message.as_ptr(),
437 message_len: message.len(),
438 path_ptr: unsafe { (*args).path_ptr },
439 path_len: unsafe { (*args).path_len },
440 source_line_text_ptr: std::ptr::null(),
441 source_line_text_len: 0,
442 level: BunLogLevel::BUN_LOG_LEVEL_ERROR as i8,
443 line: 0,
444 lineEnd: 0,
445 column: 0,
446 columnEnd: 0,
447 };
448 // SAFETY: The `log` function pointer is guaranteed to be valid by the Bun runtime.
449 unsafe {
450 ((*result).log.unwrap())(args, &mut log_options);
451 }
452 return Err(Error::IncompatiblePluginVersion);
453 }
454
455 Ok(Self {
456 args_raw: args,
457 result_raw: result,
458 compilation_context: std::ptr::null_mut() as *mut _,
459 __phantom: Default::default(),
460 })
461 }
462
463 pub fn path(&self) -> PluginResult<Cow<'_, str>> {
464 unsafe { get_from_raw_str((*self.args_raw).path_ptr, (*self.args_raw).path_len) }
465 }
466
467 pub fn namespace(&self) -> PluginResult<Cow<'_, str>> {
468 unsafe {
469 get_from_raw_str(
470 (*self.args_raw).namespace_ptr,
471 (*self.args_raw).namespace_len,
472 )
473 }
474 }
475
476 /// # Safety
477 /// This is unsafe as you must ensure that no other invocation of the plugin (or JS!)
478 /// simultaneously holds a mutable reference to the external.
479 ///
480 /// Get the external object from the `OnBeforeParse` arguments.
481 ///
482 /// The external object is set by the plugin definition inside of JS:
483 /// ```js
484 /// await Bun.build({
485 /// plugins: [
486 /// {
487 /// name: "my-plugin",
488 /// setup(builder) {
489 /// const native_plugin = require("./native_plugin.node");
490 /// const external = native_plugin.createExternal();
491 /// builder.external({ napiModule: native_plugin, symbol: 'onBeforeParse', external });
492 /// },
493 /// },
494 /// ],
495 /// });
496 /// ```
497 ///
498 /// The external object must be created from NAPI for this function to be safe!
499 ///
500 /// This function will return an error if the external object is not a
501 /// valid tagged object for the given type.
502 ///
503 /// This function will return `Ok(None)` if there is no external object
504 /// set.
505 ///
506 /// # Example
507 /// The code to create the external from napi-rs:
508 /// ```rs
509 /// #[no_mangle]
510 /// #[napi]
511 /// pub fn create_my_external() -> External<MyStruct> {
512 /// let external = External::new(MyStruct::new());
513 ///
514 /// external
515 /// }
516 /// ```
517 ///
518 /// The code to extract the external:
519 /// ```rust
520 /// let external = match handle.external::<MyStruct>() {
521 /// Ok(Some(external)) => external,
522 /// _ => {
523 /// handle.log_error("Could not get external object.");
524 /// return;
525 /// },
526 /// };
527 /// ```
528 pub unsafe fn external<'b, T: 'static + Sync>(
529 &self,
530 from_raw: unsafe fn(*mut c_void) -> Option<&'b T>,
531 ) -> PluginResult<Option<&'b T>> {
532 if unsafe { (*self.args_raw).external.is_null() } {
533 return Ok(None);
534 }
535
536 let external = unsafe { from_raw((*self.args_raw).external as *mut _) };
537
538 Ok(external)
539 }
540
541 /// The same as [`crate::bun_native_plugin::OnBeforeParse::external`], but returns a mutable reference.
542 ///
543 /// # Safety
544 /// This is unsafe as you must ensure that no other invocation of the plugin (or JS!)
545 /// simultaneously holds a mutable reference to the external.
546 pub unsafe fn external_mut<'b, T: 'static + Sync>(
547 &mut self,
548 from_raw: unsafe fn(*mut c_void) -> Option<&'b mut T>,
549 ) -> PluginResult<Option<&'b mut T>> {
550 if unsafe { (*self.args_raw).external.is_null() } {
551 return Ok(None);
552 }
553
554 let external = unsafe { from_raw((*self.args_raw).external as *mut _) };
555
556 Ok(external)
557 }
558
559 /// Get the input source code for the current file.
560 ///
561 /// On Windows, this function may return an `Err(Error::Utf8(...))` if the
562 /// source code contains invalid UTF-8.
563 pub fn input_source_code(&self) -> PluginResult<Cow<'_, str>> {
564 let fetch_result = unsafe {
565 ((*self.result_raw).fetchSourceCode.unwrap())(
566 self.args_raw as *const _,
567 self.result_raw,
568 )
569 };
570
571 if fetch_result != 0 {
572 Err(Error::Unknown)
573 } else {
574 // SAFETY: We don't hand out mutable references to `result_raw` so dereferencing here is safe.
575 unsafe {
576 get_from_raw_str((*self.result_raw).source_ptr, (*self.result_raw).source_len)
577 }
578 }
579 }
580
581 /// Set the output source code for the current file.
582 pub fn set_output_source_code(&mut self, source: String, loader: BunLoader) {
583 let source_cap = source.capacity();
584 let source = source.leak();
585 let source_ptr = source.as_mut_ptr();
586 let source_len = source.len();
587
588 if self.compilation_context.is_null() {
589 self.compilation_context = Box::into_raw(Box::new(SourceCodeContext {
590 source_ptr,
591 source_len,
592 source_cap,
593 }));
594
595 // SAFETY: We don't hand out mutable references to `result_raw` so dereferencing it is safe.
596 unsafe {
597 (*self.result_raw).plugin_source_code_context =
598 self.compilation_context as *mut c_void;
599 (*self.result_raw).free_plugin_source_code_context =
600 Some(free_plugin_source_code_context);
601 }
602 } else {
603 unsafe {
604 // SAFETY: If we're here we know that `compilation_context` is not null.
605 let context = &mut *self.compilation_context;
606
607 drop(String::from_raw_parts(
608 context.source_ptr,
609 context.source_len,
610 context.source_cap,
611 ));
612
613 context.source_ptr = source_ptr;
614 context.source_len = source_len;
615 context.source_cap = source_cap;
616 }
617 }
618
619 // SAFETY: We don't hand out mutable references to `result_raw` so dereferencing it is safe.
620 unsafe {
621 (*self.result_raw).loader = loader as u8;
622 (*self.result_raw).source_ptr = source_ptr;
623 (*self.result_raw).source_len = source_len;
624 }
625 }
626
627 /// Set the output loader for the current file.
628 pub fn set_output_loader(&self, loader: BunLoader) {
629 // SAFETY: We don't hand out mutable references to `result_raw` so dereferencing it is safe.
630 unsafe {
631 (*self.result_raw).loader = loader as u8;
632 }
633 }
634
635 /// Get the output loader for the current file.
636 pub fn output_loader(&self) -> BunLoader {
637 unsafe { std::mem::transmute((*self.result_raw).loader as u32) }
638 }
639
640 /// Log an error message.
641 pub fn log_error(&self, message: &str) {
642 self.log(message, BunLogLevel::BUN_LOG_LEVEL_ERROR)
643 }
644
645 /// Log a message with the given level.
646 pub fn log(&self, message: &str, level: BunLogLevel) {
647 let mut log_options = log_from_message_and_level(
648 message,
649 level,
650 unsafe { (*self.args_raw).path_ptr },
651 unsafe { (*self.args_raw).path_len },
652 );
653 unsafe {
654 ((*self.result_raw).log.unwrap())(self.args_raw, &mut log_options);
655 }
656 }
657}
658
659pub fn log_from_message_and_level(
660 message: &str,
661 level: BunLogLevel,
662 path: *const u8,
663 path_len: usize,
664) -> sys::BunLogOptions {
665 sys::BunLogOptions {
666 __struct_size: std::mem::size_of::<sys::BunLogOptions>(),
667 message_ptr: message.as_ptr(),
668 message_len: message.len(),
669 path_ptr: path as *const _,
670 path_len,
671 source_line_text_ptr: std::ptr::null(),
672 source_line_text_len: 0,
673 level: level as i8,
674 line: 0,
675 lineEnd: 0,
676 column: 0,
677 columnEnd: 0,
678 }
679}