Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C#: Move marshaling logic and generated glue to C# #48409

Merged
merged 1 commit into from
Aug 20, 2021

Conversation

neikeq
Copy link
Contributor

@neikeq neikeq commented May 3, 2021

We will be progressively moving most code to C#.
The plan is to only use Mono's embedding APIs to set things at launch.
This will make it much easier to later support CoreCLR too which doesn't have rich embedding APIs.

Additionally the code in C# is more maintainable and makes it easier to implement new features, e.g.: runtime codegen which we could use to avoid using reflection for marshaling everytime a field, property or method is accessed.

Some notes on interop

We make the same assumptions as GDNative about the size of the Godot structures we use. We take it a bit further by also assuming the layout of fields in some cases, which is riskier but let's us squeeze out some performance by avoiding unnecessary managed to native calls.

Code that deals with native structs is less safe than before as there's no RAII and copy constructors in C#. It's like using the GDNative C API directly. One has to take special care to free values they own. Perhaps we could use roslyn analyzers to check this, but I don't know any that uses attributes to determine what's owned or borrowed.

As to why we maily use pointers for native structs instead of ref/out:

  • AFAIK (and confirmed with a benchmark) ref/out are pinned during P/Invoke calls and that has a cost.
  • Native struct fields can't be ref/out in the first place.
  • A using local can't be passed as ref/out, only in. Calling a method or property on an in value makes a silent copy, so we want to avoid in.

Regarding the build system

There's no longer a mono_glue=yes/no SCons options. We no longer need to build with mono_glue=no, generate the glue and then build again with mono_glue=yes. We build only once and generate the glue (which is in C# now).
However, SCons no longer builds the C# projects for us. Instead one must run build_assemblies.py, e.g.:

