bun icon indicating copy to clipboard operation
bun copied to clipboard

Feature Request: Embedding Bun within other native applications (like a Rust program)

Open alshdavid opened this issue 1 year ago • 5 comments

What is the problem this feature would solve?

Many native projects would like to be able to embed a JavaScript runtime that can leverage the Nodejs ecosystem and Nodejs standard library.

  • AWS Lambda
  • Supabase
  • Bundlers (Parcel, Rspack, etc)
  • Mongo
  • Applications that want to use JavaScript for plugins (like Lapce)
  • etc

What is the feature you are proposing to solve the problem?

Offer a dynamic C library (or a Rust crate) that allows Bun to be embedded within another native application

What alternatives have you considered?

  • Nodejs Embedding API
    • Very difficult to use, many projects try to use this and drop it
  • Deno
    • Embedding Deno is reasonably easy to get started with however using the Nodejs standard library is not easy. Deno requires you to fork the Deno CLI to extract support for the Nodejs API. Additionally, due to differences between Deno and Nodejs, certain features (like workspaces support) do not work as expected.

alshdavid avatar Jun 20 '24 22:06 alshdavid

+1 from my side cases: adding bun as a scripting provider to game engines

0xF6 avatar Jun 23 '24 19:06 0xF6

@0xF6 I was looking for that as well.

Basically what I think Bun could do there:

  1. Offer an option to bun build --compile into a shared library instead of an exe, this shouldn't be too complicated.
  2. Extend bun ffi to also work backwards, i.e. define exported C ABI functions from javascript.

DoctorGester avatar Aug 13 '24 11:08 DoctorGester

+1

bayasdev avatar Sep 04 '24 02:09 bayasdev

+1

paranoikcodit avatar Oct 05 '24 08:10 paranoikcodit

+1

treesandflowers avatar Oct 18 '24 23:10 treesandflowers

+1

gornostay25 avatar Nov 19 '24 16:11 gornostay25

+1

JorgeO3 avatar Jan 11 '25 13:01 JorgeO3

+1

jamroks avatar Feb 15 '25 18:02 jamroks

+1 tinybun

chaoyangnz avatar Feb 22 '25 21:02 chaoyangnz

+1

mikehostetler avatar Feb 23 '25 16:02 mikehostetler

I'm willing to donate some money if this is implemented. Anyone want to join?

jcubic avatar Feb 26 '25 17:02 jcubic

I'm willing to donate some money if this is implemented. Anyone want to join?

Yes. I would throw in a few thousand USD. Wish I could do more, but it would be a personal contribution.

masegraye avatar Mar 09 '25 15:03 masegraye

+1

D3m4ciaa avatar Mar 15 '25 10:03 D3m4ciaa

I'm willing to donate some money if this is implemented. Anyone want to join?

Not to sound rude or anything - is that a real suggestion? How do such bounties work? I'm thinking I might take on the task

alshdavid avatar Apr 11 '25 23:04 alshdavid

+1

Gabriel-Grechuk avatar Apr 15 '25 13:04 Gabriel-Grechuk

Update, I have taken on the task of adding C bindings for Nodejs along with writing a high level Rust API which features;

  • non blocking calls on the main thread (via an async Rust runtime that works cooperatively with the Node.js event loop)
  • a high level API forked from napi-rs.
  • Passing options into Nodejs contexts
  • Evaluating CJS and MJS code
  • Executing native code in the Nodejs context

This meets pretty much all of my use cases (other than libnode not being compilable to a static library). I might try my hand at doing the same for Bun

alshdavid avatar May 11 '25 08:05 alshdavid

+1, but my use case is slightly different: I'd like to use Bun as an embedded, sandboxed Javascript runtime in Zig project. Like QuickJS. Is this already possible?

yhslai avatar Jun 07 '25 15:06 yhslai

+1, but my use case is slightly different: I'd like to use Bun as an embedded, sandboxed Javascript runtime in Zig project. Like QuickJS. Is this already possible?

