class Callback – wskaźnik do funkcji i metod

Mały update (tudzież info) dotyczący tego bloga: jako że będę pisał teraz częściej na temat codingu, zrezygnowałem z numeracji postów.

Dzisiaj pracowałem nad czymś, co pozwoliłoby mi trochę uelastyczniść pisany kod przy użyciu mojego frameworka. Chodzi mianowicie o tzw. callbacki. Czemu uelastycznić? W silniku używam Mediatora. Jest to klasa zajmująca się modułami Graphs, Input itd. Czasem nie mam ochoty tworzyć wskaźnika do niego, a później dopiero do poszczególnych modułów i jeszcze ich metod.


Na początku było tak:

void loadGame( Mediator* med )
{
    med->loadSprite( loadTexture( "file.png", ... ), ... ); //nie bede pisal szczegolow :P
}
void main(void)
{
    med().setEvent_Load( &loadGame );
}

Później zażyczyłem sobie ukrócenia korzystania z wskaźnika do Mediatora w funkcji loadGame. Tym życzeniem doszedłem do dziedziczenia po klasie Mediator i tworzenia swojego mediatora (czyli spoza silnika) – MyMediator.

Z tym wiązało się kilka problemów z założenia projektu, to wszystko rozwiązałem i było ok. Powstało coś takiego:

class MyMediator : public Mediator
{
    public:
    void loadGame()
    {
        med->loadSprite( loadTexture( "file.png", ... ), ... );
    }
};
 
void main(void)
{
    med().setEvent_Load( &MyMediator::loadGame );
}

I to właściwie się już nie skompiluje. C++ (a przynajmniej Visual) nie pozwala na podanie wskaźnika do metody jak wskaźnika do zwykłej funkcji, choćbyśmy nawet podali instancję obiektu i było by to dla nas logiczne. Nie da się bowiem zrzutować tego na void(*func)(void) itp., ponieważ po prostu kompilator nie pozwoli jeszcze przed jakimkolwiek dojściem do linkera :)

Zostałem więc zmuszony do wymyślenia czegoś, co by mi pozwoli na elastyczne pobiera adresu funkcji, aby można sobie później wygodnie ją wywołać – bez względu na to czy jest to funkcja globalna czy z jakiejś klasy (metoda). A oto, do czego udało mi się dojść (przykład omijający już stricte mediatora z silnika):

Najpierw załóżmy, że jakaś funkcja w naszej biblioteczce ma mniej więcej taką postać:

void Mediator::setEvent_Load( Callback& func );

Tą funkcje nie interesuje nic – czy podajemy adres funkcji czy metody – ona ma to przyjąć. Kopiuje sobie gdzieś tam na swoje miejsce w klasie Mediator i zachowuje do momentu aż będzie potrzebna. Nagle nadchodzi ten moment:

m_eventLoad.call();    //przywolujemy, nie chcac zadnego parametru
m_eventLoad.callInt();     //przywolujemy funkcje usera, która ma cos zwrocic silnikowi

No i po skrócie to by było tyle ;) Ale pokażę jednak jeszcze, jak tego użyć – w kilku przypadkach.

Najpierw coś najprostrzego:

//test A
void some_func( void* par )
{
    cout < < "called some_func" << endl;
}
 
Callback a( 0, &some_func, 0 );
a.call();

Najzwyklejszy rodzaj, nie ma tu wiele do tłumaczenia. Klasa Callback na pierwszym miejscu przyjmuje wskaźnik ‘this’ (nie dosłownie, ale tak można go nazwać). Będzie to przydatne, kiedy jako argument (trzeci parametr) podamy wskaźnik do czegoś, co jest własnością blokową jakiejś innej funkcji (z której będziemy wywoływać callback). Raczej rzadziej potrzebne. Pominęliśmy drugi parametr – to, jak wiadomo, wskaźnik do funkcji.

Drugi test:

