diff --git a/README.md b/README.md index b424f020..19c98a6e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- + Bootsharp

@@ -14,10 +14,12 @@ # Use C# in web apps with comfort -Bootsharp streamlines consuming .NET C# apps and libraries in web projects. It's ideal for building web applications, where domain (backend) is authored in .NET C#, while the UI (frontend) is a standalone TypeScript or JavaScript project. Think of it as [Embind](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html) for C++ or [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) for Rust. +Bootsharp streamlines the integration of .NET C# apps and libraries into web projects. It's ideal for building applications where the domain (backend) is authored in .NET C#, while the UI (frontend) is a standalone TypeScript or JavaScript project. Think of it as [Embind](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html) for C++ or [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) for Rust. ![](https://raw.githubusercontent.com/elringus/bootsharp/main/docs/public/img/banner.png) +Facilitating high-level interoperation between C# and TypeScript, Bootsharp lets you build the UI layer within its natural ecosystem using industry-standard tooling and frameworks, such as [React](https://react.dev) and [Svelte](https://svelte.dev). 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). + ## Features ✨ High-level C# <-> TypeScript interop @@ -32,75 +34,8 @@ Bootsharp streamlines consuming .NET C# apps and libraries in web projects. It's 🛠️ Allows customizing emitted bindings -🔥 Supports WASM multi-threading, AOT, trimming - +🔥 Supports multi-threading, NativeAOT-LLVM, trimming ## 🎬 Get Started -https://sharp.elringus.com/guide/getting-started - -### Why not Blazor? - -In contrast to solutions like Blazor, which attempt to bring the entire web platform inside .NET, Bootsharp facilitates high-level interoperation between C# and TypeScript, allowing to build the UI layer under its natural ecosystem using industry-standard tooling and frameworks, such as [React](https://react.dev) and [Svelte](https://svelte.dev). - -### Why not `System.JavaScript`? - -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 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 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() - .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. ![](/img/banner.png) -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