Files
bun.sh/packages/bun-native-plugin-rs/src/lib.rs

680 lines
22 KiB
Rust

//! > ⚠️ 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.
//!
//! # Bun Native Plugins
//!
//! This crate provides a Rustified wrapper over the Bun's native bundler plugin C API.
//!
//! Some advantages to _native_ bundler plugins as opposed to regular ones implemented in JS:
//!
//! - Native plugins take full advantage of Bun's parallelized bundler pipeline and run on multiple threads at the same time
//! - Unlike JS, native plugins don't need to do the UTF-8 <-> UTF-16 source code string conversions
//!
//! What are native bundler plugins exactly? Precisely, they are NAPI modules which expose a C ABI function which implement a plugin lifecycle hook.
//!
//! The currently supported lifecycle hooks are:
//!
//! - `onBeforeParse` (called immediately before a file is parsed, allows you to modify the source code of the file)
//!
//! ## Getting started
//!
//! 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:
//!
//! ```bash
//! bun add -g @napi-rs/cli
//! napi new
//! ```
//!
//! Then install this crate:
//!
//! ```bash
//! cargo add bun-native-plugin
//! ```
//!
//! 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.
//!
//! For example, implementing `onBeforeParse`:
//!
//! ```rust
//! use bun_native_plugin::{OnBeforeParse};
//!
//! /// This is necessary for napi-rs to compile this into a proper NAPI module
//! #[napi]
//! pub fn register_bun_plugin() {}
//!
//! /// Use `no_mangle` so that we can reference this symbol by name later
//! /// when registering this native plugin in JS.
//! ///
//! /// Here we'll create a dummy plugin which replaces all occurrences of
//! /// `foo` with `bar`
//! #[no_mangle]
//! pub extern "C" fn on_before_parse_plugin_impl(
//! args: *const bun_native_plugin::sys::OnBeforeParseArguments,
//! result: *mut bun_native_plugin::sys::OnBeforeParseResult,
//! ) {
//! let args = unsafe { &*args };
//! let result = unsafe { &mut *result };
//!
//! // This returns a handle which is a safe wrapper over the raw
//! // C API.
//! let mut handle = OnBeforeParse::from_raw(args, result) {
//! Ok(handle) => handle,
//! Err(_) => {
//! // `OnBeforeParse::from_raw` handles error logging
//! // so it fine to return here.
//! return;
//! }
//! };
//!
//! let input_source_code = match handle.input_source_code() {
//! Ok(source_str) => source_str,
//! Err(_) => {
//! // If we encounter an error, we must log it so that
//! // Bun knows this plugin failed.
//! handle.log_error("Failed to fetch source code!");
//! return;
//! }
//! };
//!
//! let loader = handle.output_loader();
//! let output_source_code = source_str.replace("foo", "bar");
//! handle.set_output_source_code(output_source_code, loader);
//! }
//! ```
//!
//! Then compile this NAPI module. If you using napi-rs, the `package.json` should have a `build` script you can run:
//!
//! ```bash
//! bun run build
//! ```
//!
//! This will produce a `.node` file in the project directory.
//!
//! With the compiled NAPI module, you can now register the plugin from JS:
//!
//! ```js
//! const result = await Bun.build({
//! entrypoints: ["index.ts"],
//! plugins: [
//! {
//! name: "replace-foo-with-bar",
//! setup(build) {
//! const napiModule = require("path/to/napi_module.node");
//!
//! // Register the `onBeforeParse` hook to run on all `.ts` files.
//! // We tell it to use function we implemented inside of our `lib.rs` code.
//! build.onBeforeParse(
//! { filter: /\.ts/ },
//! { napiModule, symbol: "on_before_parse_plugin_impl" },
//! );
//! },
//! },
//! ],
//! });
//! ```
//!
//! ## Very important information
//!
//! ### Error handling and panics
//!
//! It is highly recommended to avoid panicking as this will crash the runtime. Instead, you must handle errors and log them:
//!
//! ```rust
//! let input_source_code = match handle.input_source_code() {
//! Ok(source_str) => source_str,
//! Err(_) => {
//! // If we encounter an error, we must log it so that
//! // Bun knows this plugin failed.
//! handle.log_error("Failed to fetch source code!");
//! return;
//! }
//! };
//! ```
//!
//! ### Passing state to and from JS: `External`
//!
//! 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.
//!
//! 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
//! the pointer and modify the data.
//!
//! 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.
//!
//! You would expose a NAPI module function which creates this state. Recall that state in native plugins must be threadsafe. This usually means
//! that your state must be `Sync`:
//!
//! ```rust
//! struct PluginState {
//! foo_count: std::sync::atomic::AtomicU32,
//! }
//!
//! #[napi]
//! pub fn create_plugin_state() -> External<PluginState> {
//! let external = External::new(PluginState {
//! foo_count: 0,
//! });
//!
//! external
//! }
//!
//!
//! #[napi]
//! pub fn get_foo_count(plugin_state: External<PluginState>) -> u32 {
//! let plugin_state: &PluginState = &plugin_state;
//! plugin_state.foo_count.load(std::sync::atomic::Ordering::Relaxed)
//! }
//! ```
//!
//! When you register your plugin from Javascript, you call the napi module function to create the external and then pass it:
//!
//! ```js
//! const napiModule = require("path/to/napi_module.node");
//! const pluginState = napiModule.createPluginState();
//!
//! const result = await Bun.build({
//! entrypoints: ["index.ts"],
//! plugins: [
//! {
//! name: "replace-foo-with-bar",
//! setup(build) {
//! build.onBeforeParse(
//! { filter: /\.ts/ },
//! {
//! napiModule,
//! symbol: "on_before_parse_plugin_impl",
//! // pass our NAPI external which contains our plugin state here
//! external: pluginState,
//! },
//! );
//! },
//! },
//! ],
//! });
//!
//! console.log("Total `foo`s encountered: ", pluginState.getFooCount());
//! ```
//!
//! Finally, from the native implementation of your plugin, you can extract the external:
//!
//! ```rust
//! pub extern "C" fn on_before_parse_plugin_impl(
//! args: *const bun_native_plugin::sys::OnBeforeParseArguments,
//! result: *mut bun_native_plugin::sys::OnBeforeParseResult,
//! ) {
//! let args = unsafe { &*args };
//! let result = unsafe { &mut *result };
//!
//! let mut handle = OnBeforeParse::from_raw(args, result) {
//! Ok(handle) => handle,
//! Err(_) => {
//! // `OnBeforeParse::from_raw` handles error logging
//! // so it fine to return here.
//! return;
//! }
//! };
//!
//! let plugin_state: &PluginState =
//! // This operation is only safe if you pass in an external when registering the plugin.
//! // If you don't, this could lead to a segfault or access of undefined memory.
//! match unsafe { handle.external().and_then(|state| state.ok_or(Error::Unknown)) } {
//! Ok(state) => state,
//! Err(_) => {
//! handle.log_error("Failed to get external!");
//! return;
//! }
//! };
//!
//!
//! // Fetch our source code again
//! let input_source_code = match handle.input_source_code() {
//! Ok(source_str) => source_str,
//! Err(_) => {
//! handle.log_error("Failed to fetch source code!");
//! return;
//! }
//! };
//!
//! // Count the number of `foo`s and add it to our state
//! let foo_count = source_code.matches("foo").count() as u32;
//! plugin_state.foo_count.fetch_add(foo_count, std::sync::atomic::Ordering::Relaxed);
//! }
//! ```
//!
//! ### Concurrency
//!
//! Your `extern "C"` plugin function can be called _on any thread_ at _any time_ and _multiple times at once_.
//!
//! Therefore, you must design any state management to be threadsafe
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
pub use anyhow;
pub use bun_macro::bun;
pub mod sys;
#[repr(transparent)]
pub struct BunPluginName(*const c_char);
impl BunPluginName {
pub const fn new(ptr: *const c_char) -> Self {
Self(ptr)
}
}
#[macro_export]
macro_rules! define_bun_plugin {
($name:expr) => {
pub static BUN_PLUGIN_NAME_STRING: &str = concat!($name, "\0");
#[no_mangle]
pub static BUN_PLUGIN_NAME: bun_native_plugin::BunPluginName =
bun_native_plugin::BunPluginName::new(BUN_PLUGIN_NAME_STRING.as_ptr() as *const _);
#[napi]
fn bun_plugin_register() {}
};
}
unsafe impl Sync for BunPluginName {}
use std::{
any::TypeId,
borrow::Cow,
cell::UnsafeCell,
ffi::{c_char, c_void},
marker::PhantomData,
str::Utf8Error,
sync::PoisonError,
};
#[repr(C)]
pub struct TaggedObject<T> {
type_id: TypeId,
pub(crate) object: Option<T>,
}
struct SourceCodeContext {
source_ptr: *mut u8,
source_len: usize,
source_cap: usize,
}
extern "C" fn free_plugin_source_code_context(ctx: *mut c_void) {
// SAFETY: The ctx pointer is a pointer to the `SourceCodeContext` struct we allocated.
unsafe {
drop(Box::from_raw(ctx as *mut SourceCodeContext));
}
}
impl Drop for SourceCodeContext {
fn drop(&mut self) {
if !self.source_ptr.is_null() {
// SAFETY: These fields come from a `String` that we allocated.
unsafe {
drop(String::from_raw_parts(
self.source_ptr,
self.source_len,
self.source_cap,
));
}
}
}
}
pub type BunLogLevel = sys::BunLogLevel;
pub type BunLoader = sys::BunLoader;
fn get_from_raw_str<'a>(ptr: *const u8, len: usize) -> PluginResult<Cow<'a, str>> {
let slice: &'a [u8] = unsafe { std::slice::from_raw_parts(ptr, len) };
// Windows allows invalid UTF-16 strings in the filesystem. These get converted to WTF-8 in Zig.
// Meaning the string may contain invalid UTF-8, we'll have to use the safe checked version.
#[cfg(target_os = "windows")]
{
std::str::from_utf8(slice)
.map(Into::into)
.or_else(|_| Ok(String::from_utf8_lossy(slice)))
}
#[cfg(not(target_os = "windows"))]
{
// SAFETY: The source code comes from Zig, which uses UTF-8, so this should be safe.
std::str::from_utf8(slice)
.map(Into::into)
.or_else(|_| Ok(String::from_utf8_lossy(slice)))
}
}
#[derive(Debug, Clone)]
pub enum Error {
Utf8(Utf8Error),
IncompatiblePluginVersion,
ExternalTypeMismatch,
Unknown,
LockPoisoned,
}
pub type PluginResult<T> = std::result::Result<T, Error>;
pub type Result<T> = anyhow::Result<T>;
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
fn description(&self) -> &str {
"description() is deprecated; use Display"
}
fn cause(&self) -> Option<&dyn std::error::Error> {
self.source()
}
}
impl From<Utf8Error> for Error {
fn from(value: Utf8Error) -> Self {
Self::Utf8(value)
}
}
impl<Guard> From<PoisonError<Guard>> for Error {
fn from(_: PoisonError<Guard>) -> Self {
Self::LockPoisoned
}
}
/// A safe handle for the arguments + result struct for the
/// `OnBeforeParse` bundler lifecycle hook.
///
/// This struct acts as a safe wrapper around the raw C API structs
/// (`sys::OnBeforeParseArguments`/`sys::OnBeforeParseResult`) needed to
/// implement the `OnBeforeParse` bundler lifecycle hook.
///
/// To initialize this struct, see the `from_raw` method.
pub struct OnBeforeParse<'a> {
pub args_raw: *mut sys::OnBeforeParseArguments,
result_raw: *mut sys::OnBeforeParseResult,
compilation_context: *mut SourceCodeContext,
__phantom: PhantomData<&'a ()>,
}
impl<'a> OnBeforeParse<'a> {
/// Initialize this struct from references to their raw counterparts.
///
/// This function will do a versioning check to ensure that the plugin
/// is compatible with the current version of Bun. If the plugin is not
/// compatible, it will log an error and return an error result.
///
/// # Example
/// ```rust
/// extern "C" fn on_before_parse_impl(args: *const sys::OnBeforeParseArguments, result: *mut sys::OnBeforeParseResult) {
/// let args = unsafe { &*args };
/// let result = unsafe { &mut *result };
/// let handle = match OnBeforeParse::from_raw(args, result) {
/// Ok(handle) => handle,
/// Err(()) => return,
/// };
/// }
/// ```
pub fn from_raw(
args: *mut sys::OnBeforeParseArguments,
result: *mut sys::OnBeforeParseResult,
) -> PluginResult<Self> {
if unsafe { (*args).__struct_size } < std::mem::size_of::<sys::OnBeforeParseArguments>()
|| unsafe { (*result).__struct_size } < std::mem::size_of::<sys::OnBeforeParseResult>()
{
let message = "This plugin is not compatible with the current version of Bun.";
let mut log_options = sys::BunLogOptions {
__struct_size: std::mem::size_of::<sys::BunLogOptions>(),
message_ptr: message.as_ptr(),
message_len: message.len(),
path_ptr: unsafe { (*args).path_ptr },
path_len: unsafe { (*args).path_len },
source_line_text_ptr: std::ptr::null(),
source_line_text_len: 0,
level: BunLogLevel::BUN_LOG_LEVEL_ERROR as i8,
line: 0,
lineEnd: 0,
column: 0,
columnEnd: 0,
};
// SAFETY: The `log` function pointer is guaranteed to be valid by the Bun runtime.
unsafe {
((*result).log.unwrap())(args, &mut log_options);
}
return Err(Error::IncompatiblePluginVersion);
}
Ok(Self {
args_raw: args,
result_raw: result,
compilation_context: std::ptr::null_mut() as *mut _,
__phantom: Default::default(),
})
}
pub fn path(&self) -> PluginResult<Cow<'_, str>> {
unsafe { get_from_raw_str((*self.args_raw).path_ptr, (*self.args_raw).path_len) }
}
pub fn namespace(&self) -> PluginResult<Cow<'_, str>> {
unsafe {
get_from_raw_str(
(*self.args_raw).namespace_ptr,
(*self.args_raw).namespace_len,
)
}
}
/// # Safety
/// This is unsafe as you must ensure that no other invocation of the plugin (or JS!)
/// simultaneously holds a mutable reference to the external.
///
/// Get the external object from the `OnBeforeParse` arguments.
///
/// The external object is set by the plugin definition inside of JS:
/// ```js
/// await Bun.build({
/// plugins: [
/// {
/// name: "my-plugin",
/// setup(builder) {
/// const native_plugin = require("./native_plugin.node");
/// const external = native_plugin.createExternal();
/// builder.external({ napiModule: native_plugin, symbol: 'onBeforeParse', external });
/// },
/// },
/// ],
/// });
/// ```
///
/// The external object must be created from NAPI for this function to be safe!
///
/// This function will return an error if the external object is not a
/// valid tagged object for the given type.
///
/// This function will return `Ok(None)` if there is no external object
/// set.
///
/// # Example
/// The code to create the external from napi-rs:
/// ```rs
/// #[no_mangle]
/// #[napi]
/// pub fn create_my_external() -> External<MyStruct> {
/// let external = External::new(MyStruct::new());
///
/// external
/// }
/// ```
///
/// The code to extract the external:
/// ```rust
/// let external = match handle.external::<MyStruct>() {
/// Ok(Some(external)) => external,
/// _ => {
/// handle.log_error("Could not get external object.");
/// return;
/// },
/// };
/// ```
pub unsafe fn external<'b, T: 'static + Sync>(
&self,
from_raw: unsafe fn(*mut c_void) -> Option<&'b T>,
) -> PluginResult<Option<&'b T>> {
if unsafe { (*self.args_raw).external.is_null() } {
return Ok(None);
}
let external = unsafe { from_raw((*self.args_raw).external as *mut _) };
Ok(external)
}
/// The same as [`crate::bun_native_plugin::OnBeforeParse::external`], but returns a mutable reference.
///
/// # Safety
/// This is unsafe as you must ensure that no other invocation of the plugin (or JS!)
/// simultaneously holds a mutable reference to the external.
pub unsafe fn external_mut<'b, T: 'static + Sync>(
&mut self,
from_raw: unsafe fn(*mut c_void) -> Option<&'b mut T>,
) -> PluginResult<Option<&'b mut T>> {
if unsafe { (*self.args_raw).external.is_null() } {
return Ok(None);
}
let external = unsafe { from_raw((*self.args_raw).external as *mut _) };
Ok(external)
}
/// Get the input source code for the current file.
///
/// On Windows, this function may return an `Err(Error::Utf8(...))` if the
/// source code contains invalid UTF-8.
pub fn input_source_code(&self) -> PluginResult<Cow<'_, str>> {
let fetch_result = unsafe {
((*self.result_raw).fetchSourceCode.unwrap())(
self.args_raw as *const _,
self.result_raw,
)
};
if fetch_result != 0 {
Err(Error::Unknown)
} else {
// SAFETY: We don't hand out mutable references to `result_raw` so dereferencing here is safe.
unsafe {
get_from_raw_str((*self.result_raw).source_ptr, (*self.result_raw).source_len)
}
}
}
/// Set the output source code for the current file.
pub fn set_output_source_code(&mut self, source: String, loader: BunLoader) {
let source_cap = source.capacity();
let source = source.leak();
let source_ptr = source.as_mut_ptr();
let source_len = source.len();
if self.compilation_context.is_null() {
self.compilation_context = Box::into_raw(Box::new(SourceCodeContext {
source_ptr,
source_len,
source_cap,
}));
// SAFETY: We don't hand out mutable references to `result_raw` so dereferencing it is safe.
unsafe {
(*self.result_raw).plugin_source_code_context =
self.compilation_context as *mut c_void;
(*self.result_raw).free_plugin_source_code_context =
Some(free_plugin_source_code_context);
}
} else {
unsafe {
// SAFETY: If we're here we know that `compilation_context` is not null.
let context = &mut *self.compilation_context;
drop(String::from_raw_parts(
context.source_ptr,
context.source_len,
context.source_cap,
));
context.source_ptr = source_ptr;
context.source_len = source_len;
context.source_cap = source_cap;
}
}
// SAFETY: We don't hand out mutable references to `result_raw` so dereferencing it is safe.
unsafe {
(*self.result_raw).loader = loader as u8;
(*self.result_raw).source_ptr = source_ptr;
(*self.result_raw).source_len = source_len;
}
}
/// Set the output loader for the current file.
pub fn set_output_loader(&self, loader: BunLoader) {
// SAFETY: We don't hand out mutable references to `result_raw` so dereferencing it is safe.
unsafe {
(*self.result_raw).loader = loader as u8;
}
}
/// Get the output loader for the current file.
pub fn output_loader(&self) -> BunLoader {
unsafe { std::mem::transmute((*self.result_raw).loader as u32) }
}
/// Log an error message.
pub fn log_error(&self, message: &str) {
self.log(message, BunLogLevel::BUN_LOG_LEVEL_ERROR)
}
/// Log a message with the given level.
pub fn log(&self, message: &str, level: BunLogLevel) {
let mut log_options = log_from_message_and_level(
message,
level,
unsafe { (*self.args_raw).path_ptr },
unsafe { (*self.args_raw).path_len },
);
unsafe {
((*self.result_raw).log.unwrap())(self.args_raw, &mut log_options);
}
}
}
pub fn log_from_message_and_level(
message: &str,
level: BunLogLevel,
path: *const u8,
path_len: usize,
) -> sys::BunLogOptions {
sys::BunLogOptions {
__struct_size: std::mem::size_of::<sys::BunLogOptions>(),
message_ptr: message.as_ptr(),
message_len: message.len(),
path_ptr: path as *const _,
path_len,
source_line_text_ptr: std::ptr::null(),
source_line_text_len: 0,
level: level as i8,
line: 0,
lineEnd: 0,
column: 0,
columnEnd: 0,
}
}