Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,60 @@ In contrast to solutions like Blazor, which attempt to bring the entire web plat

Bootsharp itself is built on top of [System.Runtime.InteropServices.JavaScript](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/import-export-interop?view=aspnetcore-8.0) introduced in .NET 7.

If you're looking to expose simple library API to JavaScript and don't need type declarations, Bootsharp would probably be an overkill. However, .NET's interop is low-level, doesn't support passing custom types by value and requires lots of boilerplate to author the bindings. It's impractical for large API surfaces.

With Bootsharp, you'll be able to just feed it your domain-specific interfaces and use them seamlessly from the other side, as if they were originally authored in TypeScript (and vice-versa). Additionally, Bootsharp provides an option to bundle all the binaries into single-file ES module and patches .NET's internal JavaScript code to make it compatible with constrained runtime environments, such as VS Code [web extensions](https://code.visualstudio.com/api/extension-guides/web-extensions).
If you need to expose a simple library API to JavaScript and don't require type declarations, Bootsharp is probably overkill. However, .NET's interop is low-level, lacks support for passing custom types by value, and requires extensive boilerplate to define bindings, making it impractical for large API surfaces.

With Bootsharp, you can simply provide your domain-specific interfaces and use them seamlessly on the other side, as if they were originally authored in TypeScript (and vice versa). This ensures a clear separation of concerns: your domain codebase won't be aware of the JavaScript environment—no need to annotate methods for interop or specify marshalling hints for arguments.

For example, consider the following abstract domain code:

```cs
public record Data (string Info, IReadOnlyList<Item> Items);
public record Result (View Header, View Content);
public interface IProvider { Data GetData (); }
public interface IGenerator { Result Generate (); }

public class Generator (IProvider provider) : IGenerator
{
public Result Generate ()
{
var data = provider.GetData();
// Process the data and generate result.
return result;
}
}
```
— the code doesn't use any JavaScript-specific APIs, making it fully testable and reusable. To expose it to JavaScript, all we need to do is add the following to `Program.cs` in a separate project for the WASM target:

```cs
using Bootsharp;
using Bootsharp.Inject;
using Microsoft.Extensions.DependencyInjection;

[assembly: JSImport(typeof(IProvider))]
[assembly: JSExport(typeof(IGenerator))]

// Bootsharp auto-injects implementation for 'IProvider'
// from JS and exposes 'Generator' APIs to JS.
new ServiceCollection()
.AddBootsharp()
.AddSingleton<IGenerator, Generator>()
.BuildServiceProvider()
.RunBootsharp();
```

— we can now provide implementation for `IProvider` and use `Generator` in JavaScript/TypeScript:

```ts
import bootsharp, { Provider, Generator } from "bootsharp";

// Implement 'IProvider'.
Provider.getData = () => ({
info: "...",
items: []
});

await bootsharp.boot();

// Use 'Generator'.
const result = Generator.generate();
```
20 changes: 0 additions & 20 deletions docs/guide/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,3 @@ import { Program } from "bootsharp";
const map = Program.map(["foo", "bar"], [0, 7]);
console.log(map.bar); // 7
```

## Configuring Serialization Behaviour

To override default JSON serializer options used for marshalling the interop data, use `Bootsharp.Serializer.Options` property before the program entry point is invoked:

```csharp
static class Program
{
static Program () // Static constructor is invoked before 'Main'
{
// Make enums serialize as strings.
var converter = new JsonStringEnumConverter();
Bootsharp.Serializer.Options.Converters.Add(converter);
}

public static void Main () { }
}
```

Refere to .NET docs for the available serialization options: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview.
8 changes: 4 additions & 4 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"docs:preview": "vitepress preview"
},
"devDependencies": {
"typescript": "5.7.2",
"@types/node": "22.10.5",
"vitepress": "1.5.0",
"typedoc-vitepress-theme": "1.1.1",
"typescript": "5.8.2",
"@types/node": "22.13.14",
"vitepress": "1.6.3",
"typedoc-vitepress-theme": "1.1.2",
"imgit": "0.2.1"
}
}
29 changes: 29 additions & 0 deletions samples/bench/bench.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Bench, hrtimeNow } from "tinybench";
import { init as initBootsharp } from "./bootsharp/init.mjs";
import { init as initDotNet } from "./dotnet/init.mjs";
import { init as initGo } from "./go/init.mjs";
import { init as initRust } from "./rust/init.mjs";

/**
* @typedef {Object} Exports
* @property {() => number} echoNumber
* @property {() => Data} echoStruct
* @property {(n: number) => number} fi
*/

await run("Rust", await initRust());
await run("DotNet", await initDotNet());
await run("Bootsharp", await initBootsharp());
await run("Go", await initGo());

/** @param {string} lang */
/** @param {Exports} exports */
async function run(lang, exports) {
console.log(`Running ${lang}...`);
const bench = new Bench({ hrtimeNow });
bench.add("Echo Number", exports.echoNumber);
bench.add("Echo Struct", exports.echoStruct);
bench.add("Fibonacci", () => exports.fi(25));
await bench.run();
console.table(bench.table())
}
18 changes: 18 additions & 0 deletions samples/bench/bootsharp/Boot.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Configuration>Release</Configuration>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<RunAOTCompilation>true</RunAOTCompilation>
<OptimizationPreference>Speed</OptimizationPreference>
<WasmEnableSIMD>true</WasmEnableSIMD>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Bootsharp" Version="0.5.0-alpha.32"/>
<PackageReference Include="Bootsharp.Inject" Version="*-*"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="*"/>
</ItemGroup>

</Project>
40 changes: 40 additions & 0 deletions samples/bench/bootsharp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Bootsharp;
using Bootsharp.Inject;
using Microsoft.Extensions.DependencyInjection;

[assembly: JSImport(typeof(IImport))]
[assembly: JSExport(typeof(IExport))]

new ServiceCollection()
.AddBootsharp()
.AddSingleton<IExport, Export>()
.BuildServiceProvider()
.RunBootsharp();

public struct Data
{
public string Info;
public bool Ok;
public int Revision;
public string[] Messages;
}

public interface IImport
{
int GetNumber ();
Data GetStruct ();
}

public interface IExport
{
int EchoNumber ();
Data EchoStruct ();
int Fi (int n);
}

public class Export (IImport import) : IExport
{
public int EchoNumber () => import.GetNumber();
public Data EchoStruct () => import.GetStruct();
public int Fi (int n) => n <= 1 ? n : Fi(n - 1) + Fi(n - 2);
}
12 changes: 12 additions & 0 deletions samples/bench/bootsharp/init.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import bootsharp, { Export, Import } from "./bin/bootsharp/index.mjs";
import { getNumber, getStruct } from "../fixtures.mjs";

/** @returns {Promise<import("../bench.mjs").Exports>} */
export async function init() {
Import.getNumber = getNumber;
Import.getStruct = getStruct;

await bootsharp.boot();

return { ...Export };
}
2 changes: 2 additions & 0 deletions samples/bench/bootsharp/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
1. Install .NET https://dotnet.microsoft.com/en-us/download
2. Run `dotnet publish -c Release`
17 changes: 17 additions & 0 deletions samples/bench/dotnet/DotNet.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Configuration>Release</Configuration>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RunAOTCompilation>true</RunAOTCompilation>
<OptimizationPreference>Speed</OptimizationPreference>
<WasmEnableSIMD>true</WasmEnableSIMD>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.Versioning.SupportedOSPlatform">
<_Parameter1>browser</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
39 changes: 39 additions & 0 deletions samples/bench/dotnet/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Runtime.InteropServices.JavaScript;
using System.Text.Json;
using System.Text.Json.Serialization;

public struct Data
{
public string Info;
public bool Ok;
public int Revision;
public string[] Messages;
}

[JsonSerializable(typeof(Data))]
internal partial class SourceGenerationContext : JsonSerializerContext;

public static partial class Program
{
public static void Main () { }

[JSExport]
public static int EchoNumber () => GetNumber();

[JSExport]
public static string EchoStruct ()
{
var json = GetStruct();
var data = JsonSerializer.Deserialize(json, SourceGenerationContext.Default.Data);
return JsonSerializer.Serialize(data, SourceGenerationContext.Default.Data);
}

[JSExport]
public static int Fi (int n) => n <= 1 ? n : Fi(n - 1) + Fi(n - 2);

[JSImport("getNumber", "x")]
private static partial int GetNumber ();

[JSImport("getStruct", "x")]
private static partial string GetStruct ();
}
22 changes: 22 additions & 0 deletions samples/bench/dotnet/init.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { dotnet } from "./bin/Release/net9.0/browser-wasm/AppBundle/_framework/dotnet.js";
import { getNumber, getStruct } from "../fixtures.mjs";

/** @returns {Promise<import("../bench.mjs").Exports>} */
export async function init() {
const runtime = await dotnet.withDiagnosticTracing(false).create();
const asm = runtime.getConfig().mainAssemblyName;

runtime.setModuleImports("x", {
getNumber,
getStruct: () => JSON.stringify(getStruct())
});

await runtime.runMain(asm, []);

const exports = await runtime.getAssemblyExports(asm);
return {
echoNumber: exports.Program.EchoNumber,
echoStruct: exports.Program.EchoStruct,
fi: exports.Program.Fi
};
}
2 changes: 2 additions & 0 deletions samples/bench/dotnet/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
1. Install .NET https://dotnet.microsoft.com/en-us/download
2. Run `dotnet publish -c Release`
18 changes: 18 additions & 0 deletions samples/bench/fixtures.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @typedef {Object} Data
* @property {string} info
* @property {boolean} ok
* @property {number} revision
* @property {string[]} messages
*/

/** @returns {number} */
export const getNumber = () => 42;

/** @returns {Data} */
export const getStruct = () => ({
info: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
ok: true,
revision: -112,
messages: ["foo", "bar", "baz", "nya", "far"]
});
2 changes: 2 additions & 0 deletions samples/bench/go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.wasm
wasm_exec.js
20 changes: 20 additions & 0 deletions samples/bench/go/init.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import fs from "fs";
import "./wasm_exec.js";
import { getNumber, getStruct } from "../fixtures.mjs";

/** @returns {Promise<import("../bench.mjs").Exports>} */
export async function init() {
global.getNumber = getNumber;
global.getStruct = getStruct;

const bin = await WebAssembly.compile(fs.readFileSync("./go/main.wasm"));
const go = new Go();
const wasm = await WebAssembly.instantiate(bin, go.importObject);
go.run(wasm);

return {
echoNumber: global.echoNumber,
echoStruct: global.echoStruct,
fi: global.fi
};
}
46 changes: 46 additions & 0 deletions samples/bench/go/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"encoding/json"
"syscall/js"
)

type Data struct {
Info string `json:"Info"`
Ok bool `json:"Ok"`
Revision int `json:"Revision"`
Messages []string `json:"Messages"`
}

func main() {
js.Global().Set("echoNumber", js.FuncOf(echoNumber))
js.Global().Set("echoStruct", js.FuncOf(echoStruct))
js.Global().Set("fi", js.FuncOf(fi))
<-make(chan struct{})
}

func echoNumber(_ js.Value, _ []js.Value) any {
return js.Global().Call("getNumber").Int()
}

func echoStruct(_ js.Value, _ []js.Value) any {
jsonStr := js.Global().Call("getStruct").String()
var data Data
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return "Error: " + err.Error()
}
resultJson, _ := json.Marshal(data)
return string(resultJson)
}

func fi(_ js.Value, args []js.Value) any {
n := args[0].Int()
return fibonacci(n)
}

func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
3 changes: 3 additions & 0 deletions samples/bench/go/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
1. Install Go https://go.dev/dl
2. Copy `{GO_INSTALL_DIR}/lib/wasm/wasm_exec.js` to this folder
3. Run `& { $env:GOOS="js"; $env:GOARCH="wasm"; go build -o main.wasm main.go }`
5 changes: 5 additions & 0 deletions samples/bench/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"devDependencies": {
"tinybench": "^4.0.1"
}
}
Loading
Loading