Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

safely passing a callback from managed code to native code

Tags:

c++11

c++-cli

I have a lot of native classes that accept some form of callbacks, usually a boost::signals2::slot-object.

But for simplicity, lets assume the class:

class Test
{
    // set a callback that will be invoked at an unspecified time
    // will be removed when Test class dies
    void SetCallback(std::function<void(bool)> callback);
}

Now I have a managed class that wraps this native class, and I would like to pass a callback method to the native class.

public ref class TestWrapper
{
public:
    TestWrapper()
        : _native(new Test())
    {

    }

    ~TestWrapper()
    {
        delete _native;
    }
private:
    void CallbackMethod(bool value);
    Test* _native;
};

now usually what I would do is the following:

  1. Declare a method in the managed wrapper that is the callback I want.
  2. Create a managed delegate object to this method.
  3. Use GetFunctionPointerForDelegate to obtain a pointer to a function
  4. Cast the pointer to the correct signature
  5. Pass the pointer to the native class as callback.
  6. I also keep the delegate alive since I fear it will be garbage collected and I will have a dangling function pointer (is this assumption correct?)

this looks kind of like this:

_managedDelegateMember = gcnew ManagedEventHandler(this, &TestWrapper::Callback);
System::IntPtr stubPointer = Marshal::GetFunctionPointerForDelegate(_managedDelegateMember);
UnmanagedEventHandlerFunctionPointer  functionPointer = static_cast<UnmanagedEventHandlerFunctionPointer >(stubPointer.ToPointer());   
_native->SetCallback(functionPointer);

I Would like to reduce the amount of code and not have to perform any casts nor declare any delegate types. I want to use a lambda expression with no delegate.

This is my new approach:

static void SetCallbackInternal(TestWrapper^ self)
{
    gcroot<TestWrapper^> instance(self);
    self->_native->SetCallback([instance](bool value)
    {
        // access managed class from within native code
        instance->Value = value;
    }
    );
}
  • Declare a static method that accepts this in order to be able to use C++11 lambda.
  • Use gcroot to capture the managed class in the lambda and extend its lifetime for as long as the lambda is alive.
  • No casts, no additional delegate type nor members, minimal extra allocation.

Question:
Is this approach safe? I'm fearing I'm missing something and that this can cause a memory leak / undefined behavior in some unanticipated scenario.

EDIT:

this approach leads to a MethodAccessException when the lambda calls a private method of its managed wrapper class. seems like this method must at least be internal.

like image 487
Elad Maimoni Avatar asked Jul 24 '19 07:07

Elad Maimoni


2 Answers

I think that you should not be using gcroot but a shared pointer. Shared pointer are made to keep an object alive as long as someone is using it.

You should also use a more c++ style in your whole code by replacing raw pointer with smart pointer and template instead of std::function (a lambda can be stored in a compile time type).

For example using the code you posted :

class Test
{
    // set a callback that will be invoked at an unspecified time
    // will be removed when Test class dies
    template <class T>
    void SetCallback(T callback); // Replaced std::function<void(bool)> with T
}

public ref class TestWrapper
{
public:
    TestWrapper()
        : _native()
    {}
private:
    void CallbackMethod(bool value);
    std::unique_ptr<Test> _native;  // Replaced Test* with std::unique_ptr<Test>
};
like image 187
Tiphaine Avatar answered Nov 16 '22 06:11

Tiphaine


After replacing the old method with this new method all over my code base, I can report that it is safe, more succinct, and as far as I can tell, no memory leaks occur.

Hence I highly recommend this method for passing managed callbacks to native code.

The only caveats I found were the following:

  • Using lambda expressions forces the use of a static method as a helper for the callback registration. This is kinda hacky. It is unclear to me why the C++-CLI compiler does no permit lambda expressions within standard methods.
  • The method invoked by the lambda must be marked internal so to not throw MethodAccessException upon invocation. This is sort of make sense as it is not called within the class scope itself. but still, delegates / lambdas with C# don't have that limitation.
like image 1
Elad Maimoni Avatar answered Nov 16 '22 06:11

Elad Maimoni