Pytanie Funkcja podpinania w C ++?


Przez "hooking" rozumiem zdolność do nieinwazyjnego nadpisywania zachowania funkcji. Kilka przykładów:

  • Wydrukuj komunikat dziennika przed i / lub po treści funkcji.
  • Zawiń ciało funkcji w ciało próbnika.
  • Zmierz czas trwania funkcji
  • itp...

Widziałem różne implementacje w różnych językach programowania i bibliotekach:

  • Programowanie aspektowe
  • Pierwsze funkcje klasy JavaScript
  • Wzór dekoratora OOP
  • Podklasy WinAPI
  • Ruby's method_missing
  • HAUST„s %exception słowo kluczowe, które ma owijać wszystkie funkcje w bloku try / catch, może być (ab) użyte w celu zahaczenia

Moje pytania to:

  • IMO jest to bardzo przydatna funkcja, zastanawiam się, dlaczego nigdy nie została zaimplementowana jako funkcja języka C ++. Czy są jakieś powody, które uniemożliwiają taką możliwość?
  • Jakie są zalecane techniki lub biblioteki do wdrożenia tego w programie C ++?

32
2017-10-12 17:03


pochodzenie


Raymond Chen niedawno napisał o tym, jak system Windows to osiąga - Praetorian
Co do cholery jest "niekonstruktywne" Jakie są zalecane techniki lub biblioteki do wdrożenia tego w programie C ++? - sbi
To jest poprawne pytanie. Proszę nie głosować, aby to zamknąć. - John Dibling
@ 6502: Jesteś oczywiście nie na miejscu. Metaprogramowanie szablonów to 2003. wyrażeń lambda są teraz najlepszą rzeczą na świecie. Zyskaj, człowieku! - John Dibling
Wiedziałem, że prędzej czy później, niektóre, erm, zła dusza z uzasadnionymi 34 awersjami do odpowiedzi w znaczniku C ++ pojawią się i bezmyślnie oddać brakujące ostatnie zamknięcie, mimo że jest to całkowicie uzasadnione pytanie, zwłaszcza po tym, jak zmieniono je tak, by pasowały do ​​krytyków! Westchnienie.  Zagłosowano na ponowne otwarcie. - sbi


Odpowiedzi:


Jeśli mówisz o wywołaniu nowej metody przed / po treści funkcji, bez zmiany jej treści, możesz ją oprzeć na to, który używa niestandardowego shared_ptr deleter do wyzwalania funkcji po ciele. Nie można z niego korzystać try/catch, ponieważ przed i po muszą być oddzielne funkcje za pomocą tej techniki.

Również używa poniższa wersja shared_ptr, ale z C ++ 11 powinieneś być w stanie używać unique_ptr aby uzyskać ten sam efekt bez kosztów tworzenia i niszczenia udostępnionego wskaźnika za każdym razem, gdy go używasz.

#include <iostream>
#include <boost/chrono/chrono.hpp>
#include <boost/chrono/system_clocks.hpp>
#include <boost/shared_ptr.hpp>

template <typename T, typename Derived>
class base_wrapper
{
protected:
  typedef T wrapped_type;

  Derived* self() {
    return static_cast<Derived*>(this);
  }

  wrapped_type* p;

  struct suffix_wrapper
  {
    Derived* d;
    suffix_wrapper(Derived* d): d(d) {};
    void operator()(wrapped_type* p)
    {
      d->suffix(p);
    }
  };
public:
  explicit base_wrapper(wrapped_type* p) :  p(p) {};


  void prefix(wrapped_type* p) {
     // Default does nothing
  };

  void suffix(wrapped_type* p) {
     // Default does nothing
  }

  boost::shared_ptr<wrapped_type> operator->() 
  {
    self()->prefix(p);
    return boost::shared_ptr<wrapped_type>(p,suffix_wrapper(self()));
  }
};




template<typename T>
class timing_wrapper : public base_wrapper< T, timing_wrapper<T> >
{
  typedef  base_wrapper< T, timing_wrapper<T> > base;
  typedef boost::chrono::time_point<boost::chrono::system_clock, boost::chrono::duration<double> > time_point;

  time_point begin;
public:
  timing_wrapper(T* p): base(p) {}


  void prefix(T* p) 
  {
    begin = boost::chrono::system_clock::now();
  }

