1
  1. I have a MainProgram.exe which calls in to MyDll.dll and uses curl to receive data on a callback function.
  2. I have wrapped curl in a function called CurlGetData which creates a curl instance and performs curl_easy_perform.

Here is my code:

//Interface class to derive from
class  ICurlCallbackHandler
{
public:
    virtual size_t CurlDataCallback( void* pData, size_t tSize ) = 0;
};

//Class that implements interface
class CurlCallbackHandler : public ICurlCallbackHandler
{
public:
    bool m_exit = false;

    virtual size_t CurlDataCallback( void* pData, size_t tSize ) override
    {
        if(m_exit)
            return CURL_READFUNC_ABORT;

       // do stuff with the curl data

        return tSize;
    }
}


CurlCallbackHandler *m_curlHandler;

//Create an instance of above class in my dll constructor
MyDll:MyDll()
{
    m_curlHandler = new CurlCallbackHandler();
}

//Cleanup above class in my dll destructor
MyDll:~MyDll()
{
    delete m_curlHandler;
    m_curlHandler = nullptr;
}

//Function to start receiving data asynchronously  
void MyDll::GetDataAsync()
{
    std::async([=]
    {
        //This will receive data in a new thread and call CurlDataCallback above
        //This basically calls easy_perform
        CurlGetData(m_curlHandler);
    }
}

//Will cause the curl callback to return CURL_READFUNC_ABORT
void MyDll::StopDataAsync()
{
    m_curlHandler->m_exit = true;
}

The function GetDataAsync is called from my main program and it basically calls curl_easy_perform and uses the m_curlHandler as its callback function which calls back up into CurlDataCallback.

This all works fine but whenever my main program exits, it calls MyDll::StopDataAsync which stops the curl data callback and then the destructor of MyDll is called which cleans up the m_curlHandler.

But I find that at that moment curl has not yet finished with this call back and the program crashes as m_curlHandler has been deleted but the curl callback in the new async thread still is using it.

Sometimes it closes down fine but other times it crashes due to the curlcallback trying to access a pointer that has been deleted by the destructor.

How can I best clean up the m_curlHandler? I want to avoid putting in wait time-outs as this this will affect the performance of my main program.

Harry Boy
  • 4,159
  • 17
  • 71
  • 122

1 Answers1

0

According to the C++ standard the MyDll::GetDataAsync() function should not return immediately, it should block until the asynchronous thread has finished, which would effectively make the operation synchronous. However I believe Microsoft intentionally violated this part of the std::async specification, so actually it does return immediately, and it's possible for you to destroy the callback while the async thread is still using it (which is exactly the problem that would be avoided if the Microsoft implementation followed the standard!)

The solution is to keep hold of the std::future that std::async returns, and then block on that future (which ensures the async thread has finished) before destroying the callback.

class MyDLL
{
    std::future<void> m_future;
    ...
};

MyDll:~MyDll()
{
    StopDataAsync();
    m_future.get();        // wait for async thread to exit.
    delete m_curlHandler;  // now it's safe to do this
}

//Function to start receiving data asynchronously  
void MyDll::GetDataAsync()
{
    m_future = std::async([=]
    {
        //This will receive data in a new thread and call CurlDataCallback above
        //This basically calls easy_perform
        CurlGetData(m_curlHandler);
    }
}

N.B. your m_exit member should be std::atomic<bool> (or you should use a mutex to protect all reads and writes to it) otherwise your program has a data race and so has undefined behaviour.

I would also use std::unique_ptr<CurlCallbackHandler> for m_curlHandler.

I want to avoid putting in wait time-outs as this this will affect the performance of my main program.

The solution above will cause your destructor to wait, but only for as long as it takes for the callback to notice that m_exit == true and cause the async thread to stop running. That means you only wait as long as necessary and no longer, unlike time-outs which would mean guessing how long is "long enough", and then probably adding a bit more to be safe.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521