Skip to content

Readd support for web platform exports when using the C# (.NET) version of the engine #70796

Open
@Kezzo

Description

@Kezzo

Godot version

v4.0.beta10.mono.official [d0398f6]

System information

macOS Monterey 12.5.1 (21G83)

Issue description

In a fresh project created with v4.0beta10 and export templates downloaded and installed, the web export templates files are missing at the expected path. This makes it impossible to create web builds afaik.

image
image
image

Steps to reproduce

  1. Create new godot project with v4.0beta10
  2. Create web export preset
  3. Download export templates
  4. Click on open folder in the export template download window
  5. Observe that the required files for web exports are missing

Minimal reproduction project

N/A

Activity

Calinou

Calinou commented on Jan 2, 2023

@Calinou
Member

There are no C# export templates for HTML5 yet due to missing upstream support in .NET 6 (.NET 7 didn't address this).

That said, there are significant issues with the HTML5 export in 4.0, such as #68647 and #70691. I would recommend using 3.x if targeting HTML5 for now, regardless of whether you're using C# or not.

reopened this on Jan 2, 2023
changed the title Web export templates not downloaded/missing Document missing C# web export templates due to lack of upstream support on Jan 2, 2023
added this to the 4.0 milestone on Jan 2, 2023
Delpire

Delpire commented on Jan 16, 2023

@Delpire

What is the missing upstream support from .NET we're waiting on to get HTML5 support? Can we link that item here and then in whatever documentation gets created?

Not having HTML5 support for Godot 4 is preventing me from using Godot 4 when participating in game jams as I prefer uploading web builds.

modified the milestones: 4.0, 4.x on Jan 27, 2023

66 remaining items

raulsntos

raulsntos commented on Dec 16, 2024

@raulsntos
Member

Now I'm curious why this was never an issue for the Blazer Wasm users. Is it true that they never use Wasm side modules?

Side modules is for dynamic linking, they may be statically linking dependencies. But that also has some requirements, the WASM you're linking needs to be relocatable.

NativeAOT-LLVM

is there a repository available where we can reproduce this scenario?

No, just build a Godot project with NativeAOT-LLVM. See instructions in their documentation.

Here's an example .csproj:

<Project Sdk="Godot.NET.Sdk/4.4.0-dev">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <EnableDynamicLoading>true</EnableDynamicLoading>
  </PropertyGroup>

  <PropertyGroup>
    <PublishTrimmed>true</PublishTrimmed>
    <SelfContained>true</SelfContained>
    <MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
  </PropertyGroup>

  <ItemGroup Label="NativeAOT-LLVM browser-wasm packages">
    <PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*" />
    <PackageReference Include="runtime.$(NETCoreSdkPortableRuntimeIdentifier).Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*" />
  </ItemGroup>

  <ItemGroup>
    <TrimmerRootAssembly Include="GodotSharp" />
    <TrimmerRootAssembly Include="$(TargetName)" />
  </ItemGroup>

  <ItemGroup Label="NativeAOT-LLVM browser-wasm linker args">
    <LinkerArg Include="-sSIDE_MODULE=2" />
  </ItemGroup>
</Project>

Then use this command to build:

dotnet publish -r browser-wasm -c ExportRelease
Delsin-Yu

Delsin-Yu commented on Dec 16, 2024

@Delsin-Yu
Contributor

the WASM you're linking needs to be relocatable

I guess this implies that the Godot Wasm can't be relocatable and hence cannot be statically linked to the Dotnet user Wasm?

jolexxa

jolexxa commented on Dec 23, 2024

@jolexxa
Contributor

@raulsntos I'm curious if it's valid solution (or why not) to leverage Godot as a library just for C# web exports and have a C# managed entry point. I imagine that'd need a bit of effort on the export side of things in Godot, but then at least the effort would be constrained to Godot itself and not dependent on .NET (which is clearly moving at its own pace).

Unrelated, but the bounty is now at ~$1,600 😄

lucasabbas

lucasabbas commented on Jan 10, 2025

@lucasabbas

Hello folks, we wanted to update you on the current state of C# web exports. We know there's a lot of people interested in exporting their C# games to the web platform, and we'd love to add support as soon as possible. Unfortunately, there are a number of challenges that have to be solved first and we haven't been able to find a good way to do it.

We've been exploring multiple ways of getting C# web export support, each of them have their own strengths and weaknesses, I'll try to explain what we've tried and why it didn't work.

dotnet.js

This is the most straight-forward way to get run C# on the web platform today. You can just publish a C# project using the browser-wasm runtime identifier and you'll get a WASM and the dotnet.js file ready to deploy. All you have to do is load dotnet.js from your HTML and initialize the runtime.

import { dotnet, exit } from './_framework/dotnet.js';

try {
const { getAssemblyExports, getConfig } = await dotnet.create();

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);

exports.Greeter.SayHello();

} catch (err) {
exit(2, err);
}

It's very simple, and since we let dotnet.js initialize the .NET runtime we can rest assured that everything is setup properly. It would require very few changes on our side.

Why this didn't work

