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

Advanced Embedding API in .NET5 like Mono #39798

Closed
BeastLe9enD opened this issue Jul 22, 2020 · 22 comments
Closed

Advanced Embedding API in .NET5 like Mono #39798

BeastLe9enD opened this issue Jul 22, 2020 · 22 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Interop-coreclr
Milestone

Comments

@BeastLe9enD
Copy link

Background and Motivation

In recent years, .NET has established itself in the gaming world. Many game engines like Unity or CryEngine use .NET for scripting entity components and other gameplay related stuff.

This was made possible using the mono runtime, which Unity and CryEngine use. It offers a diversified embedding API with detailed methods that provide precise access to the CLR and all required components.
Unfortunately, CoreCLR only offers a very superficial interface, so for me as a game engine developer it is unfortunately difficult to use CoreCLR instead of mono, although it offers significantly more performance in some places.

Proposed API

One really important function in Mono is the concept of internal calls. You can register internal calls with mono_add_internal_call and they act as native functions you can call from the CLR universe. Because .NET Core has no internal calls that can be controlled by the runtime host, you have to use PInvoke, which has some downsides for example that garbage collected objects must be marshalled (for example System.String -> const char*)

        [DllImport("GameEngineLibrary.dll")]
        private static extern void OldInternalCall(); //PInvoke fallback because there is no Internal Call
        
        [MethodImpl(MethodImplOptions.InternalCall)]
        public static extern void NewInternalCall();

Usage Examples

To store managed objects in the native universe, mono_object_new/mono_string_new are really useful to create managed objects from the native universe, and to ensure that the object is not garbage collected, mono_gchandle_new is unavoidable.

To enumerate properties of components, its required to enumerate/access field and method properties of managed types, so methods like mono_class_get_methods/mono_class_get_field really help!

These few examples are just a small insight of what the Mono Embedding API offers for game developers, and I'm sure that all game developers willing to use .NET will be really excited to see a comparable API in .NET 5 runtime!

@BeastLe9enD BeastLe9enD added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Jul 22, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Interop-coreclr untriaged New issue has not been triaged by the area owner labels Jul 22, 2020
@zmarlon
Copy link

zmarlon commented Jul 22, 2020

Very good ideas 👍

@jkoritzinsky
Copy link
Member

cc: @AaronRobinsonMSFT @elinor-fung

@AaronRobinsonMSFT
Copy link
Member

@BeastLe9enD Thanks for filing this issue. This feature is something we have been mulling over for quite some time. We have actually started exploring this path by providing some basic building blocks. One of the most important is UnmanagedCallersOnlyAttribute - usage in runtime. This attribute allows unmanaged code (i.e. native) to call managed functions with as minimal overhead as possible. Using this attribute as a potential starting point an entire API that is designed around exposing a similar surface area to mono's embedding API or the JVM's JNI is possible.

At this point in the release there isn't enough time to design and provide an official API but this is definitely something we are discussing in the .NET 6 release. These decisions are based on user feedback so please keep it coming.

/cc @jeffschwMSFT @jkotas @richlander

@AaronRobinsonMSFT AaronRobinsonMSFT removed the untriaged New issue has not been triaged by the area owner label Jul 23, 2020
@AaronRobinsonMSFT AaronRobinsonMSFT added this to the 6.0.0 milestone Jul 23, 2020
@AaronRobinsonMSFT
Copy link
Member

you have to use PInvoke, which has some downsides for example that garbage collected objects must be marshalled (for example System.String -> const char*)

This statement is interesting to me. Some "marshalling" of a string is likely to always need to happen. We can of course debate how work should be done, but a managed string object at a minimum will need to be pinned or else the GC could move it during the native call. The fact that the native side defines char* so requires expensive marshalling is more a matter of how the runtime internally stores strings - UTF-16. If the native caller defined the native type as wchar_t* then we optimize that and limit the amount of marshalling needed but still require the object to be pinned.

The registration of P/Invokes can possibly be handled in a different way by manually using the NativeLibrary and the recently added C# function pointers support in Roslyn.

None of the above diminishes the value of a similar API to mono's Embedding or the JVM's JNI, I just want to make sure I am understanding all the suggestions/pain points in the issue.

@jkotas
Copy link
Member

jkotas commented Jul 23, 2020

mono_object_new/mono_string_new are really useful to create managed objects from the native universe

There are performance trade-offs that come with exposing raw object allocation APIs like this one to 3rd party code. Exposing APIs like this typically requires conservative stack scanning that comes with non-trivial performance hit (~20% for real world workloads when we have measured it last time). CoreCLR offers significantly more performance in part by not having raw embedding APIs like Mono.

