Skip to content

[.NET] Add support for web export using static LibGodot#118976

Draft
NoctemCat wants to merge 1 commit into
godotengine:masterfrom
NoctemCat:libgodot_mono_web
Draft

[.NET] Add support for web export using static LibGodot#118976
NoctemCat wants to merge 1 commit into
godotengine:masterfrom
NoctemCat:libgodot_mono_web

Conversation

@NoctemCat

@NoctemCat NoctemCat commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

This PR serves as an alternative to #106125.

Overview

Uses static LibGodot to link Godot as a library for web export.

A huge portion of this PR was adding LibGodot support to web platform and figuring out a way to pass a static library to the final step. In comparison the changes to the way mono itself works are minor.

LibGodot setup itself doesn't use the module mono and instead uses a simple interface written natively in C#. To initialize the module mono it now exports a function when LibGodot is enabled that can be called to set the initialization function for the module, this indirection allows us to call it early from the C# side and be sure that the module mono will safely check it later, and if it is set it will call it when the module begin initializing. Doing it this way preserves the normal order GDMono::initialize is initialized. The initialization function itself passes normal initialization from InitializeFromGameProject.

Works:

  • JS import and export with [JSExport]/[JSImport].
  • Single-threaded and multi-threaded builds.
  • .NET 9 and .NET 10, currently only tested on demo.
  • Normal build and Web AOT compilation enabled with RunAOTCompilation.
  • It's basically a normal browser-wasm publish with a statically linked Godot to it, so everything that works on browser-wasm should work here, this was briefly tested with System.Security.Cryptography and System.Net.Http.HttpClient.

Downsides:

Major downsides:

Small downsides:

Hacks:

  • Calls web's _export_begin after extracting templates. It's needed for passing the path to the extracted static library in ExportPlugin.cs._ExportBegin. I think it is possible to add a separate step after extracting templates, but before _export_file begins, but I'm not sure.
  • Some functions needed stubs because they were missing.

Closes: #70796.
Demo repo: https://github.com/NoctemCat/DotnetWebExportDemo.

Thanks to @GabCoolDude for help writing it, testing export on Windows, and testing it overall.

@Zireael07

Copy link
Copy Markdown
Contributor

On Windows it requires the project to be on the C: drive.

That's a super weird limitation, why?

@GabCoolDude

This comment was marked as outdated.

@NoctemCat

Copy link
Copy Markdown
Contributor Author

On Windows it requires the project to be on the C: drive.

That's a super weird limitation, why?

Sorry, it was because of the WasmCachePath emscripten-core/emscripten#25463, when I was just began writing some combinations of the flags tried to compile additional libraries to the cache, but the default C# cache Microsoft.NET.Runtime.Emscripten.3.1.56.Cache.linux-x64 is frozen, so it errored. Because of this I decided to always add custom cache to $(BaseIntermediateOutputPath)/em_cache which caused this error on Windows.

I tried exporting again with custom cache removed and it worked. And once again thanks to @GabCoolDude for testing the export on Windows from a different drive.

@NoctemCat

Copy link
Copy Markdown
Contributor Author

Fixed a couple of bugs and ported a new demo https://noctemcat.itch.io/port-chickensoft-demo from https://github.com/chickensoft-games/GameDemo

Fixed bugs:

  1. Calling getAssemblyExports without any [JsExport] causes error.
  2. Don't compile out xr fully, only disable webxr only, fully disabling xr caused errors when trying to use Viewport and SubViewport classes in C# code, because of web library mismatch with base editor binaries that has them enabled.
Co-authored-by: GabCoolDude <gabrielfreville@proton.me>
@NoctemCat

Copy link
Copy Markdown
Contributor Author

Changes:

  • Added a couple of missing UnmanagedFunctionPointer for managed to unmanaged trampolines.
  • Fixed RuntimeError: function signature mismatch when godotsharp_variant_as_string, godotsharp_variant_as_string_name, godotsharp_variant_as_node_path, and godotsharp_variant_as_rid gets called. Not too happy about the fix, but trying to change the struct definition didn't work and I tried to change it from both sides separately and combined.
  • Now parses if it needs threads from the config file compiled with the template instead of the export option.
