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}