mono_class_get_methods/mono_class_get_field

System.Reflection has equivalent managed APIs. You can get the same functionality by calling the managed reflection APIs, and sending back the answers to your engine via regular interop.

If the reflection APIs do not have the right performance for your scenario or do not offer right features, I believe that we would be more interested in improving managed reflection APIs. Better reflection API would provide more value accross the board.

@BeastLe9enD
Copy link
Author

@AaronRobinsonMSFT
Perhaps I expressed myself a little unfavorably in the case. I just wanted to mention that for example
if you pass a managed string object to C++, with PInvoke you are only able to use the string in C++ by marshalling to a const char* or pinning the object that the fixed UTF-16 string isnt moved by the GC while working with it on the native side.
In Mono, you can register an internal call and use the managed string a as a MonoString* on the native side, so its possible to for example edit the string from the native side.

The thing with the managed reflection APIs is very interesting and sounds understandable to me. For my engine I'm working on at the moment I am forced to use the managed refleciton API's and then pass the results back to C++. I do not perceive this as cumbersome because, for example, instead of giving long names to the types, I can use keywords such as typeof, which is easier.

As internal call replacement, I'm currently using(for theese who are interested) an own internal call attribute with an external method like the [MethodImpl(MethodImplOptions.InternalCall)] is working:

	[XInternalCall]
	private static extern void MyCustomInternalCall();

Obviously this would normally crash with a TypeLoadException because the method has no implementation!

To work around this problem, I load the assembly with its own assembly, which searches all types for the attribute XInternalCall via Mono.Cecil and then fills the method body with the Calli instructions.
For me, its working very great and I also see a ~30% performance win compared to PInvoke in my case (this can vary depending what internal calls you are doing).

@AaronRobinsonMSFT
Copy link
Member

register an internal call and use the managed string a as a MonoString* on the native side, so its possible to for example edit the string from the native side.

@BeastLe9enD Thanks for the clarification here. It seems that @jkotas understood this point better than I did. This is an useful scenario but as mentioned does come with some drawbacks. Either conservative garbage collection or expensive object pinning must be done. The mono solution is conservative garbage collection but that comes with the performance impact which appears to be why some would like to use coreclr instead. This means we would need to require pinning and then we have a different performance impact. The trade-off is difficult to determine in a general way and allowing user configuration makes the feature even more complicated.

The idea behind the UnmanagedCallersOnlyAttribute approach is to allow users to build up their own "Embedding API" as they see fit. Projecting the coreclr's representation of a managed object into an unmanaged environment isn't an option presently but as we learn more from the community about these scenarios that may become an approach. We will be publishing a blog post soon about interoperability and will be looking for deeper understanding of how we can better support developers in these scenarios.

/cc @elinor-fung

@srxqds
Copy link
Contributor

srxqds commented Aug 6, 2020

like mono, wish mono support alc unload assembly soon.

@xgalaxy
Copy link

xgalaxy commented Aug 26, 2020

@AaronRobinsonMSFT that Interop blog post you mentioned get posted anywhere ?

@AaronRobinsonMSFT
Copy link
Member

@xgalaxy I just got back to work. The blog is close to complete - still updating/clarifying some technical details that have been in flux. The survey has been published and can be found at #40484. The blog will be published in the normal manner so currently following .NET blogs should be sufficient when it is finally published. Thanks for keeping it in mind.

@AaronRobinsonMSFT
Copy link
Member

@xgalaxy The blog is posted: https://devblogs.microsoft.com/dotnet/improvements-in-native-code-interop-in-net-5-0/

@BeastLe9enD
Copy link
Author

As welcome as these detailed blog posts are, they don't really address the issue of how to replace Mono with Net5 CoreCLR.
When compared to Net5, Mono's runtime performance is bad, however the embedding API is really useful to allow C# as a scripting language. Let me try to illustrate:

Mono's embedding API allows for creation of managed objects from C++ host, iterate over their attributes, properties, fields, and methods, then pass this information around to other native components unrelated to the managed world of C#.
It's not about simply invoking a static method...

Yes, I get get your point, the NET5 embedding API is a strength and a weakness at the same time. The runtime performance has really improved, but in situations like you described, its way more complicated and you have to expose parts of the managed reflection api to c++ on your own.

Thats what for example UnrealCLR (a plugin that natively integrates NET5 into UnrealEngine) is doing:
https://github.com/nxrighthere/UnrealCLR

But there could be more advanced functionality in that direction already integrated into NET6+, we will see

@BrUnOXaVIeRLeiTE
Copy link