Every WASM is executed isolated on its own reserved memory, so loading the Godot WASM and the C# WASM separately means they can't communicate with each other.

The only way for WASMs to interact with each other is to link them, and that requires all the WASM libraries to be compiled with compatible flags. In this case, the WASM file built by .NET and the Godot WASM are built with incompatible flags. But what if we built the .NET WASM ourselves with the right flags? See the NativeAOT-LLVM section below.

NativeAOT-LLVM

There is a promising runtime experiment in the dotnet/runtimelab repo that adds support for building a WASM using NativeAOT.

Godot already supports loading C# using NativeAOT, so the changes required to make this work should be minimal.

The reason why this is a very promising option is that, since NativeAOT means you are using native compilers, you get a chance to set custom flags to use in that native compilation. This would allow us to ensure the WASM built for the C# project matches the flags used to build the Godot WASM, and thus make it compatible.

Why this didn't work

Unfortunately, adding the -sSIDE_MODULE=2 flag to the WASM compilation wasn't enough to make this work. There are other libraries linked that don't seem to have compatible flags, so the compilation fails. Here's an example of one of the errors we get:

wasm-ld : error : $HOME/.nuget/packages/runtime.browser-wasm.microsoft.dotnet.ilcompiler.llvm/10.0.0-alpha.1.24568.1/sdk/libbootstrapperdll.o: relocation R_WASM_TABLE_INDEX_SLEB cannot be used against symbol `__managed__Startup`; recompile with -fPIC

The -fPIC flag is also one of the flags set by -sSIDE_MODULE=2, which means it's required that the linked library is compiled with that flag. Unfortunately, it doesn't seem to be the case.

It's possible that this could be fixed by compiling the entire .NET runtime instead, but I don't think it'd be a good approach. I've discussed with Microsoft employees, and I've been told that they tried to compile the .NET runtime as a side module a long time ago unsuccessfully, they haven't tried since so it's likely not going to work.

Statically linking Mono

Since dynamically linking WASMs won't work, we could try statically linking Mono instead. This is the worse approach we tried by far, we ended up duplicating a lot of the setup in the .NET runtime to compile the WASM, it'd be a lot of work to maintain this and keep it in sync with upstream, not to mention supporting multiple .NET versions, since we don't know what TFM the user project targets at the time of building the Godot templates.

Why this didn't work

Godot's C# bindings rely on function pointers to communicate with the engine. However, when we set this up we were unable to get Mono to retrieve function pointers. It seems, Mono has a mechanism to build a table of function pointers at compile time so it can retrieve them later, but we were unable to get this to work.

We also tried MonoAOT, but since GodotSharp doesn't support trimming, it seems this assembly is too big and won't compile.

This approach would be very brittle and hard to maintain, but I was willing to give it a try so we could at least get some experimental support to at least unblock the users that have been patiently waiting. Unfortunately, it didn't pan out.

Conclusion

We are sorry to say that web support is still not available for C# projects. We know this is disappointing to hear, but we are constantly looking at new ideas and would love to get this working as soon as we can.

If the Godot and .NET can't be linked together, why not try running .NET code using a WASM interperter like WASM-3

danroth27

danroth27 commented on Jan 11, 2025

@danroth27

@raulsntos I'd be interested in discussing what godot need on the .NET side to unblock this scenario so that we at least understand what the work is involved here. It sounds like you're blocked on dotnet/runtime#75257, correct? Is there anything else?

raulsntos

raulsntos commented on Jan 12, 2025

@raulsntos
Member

Hello @danroth27 👋, thanks for reaching out.

Yes, dotnet/runtime#75257 seems to be the main blocker. We intend to load the .NET WASM dynamically from the main Godot WASM. To give more context, the main Godot WASM is pre-built and distributed to users so they don't have to build the engine themselves.

We also tried the experimental NativeAOT-LLVM which allows us to specify additional emscripten flags with <LinkerArg Include="-sSIDE_MODULE=2"/>, but run into wasm-ld errors (likely because some of the .NET libraries are built with incompatible flags).

As I understand it, Microsoft had a lot of problems with emscripten's dynamic linking in the past, so I assume it's unsupported but I'm not aware of the specific issues you ran into.

We also tried linking Mono statically and in this case we run into issues with retrieving function pointers. It seems .NET is supposed to build a table of function pointers out of methods annotated with [UnmanagedCallersOnly] but I haven't been able to trigger that, maybe there's an undocumented MSBuild property that I'm not aware of. We'd still prefer the dynamic linking options mentioned above, but I thought I'd also mention this because it could unblock us in the short term.

There's more details about what we've tried in my previous comment: #70796 (comment)

danroth27

danroth27 commented on Jan 12, 2025

@danroth27

We also tried linking Mono statically and in this case we run into issues with retrieving function pointers. It seems .NET is supposed to build a table of function pointers out of methods annotated with [UnmanagedCallersOnly] but I haven't been able to trigger that

@lewing Any ideas on what might be happening here?
@raulsntos Can you share with us more specifics on these issues you hit with static linking, like the related code and the specific errors?

raulsntos

raulsntos commented on Jan 12, 2025

@raulsntos
Member

