Search code examples
c#c++.netc++-cliclr

Wrap delegate in std::function?


I have a native, unmanaged C++ library that I want to wrap in a managed C++ class to provide clean and type safe way to access the unmanaged class from C# without having to do PInvoke.

One the methods I'm trying to wrap have the following signature:

void Unmanaged::login(
  const std::wstring& email,
  const std::wstring& password,
  std::function<void()> on_success,
  std::function<void(int, const std::wstring&)> on_error);

Trying to wrap this however turns out to be not easy at all. The obvious way:

public delegate void LoginSuccess();
public delegate void LoginFailed(int, String^);

void Wrapper::login(String ^ email, String ^ password, LoginSuccess ^ onSuccess, LoginFailed ^ onError)
{
    unmanaged->login(convert_to_unmanaged_string(email), convert_to_unmanaged_string(password), [onSuccess]() {onSuccess(); }, [](int code, const std::wstring& msg) {onError(code,convert_to_managed_string(msg))});
}

Fails because managed C++ doesn't allow (local) lambdas (in members).

I know I can use Marshal::GetFunctionPointerForDelegate to get a native pointer to the delegate, but I still need to provide a "middleware" to convert between managed/unmanaged types (such as std::wstring).

Is there perhaps a better way than using managed C++ altogether?


Solution

  • Your code doesn't compile because you can't capture a managed object in a native lambda. But you can easily wrap a managed object in an unmanaged one, with the help of the gcroot class:

    You'll need these headers:

    #include <vcclr.h>
    #include <msclr/marshal_cppstd.h>
    

    And here's the wrapper code:

    static void managedLogin(String^ email, String^ password, LoginSuccess^ onSuccess, LoginFailed^ onError)
    {
        gcroot<LoginSuccess^> onSuccessWrapper(onSuccess);
        gcroot<LoginFailed^> onErrorWrapper(onError);
    
        Unmanaged::login(
            msclr::interop::marshal_as<std::wstring>(email),
            msclr::interop::marshal_as<std::wstring>(password),
            [onSuccessWrapper]() {
                onSuccessWrapper->Invoke();
            },
            [onErrorWrapper](int code, const std::wstring& message) {
                onErrorWrapper->Invoke(code, msclr::interop::marshal_as<String^>(message));
            }
        );
    }
    
    public ref class Wrapper
    {
    public:
        static void login(String ^ email, String ^ password, LoginSuccess ^ onSuccess, LoginFailed ^ onError)
        {
            managedLogin(email, password, onSuccess, onError);
        }
    };
    

    The gcroot object wraps a System::Runtime::InteropServices::GCHandle, which will keep the managed object alive. It's an unmanaged class you can capture in a lambda. Once you know this, the rest is straightforward.

    For some reason, the compiler complains if you try to use a lambda in a member function, but it's totally fine with a lambda in a free function. Go figure.