Thanks for taking note, I will check back when NET6 is released.
For now I am stuck with old Net Framework DLL projects (just like Unity, CryEngine, Godot engine, etc), no NetCore it is :(

@marek-safar
Copy link
Contributor

When compared to Net5, Mono's runtime performance is bad

Please compare apples to apples. Here is one example of where MonoVM is compared to CoreCLR https://twitter.com/ben_a_adams/status/1314401170652377088. As you can see the gap is in single percentage point range specifically for this complex benchmark.

@jkotas
Copy link
Member

jkotas commented Nov 23, 2020

there could be more advanced functionality in that direction already integrated into NET6+

We have no plans to recreate the current shape of Mono embedding APIs in CoreCLR.

@BrUnOXaVIeRLeiTE
Copy link

We have no plans to recreate the current shape of Mono embedding APIs in CoreCLR.

Good to know! So I don't invest any significant efforts into .NET then.
Will have to experiment with a more lightweight scripting engine for games.

@0xF6
Copy link

0xF6 commented Nov 23, 2020

We have no plans to recreate the current shape of Mono embedding APIs in CoreCLR.

Very bad, because systems like Unity3D are very much tied to outdated Mono CLR and don't want to update .NET to CoreCLR versions due to porting difficulties and embedding APIs would make it easier to update and maintain CLR in GameEngines

In fact, it would be great to archive mono and finally transfer all features from Mono to CoreCLR :\

@BeastLe9enD
Copy link
Author

We have no plans to recreate the current shape of Mono embedding APIs in CoreCLR.

Very bad, because systems like Unity3D are very much tied to outdated Mono CLR and don't want to update .NET to CoreCLR versions due to porting difficulties and embedding APIs would make it easier to update and maintain CLR in GameEngines

In fact, it would be great to archive mono and finally transfer all features from Mono to CoreCLR :\

Sorry, I missclicked my mouse (thats the reason why I closed it). In fact, not exposing an real embedding API is really unpractical for using C# as a scripting language, however engines like Unity could write an abstraction from Mono to CoreCLR which should be possible (as far as I think)

@jkotas
Copy link
Member

jkotas commented Nov 23, 2020

engines like Unity could write an abstraction from Mono to CoreCLR which should be possible

https://xoofx.com/blog/2018/04/06/porting-unity-to-coreclr/

@0xF6
Copy link

0xF6 commented Nov 23, 2020

In fact, not exposing an real embedding API is really unpractical for using C# as a scripting language

Why not? C# has a huge ecosystem that allows you to use the language in all sorts of scenarios. (top-level, csx, etc)
Much better than JS or LUA :\

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Nov 23, 2020

@BrUnOXaVIeRLeiTE and those disappointed by the statement of no planned support. An explicit embedding API isn't planned, but that doesn't mean scripting and other scenarios leveraging mono's Embedding API aren't important to us nor something we don't want to enable - for example see our Objective-C plan. What would be very helpful to understand is not so much a request for an existing API (i.e. mono's embedding API) that satisfies these needs but rather a description of what functionality is needed.

There was mention of the Reflection API above. This could be exposed through a series of static P/Invokes using DNNE. As mentioned the static P/Invoke method is burdensome and clunky, so if DNNE instead permitted exposing a managed class with blittable function signatures to a C++ environment, would that be helpful? What other pieces of functionality are needed?

We should also avoid conflating lack of functionality with owning and writing boilerplate code. Much, not all, of mono's Embedding API functionality can be exposed to unmanaged code from CoreCLR manually or using something like DNNE to generate the boilerplate. The real issue for much of the API then is it isn't as direct or simple as mono's Embedding API. This means we should break down the request(s) into groups:

  1. Not possible to enable scenario in CoreCLR.
  2. Burdensome in CoreCLR.

@srxqds
Copy link
Contributor

srxqds commented Nov 25, 2020

As welcome as these detailed blog posts are, they don't really address the issue of how to replace Mono with Net5 CoreCLR.
When compared to Net5, Mono's runtime performance is bad, however the embedding API is really useful to allow C# as a scripting language. Let me try to illustrate:

Mono's embedding API allows for creation of managed objects from C++ host, iterate over their attributes, properties, fields, and methods, then pass this information around to other native components unrelated to the managed world of C#.
It's not about simply invoking a static method...

Here's example in Unreal Engine's editor, compiling a C# class without Visual Studio then using Mono's API to generate a "Blueprint Node" that the engine can use... It's possible with Mono, while I gave up CoreCLR

BP C# Node

https://youtu.be/1jZY7r-3M5c

Hi, @BrUnOXaVIeRLeiTE it's look so interesting, how you made it? can you share the full code?
thank you.

@ghost ghost locked as resolved and limited conversation to collaborators Jan 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Interop-coreclr
Projects
None yet
Development

No branches or pull requests