I don't know if anything is published but you could probably fork Bun and rewrite the CLI entrypoints to be lib entrypoints. I did that with Deno+Rust which worked well enough

alshdavid avatar Jun 07 '25 23:06 alshdavid

I’ve added an option to build Bun as a static library and have successfully tested calling it from Golang. Below is the Go code I used for testing:

package main

/*
#cgo linux,amd64 CFLAGS: -fsanitize=address
#cgo linux,amd64 LDFLAGS: -fsanitize=address -Wl,--strip-all -Wl,--strip-debug -Wl,--discard-all -fuse-ld=lld -no-pie -fsanitize=address

//ZIG_LIBS
#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/libs/linux/x86_64 -Wl,--whole-archive -lbun -Wl,--no-whole-archive

//CPP_LIBS
#cgo linux,amd64 LDFLAGS: -lzstd -lbrotlidec -lhwy -lssl -lbrotlicommon
#cgo linux,amd64 LDFLAGS: -larchive -lcrypto -lbrotlienc -lz -lcares -ltcc -lls-hpack
#cgo linux,amd64 LDFLAGS: -lsqlite3 -lhdr_histogram_static -ldeflate -ldecrepit -llolhtml

//WEBKIT_LIBS
#cgo linux,amd64 LDFLAGS: -lJavaScriptCore -lWTF -lbmalloc -licui18n -licuuc -licudata -licutu
#cgo linux,amd64 LDFLAGS: -lstdc++ -latomic

#include "zgo.h"
*/
import "C"

func main() {
	C.run_js_with_bun()
}

The required changes are available in my forked branch, and you can find the commits below:

These changes introduce a new option to build Bun as a static library using the bun run build:lib:release command.

0xbytejay avatar Aug 14 '25 09:08 0xbytejay

@0xbytejay Want to raise a PR? I can start writing Rust bindings for it. Would be good to have Github releases of the static libs too

alshdavid avatar Aug 14 '25 10:08 alshdavid

@0xbytejay Want to raise a PR? I can start writing Rust bindings for it. Would be good to have Github releases of the static libs too

Thanks for the suggestion! Currently, the project only exports the run_js_with_bun method for testing purposes, just for initial testing. I’m not ready to submit a PR yet because the functionality is still in its early stages. However, I will continue working on it in my forked branch and will try to export more functions for more complete functionality in the future. The release for the static libraries will also be published in the near future.

Once the functionality is more complete, I’ll consider submitting a PR. Feel free to keep an eye on the updates, and let me know if you have any other thoughts or suggestions!

0xbytejay avatar Aug 14 '25 11:08 0xbytejay

@0xbytejay You can create a Draft PR, so everybody will be able to track progress. The PR doesn't need to be ready for merge. You will also see if there are any breaking changes and if the code is mergeable.

jcubic avatar Aug 14 '25 11:08 jcubic

the project only exports the run_js_with_bun method for testing purposes

@0xbytejay this is a similar approach to what I am doing over on the Nodejs side (PR). The idea is to export a simple C function that mimics the CLI - and later add more features.

So I'm adding node_embedding_start which mirrors node cli commands. For example

# bash
node --test ./foo.test.ts

# is equivalent to Rust:
let status_code: usize = node_embedding_start(&["nil", "--test", "./foo.test.ts"]);

Internally node exposes the napi bindings and v8 bindings so you can interact with them without needing additional features in the embedder API - though we are looking at additional features for things like controlling the event loop, spawning isolates, and so on.

Despite only having a single function that only launches Node, the existing napi bindings are powerful enough to enable a very feature rich high level API to interact with repo. The annoying thing about libnode is it can't (or at least I haven't been able to) compile it to a static library - which makes consuming & distributing it pretty unergonomic.

I believe Bun supports much of the napi bindings so we can actually get pretty far with just an entry point.

alshdavid avatar Aug 14 '25 23:08 alshdavid

So I'm adding node_embedding_start which mirrors node cli commands. For example

bash