  void suffix(T* p)
  {
    time_point end = boost::chrono::system_clock::now();

    std::cout << "Time: " << (end-begin).count() << std::endl;
  }
};

template <typename T>
class logging_wrapper : public base_wrapper< T, logging_wrapper<T> >
{
  typedef  base_wrapper< T, logging_wrapper<T> > base;
public:
  logging_wrapper(T* p): base(p) {}

  void prefix(T* p) 
  {
    std::cout << "entering" << std::endl;
  }

  void suffix(T* p) 
  {
    std::cout << "exiting" << std::endl;
  }

};


template <template <typename> class wrapper, typename T> 
wrapper<T> make_wrapper(T* p) 
{
  return wrapper<T>(p);
}


class X 
{
public:
  void f()  const
  {
    sleep(1);
  }

  void g() const
  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }

};



int main () {

  X x1;


  make_wrapper<timing_wrapper>(&x1)->f();

  make_wrapper<logging_wrapper>(&x1)->g();
  return 0;
}

13
2017-10-12 17:54



To wygląda bardzo interesująco. Zwłaszcza link do artykułu Bjarne'a. - StackedCrooked


Istnieją funkcje specyficzne dla kompilatora, które można wykorzystać, na przykład GCC -funkcje instrumentów. Inne kompilatory będą prawdopodobnie miały podobne funkcje. Zobacz WIĘC pytanie dla dodatkowych szczegółów.

Innym podejściem jest użycie czegoś podobnego Owijanie funkcji Bjarne Stroustrup technika.


5
2017-10-12 17:55



Odpowiedni przełącznik dla MSVC ++ jest /Gh: msdn.microsoft.com/en-us/library/c63a9b7h.aspx - j_random_hacker


Aby odpowiedzieć na pierwsze pytanie:

  • Większość dynamicznych języków ma swoje method_missing konstruuje, PHP ma magiczne metody (__call i __callStatic) i Python ma __getattr__. Myślę, że powód, dla którego nie jest dostępny w C ++, jest sprzeczny ze specyfikacją C ++. Wdrożenie tego na klasie oznacza, że ​​wszystkie literówki będą wywoływać tę funkcję (w czasie wykonywania!), Co zapobiega wychwyceniu tych problemów podczas kompilacji. Mieszanie C ++ z pisaniem kaczek nie wydaje się dobrym pomysłem.
  • C ++ stara się być tak szybki, jak to możliwe, więc funkcje pierwszej klasy nie wchodzą w grę.
  • AOP. Teraz jest to bardziej interesujące, nie ma nic, co mogłoby zapobiec dodaniu go do standardu C ++ (pomijając fakt, że dodanie kolejnej warstwy złożoności do już bardzo skomplikowanego standardu może nie być dobrym pomysłem). W rzeczywistości są kompilatory, które są w stanie falować kod, AspectC ++ jest jednym z nich. Rok temu nie był stabilny, ale wygląda na to, że od tego czasu udało im się wydać 1.0 z całkiem przyzwoitym zestawem testowym, więc teraz może to zrobić.

Istnieje kilka technik, oto powiązane pytanie:

Emulacja CLOS: before,: after i: around w C ++.


4
2017-10-12 17:38



Dzięki, AspectC ++ wydaje się dostarczać tego, o co proszę. Wymaganie niestandardowego kompilatora dla tego rozszerzenia języka jest trochę przerażające :) - StackedCrooked


IMO jest to niezwykle przydatna funkcja, więc dlaczego nie jest to funkcja języka C ++? Czy są jakieś powody, które uniemożliwiają taką możliwość?

C ++ język nie zapewnia żadnych środków, aby zrobić to bezpośrednio. Jednak nie stanowi to również bezpośredniego ograniczenia tego (AFAIK). Ten typ funkcji jest łatwiejszy do implementacji w tłumaczu niż w kodzie natywnym, ponieważ interpretacja jest fragmentem oprogramowania, a nie instrukcją maszyny strumieniowej CPU. Jeśli chcesz, możesz dobrze zapewnić interpreter C ++ z obsługą hooków.