//test B
int some_func3_3( void* myInt )
{
    int dTmp = (int) myInt;
 
    cout < < "called some_func3_3" << endl;
    cout << "\tgot param: " << dTmp << endl;
 
    cout << "\tret val: ";
    if( dTmp != 4 )
    {
        cout << "false (par != 4)" << endl;
        return false;
    }
 
    cout << "true (par == 4)" << endl;
    return true;
}
 
Callback b( 0, &some_func3_3, (void*)4 );
int ret = b.callInt();

Ta funkcja z kolei chce parametru i na jego podstawie zwraca nam jakąś wartość. Poza tym nic szokującego.

//test C
//global func inside another func
 
Callback c( &some_func3_1, &some_func3_2, NULL );
 
void some_func3_2( void* myInt )
{
    int dTmp = (int) myInt;
 
    cout < < "called some_func3_2" << endl << "\tgot param: " << dTmp << endl;
}
 
void some_func3_1( void* )
{
    int myPrivateInt = 6;
 
    //podajemy wskaznik do 'prywatnej' zmiennej.
    c.call( (void*) myPrivateInt );
}

Podajemy wskaźnik do jakby prywatnej zmiennej funkcji. Dlatego teraz przydał się pierwszy parametr w konstruktorze. Chodzi o – w pewnym sensie – uprawnienia. Ponadto należy także zauważyć, że gdybyśmy nie podali metodzie ‘call’ tej zmiennej, to przyjęła by ona za argument dla naszego some_func3_2 zero (czyli to co podaliśmy jako trzeci argument w konstruktorze).

Ostatni z przewidzianych na dzisiaj przypadków – zapisujemy callbacka do metody, czyli to co nas najbardziej interesuje:

//test D
class SomeClass
{
 
public:
 
    SomeClass(){}
    ~SomeClass(){}
 
    void someMethod( void* par )
    {
        cout < < "called someMethod" << endl;
        //nie bawimy sie juz w wyswietlanie parametru, bo juz wiemy ze to mozna :)
    }
 
};
 
SomeClass obj;
Callback d( Func<SomeClass>(&obj, false, &SomeClass::someMethod, 0) );

Tak wygląda inna wersja konstruktora mojej klasy Callback – przyjmuje za argument referencję do obiektu typu klasy Func, która z kolei jest szablonem. Potrzebujemy szablonów w tym miejscu tylko po to, aby oszukać kompilator.

Pierwszy argument konstruktora Func to adres do instancji obiektu klasy, drugim (bool) informujemy klasę, czy to metoda zwracająca coś czy tylko void (patrz do dwóchpierwszych przykładów). Trzeci parametr konstruktora to „…”. Przekazujemy tam „nazwę” (w zasadzie wskaźnik) naszej metody z klasy, a zaraz za nim argument dla tej metody :)

I co? I wywołujemy sobie tego „metodo-callbacka” tak samo jak „funkcjo-callbacka”:

d.call();

Ma działać. Jeśli ktoś jest zainteresowany obejrzeniem/użyciem/przetestowaniem mojego Callbacka, to proszę bardzo (na końcu posta) :) Wszelkie uwagi bardzo mile widziane.

 

Ciekawsze linki związane z tematem (skorzystałem z tego dopiero jak wpadłem na pomysł jak zrobić tą klasę, ale skorzystałem :P):

http://www.oleeichhorn.com/articles/040617-C++_method_pointer.html

http://www.partow.net/programming/templatecallback/index.html
http://www.newty.de/fpt/fpt.html

EDIT (addon):

A więc stało się jak obiecywałem. Zedytowałem te callbacki dla znacznego uproszczenia. Teraz i dla metod, i dla funkcji stosuje się je dokładnie tak samo i nie mam wielu niepotrzebnych zaśmieceń. Nawet konstruktor jest tylko jeden. Nie trzeba dokładnie martwić się o typy parametrów, ale tylko o to, aby wszystkie były tego samego rozmiaru (wskaźniki mają 4 bajty w x32/x86). Teoretycznie można przekazać ich dowolną ilość. Oto dwa dodatkowe testy:

