7

I am wrapping a C++ library to be used from .NET. There are functions in the C++ API that return std::future. I want to have the .NET proxy classes return System.Threading.Tasks.Task.

I thought of adding a replacement method on the C++ side that would take in a function pointer in addition to the methods parameters. I could then start up a new thread (using std::async for example) and have that wait on std::future::get. Once std::future::get returned I could call the passed in function pointer. On the C# side I would pass the a pointer to a function that would complete the returned Task. Something like:

Task CallCpp(int a, int b) {
  TaskCompletionSource<int> tcs;
  Action callback = () => tcs.SetResult(0);
  IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(callback);
  CppMethod(callbackPtr, a, b);
  return tcs.Task;
}
[DllExport]
external void CppMethod(IntPtr callback, int a, int b);
void CppMethod(void (*callback)(), int a, int b) {
  std::future f = realmethod(a, b);
  std::async([&]{ f.get; callback(); });
}

std::future realmethod(int a, int b);

(Yes, the code has memory management issues. It should be enough to get the idea across though)

shmuelie
  • 1,205
  • 1
  • 9
  • 24
  • Don't know if this will help, but the check this link: https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-return-a-value-from-a-task – Camadas May 15 '19 at 15:27
  • I'm confused as to your suggestion of using `std::async` to solve this. Is there some obvious solution with `std::async` that I'm not seeing? Because as I see it you will have to inherit .NETs `Task` and override the Wait methods to internally wait for the cpp future or something like that. I'm not even sure it's doable right now. – Wutz May 15 '19 at 15:35
  • @Wutz I added more to clarify what I meant. Hope that helps? – shmuelie May 15 '19 at 15:39
  • A little, but I don't see what that would accomplish. You'd basically have a wrapper function that starts a "waiter" thread to wait for the actual work to be done. But how do you wait for the waiter thread to be done? Basically, even if you do make a second thread with `std::async`, how does the result get back to C#? Unless you start a Task in C# that calls a C++ function which then waits for the future. But then you don't use `std::async` at all. Was that what you meant? – Wutz May 15 '19 at 15:48
  • I would pass a function pointer to a method that would call `SetResult` on a Task from the C# to the C++. Let me try clarifying some more in the question – shmuelie May 15 '19 at 16:09
  • That does make it more clear, it's basically the same as my idea of starting a thread in C# and using a C++ method to wait for the future. Thanks for clarifying! I thought I had some basic misunderstanding :) – Wutz May 16 '19 at 07:32

1 Answers1

2

Marshalling asynchronous completion or failure is relatively easy part. I would personally use a COM interface implemented in C#, marked [InterfaceType( ComInterfaceType.InterfaceIsIUnknown )], with 2 methods like setCompleted() and setFailed( HRESULT status ), and pass it when I start the task, but that’s a matter of taste.

The problem is that C++ multithreading implementation is lacking.

Boost is better in that regard, it’s future class has then method that can be used to propagate the status without blocking the thread.

Currently, standard C++ futures don’t offer anything comparable. I think you're out of luck.

If you can modify that C++ library, change the API to something that makes more sense, i.e. can notify about the completion in non-blocking manner.


Without COM, lifetime management is complicated. After you’ve launched the async task but before it completes, someone on C# side needs to retain that function pointer. Here’s a possible solution, note how it uses GCHandle to make callback outlive the task:

[UnmanagedFunctionPointer( CallingConvention.Cdecl )]
delegate void pfnTaskCallback( int hResult );

[DllImport( "my.dll" )]
static extern void launch( [MarshalAs( UnmanagedType.FunctionPtr )] pfnTaskCallback completed );

public static async Task runAsync()
{
    var tcs = new TaskCompletionSource<bool>();
    pfnTaskCallback pfn = delegate ( int hr )
    {
        if( hr >= 0 )   // SUCEEDED
            tcs.SetResult( true );
        else
            tcs.SetException( Marshal.GetExceptionForHR( hr ) );
    };
    var gch = GCHandle.Alloc( pfn, GCHandleType.Normal );

    try
    {
        launch( pfn );
        await tcs.Task;
    }
    finally
    {
        gch.Free();
    }
}
Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
Soonts
  • 20,079
  • 9
  • 57
  • 130
  • yeah, you have pretty much read my mind. The COM idea is interesting but I need to be cross platform so no COM. – shmuelie May 15 '19 at 16:59
  • BTW, HRESULT codes are cross-platform, I’m using them with native interop on ARM Linux. The runtime marshals E_INVALIDARG and the rest of them just fine, despite no COM support. On C++ side, I have `typedef uint32_t HRESULT;` – Soonts May 15 '19 at 18:11
  • Yeah, lifetime management wouldn't be fun. I left it out of my example above to save space. – shmuelie May 16 '19 at 11:35