%godot_src_root%/modules/mono/build_scripts/build_assemblies.py \
        --godot-output-dir=%godot_src_root%/bin \
        --godot-target=release_debug`

We could turn this into a custom build target, but I don't know how to do that with SCons (it's possible with Meson).

Other notes

Most of the moved code doesn't follow the C# naming convention and still has the word Mono in the names despite no longer dealing with Mono's embedding APIs. This is just temporary while transitioning, to make it easier to understand what was moved where.

@neikeq neikeq added this to the 4.0 milestone May 3, 2021
@neikeq neikeq requested a review from a team as a code owner May 3, 2021 14:53
@neikeq
Copy link
Contributor Author

neikeq commented May 3, 2021

This is the first step towards godotengine/godot-proposals#2333

The changes are being done in a way that don't require migrating to .NET 5/6, as that may not make it in time for Godot 4.0. This way we make make progress towards the .NET 5/6 goal without switching to it yet.
At some point, further changes will require migrating to .NET 5/6, at which point we could make a new branch to develop it in parallel to the Godot 4.0 branch.

@akien-mga
Copy link
Member

However, SCons no longer builds the C# projects for us. Instead one must run build_assemblies.py ...
We could turn this into a custom build target, but I don't know how to do that with SCons (it's possible with Meson).

You can maybe have a look at how @Faless handled packaging the JavaScript templates in platform/javascript/SCsub? I assume something similar could be done to run build_assemblies.py as part of the scons command.

@neikeq
Copy link
Contributor Author

neikeq commented May 3, 2021

You can maybe have a look at how @Faless handled packaging the JavaScript templates in platform/javascript/SCsub? I assume something similar could be done to run build_assemblies.py as part of the scons command.

This can't be done as part as the same scons command that builds Godot because the generated C# glue may not exist yet (and will definitely not exist yet if it's a clean build). This was not a problem before because we could determine this with mono_glue=yes/no. It's worth only having to build Godot once though.
My idea was a custom target that you run separately. In meson this would be something like meson compile build_csharp. This way we wouldn't need to specify --godot-output-dir and --godot-target. Even custom targets are possible with SCons, configuration and compilation are part of the same command, so this likely not doable unless we were to also generate the C# glue as part of the SCons command.

@GeorgeS2019
Copy link

GeorgeS2019 commented May 8, 2021

@neikeq it is unclear what is your time line for this as an interim transition before aligning with .NET5/6, [.NET6 scheduled for Nov 2021]

How shall users contribute to the testing?

  • Godot4 mono without this PR (before merging this PR)
  • Godot4 mono with this PR, how long will we stay with this FIRST step, before naming will be stabilized?
  • In your view, what will then be the SECOND step?

The plan is to only use Mono's embedding APIs for set things up.

From .NET6 point of view, Mono embedding API will only be relevant for mobile, consoles and WebAssembly, not desktop for e.g. linux and windows. I assume that the second step is to replace the Mono embedding API for NON (mobile, console and WebAssembly) with that of CoreCLR [for desktop] or even better CoreRT?

(migueldeicaza) Mono is the VM that supports Android, iOS, consoles and WebAssembly, and also part of the .net 6 platform. Switching to CoreCLR as the VM means none of those platforms will be supported by Godot.

@neikeq
Copy link
Contributor Author

neikeq commented May 9, 2021

it is unclear what is your time line for this as an interim transition before aligning with .NET5/6, [.NET6 scheduled for Nov 2021]

There's no timeline. Nothing is set on stone right now and we don't know enough about .NET 6 yet. If in one month or two I conclude we won't have .NET 6 support in time for Godot 4.0, then I will put that aside for the time being while I focus on the stable release.

How shall users contribute to the testing?

You can try it with different projects to see if anything breaks. Other than that, there's no unit tests set up right now. That's something I would like to change for Godot 4.1 if things go well.

  • Godot4 mono with this PR, how long will we stay with this FIRST step, before naming will be stabilized?
  • In your view, what will then be the SECOND step?

I'm not sure what you mean by the naming being stabilized. I'll continue to move more code to C# this month.

The plan is to only use Mono's embedding APIs for set things up.

From .NET6 point of view, Mono embedding API will only be relevant for mobile, consoles and WebAssembly, not desktop for e.g. linux and windows. I assume that the second step is to replace the Mono embedding API for NON (mobile, console and WebAssembly) with that of CoreCLR [for desktop] or even better CoreRT?

This has nothing to do with platforms. The idea is to limit the use of embedding APIs to only startup tasks in order to make it easy to later support to CoreCLR which has a much more limited embedding API.
The second step is to move more code to C#, and after that I can't say atm. We're not adding support for CoreCLR yet.

Let's leave discussion about roadmaps and global vision out of this PR unless it has significant impact on its review.

@aaronfranke
Copy link
Member

With this PR, when I try to build the assemblies, it fails, and this is the output (I'm on Ubuntu 20.04 64-bit):

$ python3 modules/mono/build_scripts/build_assemblies.py --godot-output-dir=bin --godot-target=release_debug
Running MSBuild:  /usr/bin/dotnet msbuild /home/aaronfranke/workspace/godot/modules/mono/glue/GodotSharp/GodotSharp.sln /restore /t:Build /p:Configuration=Debug /p:NoWarn=1591
Microsoft (R) Build Engine version 16.7.2+b60ddb6f4 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored /home/aaronfranke/workspace/godot/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj (in 168 ms).
  Restored /home/aaronfranke/workspace/godot/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj (in 918 ms).
/home/aaronfranke/workspace/godot/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj(92,3): error MSB4019: The imported project "/home/aaronfranke/workspace/godot/modules/mono/glue/GodotSharp/GodotSharp/Generated/GeneratedIncludes.props" was not found. Confirm that the expression in the Import declaration "Generated/GeneratedIncludes.props" is correct, and that the file exists on disk.                                                       
/home/aaronfranke/workspace/godot/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj(27,3): error MSB4019: The imported project "/home/aaronfranke/workspace/godot/modules/mono/glue/GodotSharp/GodotSharpEditor/Generated/GeneratedIncludes.props" was not found. Confirm that the expression in the Import declaration "Generated/GeneratedIncludes.props" is correct, and that the file exists on disk.

@neikeq
Copy link
Contributor Author

neikeq commented May 9, 2021

@aaronfranke Did you generate the glue before attempting to build the assemblies? You still need to run ./bin/godot --generate-mono-glue ./modules/mono/glue after building the Godot editor the first time. What changed is you don't need to build it a second time.

@aaronfranke
Copy link
Member

@neikeq Yes, that was it. I read the post wrong and thought this script replaced the glue generation code. So to be clear, the process used to be:

  • Build Godot without glue.
  • Generate glue using Godot.
  • Build Godot with glue.

And now the process is:

  • Build Godot.
  • Generate glue using Godot.
  • Run the build_assemblies.py Python script.

I suppose this is an improvement, since the Python script runs faster than rebuilding Godot, and now the first build doesn't need to run with different settings on repeated builds, making recompiles faster.

Eventually, it would be nice to get to a point where this is a one-step process inside of SCons, and it seems we're not far off from that. Is there any reason we can't just execute these commands from within SCons after the binary has been built?

modules/mono/build_scripts/build_assemblies.py Outdated Show resolved Hide resolved
modules/mono/SCsub Show resolved Hide resolved
Comment on lines +51 to +54
default:
{
if (type == typeof(Vector2))
return Variant.Type.Vector2;
Copy link
Member

Choose a reason for hiding this comment

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

VS Code's C# formatter indents these blocks further (starting with the {). I don't really have an opinion on this, but I'm mentioning it in case we wanted to change this to avoid fighting with tools.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, no. We don't want to change it. That indentation style is such a waste of horizontal space. I remember MonoDevelop doing the same thing; very annoying. Perhaps we can solve this with .editorconfig?

Copy link
Member

@raulsntos raulsntos Jul 22, 2021

Choose a reason for hiding this comment

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

This is the .editorconfig rule you are looking for:

csharp_indent_case_contents_when_block = false

https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#csharp_indent_case_contents_when_block

@neikeq
Copy link
Contributor Author

neikeq commented May 10, 2021

I suppose this is an improvement, since the Python script runs faster than rebuilding Godot, and now the first build doesn't need to run with different settings on repeated builds, making recompiles faster.

It's not just that. Previously the third-step would do both re-build the engine (only mono module; most time was spent linking) and build the C# assemblies. Now we only do the latter.

Eventually, it would be nice to get to a point where this is a one-step process inside of SCons, and it seems we're not far off from that. Is there any reason we can't just execute these commands from within SCons after the binary has been built?

We would need to make Godot runnable without a window on all desktop platforms and guarantee it doesn't exit with error code for a reason other than glue generation failing.
Another problem is that this would slow empty builds even more as now glue generation is also part of it.
For me it's not as much a problem it being a separate command as it's having to specify output dir and target again. I even prefer it as a second command to avoid slowing empty engine builds.
With Meson the second command can simply be meson compile csharp_projs.

@Calinou
Copy link
Member

Calinou commented May 10, 2021

We would need to make Godot runnable without a window on all desktop platforms

Since 3.3, --no-window is supported on Linux and macOS. Previously, it was only supported on Windows.

@neikeq neikeq changed the base branch from master to dotnet6 May 11, 2021 22:02
@neikeq
Copy link
Contributor Author

neikeq commented May 11, 2021

In the end I decided to merge this into a branch were .NET 6 support will developed in parallel with the master branch. It's better not to divert too much in the module code from 3.x to master in order to make working with both easier in case .NET 6 ends up happening in 4.1 instead of 4.0.

@neikeq neikeq force-pushed the oh-im-die-ty-4ever branch from 296693b to 719d759 Compare May 11, 2021 22:30
@aaronfranke
Copy link
Member

I made a new project with this PR, it fails to load a minimal script: 48409.zip

Screenshot from 2021-05-13 04-48-51

@akien-mga
Copy link
Member

@neikeq Seems like we forgot to merge this, do you want to rebase and merge now, or should further work be done first to fix C# bindings after recent core changes?

@neikeq
Copy link
Contributor Author

neikeq commented Jul 22, 2021

I'll fix the bindings first and then rebase this.

@neikeq neikeq force-pushed the oh-im-die-ty-4ever branch from 719d759 to 6499fe2 Compare August 20, 2021 05:32
We will be progressively moving most code to C#.
The plan is to only use Mono's embedding APIs to set things at launch.
This will make it much easier to later support CoreCLR too which
doesn't have rich embedding APIs.

Additionally the code in C# is more maintainable and makes it easier
to implement new features, e.g.: runtime codegen which we could use to
avoid using reflection for marshaling everytime a field, property or
method is accessed.

SOME NOTES ON INTEROP

We make the same assumptions as GDNative about the size of the Godot
structures we use. We take it a bit further by also assuming the layout
of fields in some cases, which is riskier but let's us squeeze out some
performance by avoiding unnecessary managed to native calls.

Code that deals with native structs is less safe than before as there's
no RAII and copy constructors in C#. It's like using the GDNative C API
directly. One has to take special care to free values they own.
Perhaps we could use roslyn analyzers to check this, but I don't know
any that uses attributes to determine what's owned or borrowed.

As to why we maily use pointers for native structs instead of ref/out:
- AFAIK (and confirmed with a benchmark) ref/out are pinned
  during P/Invoke calls and that has a cost.
- Native struct fields can't be ref/out in the first place.
- A `using` local can't be passed as ref/out, only `in`. Calling a
  method or property on an `in` value makes a silent copy, so we want
  to avoid `in`.

REGARDING THE BUILD SYSTEM

There's no longer a `mono_glue=yes/no` SCons options. We no longer
need to build with `mono_glue=no`, generate the glue and then build
again with `mono_glue=yes`. We build only once and generate the glue
(which is in C# now).
However, SCons no longer builds the C# projects for us. Instead one
must run `build_assemblies.py`, e.g.:
```sh
%godot_src_root%/modules/mono/build_scripts/build_assemblies.py \
        --godot-output-dir=%godot_src_root%/bin \
        --godot-target=release_debug`
```
We could turn this into a custom build target, but I don't know how
to do that with SCons (it's possible with Meson).

OTHER NOTES

Most of the moved code doesn't follow the C# naming convention and
still has the word Mono in the names despite no longer dealing with
Mono's embedding APIs. This is just temporary while transitioning,
to make it easier to understand what was moved where.
@neikeq neikeq force-pushed the oh-im-die-ty-4ever branch from 6499fe2 to 4830717 Compare August 20, 2021 08:25
@neikeq
Copy link
Contributor Author

neikeq commented Aug 20, 2021

Rebased, this should be good to merge now (into the dotnet6 branch).

@akien-mga akien-mga merged commit f580b1e into godotengine:dotnet6 Aug 20, 2021
@akien-mga
Copy link
Member

Thanks!

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

Successfully merging this pull request may close these issues.

6 participants