//test E
void some_func3_4( char* txt )
{
    cout >> "called some_func3_4" >> endl;
    cout >> "par(txt): " >> txt >> endl;
}
 
char pszStr[] = "jakies info do przekazania";
Callback e( 1, &main, &some_func3_4, pszStr );
 
e.call(); //to dziala :)

Jak widać, typ parametru nie jest już void*, a więc nie ma konieczności rzutowania. Po krótce jednak wyjaśnię konstruktor Callback:

Callback::Callback( int parNum, ... );

Pierwszy parametr określała nam, czy funkcja coś zwraca. Teraz to wyrzuciłem, bo nie korzystam już tego (okazuje się, że ten assembler na to pozwala w ten sposób, zajrzyjcie do źródła na metodę callInt). Tak więc teraz pierwszy argument określa nam ile parametrów przyjmuje dana funkcja. Można wpisać 0. Parametry wpisujemy na końcu (od czwartego argumentu). Drugi argument to wskaźnik do miejsca wywołania ('This'), a trzeci to to, co najważniejsze - nasza funkcyjka bądź metoda. Później opcjonalnie wymieniamy parametry dla naszej funkcji/metody.

Teraz test na dowolną ilość parametrów:

//test F
char* some_func3_5( char* someTxt, int someInt )
{
    cout >> "called some_func3_5" >> endl;
    cout >> "someTxt: " >> someTxt >> endl;
    cout >> "someInt: " >> someInt >> endl;
 
    char* tmpTxt = new char[5];
    memset( tmpTxt, 0, 5 );
    tmpTxt[0] = 32;
    tmpTxt[1] = 65;
 
    return tmpTxt;
}
 
int dInt = 613;
Callback f( 2, &main, &some_func3_5, pszStr, dInt );
 
cout >> (char*) f.callInt() >> endl;

Kod chyba wystarczająco dużo wyjaśnia. Teraz jeszcze poprawimy test D - ten dla metody:

SomeClass obj;
Callback d( 1, &obj, &SomeClass::someMethod, 0 );
 
d.call();

Argumenty przyjmowane są dokładnie tak jak dla funkcji, co jest wyjaśnione powyżej.

Można np. jeszcze dopisać dynamiczne dodawanie parametrów, a nawet rozróżnić ich rozmiary itp., ale ja z tego raczej nie będę korzystał (na pewno nie w silniku), bo dużo łatwiej jest podać np. jeden wskaźnik do jakiegoś container'a wielu danych.

Gotowy kod można znaleźć tutaj (na końcu posta).

EDIT2:

Poprawiłem delikatnie kod wyżej, bo okazało się, że pierwszy argument klasy jest zupełnie zbędny (kolejna pozostałość). Myślę, że teraz można tego spokojnie używać :) A zaproponowany boost::function nie jest tak fajny jak moja klasa. Trzeba znać odpowiednie typy. A mój silnik wewnątrz ma się tym zupełnie nie przejmować. On trzyma tylko handler typu Callback z jakąś moją funkcją i ma ją wywołać, bez wiedzy o typach parametrów czy typu zwracanej wartości. Pomaga w tym kod assemblerowy :) Dla leniwych, wstawię go tutaj:

