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

.NET core hosting: how to instantiate objects, call members functions ? #18174

Closed
gabrieldevillers opened this issue Apr 30, 2020 · 16 comments
Closed
Labels
doc-enhancement Improve the current content [org][type][category] Pri2 won't fix Issues that were closed as part of automated backlog grooming

Comments

@gabrieldevillers
Copy link

gabrieldevillers commented Apr 30, 2020

The current documentation does not tell whether instantiating objects and calling member functions is impossible or just out of scope for this tutorial. I searched a lot online and found no more information on that.

This is something which is possible to do with Mono (using mono_object_new and mono_runtime_invoke according to https://www.mono-project.com/docs/advanced/embedding/).

Is this something that is not currently possible with .NET core ? Do you think this will be possible in the future, and when approximately would you expect it to be implemented ?

This feature is appealing because embedding C# is the best option I found to call C# from C++ in a cross-platform way:

Thanks in advance.

[Edited by gewarren to add document details]


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

@dotnet-bot dotnet-bot added the ⌚ Not Triaged Not triaged label Apr 30, 2020
@Thraka Thraka added doc-enhancement Improve the current content [org][type][category] waiting-on-feedback Waiting for feedback from SMEs before they can be merged and removed ⌚ Not Triaged Not triaged labels May 1, 2020
@Thraka
Copy link
Contributor

Thraka commented May 1, 2020

@mjrousos Perhaps you can comment?

@gabrieldevillers
Copy link
Author

@mjrousos I would be really helped by any comment, even just "that's not planned" or "that's not a good idea, look at X instead".

@lappelsmeier
Copy link

Any news? I'd also be interested :)

@Thraka
Copy link
Contributor

Thraka commented Jun 3, 2020

Sent an email to @mjrousos

@mjrousos
Copy link
Member

mjrousos commented Jun 3, 2020

I believe .NET Core hosting APIs can only call static managed methods, but I haven't worked on hosting recently so it's possible things have changed. @AaronRobinsonMSFT, are you the right person to ask if this is still the case or if there are now ways to instantiate managed objects directly from .NET Core hosting APIs?

As far as I know, the recommendation would be to have a static managed entry point that does the necessary object instantiation, calling of instance methods, etc.

@AaronRobinsonMSFT
Copy link
Member

As far as I know, the recommendation would be to have a static managed entry point that does the necessary object instantiation, calling of instance methods, etc.

@mjrousos Yes. That recommendation is still correct. As @gabrieldevillers mentions there is the COM/IUnknown approach but that is presently only supported for Windows. The new ComWrappers API in .NET 5.0 has been requested to be supported cross-platform. That would enable the IUnknown ABI in a cross-platform way.

/cc @jkoritzinsky @elinor-fung

@gabrieldevillers
Copy link
Author

I updated my initial comment to mention CoreRT which might also be a possibility to call C# from C++ in a cross-platform way (I do not know what @MichalStrehovsky or @jkotas would think of this idea). Droping reflection could be an acceptable tradeoff for us given that we would write new code.

@AaronRobinsonMSFT
Copy link
Member

@gabrieldevillers The problem isn't necessarily calling C# from C++, but rather interacting with an object in a "natural" (read OOP) manner that is troublesome. An example of consuming C# from native code is https://github.com/AaronRobinsonMSFT/DNNE or loading the CLR manually and getting a delegate - see https://github.com/dotnet/samples/tree/master/core/hosting/HostWithHostFxr/src.

The ability to interact with objects in a "natural" way would require some kind of embedding API or building up an IUnknown type of solution. Mono's embedding API is similar to JNI which is a valid approach. We are still considering what options make sense for CoreCLR. CoreRT does contain some interesting tech that tends to migrate into CoreCLR so something out of there is also an option.

@gabrieldevillers
Copy link
Author

gabrieldevillers commented Jun 10, 2020

@AaronRobinsonMSFT Thanks for this answer, I was able to build & run your DNNE project.

Then I tried to use GetIUnknownForObject() to get a intptr_t handle to a managed object that I would like to pass to the C side, as I am on Linux this failed (obviously) with "System.PlatformNotSupportedException: COM Interop is not supported on this platform."

I also found this SO thread: https://stackoverflow.com/questions/32295586/save-reference-to-managed-object-in-unmanaged-memory which makes me question this strategy (even if we had COM on Linux).

Do you see any way to build a solution on top of DNNE to pass to the unmanaged side a handle to a managed object on Linux ? I do not care about nice OO semantics for now.

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Jun 10, 2020

@gabrieldevillers Thanks for keeping this conversation going. It is something we are looking into working on in the next release but need more user feedback before making that decision. This conversation feeds into the planning calculus so please keep it going.

As mentioned, the Marshal.GetIUnknownForObject() isn't going to work on a non-Windows platform. That is by-design for now. There are two approaches one can take here.

  1. Implement an IUnknown ABI platform. There are actually two options here, use something like https://github.com/SharpGenTools/SharpGenTools or do it yourself. The manual process has varying levels of complexity and depending on your knowledge of C++ vtable dispatch can be relatively straight forward or a deep dive into the interop world.

  2. Use a GCHandle that is mentioned in the SO post. This would also require creating a static export for each and every function that would be needed on the object instance passed to native. Let sketch out an example from the C# side below.

class Foo
{
    public int DoubleInt(int a)
    {
        return a * 2;
    }
}

[UnmanagedCallersOnly]
public static IntPtr NewFoo()
{
    return GCHandle.ToIntPtr(GCHandle.Alloc(new Foo()));
}

[UnmanagedCallersOnly]
public static void DeleteFoo(IntPtr fooRaw)
{
    GCHandle.FromIntPtr(fooRaw).Free();
}

[UnmanagedCallersOnly]
public static int CallFooDoubleInt(IntPtr fooRaw, int a)
{
    var foo = (Foo)GCHandle.FromIntPtr(fooRaw).Target;
    return foo.DoubleInt(a);
}

Notice that we have converted an OOP style into how it would be done if we wanted classes in C. The benefit of (1) is that all of the lifetime and much of the marshalling code can be generated and handled for you. The benefit of (2) is that if you have performance constraints or special marshalling requirements you have full control. Choosing one is based on product need and level of complexity that one would like to introduce into their project.

@gabrieldevillers
Copy link
Author

Thanks for these explanations, they are very helpful. I will try to go with (2) and maybe do some benchmarks when I have time to see the impact of the pinning on the C# performance (but I suspect this will be very application-specific).

Do you think that the pinning cost could be reduced by adding 1 level of indirection in C# made of small objects holding the reference to the "real" C# objects ? An pool of arbitrary size of these small objects could be created at the very beginning of the program and then be recycled as the "real" C# objects are deleted/created. This way the "real" C# objects would never be pinned and could be moved around in memory to protect the cache, only the "small objects" would be pinned and (supposedly) the performance cost would be lower as they would be tightly packed and at the "start" of the memory.

@jkotas
Copy link
Member

jkotas commented Jun 11, 2020

There should be no pinning involved when passing GCHandles around. The code snippet above has a bug. The NewFoo method should be:

public static IntPtr NewFoo()
{
    return GCHandle.ToIntPtr(GCHandle.Alloc(new Foo()));
}

The GCHandles are very similar to your pinned small object idea. They are small, never move around, and allocated in a different part of memory than regular objects to avoid impacting performance.

@jkotas
Copy link
Member

jkotas commented Jun 11, 2020

I have edited @AaronRobinsonMSFT 's post to fix the bug.

@gabrieldevillers
Copy link
Author

Thanks @jkotas So to be sure I understood: pinning is not needed in this case, but still useful in other cases like I/O to a C# buffer from C ?

@jkotas
Copy link
Member

jkotas commented Jun 11, 2020

Right.

@dotnet-bot
Copy link
Contributor

This issue has been closed as part of the issue backlog grooming process outlined in #22351.

That automated process may have closed some issues that should be addressed. If you think this is one of them, reopen it with a comment explaining why. Tag the @dotnet/docs team for visibility.

@dotnet-bot dotnet-bot added the won't fix Issues that were closed as part of automated backlog grooming label Jan 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
doc-enhancement Improve the current content [org][type][category] Pri2 won't fix Issues that were closed as part of automated backlog grooming
Projects
None yet
Development

No branches or pull requests