Problemem jest czemu ludzie używają C ++. Wiele osób używa C ++, ponieważ chcą one czystej prędkości wykonywania. Aby osiągnąć ten cel, kompilatory wyprowadzają natywny kod w preferowanym formacie systemu operacyjnego i próbują włożyć twardy kod do kompilowanego pliku wykonywalnego. Ostatnia część często oznacza adresy obliczeniowe w czasie kompilacji / łączenia. Jeśli w tym momencie ustalisz adres funkcji (lub, co gorsza, wstawisz treść funkcji), nie będzie już obsługi haków.

To powiedziawszy, tam  sposoby na sprawne zaczepianie, ale wymaga to rozszerzenia kompilatora i nie jest całkowicie przenośne. Raymond Chen blogował o tym, jak gorące łatanie jest zaimplementowany w Windows API. Zaleca także, aby nie używać go w zwykłym kodzie.


2
2017-10-12 17:42





Przynajmniej w środowisku c ++, którego używam, zapewnia zestaw czystych wirtualnych klas

class RunManager;
class PhysicsManager;
// ...

Każdy z nich zdefiniował zestaw działań

void PreRunAction();
void RunStartAction()
void RunStopAction();
void PostRunAction();

które są NOPami, ale które użytkownik może przesłonić, skąd pochodzą z klasy Parent.

Połącz to z kompilacją warunkową (tak, wiem "Yuk!") i możesz dostać to, czego chcesz.


1
2017-10-12 17:59



Czy możesz podać nazwę tego frameworka? - Björn Pollex


Nie jest to kwestia C ++, ale w celu wykonania niektórych rzeczy, o których wspomniałeś, użyłem zmiennej środowiskowej LD_PRELOAD w systemach * nix. Dobrym przykładem tej techniki w działaniu jest faketime biblioteka, która podpina funkcje czasu.


0
2017-10-12 17:46





  1. Musi istnieć sposób implementacji funkcjonalności bez wpływu na wydajność kodu, który nie używa tej funkcji. C ++ został zaprojektowany z założeniem, że ponosisz tylko koszty wydajności dla używanych funkcji. Wstawianie, czy kontrole w każdej funkcji w celu sprawdzenia, czy zostały nadpisane, byłyby nie do przyjęcia dla wielu projektów C ++. W szczególności sprawienie, by działało tak, aby nie występowały koszty wydajności, a jednocześnie pozwalało na niezależną kompilację nadpisanych i nadrzędnych funkcji, będzie trudne. Jeśli pozwalasz tylko na przesłonięcie czasu kompilacji, wtedy łatwiej jest wykonać je wydajnie (linker może zająć się nadpisywaniem adresów), ale porównujesz do ruby ​​i javascript, które pozwalają ci zmieniać te rzeczy w czasie wykonywania.

  2. Ponieważ podważyłoby to system typu. Co to znaczy, że dana funkcja jest prywatna lub nie-wirtualna, skoro i tak ktoś może zmienić jej zachowanie?

  3. Czytelność bardzo ucierpi. Każda funkcja może mieć swoje zachowanie nadpisane gdzieś indziej w kodzie! Im więcej kontekstów potrzebujesz, aby zrozumieć, co funkcja ma, tym trudniej jest znaleźć dużą podstawę kodu. Hooking to błąd, a nie funkcja. Przynajmniej jeśli jest w stanie przeczytać to, co napisałeś kilka miesięcy później, jest wymogiem.


0
2017-10-12 17:58



Hooking to błąd, a nie funkcja - AOP nie zgadza się z tobą, podobnie jak narzędzia, takie jak profilery z instrumentami niewprostowymi. - Björn Pollex
Istnienie błędu nie obala, że ​​błąd jest błędem;) Dlatego nie uważam istnienia AOP za szczególnie przekonujący. Profiler nie wstawia dowolnego kodu zmieniającego zachowanie funkcji. Jeśli ktoś wymyśliłby system typu AOP, który mógłby udowodnić, że nadpisanie nie spowodowałoby efektów ubocznych, które naruszają założenia użytkowników kodu, mógłbym na nim wejść. - Joseph Garvin
Zgadzam się z (1) i (2), ale ponownie (3), nie widzę, że zahaczanie jest więcej błąd niż posiadanie otwartych przestrzeni nazw i pozwala na przesłonięcie. Obaj mają tę jakość działania na odległość. Oczywiście możesz twierdzić, że to oba błędy :) - j_random_hacker
@j_random_hacker: jestem skłonny powiedzieć, że oboje są;) - Joseph Garvin