int Callback::callInt( void* par = NULL )
{
    if( par != NULL )
    {
        setPar( par );
    }
 
    if( m_bIsInstantiated )
    {
        void* pFunc = (void*) m_pFunc;
        void* pThis = m_pThis;
 
        int dSize = 0;
        int dNumParameters = m_dNumParameters;
        int dOut = 0;
 
        /* w tym miejscu wszystkie parametry sa juz ustawione w odpowiedniej konwencji wywolania */
        int* pParameter = m_vpParameters;
        __asm
        {
            mov ecx, 0                        //zerujemy iterator
 
                                              //while( true )
            our_for:                          //{
 
                cmp ecx, [dNumParameters]     //if( i < dNumParameters )
                jae end_our_for               //goto end_our_for;
 
                mov ebx, pParameter           //wrzucamy parametr ze wskaznika
                push [ebx]                    //na stos
 
                add ecx, 1                    //i++;
 
                add dSize, 4                  //zwiekszamy wielkosc oczyszczajaca stos
                add pParameter, 4             //wybieramy nastepny parametr
 
                jmp our_for       
 
            end_our_for:                      //}
 
 
            mov ecx, pThis;              //ustawiamy funkcje i wywolujemy ja
            call pFunc;                 //
 
            add esp, [dSize]            //czas oczyscic stos
 
            mov [dOut], eax;            //umieszczamy wynik w zmiennej
                                        //potrzebne to jest w zasadzie 
                                        //tylko dla wywolania callInt
                                        //ale nic nie stoi na przeszkodzie
                                        //aby wywolac to takze dla void
        }
 
        return dOut;
    }
 
    //aby kompilator sie nie plul w zasadzie :P
    return 0;
}

EDIT3: (chyba) Gotowa wersja (dopiero w konkretnym praniu wyszły jescze bugi):

callback.hpp

callback.cpp

