embeddedmz / ftpclient-cpp

C++ client for making FTP requests
MIT License
204 stars 65 forks source link

Question: how to pass class member as a callback? #27

Closed quarcko closed 3 years ago

quarcko commented 3 years ago

Sorry to bug you again, but i'm no expert in c++ templates, and can't wrap my head around how to pass a class member method to: ftp->SetProgressFnCallback(this, &Class::method);

it gives: no known conversion for argument 2 from ‘int (MainWindow::)(void, double, double, double, double)’ to ‘const ProgressFnCallback& {aka const std::function<int(void*, double, double, double, double)>&}’

quarcko commented 3 years ago

So, for anybody else interested in similar thing, here is the solution, with the help of @embeddedmz :

Firstly, your callback function has to be a static method of your class.

then when calling SetProgressFnCallback pass 'this' or pointer to your class instance:

ftp->SetProgressFnCallback(this, &MainWindow::extProgCallback);

and here is callback function with access to class private variables and etc... :

int MainWindow::extProgCallback(void *ptr, double dTotalToDownload, double dNowDownloaded, double dTotalToUpload, double dNowUploaded)
{
    qDebug() << "Upload progress: " << dNowUploaded << "/" << dTotalToUpload;

    if(ptr)
    {
        embeddedmz::CFTPClient::ProgressFnStruct *prFnStruct = static_cast<embeddedmz::CFTPClient::ProgressFnStruct*>(ptr);
        if(prFnStruct && prFnStruct->pOwner)
        {
            MainWindow *thisWin = static_cast<MainWindow*>(prFnStruct->pOwner);
            if(thisWin)
            {
                thisWin->progDiag->setRange(0, dTotalToUpload); //Access private progDiag
                thisWin->progDiag->setValue(dNowUploaded);
            }
        }
    }

    QApplication::processEvents( QEventLoop::AllEvents, 100 );
    return 0;
}
embeddedmz commented 3 years ago

@quarcko In C++, object methods (not static), there's a hidden parameter in the arguments list and it's the "this" parameter. So if you need to pass a function pointer to a C API, you need to use class methods (ie static methods that doesn't require un object to be created). To keep your stuff encapsulated, use the static keyword instead of using a normal function (and only in the signature, just like the virtual keyword, static needs only to be declared in the method declaration, the header file).

std::function is just another way to declare a pointer to a function and you can use to store both classic function pointers and object methods (http://www.cplusplus.com/reference/functional/function/function/).

IIRC, in C++, normal functions (or static methods) and object methods of classes not using a virtual table (polymorphism with the keyword virtual) pointers contains an address to the code in the text segment of a program (e.g. 0xABCD015) as the compiler can statically determine the code that will be executed.

However, object methods where the keyword virtual is used, are just offsets (0, 1, 2 etc...) and the segment that contains methods begins from the pointer to the virtual table which is hidden in object members (dynamic dispatch as the compiler can't determine what objects will created and thus cannot determine the method to be executed ie the base class method or the overriden subclass method - google virtual table, it's an interesting subject, if you need to do OOP with C, you will have to do the same thing a C++ compiler does ;) ).

quarcko commented 3 years ago

@embeddedmz

I tried to change from 'friend function' to 'static method' and yes - everything works perfectly and looks a lot nicer :) Thanks for the tip!

More advanced reading i will leave for later, it's Friday already! :) Thanks again!

embeddedmz commented 3 years ago

@quarcko I am not sure if your code is thread safe for Qt, if libcurl calls the function pointer from another thread this is not good as Qt doesn't like when you modify widgets properties from a different thread where the QApplication event loop is running (QObjects like QSerialPort are not thread safe - you can check by putting a breakpoint in the progress callback pointer and noting the thread ID then compare it to the main thread ID, in your favourite IDE Qt Creator or Visual Studio, you have a thread view). You will have random crashes if it is not handled correctly.

I can give you a solution, use this function in your callback (it's like sending a signal to the main window) :

QMetaObject::invokeMethod(pointer_to_your_main_window->pointer_to_your_widget, "setValue",
                                  Qt::QueuedConnection, Q_ARG(int, 100));

Thus, you send a request (like a mail - Qt::QueuedConnection) to the widget's thread safe callback list to execute your slot when it processes its event loop.

Qt::DirectConnection, calls your method (slot) immediately and this is not good. The default parameter is AUTO, and Qt will choose QueuedConnection if the thread of the caller if different from the QObject thread (search for Qt moveToThread method).

Qt event loop mechanism is powerful (think of it as a mail box of a thread and threads can communicate safely by sending messages and thus you avoid locking mechanisms such as mutexes that can lead to deadlocks and data corruption). This mechanism it is also used in GUI app coded in C# or Java, however, in these languages (environments is better) there aren't event loops for threads (unfortunately - only for GUI thread, in C#/WPF there's Application.Current.Dispatcher.InvokeAsync(agileCallback); but you can't have event loops in threads so you can't execute stuff in a queued fashion like Qt does, you have to rely on ugly/unsecure stuff such as locks and manual resent event etc...), so that's why I prefer Qt). Multithreading is a delicate and difficult subject and that's why Qt signal/slots can be useful. (The true essence of OOP as Alan Kay stated it is message sending and not the inhertiance/polymorphism stuff)

Enjoy your weekend !

quarcko commented 3 years ago

Thanks for great info again, yeah i know Qt stuff waaay better than C++ standarts :rofl: My app uses alot of Multithreading and Event loop stuff as well, I just did not thought or checked that curl can be running in another thread.. but you know your code better, so will use invokeMethod for sure!

Have a great weekend too!