Sure, here are the details of what I've done:

I'm building the main Godot WASM including the sources from Microsoft.NETCore.App.Runtime.Mono.multithread.browser-wasm/9.0.0 (I've also used 8.0.0 in the past and I had the same results).

Then, on C++ I use mono_wasm_load_runtime, mono_assembly_load, and mono_runtime_invoke to initialize the .NET runtime, load the assembly, and call a function on the C# side to initialize some Godot stuff.

For our initialization, we take the function pointer of some C# methods and pass them to the C++ side so we can invoke them from the native engine side. The problem is that these function pointers all seem to be null. Here's an example of what I mean:

using System;

public static class InitializationClass
{
    [UnmanagedCallersOnly]
    private static int Sum(int left, int right)
    {
        return left + right;
    }

    public unsafe static void Init()
    {
        delegate* unmanaged<int, int, int> sumPtr = &Sum;
        Console.WriteLine($"sumPtr = {(nint)sumPtr}"); // Prints 'sumPtr = 0'
    }
}

No errors or exceptions, but since the value is null we can't call the function, so we can't finish the engine initialization.

Here's the command we use to publish the .NET project:

dotnet publish UserProject.csproj -c ExportDebug -r browser-wasm --self-contained true -p:GodotTargetPlatform=web -o /tmp/godot-publish-dotnet/192660-ExportDebug-browser-wasm

Note that, since we have custom configurations, we also set the property <WasmBuildNative> to true in our MSBuild SDK, which seems to match what the Release configuration does. We may have missed other properties, but this seems to be something we can workaround. See relevant issues: dotnet/runtime#104540 and dotnet/sdk#31918

xuanyusong

xuanyusong commented on Jan 20, 2025

@xuanyusong

Sure, here are the details of what I've done:

I'm building the main Godot WASM including the sources from Microsoft.NETCore.App.Runtime.Mono.multithread.browser-wasm/9.0.0 (I've also used 8.0.0 in the past and I had the same results).

Then, on C++ I use mono_wasm_load_runtime, mono_assembly_load, and mono_runtime_invoke to initialize the .NET runtime, load the assembly, and call a function on the C# side to initialize some Godot stuff.

For our initialization, we take the function pointer of some C# methods and pass them to the C++ side so we can invoke them from the native engine side. The problem is that these function pointers all seem to be null. Here's an example of what I mean:

using System;

public static class InitializationClass
{
[UnmanagedCallersOnly]
private static int Sum(int left, int right)
{
return left + right;
}

public unsafe static void Init()
{
    delegate* unmanaged<int, int, int> sumPtr = &Sum;
    Console.WriteLine($"sumPtr = {(nint)sumPtr}"); // Prints 'sumPtr = 0'
}

}
No errors or exceptions, but since the value is null we can't call the function, so we can't finish the engine initialization.

Here's the command we use to publish the .NET project:

dotnet publish UserProject.csproj -c ExportDebug -r browser-wasm --self-contained true -p:GodotTargetPlatform=web -o /tmp/godot-publish-dotnet/192660-ExportDebug-browser-wasm
Note that, since we have custom configurations, we also set the property <WasmBuildNative> to true in our MSBuild SDK, which seems to match what the Release configuration does. We may have missed other properties, but this seems to be something we can workaround. See relevant issues: dotnet/runtime#104540 and dotnet/sdk#31918

Unity Chinese version engine(团结引擎) converts Dll into WebCIL and interprets it through mono runtime. Can Godot use this method to run?

https://docs.unity.cn/cn/tuanjiemanual/Manual/WeixinMiniGameDotNetSupport.html

Delsin-Yu

Delsin-Yu commented on Jan 20, 2025

@Delsin-Yu
Contributor

I believe the CN fork of Unity (aka Tuanjie) is only utilizing a small portion of the .Net 8 WASM toolkits for its .Net 8 backend, which requires many customized solutions. I doubt it can be used freely with future releases of .Net.

Image

Image

xuanyusong

xuanyusong commented on Jan 21, 2025

@xuanyusong

unity(团结)Engine mentioned some implementation details. The il2cpp solution for generating WebAssembly will package the user code and the engine code together, and also deal with the generation of generic template code, which makes the Wasm file very large. Therefore, the il2cpp solution was abandoned and the Microsoft Blazor solution was adopted to separate the user C# code and Wasm runtime to interpret and execute C# code. The DotNet Wasm solution is based on .NET8, relies on the Emscripten tool chain to build WebAssembly, and uses the trimmed and optimized mono as the .Net runtime.
https://mp.weixin.qq.com/s/5xfq-EveRWmBnHMfC-icRA

Antoniojesus122

Antoniojesus122 commented on Feb 4, 2025

@Antoniojesus122
WithinAmnesia

WithinAmnesia commented on Feb 6, 2025

@WithinAmnesia
WithinAmnesia

WithinAmnesia commented on Feb 6, 2025

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Participants

    @weebs@Calinou@lewiji@abbychau@Sythelux

    Issue actions

      Readd support for web platform exports when using the C# (.NET) version of the engine · Issue #70796 · godotengine/godot