10 odpowiedzi na temat “class Callback – wskaźnik do funkcji i metod”

  1. icek napisał:

    Z tego co widze, mozna przekazac tylko jeden parametr (wskaznik na void). Mysle, czy nie daloby sie tego rozwiazac stosujac szablony, tak aby mozna bylo przekazac metode o dowolnej ilosci i typach parametrow?

  2. namek napisał:

    Da się bez problemu, ale wtedy na x64 nie będzie już możliwości edycji, aby to tam działało dla więcej jak jeden parametr. Byłby problem z podawaniem takich parametrów. C++’owo jako tako się nie da, potrzebny jest do tego assembler i wrzucanie na stos w odpowiedniej konwencji wywołania.

    http://dexter2206.wordpress.com/2008/01/09/calling-conventions/
    http://www.i-lo.tarnow.pl/edu/inf/prg/win32asm/pages/01.htm

    Właśnie nad tym teraz pracuję i wypuszczę drugą wersję :) Z kolei też próbuję wyrzucić sam szablon, aby było przejrzyściej. Forma jaka jest, to trochę pozostałości z kombinowania inną metodą.

  3. Kurak napisał:

    Zamiast pisać to wszystko można skorzystać z boost::function i olać, co poda użytkownik – wskaźnik na funkcję, metodę czy obiekt z operatorem () ;>

  4. namek napisał:

    Zedytowałem powyższy post :) Teraz można zajrzeć do kodu źródłowego, który już nie tylko działa ale i jako tako wygląda (mowa o klasie Callback).
    http://pastebin.com/f69a6224b

    Kurak: a można też napisać samemu dla siebie :P „Nie lubię boosta”.

  5. peanut napisał:

    Hm…
    Jesli moge sobie pozwolic na odrobine krytyki …

    Po pierwsze Twoja klasa Namek nawet przy singletonie nie stala :).
    W singletonach chodzi o to, ze sa one niejako calosciowym obiektem programu, a obiekt klasy singletonu jest nadrzednym wobec wszystkich innych, I CO NAJWAZNIEJSZE KAZDA KLASA SINGLETONU WYSTEPUJE TYLKO W POJEDNYCZEJ INSTANCJI ZE WZGLEDU NA JEJ UNIKTOWA ROLE.
    Do czego zmierzam, popatrz na taki „kawalek” kodu:

    CCallbackIdCreator c1; // O mam instancje „sinletonu”
    CCallbackIdCreator c2; // O kurna! Mam jeszcze jedna!
    CCallbackIdCreator c3; // I jeszcze!!!
    CCallbackIdCreator c4; // mam pisac dalej? :)

    Lammiesz tutaj podstawowa wlasciwosc jaka posiadaja singletony – ich instancja jest UNIKATOWA I JEDYNA – a kod wpisany (ktory na 10000000% sie skompiluje i bedzie „dzialal”) na to nie wskazuje! Jesli Twoja klasa byla by prawdziwym singletonem to na etapie perwszego wywolania kompilator powinien sie juz zapluc. Chodzi o to ze Twoj konstruktor jest w sekcji public, a to fatal error :) Powinien on byc w opatrzony modyfikatorem private i zaloze sie ze jesli tak bedzie, to powyzszy kod sie nie skompiluje :) a jedna i JEDYNA instancja klasy bedzie dostepna TYLKO prze statyczna metode inst(); – otrzymasz dzieki temu JEDYNA instancje klasy.
    No to na tyle o singletonach.
    A jeszcze jedno: kod taki jak:
    if( m_id != obj.m_id )
    return true; else return false;

  6. peanut napisał:

    Sorry submit wcisnolem … shit!
    Kontynuujac mysl:
    if( m_id != obj.m_id )
    return true;
    else
    return false;

    powyzsze nie ma sensu :(. Dlaczego? A po co to else? No i ze glebiej wejde, chyba lepiej napisac tak:

    return (m_id != obj.m_id) ? true : false;
    no nie?

    A tak na koniec, to calkiem niezle :) ciekawe masz pomysly … a masz czas zeby cos innego robic? :)
    Pozdro!

  7. misiaczekk napisał:

    @peanut
    stary jak juz sie przyczepiles do if’a, to ja sie przyczepie do Twojego:
    „return (m_id != obj.m_id) ? true : false;”

    przeciez wystarczy samo:
    return (m_id != obj.m_id);

    no nie?

  8. peanut napisał:

    No racja racja!!
    Dodam jeszcze:
    Po pierwsze Twoja klasa Namek nawet przy singletonie nie stala :).
    W singletonach chodzi o to, ze sa one niejako calosciowym obiektem programu, a obiekt klasy singletonu jest nadrzednym wobec wszystkich innych, I CO NAJWAZNIEJSZE KAZDA KLASA SINGLETONU WYSTEPUJE TYLKO W POJEDNYCZEJ INSTANCJI ZE WZGLEDU NA JEJ UNIKTOWA ROLE.
    Do czego zmierzam, popatrz na taki „œkawalek” kodu:
    „¦
    CCallbackIdCreator c1; // O mam instancje „œsinletonu”
    CCallbackIdCreator c2; // O kurna! Mam jeszcze jedna!
    CCallbackIdCreator c3; // I jeszcze!!!
    CCallbackIdCreator c4; // mam pisac dalej? :)
    Lammiesz tutaj podstawowa wlasciwosc jaka posiadaja singletony – ich instancja jest UNIKATOWA I JEDYNA – a kod wpisany (ktory na 10000000% sie skompiluje i bedzie „œdzialal”) na to nie wskazuje! Jesli Twoja klasa byla by prawdziwym singletonem to na etapie perwszego wywolania kompilator powinien sie juz zapluc. Chodzi o to ze Twoj konstruktor jest w sekcji public, a to fatal error :) Powinien on byc w opatrzony modyfikatorem private i zaloze sie ze jesli tak bedzie, to powyzszy kod sie nie skompiluje :) a jedna i JEDYNA instancja klasy bedzie dostepna TYLKO prze statyczna metode inst(); – otrzymasz dzieki temu JEDYNA instancje klasy.
    No to na tyle o singletonach.

  9. namek napisał:

    CCallbackIdCreator nie jest klasa dla usera. To chyba wszystko, co moge powiedziec. Co do tego if’a… mialo byc czytelne dla tych, ktorzy nie rozumieja takich konstrukcji :) Ale thx za uwage, czasami nie wiem po prostu co pisze :P

  10. online napisał:

    bardzo ciekawe, dzieki

Zostaw odpowiedź