Native callback binding to C# events and overall performance
PhilippPaetzold opened this issue · 4 comments
Hello,
I have a native C++ application that is running time critical algorithms and is optimized for low-latency. As of right now, it is running on an optimized linux machine and I am very happy with the performance so far.
On the other hand, I want to write higher-level stuff of the application in C#, so I need some kind of interop between the two languages. I looked at a lot of IPC related stuff, but since there is .NET Core running on linux, my idea was to wrap the API of the native application into a shared library and access it from a C# executable with an interface and by using this awesome library.
The native API will be very simple and it basically has methods for starting and stopping the processing. This will setup a couple of threads and do the calculation intensive stuff natively and asynchronously. Implementing this methods and accessing them by AdvanceDLSupport or P/Invoke seems not a problem.
One critical thing I also need on the C# side, is to asynchronously receive statistical data from the native processing side. The statistic does not include big data (only a couple of floats and integer) and will be generated every 1-5 seconds.
My idea was to declare a callback on the C++ side (std::function mayby) that gets called whenever the native side wants to report.
This leads to the following questions:
How can I bind the native callback on the C++ side to their implementations on the C# side. Can I use standard C# events in the interface for this? And is this safe when using the GC at all?
Will the performance of the native part be degraded because the whole thing runs in a managed process with a GC? Even though the native code does its calculations asynchronously on multiple pthreads, I fear that the GC will interfer with it. It would be great if some of you could share your experiences with such implementations.
I am gratefull for any replies. Thank you
Philipp
Hi Philipp,
Thank you for the kind words! I'll try to answer as best I can.
So, generally speaking, this sounds very doable - as I'm sure you know, anything ADL binds to will need to be exported as a raw C API by your library (i.e, extern "C"
) for binding to work properly. You can bind to raw C++ as well, but the ABI and name mangling is not stable, which makes things quite a lot more complex.
When using callbacks, you'd typically declare a delegate in C#, and a matching C function pointer (usually typedef'ed) on your native side. Then, you'd accept an instance of that function pointer through your native API, provided by C#, and store it somewhere for the duration of its use. By default, ADL caches any delegates you pass into it for the duration of your application's lifetime so that the GC doesn't yank it out from underneath the native code.
As for the performance, the GC won't touch your native code at all :) It should run just as if it was started in a completely native application.
If you want to look at some code examples of callbacks and how to use them, check out the unit tests for that portion of the library.
https://github.com/Firwood-Software/AdvancedDLSupport/blob/master/AdvancedDLSupport.Tests/Tests/Integration/DelegateTests.cs
https://github.com/Firwood-Software/AdvancedDLSupport/blob/master/AdvancedDLSupport.Tests/Tests/Integration/GenericDelegateTests.cs
https://github.com/Firwood-Software/AdvancedDLSupport/blob/master/AdvancedDLSupport.Tests/c/src/GenericDelegateTests.c
Thank you Jarl for the comprehensive answer and the great work with AdvancedDLSupport. I did look into the unit-tests and found what I was looking for.
I have indeed a C++ (17) API on the native side, but I can wrap it into several plain C functions inside a wrapper DLL and export them with extern "C". I will share instances to classes between those functions as global variables wrapped into unique_ptr and shared_ptr in my native wrapper. This is maybe not the most elegant solution, but then again its really nice I can bring back structure to my code in C#, when I declare the wrapper interfaces for those functions. Thats what I really like about the AdvanceDLSupport library.
There is one thing I am still struggeling with. My native API works with unicode, so I need to bind LPWCSTR to System.String. I had a look at the code examples and unit-tests and tried to implement it in the same manner.
C-side:
typedef const wchar_t* LPWCSTR;
LPWCSTR PLATFORM_GetVersion()
{
return L"v1.0.0";
}
C#-side:
[NativeSymbols(Prefix = "PLATFORM_")]
public interface IPlatform
{
string GetVersion ();
}
The binding works, but GetVersion () only reports the very first wchar: "v".
Do you know why this is happening? For my application in general, it is important that marshalling does not involve any heap allocations on the native side. When my C++ stuff is running its hot path, I would like to avoid any system-calls like implicit allocations with new/delete, SysAllocString() because they will have a negative impact on the latency. This will be critival for marshalling of strings and (fixed size) arrays of floats and integers (and maybe also string). My idea here was to use a pooling / preallocation strategy on the native side. Could span be used here and is it possible to avoid heap allocations on the native side?
When marshalling wide strings, you need to tell ADL that the string uses 2-byte characters via an attribute. Change the managed side to
[NativeSymbols(Prefix = "PLATFORM_")]
public interface IPlatform
{
[return: MarshalAs(UnmanagedType.LPWStr)
string GetVersion ();
}
and it should work.
Keep in mind that strings are somewhat tricky to get right between C# and C/C++, and there is a heap allocation on the managed side whenever you use a string. The data pointed to by the string passed from native code is copied into managed memory, and becomes a true managed string. This can be tweaked by allowing ADL to free the native string after copying it, but it's not a common use case.
As for passing around fixed-size arrays of floats and integers, spans will be a perfect fit. Check out these unit tests for how to use them:
https://github.com/Firwood-Software/AdvancedDLSupport/blob/master/AdvancedDLSupport.Tests/Data/Interfaces/ISpanMarshallingTests.cs
https://github.com/Firwood-Software/AdvancedDLSupport/blob/master/AdvancedDLSupport.Tests/Tests/Integration/SpanMarshallingTests.cs
https://github.com/Firwood-Software/AdvancedDLSupport/blob/master/AdvancedDLSupport.Tests/c/src/SpanMarshallingTests.c
Thats great, thank you for all the support!