bun
bun copied to clipboard
Feature Request: Embedding Bun within other native applications (like a Rust program)
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.
+1 from my side cases: adding bun as a scripting provider to game engines
@0xF6 I was looking for that as well.
Basically what I think Bun could do there:
- Offer an option to
bun build --compileinto a shared library instead of an exe, this shouldn't be too complicated. - Extend bun ffi to also work backwards, i.e. define exported C ABI functions from javascript.
+1
+1
+1
+1
+1
+1
+1 tinybun
+1
I'm willing to donate some money if this is implemented. Anyone want to join?
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.
+1
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
+1
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
+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?
+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
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 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
@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 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.
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.
So I'm adding
node_embedding_startwhich mirrorsnodecli commands. For examplebash
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.
@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
- Go starts Bun and runs a JS script via Command.
- JS registers a callback function that Go can invoke.
- Go can call JS callback periodically and receives the returned boolean.
- JS can run independent interval tasks simultaneously.
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 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.