node --test ./foo.test.ts

@alshdavid Thanks for your suggestion! I’ve done some initial testing on calling the Bun CLI to execute JavaScript code and interacting with JSValue. Passing command-line arguments to run JS files and reading strings from JS works fine. Here’s my repo for reference: go_call_bun_demo, which includes Golang test code, the static library, and header files.

One issue I’ve noticed is that calling JS functions from Golang can sometimes cause the program to crash randomly.

0xbytejay avatar Aug 15 '25 16:08 0xbytejay

@alshdavid

We have implemented the ability for Go to send commands to Bun to execute JavaScript scripts and register JS callback functions that can be invoked from Go. This enables communication between Go and JS.


Go Implementation

package main

/*
// Include Bun C API
#cgo CFLAGS: -I${SRCDIR}/include

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "lbun.h"

typedef bool (*JSCallback)(int16_t);

static bool callJSCallback(JSCallback cb, int16_t i) {
    return cb(i);
}
*/
import "C"
import (
	"context"
	"fmt"
	"log"
	"os"
	"runtime"
	"time"
	"unsafe"
)

var jsCallback unsafe.Pointer

var ctx, ready = context.WithCancel(context.Background())

//export BeforeLoadEntryPoint
func BeforeLoadEntryPoint(vm *C.VirtualMachine) {
	log.Println("Bun: BeforeLoadEntryPoint")
}

//export RegisterJSCallback
func RegisterJSCallback(globalThis *C.JSGlobalObject, str *C.char, ptr unsafe.Pointer) C.JSValue {
	defer ready()
	log.Printf("Bun: RegisterJSCallback: %s", C.GoString(str))
	jsCallback = ptr
	return 0x7
}

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Need script path")
		return
	}

	args := []string{"run", os.Args[1]}

	cArgs := make([]*C.char, len(args))
	for i, s := range args {
		cArgs[i] = C.CString(s)
		defer C.free(unsafe.Pointer(cArgs[i]))
	}

	var p runtime.Pinner
	p.Pin(&cArgs[0])
	defer p.Unpin()

	go func() {
		runtime.LockOSThread()
		defer runtime.UnlockOSThread()
		C.startBunCommand((*C.GoSliceHeader)(unsafe.Pointer(&cArgs)))
	}()

	<-ctx.Done()

	go func() {
		var goValue int16 = 0
		for {
			result := bool(C.callJSCallback((C.JSCallback)(jsCallback), C.int16_t(goValue)))
			log.Printf("Result from JS: %v", result)
			goValue++
			time.Sleep(2 * time.Second)
		}
	}()

	<-make(chan interface{})
}

JS Implementation

import koffi from 'koffi';

console.log("Start Running!");

// Define JS callback type
const PrintCallback = koffi.proto('bool PrintCallback(uint16_t)');

// Register JS callback
let cb = koffi.register(function(arg){
    console.log(`js callback called from golang, value: ${arg}`);
    return arg % 2 === 0;
}, koffi.pointer(PrintCallback));

// Expose callback to Go
globalThis.registerCallback("jsCallbackDemo", Number(koffi.address(cb)));

// JS independent interval task
let count = 0;
setInterval(() => {
    console.log(`Count: ${count++}`);
}, 1000);

Current Behavior

  1. Go starts Bun and runs a JS script via Command.
  2. JS registers a callback function that Go can invoke.
  3. Go can call JS callback periodically and receives the returned boolean.
  4. JS can run independent interval tasks simultaneously.

0xbytejay avatar Aug 17 '25 03:08 0xbytejay

i shamelessly just run bun inside of rust and it works well:

https://github.com/mediar-ai/terminator/blob/main/crates/terminator-mcp-agent/src/scripting_engine.rs

louis030195 avatar Nov 12 '25 17:11 louis030195

@louis030195 executing bun binary from Rust is not the same as embedding Bun as a shared library so it can be included with output file without the need to install it.

jcubic avatar Nov 12 '25 19:11 jcubic