@acidstorm2024-star

Copy link
Copy Markdown

Small cleanup I found while looking through this PR:

  • tempfile.NamedTemporaryFile(..., delete_on_close=False) is Python 3.12-only. Godot build scripts are still commonly run with older Python 3 versions, so platform_methods.py should use delete=False instead.
  • Fixed a minor user-facing typo in Browser.targets: placee -> place.

I verified the patch with:

PYTHONPYCACHEPREFIX=$PWD/.pycache python3 -m py_compile platform_methods.py
git diff --check

Patch:

From d153b0070c7947afc2f8789de6a8a25a8dcab3d2 Mon Sep 17 00:00:00 2001
From: Admin <ADMIN@Macbooks-MacBook-Pro.local>
Date: Sun, 10 May 2026 18:14:52 -0500
Subject: [PATCH] Fix web mono export cleanup nits

---
 .../mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Browser.targets | 2 +-
 platform_methods.py                                             | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Browser.targets b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Browser.targets
index 34ee9d4..081ce7a 100644
--- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Browser.targets
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Browser.targets
@@ -51,6 +51,6 @@
 
   <Target Name="GodotCheckLibGodotPath" BeforeTargets="Build">
     <Error Text="Cannot build with empty 'BaseLibGodotPath'." Condition=" $(_BaseLibGodotPath) == '' " />
-    <Error Text="Cannot find 'libgodot.a' in expected placee." Condition=" !Exists('$(_BaseLibGodotPath)\libgodot.a') " />
+    <Error Text="Cannot find 'libgodot.a' in expected place." Condition=" !Exists('$(_BaseLibGodotPath)\libgodot.a') " />
   </Target>
 </Project>
diff --git a/platform_methods.py b/platform_methods.py
index e1e7188..c7e8c8b 100644
--- a/platform_methods.py
+++ b/platform_methods.py
@@ -185,7 +185,7 @@ def combine_libs_ar(target, source, env):
         if isinstance(lib, str) and (lib.endswith(".a") or lib.endswith(".lib")):
             paths.append(lib)
 
-    with tempfile.NamedTemporaryFile(mode="w", suffix=".mri", delete_on_close=False) as fp:
+    with tempfile.NamedTemporaryFile(mode="w", suffix=".mri", delete=False) as fp:
         fp.write(f"create {lib_path}\n")
         for path in paths:
             fp.write(f"addlib {path}\n")
-- 
2.39.2 (Apple Git-143)

@Armynator

Armynator commented May 27, 2026

Copy link
Copy Markdown
Contributor

I've done some testing with a rather complex multiplayer project. Most things seem to work fine out of the box. The .NET runtime + the GodotSharp bindings are ~10mb in size and perform better than the 60mb Mono JIT version from #106125. The engine itself is ~60mb instead of ~40mb because of missing LTO however.

Some notes:

  • System.Net.WebSockets didn't work reliably for me (async behaviour seems to be off, I didn't investigate yet and switched to Godots builtin WebSocketPeer instead which works fine)
  • GDExtensions work when compiled statically into the engine (I've tried something similar to this: https://github.com/appsinacup/documentation_gdextension_to_module?tab=readme-ov-file)
  • using <Nullable>disable</Nullable> in a Godot project will cause MSBuild warnings (because nullables are currently used in this PR without being enabled explicitly?)
  • exporting is ~20% faster with -O0, current export seems to default to -O1 for debug builds
  • missing LTO is a pretty big downside, as the WASM file is very hard to shrink without it, getting even worse when adding GDExtensions into it

Honestly I'm quite impressed how well this works already. This whole PR is very close to clicking export -> it just works at this point. Of course the browser-wasm target itself is still full of smaller issues, but that's up to Microsoft to fix, as this PR makes full use of the unmodified .NET runtime which is officially supported by them.

Another sidenote (not really related to this PR): since GDExtensions will most likely remain unsupported here, it might be worth considering supporting a simple way to convert GDExtensions into engine modules.

@spirit122

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment