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
3 changes: 2 additions & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export default defineConfig({
{ text: "Interop Instances", link: "/guide/interop-instances" },
{ text: "Emit Preferences", link: "/guide/emit-prefs" },
{ text: "Build Configuration", link: "/guide/build-config" },
{ text: "Sideloading Binaries", link: "/guide/sideloading" }
{ text: "Sideloading Binaries", link: "/guide/sideloading" },
{ text: "NativeAOT-LLVM", link: "/guide/llvm" }
]
},
{
Expand Down
1 change: 1 addition & 0 deletions docs/guide/build-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Build and publish related options are configured in `.csproj` file via MSBuild p
| BootsharpName | bootsharp | Name of the generated JavaScript module. |
| BootsharpEmbedBinaries | true | Whether to embed binaries to the JavaScript module file. |
| BootsharpAggressiveTrimming | false | Whether to disable some .NET features to reduce binary size. |
| BootsharpLLVM | false | Enable experimental [NativeAOT-LLVM](/guide/llvm) backend. |
| BootsharpBundleCommand | npx rollup | The command to bundle generated JavaScrip solution. |
| BootsharpPublishDirectory | /bin | Directory to publish generated JavaScript module. |
| BootsharpTypesDirectory | /types | Directory to publish type declarations. |
Expand Down
61 changes: 61 additions & 0 deletions docs/guide/llvm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# NativeAOT-LLVM

Starting with v0.6.0 Bootsharp supports .NET's experimental [NativeAOT-LLVM](https://github.com/dotnet/runtimelab/tree/feature/NativeAOT-LLVM) backend.

By default, when targeting `browser-wasm`, .NET is using the Mono runtime, even when compiled in AOT mode. Compared to the modern NativeAOT (previously CoreRT) runtime, Mono's performance is lacking in speed, binary size and compilation times. NativeAOT-LLVM backend not only uses the modern runtime instead of Mono, but also optimizes it with the [LLVM](https://llvm.org) toolchain, further improving the performance.

Below is a benchmark comparing interop and compute performance of various languages and .NET versions compiled to WASM to give you a rough idea on the differences:

| | Rust | .NET LLVM | .NET AOT | Go |
|-------------|-------|-----------|-----------|---------|
| Echo Number | `1.0` | `11.9` | `21.1` | `718.7` |
| Echo Struct | `1.0` | `1.6` | `4.3` | `20.8` |
| Fibonacci | `1.0` | `1.1` | `1.5` | `3.8` |

— the results are relative to the Rust baseline (lower is better). Sources of the benchmark are here: https://github.com/elringus/bootsharp/tree/main/samples/bench.

## Setup

Use following `.csproj` as a reference for enabling NativeAOT-LLVM with Bootsharp:

```xml
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- Notice '-browser' postfix. -->
<TargetFramework>net9.0-browser</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<!-- Let Bootsharp know you're using the LLVM backend. -->
<BootsharpLLVM>true</BootsharpLLVM>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Bootsharp" Version="*-*"/>
</ItemGroup>

<!-- Below are properties required to enable LLVM backend. -->
<!-- Due to experimental nature of the project, specifics may change over time. -->

<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<DotNetJsApi>true</DotNetJsApi>
<DebugType>none</DebugType>
<EmccFlags>$(EmccFlags) -O3</EmccFlags> <!-- optimize speed; use -Oz for min. size -->
<UsingBrowserRuntimeWorkload>false</UsingBrowserRuntimeWorkload>
<RestoreAdditionalProjectSources>
https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json;
</RestoreAdditionalProjectSources>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*"/>
<PackageReference Condition="'$([MSBuild]::IsOSPlatform(&quot;Windows&quot;))' == 'true'" Include="runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*"/>
<PackageReference Condition="'$([MSBuild]::IsOSPlatform(&quot;Windows&quot;))' == 'false'" Include="runtime.linux-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*"/>
<EmscriptenEnvVars Include="DOTNET_EMSCRIPTEN_LLVM_ROOT=$(EmscriptenUpstreamBinPath)"/>
<EmscriptenEnvVars Include="DOTNET_EMSCRIPTEN_BINARYEN_ROOT=$(EmscriptenSdkToolsPath)"/>
<EmscriptenEnvVars Include="DOTNET_EMSCRIPTEN_NODE_JS=$(EmscriptenNodeBinPath)node$(ExecutableExtensionName)"/>
<EmscriptenEnvVars Include="EM_CACHE=$(EmscriptenCacheSdkCacheDir)"/>
</ItemGroup>

</Project>
```
2 changes: 1 addition & 1 deletion samples/bench/bench.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (!lang || lang.toLowerCase() === "rust")
if (!lang || lang.toLowerCase() === "llvm")
await run(".NET LLVM", await initDotNetLLVM());
if (!lang || lang.toLowerCase() === "net")
await run(".NET", await initDotNet());
await run(".NET AOT", await initDotNet());
if (!lang || lang.toLowerCase() === "boot")
await run("Bootsharp", await initBootsharp());
if (!lang || lang.toLowerCase() === "go")
Expand Down
30 changes: 25 additions & 5 deletions samples/bench/bootsharp/Boot.csproj
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
Expand All @@ -15,4 +11,28 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="*"/>
</ItemGroup>

<!-- https://github.com/dotnet/runtime/issues/113979#issuecomment-2759220563 -->

<PropertyGroup>
<BootsharpLLVM>true</BootsharpLLVM>
<PublishTrimmed>true</PublishTrimmed>
<DotNetJsApi>true</DotNetJsApi>
<DebugType>none</DebugType>
<EmccFlags>$(EmccFlags) -O3</EmccFlags>
<UsingBrowserRuntimeWorkload>false</UsingBrowserRuntimeWorkload>
<RestoreAdditionalProjectSources>
https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json;
</RestoreAdditionalProjectSources>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*"/>
<PackageReference Condition="'$([MSBuild]::IsOSPlatform(&quot;Windows&quot;))' == 'true'" Include="runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*"/>
<PackageReference Condition="'$([MSBuild]::IsOSPlatform(&quot;Windows&quot;))' == 'false'" Include="runtime.linux-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*"/>
<EmscriptenEnvVars Include="DOTNET_EMSCRIPTEN_LLVM_ROOT=$(EmscriptenUpstreamBinPath)"/>
<EmscriptenEnvVars Include="DOTNET_EMSCRIPTEN_BINARYEN_ROOT=$(EmscriptenSdkToolsPath)"/>
<EmscriptenEnvVars Include="DOTNET_EMSCRIPTEN_NODE_JS=$(EmscriptenNodeBinPath)node$(ExecutableExtensionName)"/>
<EmscriptenEnvVars Include="EM_CACHE=$(EmscriptenCacheSdkCacheDir)"/>
</ItemGroup>

</Project>
1 change: 0 additions & 1 deletion samples/bench/dotnet-llvm/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ public struct Data
public string[] Messages;
}


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

Expand Down
10 changes: 5 additions & 5 deletions samples/bench/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ All results are relative to the Rust baseline (lower is better).

## 2024 (.NET 9)

| | Rust | .NET LLVM | .NET | Bootsharp | Go |
|-------------|-------|-----------|--------|-----------|---------|
| Echo Number | `1.0` | `14.4` | `21.1` | `25.7` | `718.7` |
| Echo Struct | `1.0` | `1.5` | `4.3` | `4.9` | `20.8` |
| Fibonacci | `1.0` | `1.1` | `1.5` | `1.6` | `3.8` |
| | 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` |
8 changes: 4 additions & 4 deletions samples/trimming/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ To test and measure build size:

### Measurements (KB)

| .NET | Raw | Brotli |
|-------|-------|--------|
| 8.0.1 | 2,298 | 739 |
| 9.0.1 | 2,369 | 761 |
| | Raw | Brotli |
|-------------|-------|--------|
| .NET 8 | 2,298 | 739 |
| .NET 9 LLVM | 1,749 | 520 |
7 changes: 5 additions & 2 deletions samples/trimming/cs/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using Bootsharp;

Log("Hello from .NET!");

public static partial class Program
{
public static void Main ()
{
Log("Hello from .NET!");
}

[JSFunction]
public static partial void Log (string message);
}
26 changes: 25 additions & 1 deletion samples/trimming/cs/Trimming.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net9.0-browser</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<!-- Not embedding binaries to source module reduces build size by ~30%. -->
<BootsharpEmbedBinaries>false</BootsharpEmbedBinaries>
Expand All @@ -19,4 +19,28 @@
WorkingDirectory="$(BootsharpPublishDirectory)"/>
</Target>

<!-- Using experimental NativeAOT-LLM backend, which yeilds a smaller WASM binary. -->

<PropertyGroup>
<BootsharpLLVM>true</BootsharpLLVM>
<PublishTrimmed>true</PublishTrimmed>
<DotNetJsApi>true</DotNetJsApi>
<DebugType>none</DebugType>
<EmccFlags>$(EmccFlags) -Oz</EmccFlags> <!-- optimize for smaller binary -->
<UsingBrowserRuntimeWorkload>false</UsingBrowserRuntimeWorkload>
<RestoreAdditionalProjectSources>
https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json;
</RestoreAdditionalProjectSources>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*"/>
<PackageReference Condition="'$([MSBuild]::IsOSPlatform(&quot;Windows&quot;))' == 'true'" Include="runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*"/>
<PackageReference Condition="'$([MSBuild]::IsOSPlatform(&quot;Windows&quot;))' == 'false'" Include="runtime.linux-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*"/>
<EmscriptenEnvVars Include="DOTNET_EMSCRIPTEN_LLVM_ROOT=$(EmscriptenUpstreamBinPath)"/>
<EmscriptenEnvVars Include="DOTNET_EMSCRIPTEN_BINARYEN_ROOT=$(EmscriptenSdkToolsPath)"/>
<EmscriptenEnvVars Include="DOTNET_EMSCRIPTEN_NODE_JS=$(EmscriptenNodeBinPath)node$(ExecutableExtensionName)"/>
<EmscriptenEnvVars Include="EM_CACHE=$(EmscriptenCacheSdkCacheDir)"/>
</ItemGroup>

</Project>
3 changes: 2 additions & 1 deletion src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ protected override void AddAssembly (string assemblyName, params MockSource[] so
BuildEngine = Engine,
TrimmingEnabled = false,
EmbedBinaries = false,
Threading = false
Threading = false,
LLVM = false
};
}
48 changes: 48 additions & 0 deletions src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,52 @@ public void WhenAssemblyInspectionFailsWarningIsLogged ()
Execute();
Assert.Contains(Engine.Warnings, w => w.Contains("Failed to inspect 'foo.dll' assembly"));
}

[Fact]
public void IgnoresAssembliesNotPresentInBuildDirectory ()
{
var buildDir = $"{Project.Root}/build";
Task.BuildDirectory = buildDir;
Directory.CreateDirectory(buildDir);
File.WriteAllText($"{buildDir}/foo.wasm", "");

foreach (var file in Directory.EnumerateFiles(Project.Root))
File.WriteAllText($"{buildDir}/{Path.GetFileName(file)}", File.ReadAllText(file));

AddAssembly("foo.dll",
WithClass("[JSInvokable] public static void InvFoo () {}")
);
AddAssembly("bar.dll",
WithClass("[JSInvokable] public static void InvBar () {}")
);
Execute();

Assert.Contains(Engine.Messages, w => w.Contains("foo"));
Assert.DoesNotContain(Engine.Messages, w => w.Contains("bar"));
}

[Fact]
public void DoesntIgnoreAssembliesWhenLLVM ()
Copy link

Copilot AI Mar 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using the correct contraction "Doesn't" for better readability in the method name.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using the correct contraction "Doesn't" for better readability in the method name.

Oh my dear AI overlord, I'd love to. But guess what happens if I use ' for an identifier in C#?

{
Task.LLVM = true;

var buildDir = $"{Project.Root}/build";
Task.BuildDirectory = buildDir;
Directory.CreateDirectory(buildDir);
File.WriteAllText($"{buildDir}/foo.wasm", "");

foreach (var file in Directory.EnumerateFiles(Project.Root))
File.WriteAllText($"{buildDir}/{Path.GetFileName(file)}", File.ReadAllText(file));

AddAssembly("foo.dll",
WithClass("[JSInvokable] public static void InvFoo () {}")
);
AddAssembly("bar.dll",
WithClass("[JSInvokable] public static void InvBar () {}")
);
Execute();

Assert.Contains(Engine.Messages, w => w.Contains("foo"));
Assert.Contains(Engine.Messages, w => w.Contains("bar"));
}
}
16 changes: 11 additions & 5 deletions src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public sealed class BootsharpPack : Microsoft.Build.Utilities.Task
public required bool TrimmingEnabled { get; set; }
public required bool EmbedBinaries { get; set; }
public required bool Threading { get; set; }
public required bool LLVM { get; set; }

public override bool Execute ()
{
Expand All @@ -32,13 +33,18 @@ private Preferences ResolvePreferences ()
private SolutionInspection InspectSolution (Preferences prefs)
{
var inspector = new SolutionInspector(prefs, EntryAssemblyName);
// Assemblies in publish dir are trimmed and don't contain some data (eg, method arg names).
// While the inspected dir contains extra assemblies we don't need in build. Hence the filtering.
var included = Directory.GetFiles(BuildDirectory, "*.wasm").Select(Path.GetFileNameWithoutExtension).ToHashSet();
var inspected = Directory.GetFiles(InspectedDirectory, "*.dll").Where(p => included.Contains(Path.GetFileNameWithoutExtension(p)));
var inspection = inspector.Inspect(InspectedDirectory, inspected);
var inspection = inspector.Inspect(InspectedDirectory, GetFiles());
new InspectionReporter(Log).Report(inspection);
return inspection;

IEnumerable<string> GetFiles ()
{
if (LLVM) return Directory.GetFiles(InspectedDirectory, "*.dll");
// Assemblies in publish dir are trimmed and don't contain some data (eg, method arg names).
// While the inspected dir contains extra assemblies we don't need in build. Hence the filtering.
var included = Directory.GetFiles(BuildDirectory, "*.wasm").Select(Path.GetFileNameWithoutExtension).ToHashSet();
return Directory.GetFiles(InspectedDirectory, "*.dll").Where(p => included.Contains(Path.GetFileNameWithoutExtension(p)));
}
}

private void GenerateBindings (Preferences prefs, SolutionInspection inspection)
Expand Down
9 changes: 2 additions & 7 deletions src/cs/Bootsharp/Bootsharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyTitle>Bootsharp</AssemblyTitle>
<PackageId>Bootsharp</PackageId>
<Description>Compile C# solution into single-file ES module with auto-generated JavaScript bindings and type definitions.</Description>
<Description>Use C# in web apps with comfort.</Description>
<NoWarn>NU5100</NoWarn>
</PropertyGroup>

Expand All @@ -15,15 +13,12 @@
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.Versioning.SupportedOSPlatform">
<_Parameter1>browser</_Parameter1>
</AssemblyAttribute>
<Content Include="Build/**" Pack="true" PackagePath="build/"/>
<Content Include="../../../src/js/dist/**" Pack="true" PackagePath="js/" Visible="false"/>
</ItemGroup>

<Target Name="PackPublisher" BeforeTargets="CoreCompile">
<MSBuild Projects="../Bootsharp.Publish/Bootsharp.Publish.csproj" Targets="Publish;PublishItemsOutputGroup" Properties="Configuration=Release">
<MSBuild Projects="../Bootsharp.Publish/Bootsharp.Publish.csproj" Targets="Publish;PublishItemsOutputGroup">
<Output TaskParameter="TargetOutputs" ItemName="_TasksProjectOutputs"/>
</MSBuild>
<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/cs/Bootsharp/Build/Bootsharp.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
<InvariantGlobalization>true</InvariantGlobalization>
<WasmGenerateAppBundle>true</WasmGenerateAppBundle>
<WasmEnableLegacyJsInterop>false</WasmEnableLegacyJsInterop>
<WasmEnableSIMD>true</WasmEnableSIMD>

<!-- Name of the generated JavaScript module; 'bootsharp' by default. -->
<BootsharpName>bootsharp</BootsharpName>
<!-- Whether to embed binaries to the JavaScript module file (true or false); true by default. -->
<BootsharpEmbedBinaries>true</BootsharpEmbedBinaries>
<!-- Whether to disable some .NET features to reduce binary size (true or false); false by default. -->
<BootsharpAggressiveTrimming>false</BootsharpAggressiveTrimming>
<!-- Whether to app is built with the new experimental NativeAOT-LLVM backend; false by default. -->
<BootsharpLLVM>false</BootsharpLLVM>
<!-- The command to run when compiling/bundling generated JavaScrip solution. -->
<BootsharpBundleCommand/>
<!-- Directory to publish generated JavaScript module; 'base-output/module-name' by default. -->
Expand Down
Loading
Loading