()
- .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();
-```
+https://bootsharp.com/guide
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index a5163284..6d4a8080 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -75,5 +75,5 @@ export default defineConfig({
"/api/": [{ text: "Reference", items: (await import("./../api/typedoc-sidebar.json")).default }]
}
},
- sitemap: { hostname: "https://sharp.elringus.com" }
+ sitemap: { hostname: "https://bootsharp.com" }
});
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
index 4082a6d1..4cc1d71d 100644
--- a/docs/guide/getting-started.md
+++ b/docs/guide/getting-started.md
@@ -46,6 +46,10 @@ public static partial class Program
}
```
+::: info NOTE
+Authoring interop via static methods is impractical for large API surfaces—it's shown here only as a simple way to get started. For real projects, consider using [interop interfaces](/guide/interop-interfaces) instead.
+:::
+
## Compile ES Module
Run following command under the solution root:
diff --git a/docs/guide/index.md b/docs/guide/index.md
index e80761e9..989ad9e4 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -2,25 +2,25 @@
## What?
-Bootsharp is a solution for building web applications, where domain is authored in .NET C# and is consumed by a standalone JavaScript or TypeScript project.
+Bootsharp is a solution for building web applications where the domain logic is authored in .NET C# and consumed by a standalone JavaScript or TypeScript project.
## Why?
-C# is a popular language for building maintainable software with complex domain logic, such as enterprise and financial applications. However, its frontend capabilities are lacking, especially compared to the web ecosystem.
+C# is a popular language for building maintainable software with complex domain logic, such as enterprise and financial applications. However, its frontend capabilities are lacking—especially when compared to the web ecosystem.
-Web platform is the industry-standard for building modern user interfaces. It has best in class tooling and frameworks, such as [React](https://react.dev) and [Svelte](https://svelte.dev) — allowing to build better frontends faster, compared to any other language/platform ecosystem.
+The web platform is the industry standard for building modern user interfaces. It offers best-in-class tooling and frameworks, such as [React](https://react.dev) and [Svelte](https://svelte.dev), enabling developers to build better frontends faster than with any other language or platform.
-In contrast to solutions like [Blazor](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor), which attempt to bring the entire web platform inside .NET (effectively reversing natural workflow), Bootsharp facilitates high-level interoperation between C# and TypeScript, allowing to build domain and UI layers under their natural ecosystems.
+In contrast to solutions like [Blazor](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor), which attempt to bring the entire web platform into .NET (effectively reversing the natural workflow), Bootsharp facilitates high-level interoperation between C# and TypeScript. This allows you to build the domain and UI layers within their natural ecosystems. The project can then be published to the web or bundled as a native desktop or mobile application with [Electron](https://electronjs.org) or [Tauri](https://tauri.app).
## How?
-Bootsharp installs as a [NuGet package](https://www.nuget.org/packages/Bootsharp) to the C# project dedicated for building the solution for web. It's specifically designed to not "leak" the dependency outside entry assembly of the web target, which is essential to keep the domain clean from any platform-specific details.
+Bootsharp is installed as a [NuGet package](https://www.nuget.org/packages/Bootsharp) into the C# project dedicated to building the solution for the web. It is specifically designed not to "leak" the dependency outside the entry assembly of the web target—essential for keeping the domain clean of any platform-specific details.
-While it's possible to author both export (C# -> JS) and import (C# <- JS) bindings via static methods, complex solutions will benefit from interface-based interop: simply feed Bootsharp C# interfaces describing export and import API surfaces, and it will automatically generate associated bindings and type declarations.
+While it's possible to author both export (C# → JS) and import (C# ← JS) bindings via static methods, complex solutions benefit from interface-based interop. Simply provide Bootsharp with C# interfaces describing the export and import API surfaces, and it will automatically generate the associated bindings and type declarations.

-Bootsharp will automatically build and bundle JavaScript package when publishing C# solution, as well as generate `package.json`, so that you can reference the whole C# solution as any other ES module in your web project.
+Bootsharp will automatically build and bundle the JavaScript package when publishing the C# solution, and generate a `package.json`, allowing you to reference the entire C# solution as any other ES module in your web project.
::: code-group
```jsonc [package.json]
diff --git a/docs/guide/interop-interfaces.md b/docs/guide/interop-interfaces.md
index f4433335..d3874f79 100644
--- a/docs/guide/interop-interfaces.md
+++ b/docs/guide/interop-interfaces.md
@@ -1,8 +1,8 @@
# Interop Interfaces
-Instead of manually authoring a binding for each method, make Bootsharp generate them automatically with `[JSImport]` and `[JSExport]` assembly attributes.
+Instead of manually authoring a binding for each method, let Bootsharp generate them automatically using the `[JSImport]` and `[JSExport]` assembly attributes.
-For example, say we have a JavaScript UI (frontend), which needs to be notified when a data is mutated on the C# domain layer (backend), so it can render the updated state; additionally, our frontend may have a setting (eg, stored in browser cache) to temporary mute notifications, which needs to be retrieved by the backend. Create the following interface in C# to describe the expected frontend APIs:
+For example, say we have a JavaScript UI (frontend) that needs to be notified when data is mutated in the C# domain layer (backend), so it can render the updated state. Additionally, the frontend may have a setting (e.g., stored in the browser cache) to temporarily mute notifications, which the backend needs to retrieve. You can create the following interface in C# to describe the expected frontend APIs:
```csharp
interface IFrontend
@@ -12,7 +12,7 @@ interface IFrontend
}
```
-Now add the interface type to the JS import list:
+Now, add the interface type to the JS import list:
```csharp
[assembly: JSImport([
@@ -20,20 +20,7 @@ Now add the interface type to the JS import list:
])]
```
-Bootsharp will generate following C# implementation:
-
-```csharp
-public static partial class JSFrontend : IFrontend
-{
- [JSFunction] public static partial void NotifyDataChanged (Data data);
- [JSFunction] public static partial bool IsMuted ();
-
- void IFrontend.NotifyDataChanged (Data data) => NotifyDataChanged(data);
- bool IFrontend.IsMuted () => IsMuted();
-}
-```
-
-— which you can use in C# to interop with the frontend and following TypeScript spec to be implemented on the frontend:
+Bootsharp will automatically implement the interface in C#, wiring it to JavaScript, while also providing you with a TypeScript spec to implement on the frontend:
```ts
export namespace Frontend {
@@ -42,7 +29,7 @@ export namespace Frontend {
}
```
-Now say we want to provide an API for frontend to request mutation of the data:
+Now, say we want to provide an API for the frontend to request a mutation of the data:
```csharp
interface IBackend
@@ -59,7 +46,7 @@ Export the interface to JavaScript:
])]
```
-Get the following implementation auto-generated:
+This will generate the following implementation:
```csharp
public class JSBackend
@@ -76,7 +63,7 @@ public class JSBackend
}
```
-— which will produce following spec to be consumed on JavaScript side:
+— which will produce the following spec to be consumed on the JavaScript side:
```ts
export namespace Backend {
@@ -84,8 +71,8 @@ export namespace Backend {
}
```
-To make Bootsharp automatically inject and inititliaize generate interop implementations, use [dependency injection](/guide/extensions/dependency-injection) extension.
+To make Bootsharp automatically inject and initialize the generated interop implementations, use the [dependency injection](/guide/extensions/dependency-injection) extension.
::: tip Example
-Find example on using interop interfaces in the [React sample](https://github.com/elringus/bootsharp/tree/main/samples/react).
+Find an example of using interop interfaces in the [React sample](https://github.com/elringus/bootsharp/tree/main/samples/react).
:::
diff --git a/docs/index.md b/docs/index.md
index f56ae81b..6e315db2 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -7,7 +7,7 @@ titleTemplate: Bootsharp • :title
hero:
name: Bootsharp
text: Use C# in web apps with comfort
- tagline: Author domain in C#, while taking full advantage of the modern JavaScript frontend ecosystem.
+ tagline: Author the domain in C#, while fully leveraging the modern JavaScript frontend ecosystem.
actions:
- theme: brand
text: Get Started
@@ -31,7 +31,7 @@ hero:
✨
High-level Interoperation
- Generates JavaScript bindings and type declarations for your C# APIs facilitating seamless interop between the domain and UI.
+ Generates JavaScript bindings and type declarations for your C# APIs, enabling seamless interop between domain and UI.
@@ -41,7 +41,7 @@ hero:
📦
Embed or Sideload
- Choose between embedding all the C# binaries into single-file ES module for portability or sideload for best performance and build size.
+ Choose between embedding all C# binaries into a single-file ES module for portability or sideloading for performance and size.
@@ -51,7 +51,7 @@ hero:
🗺️
Runs Everywhere
- Node, Deno, Bun, web browsers and even constrained environments, such as VS Code extensions — your app will work everywhere.
+ Node, Deno, Bun, web browsers—even constrained environments like VS Code extensions—your app runs everywhere.
@@ -63,7 +63,7 @@ hero:
⚡
Interop Interfaces
- Manually author interop APIs via static C# methods or simply feed Bootsharp your domain-specific interfaces — it'll figure the rest.
+ Manually author interop APIs via static C# methods or feed Bootsharp your domain-specific interfaces—it'll handle the rest.
@@ -73,7 +73,7 @@ hero:
🏷️
Instance Bindings
- When an interface value is specified in interop API, instance binding is generated allowing to interoperate on stateful objects.
+ When an interface value is used in interop, instance binding is generated to interoperate with stateful objects.
@@ -83,7 +83,7 @@ hero:
🛠️
Customizable
- Configure namespaces for emitted bindings, function and event names, C# -> TypeScript type mappings and more.
+ Configure namespaces for emitted bindings, function and event names, C# -> TypeScript type mappings, and more.
@@ -93,7 +93,7 @@ hero:
🔥
Modern .NET
- Supports latest runtime features: WASM multi-threading, AOT compilation, assembly trimming, streaming module instantiation.
+ Supports latest runtime features: WASM multi-threading, assembly trimming, NativeAOT-LLVM, streaming instantiation.
diff --git a/docs/public/img/llvm-bench.png b/docs/public/img/llvm-bench.png
index 234d795f..6335661d 100644
Binary files a/docs/public/img/llvm-bench.png and b/docs/public/img/llvm-bench.png differ
diff --git a/docs/public/imgit/covers.json b/docs/public/imgit/covers.json
index 4757ff64..2833a33b 100644
--- a/docs/public/imgit/covers.json
+++ b/docs/public/imgit/covers.json
@@ -1 +1 @@
-{"/img/banner.png":"AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUEAAAGobWV0YQAAAAAAAAAvaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAFBpY3R1cmVIYW5kbGVyAAAAAA5waXRtAAAAAAABAAAALGlsb2MAAAAARAAAAgABAAAAAQAAAdAAAAA6AAIAAAABAAACCgAAABcAAABCaWluZgAAAAAAAgAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAGmluZmUCAAAAAAIAAGF2MDFBbHBoYQAAAAAaaXJlZgAAAAAAAAAOYXV4bAACAAEAAQAAANdpcHJwAAAAsWlwY28AAAAUaXNwZQAAAAAAAAAiAAAAGQAAABBwaXhpAAAAAAMICAgAAAAMYXYxQ4EgAAAAAAATY29scm5jbHgAAQACAACAAAAAFGlzcGUAAAAAAAAAIgAAABkAAAAOcGl4aQAAAAABCAAAAAxhdjFDgQAcAAAAADhhdXhDAAAAAHVybjptcGVnOm1wZWdCOmNpY3A6c3lzdGVtczphdXhpbGlhcnk6YWxwaGEAAAAAHmlwbWEAAAAAAAAAAgABBAECgwQAAgQFBocIAAAAWW1kYXQKCDgVIcNICGgBMi4VwAggQQSBBADGj7ypuE7UHxPhV1G1GHE5FG93ilmzJJiYEiHAeEM2IxmfQp+aCgkYFSHDTAQICoAyChXAAAEgAAY04OY=","/img/llvm-bench.png":"AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUEAAAGobWV0YQAAAAAAAAAvaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAFBpY3R1cmVIYW5kbGVyAAAAAA5waXRtAAAAAAABAAAALGlsb2MAAAAARAAAAgABAAAAAQAAAdAAAACDAAIAAAABAAACUwAAABgAAABCaWluZgAAAAAAAgAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAGmluZmUCAAAAAAIAAGF2MDFBbHBoYQAAAAAaaXJlZgAAAAAAAAAOYXV4bAACAAEAAQAAANdpcHJwAAAAsWlwY28AAAAUaXNwZQAAAAAAAAAiAAAAGQAAABBwaXhpAAAAAAMICAgAAAAMYXYxQ4EgAAAAAAATY29scm5jbHgAAgACAACAAAAAFGlzcGUAAAAAAAAAIgAAABkAAAAOcGl4aQAAAAABCAAAAAxhdjFDgQAcAAAAADhhdXhDAAAAAHVybjptcGVnOm1wZWdCOmNpY3A6c3lzdGVtczphdXhpbGlhcnk6YWxwaGEAAAAAHmlwbWEAAAAAAAAAAgABBAECgwQAAgQFBocIAAAAo21kYXQKCDgVIcNICGgBMncVwAAASAEAxo4EctKG+r4vl5yORtcckfqWb4GNYVA+pMymELVTgMc8qL1UiQ2P01UNiFN4q88kJ/YJO06DwdqT1PUt/MMgN+/0/3vsEcMRpqZXRgQFadvVQNJ/M1nPAie1LaeZNY7Zwr2EBJj/s5RRzgDRoyMUdAoGGBUhw0qAMg5FcAAASQCzkAUXycuZgA=="}
\ No newline at end of file
+{"/img/banner.png":"AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUEAAAGobWV0YQAAAAAAAAAvaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAFBpY3R1cmVIYW5kbGVyAAAAAA5waXRtAAAAAAABAAAALGlsb2MAAAAARAAAAgABAAAAAQAAAdAAAAA6AAIAAAABAAACCgAAABcAAABCaWluZgAAAAAAAgAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAGmluZmUCAAAAAAIAAGF2MDFBbHBoYQAAAAAaaXJlZgAAAAAAAAAOYXV4bAACAAEAAQAAANdpcHJwAAAAsWlwY28AAAAUaXNwZQAAAAAAAAAiAAAAGQAAABBwaXhpAAAAAAMICAgAAAAMYXYxQ4EgAAAAAAATY29scm5jbHgAAQACAACAAAAAFGlzcGUAAAAAAAAAIgAAABkAAAAOcGl4aQAAAAABCAAAAAxhdjFDgQAcAAAAADhhdXhDAAAAAHVybjptcGVnOm1wZWdCOmNpY3A6c3lzdGVtczphdXhpbGlhcnk6YWxwaGEAAAAAHmlwbWEAAAAAAAAAAgABBAECgwQAAgQFBocIAAAAWW1kYXQKCDgVIcNICGgBMi4VwAggQQSBBADGj7ypuE7UHxPhV1G1GHE5FG93ilmzJJiYEiHAeEM2IxmfQp+aCgkYFSHDTAQICoAyChXAAAEgAAY04OY=","/img/llvm-bench.png":"AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUEAAAGobWV0YQAAAAAAAAAvaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAFBpY3R1cmVIYW5kbGVyAAAAAA5waXRtAAAAAAABAAAALGlsb2MAAAAARAAAAgABAAAAAQAAAdAAAACiAAIAAAABAAACcgAAADMAAABCaWluZgAAAAAAAgAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAGmluZmUCAAAAAAIAAGF2MDFBbHBoYQAAAAAaaXJlZgAAAAAAAAAOYXV4bAACAAEAAQAAANdpcHJwAAAAsWlwY28AAAAUaXNwZQAAAAAAAAAiAAAAGQAAABBwaXhpAAAAAAMICAgAAAAMYXYxQ4EgAAAAAAATY29scm5jbHgAAgACAACAAAAAFGlzcGUAAAAAAAAAIgAAABkAAAAOcGl4aQAAAAABCAAAAAxhdjFDgQAcAAAAADhhdXhDAAAAAHVybjptcGVnOm1wZWdCOmNpY3A6c3lzdGVtczphdXhpbGlhcnk6YWxwaGEAAAAAHmlwbWEAAAAAAAAAAgABBAECgwQAAgQFBocIAAAA3W1kYXQKCDgVIcNICGgBMpUBFcAEEQIEgQUA5l/q3v/AJ7WYIgP0XD+VGdmKuneYNQOET3aoK6yBuCRGodXjF9ZUcK36D3EpOroxi2fcEh7/egEq9mDtXBtQiYt+yR/K5tWwXYC/MVOnojLDZnxdXUwoHhm3Yj+Ave6oh41GfnBFqBUEFIBOiW+0F8ZM6Yh8jJMSKfgKZ1Z3AemlCF8rDraRuKxrExwKBhgVIcNKgDIpFcAIISBA168tFtIk7x1uhDQrbZvAyBS3lwml3DDvyCh1UUW+IIQkcPA="}
\ No newline at end of file
diff --git a/docs/public/imgit/encoded/img-llvm-bench.png@cover.avif b/docs/public/imgit/encoded/img-llvm-bench.png@cover.avif
index 3d49937c..d82fde7c 100644
Binary files a/docs/public/imgit/encoded/img-llvm-bench.png@cover.avif and b/docs/public/imgit/encoded/img-llvm-bench.png@cover.avif differ
diff --git a/docs/public/imgit/encoded/img-llvm-bench.png@dense.avif b/docs/public/imgit/encoded/img-llvm-bench.png@dense.avif
index 7d2050d7..b6ea7605 100644
Binary files a/docs/public/imgit/encoded/img-llvm-bench.png@dense.avif and b/docs/public/imgit/encoded/img-llvm-bench.png@dense.avif differ
diff --git a/docs/public/imgit/encoded/img-llvm-bench.png@main.avif b/docs/public/imgit/encoded/img-llvm-bench.png@main.avif
index 26c7aec9..89b508c1 100644
Binary files a/docs/public/imgit/encoded/img-llvm-bench.png@main.avif and b/docs/public/imgit/encoded/img-llvm-bench.png@main.avif differ
diff --git a/docs/public/imgit/specs.json b/docs/public/imgit/specs.json
index ddf7d76a..60b66d83 100644
--- a/docs/public/imgit/specs.json
+++ b/docs/public/imgit/specs.json
@@ -1 +1 @@
-{"/img/banner.png@main":{"ext":"avif","codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5","scale":0.8289156626506025},"/img/banner.png@cover":{"ext":"avif","select":0,"scale":0.041445783132530126,"blur":0.4,"codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5"},"/img/llvm-bench.png@main":{"ext":"avif","codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5","scale":0.378021978021978},"/img/llvm-bench.png@dense":{"ext":"avif","codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5"},"/img/llvm-bench.png@cover":{"ext":"avif","select":0,"scale":0.018901098901098902,"blur":0.4,"codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5"}}
\ No newline at end of file
+{"/img/banner.png@main":{"ext":"avif","codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5","scale":0.8289156626506025},"/img/llvm-bench.png@main":{"ext":"avif","codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5","scale":0.378021978021978},"/img/banner.png@cover":{"ext":"avif","select":0,"scale":0.041445783132530126,"blur":0.4,"codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5"},"/img/llvm-bench.png@dense":{"ext":"avif","codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5"},"/img/llvm-bench.png@cover":{"ext":"avif","select":0,"scale":0.018901098901098902,"blur":0.4,"codec":"libaom-av1 -still-picture 1 -crf 23 -cpu-used 5"}}
\ No newline at end of file
diff --git a/samples/bench/bench.mjs b/samples/bench/bench.mjs
index 3a4f8581..64cb0ba4 100644
--- a/samples/bench/bench.mjs
+++ b/samples/bench/bench.mjs
@@ -3,6 +3,8 @@ import { init as initDotNet } from "./dotnet/init.mjs";
import { init as initDotNetLLVM } from "./dotnet-llvm/init.mjs";
import { init as initGo } from "./go/init.mjs";
import { init as initRust } from "./rust/init.mjs";
+import { init as initZig } from "./zig/init.mjs";
+import * as fixtures from "./fixtures.mjs";
/**
* @typedef {Object} Exports
@@ -12,16 +14,18 @@ import { init as initRust } from "./rust/init.mjs";
*/
const lang = process.argv[2];
-const baseline = [];
+const baseline = new Map;
if (!lang || lang.toLowerCase() === "rust")
await run("Rust", await initRust());
+if (!lang || lang.toLowerCase() === "zig")
+ await run("Zig", await initZig());
if (!lang || lang.toLowerCase() === "llvm")
await run(".NET LLVM", await initDotNetLLVM());
-if (!lang || lang.toLowerCase() === "net")
- await run(".NET AOT", await initDotNet());
if (!lang || lang.toLowerCase() === "boot")
await run("Bootsharp", await initBootsharp());
+if (!lang || lang.toLowerCase() === "net")
+ await run(".NET AOT", await initDotNet());
if (!lang || lang.toLowerCase() === "go")
await run("Go", await initGo());
@@ -29,28 +33,54 @@ if (!lang || lang.toLowerCase() === "go")
* @param {Exports} exports */
async function run(lang, exports) {
console.log(`\n\nBenching ${lang}...\n`);
- console.log(`Echo number: ${iterate(0, exports.echoNumber, 100, 3, 1000)}`);
- console.log(`Echo struct: ${iterate(1, exports.echoStruct, 100, 3, 100)}`);
- console.log(`Fibonacci: ${iterate(2, () => exports.fi(33), 100, 3, 1)}`);
+
+ global.gc();
+ await new Promise(r => setTimeout(r, 100));
+
+ bench("Fibonacci", () => exports.fi(33), 100, 3, 1);
+ bench("Echo number", exports.echoNumber, 100, 3, 100000, fixtures.getNumber());
+ bench("Echo struct", exports.echoStruct, 100, 3, 1000, fixtures.getStruct());
}
-function iterate(idx, fn, iterations, warms, loops) {
+function bench(name, fn, iters, warms, loops, expected = undefined) {
+ if (expected) {
+ expected = JSON.stringify(expected);
+ const actual = JSON.stringify(fn());
+ if (actual !== expected) {
+ console.error(`Wrong result of '${name}'. Expected: ${expected} Actual: ${actual}`);
+ return;
+ }
+ }
+
const results = [];
warms *= -1;
- for (let i = warms; i < iterations; i++) {
+ for (let i = warms; i < iters; i++) {
const start = performance.now();
for (let l = 0; l < loops; l++) fn();
if (i >= 0) results.push(performance.now() - start);
}
- let media = median(results);
- if (baseline[idx]) return `${(media / baseline[idx]).toFixed(1)}`;
- else baseline[idx] = media;
- return `${media.toFixed(3)} ms`;
+ const med = getMedian(results, 0.3);
+ const dev = getDeviation(results);
+ if (baseline.has(name)) {
+ const flr = Math.floor((med / baseline.get(name)) * 10) / 10;
+ console.log(`${name}: ${(flr).toFixed(1)} ${dev}`);
+ } else {
+ baseline.set(name, med);
+ console.log(`${name}: ${med.toFixed(3)} ms ${dev}`);
+ }
}
-function median(numbers) {
+function getMedian(numbers, trim) {
const sorted = [...numbers].sort((a, b) => a - b);
- const middle = Math.floor(sorted.length / 2);
- if (sorted.length % 2 === 1) return sorted[middle];
- return (sorted[middle - 1] + sorted[middle]) / 2;
+ const trimAmount = Math.floor(sorted.length * trim);
+ const trimmed = sorted.slice(trimAmount, sorted.length - trimAmount);
+ return trimmed.reduce((sum, val) => sum + val, 0) / trimmed.length;
+}
+
+function getDeviation(numbers) {
+ const mean = numbers.reduce((sum, val) => sum + val, 0) / numbers.length;
+ const sqr = numbers.map(value => Math.pow(value - mean, 2));
+ const variance = sqr.reduce((sum, val) => sum + val, 0) / numbers.length;
+ const dev = Math.sqrt(variance);
+ return `±${((dev / mean) * 100).toFixed(0)}%`;
}
diff --git a/samples/bench/bootsharp/Boot.csproj b/samples/bench/bootsharp/Boot.csproj
index 91e80bff..68a8b779 100644
--- a/samples/bench/bootsharp/Boot.csproj
+++ b/samples/bench/bootsharp/Boot.csproj
@@ -3,6 +3,8 @@
net9.0-browser
browser-wasm
+
+ false
@@ -11,8 +13,6 @@
-
-
true
true
diff --git a/samples/bench/bootsharp/Program.cs b/samples/bench/bootsharp/Program.cs
index c6ab5f17..8fcd8a97 100644
--- a/samples/bench/bootsharp/Program.cs
+++ b/samples/bench/bootsharp/Program.cs
@@ -13,10 +13,10 @@
public struct Data
{
- public string Info;
- public bool Ok;
- public int Revision;
- public string[] Messages;
+ public string Info { get; set; }
+ public bool Ok { get; set; }
+ public int Revision { get; set; }
+ public string[] Messages { get; set; }
}
public interface IImport
@@ -36,5 +36,8 @@ 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);
+ public int Fi (int n) => F(n);
+ // Due to heavy recursion, a significant degradation accumulates due to constant
+ // dereferencing of the instance on each iteration, hence using the static version.
+ private static int F (int n) => n <= 1 ? n : F(n - 1) + F(n - 2);
}
diff --git a/samples/bench/bootsharp/init.mjs b/samples/bench/bootsharp/init.mjs
index 9b455f81..fee81961 100644
--- a/samples/bench/bootsharp/init.mjs
+++ b/samples/bench/bootsharp/init.mjs
@@ -1,12 +1,21 @@
import bootsharp, { Export, Import } from "./bin/bootsharp/index.mjs";
import { getNumber, getStruct } from "../fixtures.mjs";
+import fs from "fs/promises";
/** @returns {Promise} */
export async function init() {
Import.getNumber = getNumber;
Import.getStruct = getStruct;
- await bootsharp.boot();
+ const content = await fs.readFile("./bootsharp/bin/bootsharp/bin/dotnet.native.wasm");
+ await bootsharp.boot({
+ root: "./bin",
+ resources: {
+ wasm: { name: "dotnet.native.wasm", content },
+ assemblies: [],
+ entryAssemblyName: "Boot.dll"
+ }
+ });
return { ...Export };
}
diff --git a/samples/bench/bootsharp/readme.md b/samples/bench/bootsharp/readme.md
index fdd43f39..cf2651ac 100644
--- a/samples/bench/bootsharp/readme.md
+++ b/samples/bench/bootsharp/readme.md
@@ -1,2 +1,2 @@
1. Install .NET https://dotnet.microsoft.com/en-us/download
-2. Run `dotnet publish -c Release`
+2. Run `dotnet publish`
diff --git a/samples/bench/dotnet-llvm/DotNetLLVM.csproj b/samples/bench/dotnet-llvm/DotNetLLVM.csproj
index 68d4d60b..3d6d8e21 100644
--- a/samples/bench/dotnet-llvm/DotNetLLVM.csproj
+++ b/samples/bench/dotnet-llvm/DotNetLLVM.csproj
@@ -2,7 +2,6 @@
net9.0-browser
- Release
browser-wasm
Exe
true
@@ -11,8 +10,6 @@
false
$(EmccFlags) -O3
false
- false
- true
.exe
$(Pkgruntime_win-x64_Microsoft_DotNet_ILCompiler_LLVM)
$(Pkgruntime_linux-x64_Microsoft_DotNet_ILCompiler_LLVM)
@@ -22,6 +19,13 @@
+
+
+
+
+
+
+
diff --git a/samples/bench/dotnet-llvm/Program.cs b/samples/bench/dotnet-llvm/Program.cs
index dfa6363c..d7f2ef58 100644
--- a/samples/bench/dotnet-llvm/Program.cs
+++ b/samples/bench/dotnet-llvm/Program.cs
@@ -1,39 +1,49 @@
-using System.Runtime.InteropServices.JavaScript;
+using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
public struct Data
{
- public string Info;
- public bool Ok;
- public int Revision;
- public string[] Messages;
+ public string Info { get; set; }
+ public bool Ok { get; set; }
+ public int Revision { get; set; }
+ public string[] Messages { get; set; }
}
[JsonSerializable(typeof(Data))]
+[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
internal partial class SourceGenerationContext : JsonSerializerContext;
-public static partial class Program
+public static unsafe class Program
{
public static void Main () { }
- [JSExport]
+ [UnmanagedCallersOnly(EntryPoint = "NativeLibrary_Free")]
+ public static void Free (void* p) => NativeMemory.Free(p);
+
+ [UnmanagedCallersOnly(EntryPoint = "echoNumber")]
public static int EchoNumber () => GetNumber();
- [JSExport]
- public static string EchoStruct ()
+ [UnmanagedCallersOnly(EntryPoint = "echoStruct")]
+ public static char* EchoStruct ()
{
- var json = GetStruct();
- var data = JsonSerializer.Deserialize(json, SourceGenerationContext.Default.Data);
- return JsonSerializer.Serialize(data, SourceGenerationContext.Default.Data);
+ var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(GetStruct());
+ var data = JsonSerializer.Deserialize(span, SourceGenerationContext.Default.Data);
+ var json = JsonSerializer.Serialize(data, SourceGenerationContext.Default.Data);
+ fixed (char* ptr = json) return ptr; // has to be pinned and freed after use in real use cases
}
- [JSExport]
- public static int Fi (int n) => n <= 1 ? n : Fi(n - 1) + Fi(n - 2);
+ [UnmanagedCallersOnly(EntryPoint = "fi")]
+ public static int FiExport (int n) => Fi(n);
+ private static int Fi (int n) => n <= 1 ? n : Fi(n - 1) + Fi(n - 2);
- [JSImport("getNumber", "x")]
- private static partial int GetNumber ();
+ [DllImport("x", EntryPoint = "getNumber")]
+ private static extern int GetNumber ();
- [JSImport("getStruct", "x")]
- private static partial string GetStruct ();
+ [DllImport("x", EntryPoint = "getStruct")]
+ private static extern char* GetStruct ();
}
+
+// NOTE: 95% of degradation compared to Rust is in the JSON de-/serialization.
+// GenerationMode = JsonSourceGenerationMode.Serialization is only implemented for serialization
+// and throws when used for de-serialization: https://github.com/dotnet/runtime/issues/55043.
diff --git a/samples/bench/dotnet-llvm/imports.js b/samples/bench/dotnet-llvm/imports.js
new file mode 100644
index 00000000..9013e091
--- /dev/null
+++ b/samples/bench/dotnet-llvm/imports.js
@@ -0,0 +1,18 @@
+// TODO: Figure how to get fixtures from "../fixtures.mjs"
+
+mergeInto(LibraryManager.library, {
+ getNumber: () => 42,
+ getStruct: () => {
+ const data = {
+ info: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
+ ok: true,
+ revision: -112,
+ messages: ["foo", "bar", "baz", "nya", "far"]
+ };
+ const json = JSON.stringify(data);
+ const size = lengthBytesUTF16(json) + 1;
+ const ptr = _malloc(size);
+ stringToUTF16(json, ptr, size);
+ return ptr; // has to be freed after use in real use cases
+ }
+});
diff --git a/samples/bench/dotnet-llvm/init.mjs b/samples/bench/dotnet-llvm/init.mjs
index 3246de01..663c1a01 100644
--- a/samples/bench/dotnet-llvm/init.mjs
+++ b/samples/bench/dotnet-llvm/init.mjs
@@ -1,22 +1,13 @@
import { dotnet } from "./bin/Release/net9.0-browser/browser-wasm/publish/dotnet.js";
-import { getNumber, getStruct } from "../fixtures.mjs";
/** @returns {Promise} */
export async function init() {
const runtime = await dotnet.withDiagnosticTracing(false).create();
- const asm = "DotNetLLVM";
+ await runtime.runMain("DotNetLLVM", []);
- 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
+ echoNumber: runtime.Module._echoNumber,
+ echoStruct: () => JSON.parse(runtime.Module.UTF16ToString(runtime.Module._echoStruct())),
+ fi: runtime.Module._fi
};
}
diff --git a/samples/bench/dotnet-llvm/readme.md b/samples/bench/dotnet-llvm/readme.md
index 07d557bf..7ddde01a 100644
--- a/samples/bench/dotnet-llvm/readme.md
+++ b/samples/bench/dotnet-llvm/readme.md
@@ -1,4 +1,4 @@
1. Install .NET https://dotnet.microsoft.com/en-us/download
-2. Run `dotnet publish -c Release`
+2. Run `dotnet publish`
https://github.com/dotnet/runtime/issues/113979#issuecomment-2759220563
diff --git a/samples/bench/dotnet/DotNet.csproj b/samples/bench/dotnet/DotNet.csproj
index 94cf0404..ddbea904 100644
--- a/samples/bench/dotnet/DotNet.csproj
+++ b/samples/bench/dotnet/DotNet.csproj
@@ -1,7 +1,6 @@
net9.0
- Release
browser-wasm
true
true
diff --git a/samples/bench/dotnet/Program.cs b/samples/bench/dotnet/Program.cs
index dfa6363c..bb8f73aa 100644
--- a/samples/bench/dotnet/Program.cs
+++ b/samples/bench/dotnet/Program.cs
@@ -4,13 +4,14 @@
public struct Data
{
- public string Info;
- public bool Ok;
- public int Revision;
- public string[] Messages;
+ public string Info { get; set; }
+ public bool Ok { get; set; }
+ public int Revision { get; set; }
+ public string[] Messages { get; set; }
}
[JsonSerializable(typeof(Data))]
+[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
internal partial class SourceGenerationContext : JsonSerializerContext;
public static partial class Program
diff --git a/samples/bench/dotnet/init.mjs b/samples/bench/dotnet/init.mjs
index f3c385b0..026c4863 100644
--- a/samples/bench/dotnet/init.mjs
+++ b/samples/bench/dotnet/init.mjs
@@ -16,7 +16,7 @@ export async function init() {
const exports = await runtime.getAssemblyExports(asm);
return {
echoNumber: exports.Program.EchoNumber,
- echoStruct: exports.Program.EchoStruct,
+ echoStruct: () => JSON.parse(exports.Program.EchoStruct()),
fi: exports.Program.Fi
};
}
diff --git a/samples/bench/dotnet/readme.md b/samples/bench/dotnet/readme.md
index fdd43f39..cf2651ac 100644
--- a/samples/bench/dotnet/readme.md
+++ b/samples/bench/dotnet/readme.md
@@ -1,2 +1,2 @@
1. Install .NET https://dotnet.microsoft.com/en-us/download
-2. Run `dotnet publish -c Release`
+2. Run `dotnet publish`
diff --git a/samples/bench/go/init.mjs b/samples/bench/go/init.mjs
index bc7863fe..da49e7fa 100644
--- a/samples/bench/go/init.mjs
+++ b/samples/bench/go/init.mjs
@@ -14,7 +14,7 @@ export async function init() {
return {
echoNumber: global.echoNumber,
- echoStruct: global.echoStruct,
+ echoStruct: () => JSON.parse(global.echoStruct()),
fi: global.fi
};
}
diff --git a/samples/bench/go/main.go b/samples/bench/go/main.go
index a721a84b..16060949 100644
--- a/samples/bench/go/main.go
+++ b/samples/bench/go/main.go
@@ -6,10 +6,10 @@ import (
)
type Data struct {
- Info string `json:"Info"`
- Ok bool `json:"Ok"`
- Revision int `json:"Revision"`
- Messages []string `json:"Messages"`
+ Info string `json:"info"`
+ Ok bool `json:"ok"`
+ Revision int `json:"revision"`
+ Messages []string `json:"messages"`
}
func main() {
diff --git a/samples/bench/readme.md b/samples/bench/readme.md
index b480890d..d5a38ca2 100644
--- a/samples/bench/readme.md
+++ b/samples/bench/readme.md
@@ -1,21 +1,21 @@
## Setup
1. Build each sub-dir (readme inside)
-2. Run `npm bench.mjs` to bench all
-3. Or `npm bench.mjs rust|llvm|net|boot|go`
+2. Run `node --expose-gc bench.mjs` to bench all
+3. Add `rust|zig|llvm|net|boot|go` to bench specific
## Benches
+- `Fibonacci` — compute with heavy recursion
- `Echo Number` — interop with raw numbers
- `Echo Struct` — interop with JSON-serialized structs
-- `Fibonacci` — compute performance
All results are relative to the Rust baseline (lower is better).
## 2024 (.NET 9)
-| | Rust | .NET LLVM | Bootsharp | .NET AOT | Go |
-|-------------|-------|-----------|-----------|-----------|---------|
-| Echo Number | `1.0` | `11.9` | `11.9` | `21.1` | `718.7` |
-| Echo Struct | `1.0` | `1.6` | `1.6` | `4.3` | `20.8` |
-| Fibonacci | `1.0` | `1.1` | `1.5` | `1.5` | `3.8` |
+| | Rust | Zig | .NET LLVM | Bootsharp | .NET AOT | Go |
+|-------------|-------|-------|-----------|-----------|----------|---------|
+| Fibonacci | `1.0` | `1.0` | `1.0` | `1.0` | `1.7` | `3.8` |
+| Echo Number | `1.0` | `0.9` | `1.6` | `14.0` | `23.5` | `718.7` |
+| Echo Struct | `1.0` | `1.1` | `2.0` | `2.5` | `5.9` | `15.2` |
diff --git a/samples/bench/rust/init.mjs b/samples/bench/rust/init.mjs
index c3aa2f49..1bc169c0 100644
--- a/samples/bench/rust/init.mjs
+++ b/samples/bench/rust/init.mjs
@@ -5,5 +5,9 @@ import { getNumber, getStruct } from "../fixtures.mjs";
export async function init() {
global.getNumber = getNumber;
global.getStruct = () => JSON.stringify(getStruct());
- return { echoNumber, echoStruct, fi };
+ return {
+ echoNumber,
+ echoStruct: () => JSON.parse(echoStruct()),
+ fi
+ };
}
diff --git a/samples/bench/zig/.gitignore b/samples/bench/zig/.gitignore
new file mode 100644
index 00000000..1af35013
--- /dev/null
+++ b/samples/bench/zig/.gitignore
@@ -0,0 +1,2 @@
+.zig-cache
+zit-out
diff --git a/samples/bench/zig/build.zig b/samples/bench/zig/build.zig
new file mode 100644
index 00000000..88465e74
--- /dev/null
+++ b/samples/bench/zig/build.zig
@@ -0,0 +1,24 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const lib = b.addExecutable(.{
+ .name = "zig",
+ .root_source_file = b.path("main.zig"),
+ .target = b.resolveTargetQuery(.{
+ .cpu_arch = .wasm32,
+ .os_tag = .freestanding,
+ .cpu_features_add = std.Target.wasm.featureSet(&.{
+ .simd128,
+ .relaxed_simd,
+ .tail_call,
+ }),
+ }),
+ .use_llvm = true,
+ .use_lld = true,
+ .optimize = b.standardOptimizeOption(.{}),
+ });
+ lib.entry = .disabled;
+ lib.rdynamic = true;
+ lib.want_lto = true;
+ b.installArtifact(lib);
+}
diff --git a/samples/bench/zig/init.mjs b/samples/bench/zig/init.mjs
new file mode 100644
index 00000000..6e58d91f
--- /dev/null
+++ b/samples/bench/zig/init.mjs
@@ -0,0 +1,44 @@
+import { getNumber, getStruct } from "../fixtures.mjs";
+import fs from "fs/promises";
+
+/** @returns {Promise} */
+export async function init() {
+ const source = await fs.readFile("./zig/zig-out/bin/zig.wasm");
+ const { instance: { exports } } = await WebAssembly.instantiate(source, {
+ x: {
+ getNumber,
+ getStruct: () => encodeString(JSON.stringify(getStruct())),
+ }
+ });
+ memory = exports.memory, cached = new Uint8Array(memory.buffer);
+
+ return {
+ echoNumber: exports.echoNumber,
+ echoStruct: () => JSON.parse(decodeString(exports.echoStruct())),
+ fi: exports.fi
+ };
+}
+
+let memory, cached;
+const encoder = new TextEncoder("utf-8");
+const decoder = new TextDecoder("utf-8");
+const mask = BigInt("0xFFFFFFFF");
+
+function encodeString(str) {
+ const memory = getMemoryCached();
+ const { written } = encoder.encodeInto(str, memory);
+ return BigInt(written) << BigInt(32) | BigInt(0);
+}
+
+function decodeString(ptrAndLen) {
+ const memory = getMemoryCached();
+ const ptr = Number(ptrAndLen & mask);
+ const len = Number(ptrAndLen >> BigInt(32));
+ const bytes = memory.subarray(ptr, ptr + len);
+ return decoder.decode(bytes);
+}
+
+function getMemoryCached() {
+ if (cached.buffer === memory.buffer) return cached;
+ return cached = new Uint8Array(memory.buffer);
+}
diff --git a/samples/bench/zig/main.zig b/samples/bench/zig/main.zig
new file mode 100644
index 00000000..ab984b72
--- /dev/null
+++ b/samples/bench/zig/main.zig
@@ -0,0 +1,51 @@
+const std = @import("std");
+
+var arena = std.heap.ArenaAllocator.init(std.heap.wasm_allocator);
+const ally = arena.allocator();
+
+const opt = .{
+ .parse = std.json.ParseOptions{
+ .ignore_unknown_fields = true,
+ },
+ .stringify = std.json.StringifyOptions{
+ .whitespace = .minified,
+ },
+};
+
+pub const Data = struct {
+ info: []const u8,
+ ok: bool,
+ revision: i32,
+ messages: []const []const u8,
+};
+
+extern "x" fn getNumber() i32;
+extern "x" fn getStruct() u64;
+
+export fn echoNumber() i32 {
+ return getNumber();
+}
+
+export fn echoStruct() u64 {
+ _ = arena.reset(.retain_capacity);
+ const input = decodeString(getStruct());
+ const json = std.json.parseFromSlice(Data, ally, input, opt.parse) catch unreachable;
+ var output = std.ArrayList(u8).init(ally);
+ std.json.stringify(json.value, opt.stringify, output.writer()) catch unreachable;
+ return encodeString(output.items);
+}
+
+export fn fi(n: i32) i32 {
+ if (n <= 1) return n;
+ return fi(n - 1) + fi(n - 2);
+}
+
+fn decodeString(ptr_and_len: u64) []const u8 {
+ const ptr = @as(u32, @truncate(ptr_and_len));
+ const len = @as(u32, @truncate(ptr_and_len >> 32));
+ return @as([*]const u8, @ptrFromInt(ptr))[0..len];
+}
+
+fn encodeString(str: []const u8) u64 {
+ return (@as(u64, str.len) << 32) | @intFromPtr(str.ptr);
+}
diff --git a/samples/bench/zig/readme.md b/samples/bench/zig/readme.md
new file mode 100644
index 00000000..8f39ea0c
--- /dev/null
+++ b/samples/bench/zig/readme.md
@@ -0,0 +1,2 @@
+1. Install Zig https://ziglang.org/download/
+2. Run `zig build -Doptimize=ReleaseFast`
diff --git a/samples/vscode/package.json b/samples/vscode/package.json
index 858d8b5a..cee4aac1 100644
--- a/samples/vscode/package.json
+++ b/samples/vscode/package.json
@@ -8,7 +8,7 @@
],
"publisher": "Elringus",
"repository": "https://github.com/Elringus/Bootsharp",
- "homepage": "https://sharp.elringus.com",
+ "homepage": "https://bootsharp.com",
"icon": "assets/package-icon.png",
"engines": {
"vscode": "^1.81.1"
diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props
index f5f5a735..679ba8b0 100644
--- a/src/cs/Directory.Build.props
+++ b/src/cs/Directory.Build.props
@@ -1,10 +1,10 @@
- 0.6.0
+ 0.6.1
Elringus
javascript typescript ts js wasm node deno bun interop codegen
- https://sharp.elringus.com
+ https://bootsharp.com
https://github.com/elringus/bootsharp.git
git
logo.png