BunSharp 0.1.11
dotnet add package BunSharp --version 0.1.11
NuGet\Install-Package BunSharp -Version 0.1.11
<PackageReference Include="BunSharp" Version="0.1.11" />
<PackageVersion Include="BunSharp" Version="0.1.11" />
<PackageReference Include="BunSharp" />
paket add BunSharp --version 0.1.11
#r "nuget: BunSharp, 0.1.11"
#:package BunSharp@0.1.11
#addin nuget:?package=BunSharp&version=0.1.11
#tool nuget:?package=BunSharp&version=0.1.11
<div align="center"> <img src="assets/BunSharp.png" width="200" alt="BunSharp" />
BunSharp
</div>
BunSharp is a .NET binding for the libbun embed API (libbun based on Bun). It lets you create a Bun runtime, execute JavaScript or TypeScript, and export C# types into the JS environment.
Features
- Evaluate JavaScript and TypeScript from .NET
- Register host functions on the JS global object
- Export C# classes with
JSExportAttribute - Support instance methods, instance properties, selected static members, and dedicated
byte[]binary marshalling viaUint8Array/ArrayBuffer - Support
T[]arrays as parameters and return values, including nested arrays (T[][]) and arrays of exported classes - Support explicit persistent JS references via
JSObjectRef,JSFunctionRef,JSArrayRef,JSArrayBufferRef,JSTypedArrayRef, andJSBufferRef - No runtime reflection; AOT friendly
Requirements
- .NET 10.0 or later
| Platform | Architecture | Supported |
|---|---|---|
| Windows | x64 | ✅ |
| Linux | x64 | ✅ |
| macOS | arm64 | ✅ |
Installation
dotnet add package BunSharp
Or in your project file:
<ItemGroup>
<PackageReference Include="BunSharp" Version="x.y.z" />
</ItemGroup>
BunSharp automatically pulls in BunSharp.Generator as a Roslyn analyzer. No additional setup is required for JSExport to work.
Explicit Generator Configuration
If you need to pin the generator version independently from the main package, exclude the bundled analyzer and add BunSharp.Generator directly:
<ItemGroup>
<PackageReference Include="BunSharp" Version="x.y.z" ExcludeAssets="analyzers" />
<PackageReference Include="BunSharp.Generator" Version="x.y.z" PrivateAssets="all" />
</ItemGroup>
Note: If analyzers are disabled without the explicit reference above,
JSExportwill not generate the required glue code and compilation will fail.
Basic Usage
using BunSharp;
using var runtime = BunRuntime.Create();
var context = runtime.Context;
context.Evaluate("globalThis.answer = 1 + 1;");
var result = context.GetProperty(context.GlobalObject, "answer");
Console.WriteLine(context.ToInt32(result));
Export a C# Class to JavaScript
using BunSharp;
[JSExport]
public sealed class DemoGreeter
{
public DemoGreeter(string name, byte[] payload)
{
Name = name;
Payload = payload;
}
public string Name { get; set; }
public byte[] Payload { get; set; }
public string describe()
{
return $"{Name}:{Payload.Length}";
}
public static string Version => "v1";
[JSExport(false)]
public string Hidden => "hidden";
}
using var runtime = BunRuntime.Create();
var context = runtime.Context;
context.ExportType<DemoGreeter>();
var value = context.Evaluate(@"(() => {
const greeter = new DemoGreeter('Ada', new Uint8Array([1, 2, 3, 4]));
return `${greeter.describe()}|${greeter.name}|${greeter.payload.length}|${DemoGreeter.version}`;
})()");
Console.WriteLine(context.ToManagedString(value));
// Ada:4|Ada|4|v1
JSExport Rules
[JSExport] // enable export
[JSExport(true)] // same as above
[JSExport("name")] // enable export and override the JS name
[JSExport(false)] // disable export
- Apply
JSExportto a class to export it - Public members of an exported class are included by default if they use a supported export shape
- Class names stay unchanged by default
- Method and property names are exported as camelCase by default
JSExport(false)excludes a member from export- Only members that are actually exported participate in generator diagnostics
Compile-time restrictions:
- Static
JSObjectRef,JSFunctionRef,JSArrayRef,JSArrayBufferRef,JSTypedArrayRef, andJSBufferRefproperties and static method return values are rejected withBSG010 - Static delegate properties and static delegate method return values are rejected with
BSG011
These diagnostics validate exported member shapes only. If application code stores runtime-affine values in its own global static state outside exported members, BunSharp cannot prove that code is safe across runtime lifetimes.
Constructors
Exported classes can expose multiple JS-callable constructors, but BunSharp still publishes a single JavaScript new Type(...) entry point. The generator selects a constructor by JS-visible argument count.
publicconstructors are JS-callable by default unless they are marked withJSExport(false)internalconstructors are not JS-callable unless they are explicitly marked withJSExportorJSExport(true)- If multiple JS-callable constructors have the same JS-visible argument count, compilation fails
BunContextcan be injected into exported constructors and instance methods; it does not count toward the JS-visible argument countJSExport("name")andStable = trueare not valid on constructors
Current scope:
- Constructor overload selection uses JS-visible argument count only
- Optional/default-value parameters and
paramsconstructors are not supported
Arrays
T[] is supported wherever any other type is supported: constructor parameters, method parameters, return values, and properties. Supported element types are bool, int, double, string, byte[], BunValue, any [JSExport] class, and nested arrays.
A JS Array maps to a C# T[]; null and undefined map to null.
[JSExport]
public sealed class DataService
{
public DataService() { }
public string[] reverseNames(string[] names)
{
Array.Reverse(names);
return names;
}
public DemoGreeter[] makeGreeters(string[] names)
=> names.Select(n => new DemoGreeter(n, [])).ToArray();
public string[][] transpose(string[][] matrix) { /* ... */ }
public static string[] Tags => ["fast", "aot", "ts"];
}
const svc = new DataService();
console.log(svc.reverseNames(["a", "b", "c"]));
console.log(svc.makeGreeters(["Alice", "Bob"])[0].describe());
console.log(DataService.tags);
Note:
byte[]does not use the generalT[]mapper. JavaScript inputs must beUint8ArrayorArrayBuffer, managedbyte[]values are exported asUint8Array, and ordinary JS arrays are rejected forbyte[]parameters and properties.
Explicit Reference Types
string, byte[], and T[] keep copy or snapshot semantics. Use explicit reference wrappers only when JS identity or shared backing storage must outlive the current call.
These wrappers use explicit ownership semantics. If a C# wrapper instance becomes unreachable and is later garbage-collected, BunSharp queues the underlying JS release back onto the runtime-owning thread so the JS object can become collectible too. That path is intentionally non-deterministic. Prefer calling Dispose() explicitly when the reference is no longer needed; finalizer-backed release, runtime teardown, and exported-instance cleanup are fallback release paths, not the normal ownership model.
JSObjectRef: retain a JS object across calls and property writesJSFunctionRef: retain a JS function and call it later from C#JSArrayRef: retain a live JSArraywith stable identityJSArrayBufferRef: retain a sharedArrayBufferJSTypedArrayRef: retain a shared typed array and inspect its native layoutJSBufferRef: retain aUint8Arrayor Buffer-like byte view explicitly
Prefer keeping your domain model plain and isolating these wrappers in a small JS-facing bridge or facade.
[JSExport]
public sealed class BinaryBridge : IDisposable
{
public BinaryBridge(JSFunctionRef onFlush)
{
// Constructor parameters are retained only because we store them.
OnFlush = onFlush;
}
public JSFunctionRef? OnFlush { get; private set; }
public JSArrayRef? Children { get; private set; }
public void RememberChildren(JSArrayRef children)
{
Children?.Dispose();
Children = children;
}
public JSArrayBufferRef? SharedBuffer { get; private set; }
public void RememberSharedBuffer(JSArrayBufferRef buffer)
{
SharedBuffer?.Dispose();
SharedBuffer = buffer;
}
public void Dispose()
{
OnFlush?.Dispose();
OnFlush = null;
Children?.Dispose();
Children = null;
SharedBuffer?.Dispose();
SharedBuffer = null;
}
}
Supported export shapes for these wrappers are constructor parameters, method parameters, instance properties, and instance method return values. Static properties and static method return values that use these wrappers are rejected with BSG010.
Passing a wrapper as a constructor parameter, method parameter, or property-setter argument does not automatically make it live for future calls. The reference stays alive only while your managed object keeps that wrapper reachable. If a later method needs the same JS object, function, or buffer, assign the incoming wrapper to a field or property yourself and dispose any replaced reference deliberately.
Dispose guidance: Call
Dispose()as soon as you are done with an explicit reference wrapper. The finalizer path exists so abandoned wrappers do not keep JS objects protected forever, but it should be treated as a safety net rather than the primary lifetime model.
Note:
BunValueis still supported, but it should be treated as a temporary value channel. Use explicit reference wrappers only when the value must outlive the current call, preserve JS identity, or expose shared backing storage intentionally.
Stable Identity
Keep the C# type plain and use Stable = true when you want stable JS identity for exported byte[] or T[] properties and method return values.
[JSExport]
public sealed class IdentityOptionDemo
{
[JSExport(Stable = true)]
public string[] Items { get; set; } = ["a", "b"];
[JSExport(Stable = true)]
public byte[] Payload { get; set; } = [1, 2, 3];
private readonly string[] _tags = ["fast", "stable"];
[JSExport(Stable = true)]
public string[] getTags()
{
return _tags;
}
}
Repeated JS reads or method calls that observe the same managed array reference reuse the same JS Array or Uint8Array. If the source switches to another reference and later switches back, JS receives a new object.
Stable applies only to exported properties and method return values. It does not apply to constructors or parameters. If you need to retain an incoming plain byte[] or T[], store it in your own state. If you need the original JS object identity or shared backing storage itself, use explicit reference wrappers instead of Stable.
Current implementation note: when ordinary C# code directly reassigns a Stable property, the existing JS-side cache is not cleared immediately. It is replaced on the next related JS read, or released during object or runtime cleanup.
Stable should be treated as stable identity plus snapshot reuse, not as a live synchronized view over mutable managed arrays. If the same managed byte[] or T[] instance is mutated in place, existing JavaScript Uint8Array or Array objects are not guaranteed to observe the new contents immediately. When the API needs shared backing storage or explicit live reference semantics, prefer JSBufferRef, JSTypedArrayRef, or JSArrayBufferRef instead of Stable.
Current scope:
Stableis supported on exportedbyte[]andT[]properties and method return values, plus delegate properties and delegate method return values where stable function-reference semantics are the default.
Delegates
Exported instance delegate properties and instance delegate method return values are supported and use stable function-reference semantics.
public delegate string MessageCallback(string message);
[JSExport]
public sealed class CallbackBridge
{
public MessageCallback? Callback { get; set; }
public MessageCallback GetCallback()
{
return message => $"default:{message}";
}
}
Rules:
- Delegate properties default to stable behavior
- Delegate method return values default to stable behavior
- Explicit
Stable = trueis allowed - Explicit
Stable = falseis rejected by the generator - Static delegate properties and static delegate method return values are rejected with
BSG011
When JS assigns a function to a delegate property, C# sees a typed delegate wrapper. When C# assigns a delegate or returns one from a method, JS sees a callable function, and repeated reads or returns reuse the same JS function object while the managed delegate reference stays the same.
Delegate parameters are not treated as stable exports. If you need to retain an incoming delegate, store it explicitly or use JSFunctionRef when the original JS function identity matters, and dispose that JSFunctionRef explicitly when you release it.
Host Functions
using BunSharp;
using var runtime = BunRuntime.Create();
var context = runtime.Context;
var hello = context.CreateFunction(
"helloFromDotNet",
static (ctx, args, _) =>
{
var name = args.Length > 0 ? ctx.ToManagedString(args[0]) : "world";
return ctx.CreateString($"Hello, {name}, from .NET.");
},
argCount: 1);
context.SetProperty(context.GlobalObject, "helloFromDotNet", hello);
context.Evaluate("console.log(helloFromDotNet('Bun')); ");
Event Loop Integration
RunPendingJobs() performs a single non-blocking tick and returns a BunPendingJobsResult:
| Value | Meaning |
|---|---|
Idle |
Fully idle — no active handles or pending work. |
Spin |
More work is runnable immediately; call again without waiting. |
Wait |
Runtime is active but waiting for I/O or timers; return to your host loop. |
On macOS and Linux you can poll EventFileDescriptor and use GetWaitHint() as the timeout so JS timers fire on time; on Windows EventFileDescriptor returns -1.
For a cross-platform wake-up path, register SetEventCallback(). The callback runs on a Bun-managed background thread, so it should only signal your host loop and let the owning thread call RunPendingJobs() later. If that callback throws, BunSharp reports the failure through the runtime Error event instead of silently discarding it.
Use the runtime Error event for diagnostics from background event callbacks, finalizer fallback paths, and cleanup aggregation. Handlers run synchronously on the reporting thread, so background event-callback failures are reported from a Bun-managed background thread and cleanup/finalizer failures may be reported during runtime teardown.
BunSharp does not catch exceptions thrown by Error handlers. If a handler throws, that exception propagates through the current reporting path immediately.
using BunSharp;
using var runtime = BunRuntime.Create();
runtime.Error += static (_, error) =>
{
Console.Error.WriteLine($"[{error.Source}] {error.Exception.Message}");
};
runtime.SetEventCallback(static (_, _) =>
{
// Wake your UI loop here, e.g. post to SynchronizationContext or enqueue work.
});
BunPendingJobsResult result;
while ((result = runtime.RunPendingJobs()) != BunPendingJobsResult.Idle)
{
// Spin means more work is ready now; Wait means the runtime is
// waiting for I/O or timers — yield to your host loop.
}
Contributing
Bug reports and pull requests are welcome on GitHub. Please open an issue before submitting large changes.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.