C++ Logo

std-proposals

Advanced search

[std-proposals] Versatility -- Classes change at runtime

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Sat, 30 Jul 2022 19:56:03 +0100
The term 'polymorphism' when used in the context of computer programming
is typically applied to so-called 'virtual' methods.

Virtual methods give us more versatility when dealing with classes, and
in particular with the classifications of classes. At runtime, a
pointer to 'Base' can be used to interact with objects of many types
of derived classes -- however the relationships between the base and the
derived classes are all set in stone at compile time (that is to say: All of
the v-tables are compile-time constants).

I wonder if we can take classes to the next level of versatility by no
longer requiring the v-table to be a compile-time constant? What if the
class could change at runtime as the program progresses from one state
to another?

I'm going to use a really simple example. Let's say we have a base class
called 'Mammal', with three derived classes, 'Cow', 'Sheep', 'Canine'.
And furthermore 'Canine' has two derived classes, 'Dog' and 'Wolf'.
Like this:

      Mammal
      |
      |
      |________ Cow
      |
      |
      |________ Sheep
      |
      |
      |________ Canine
                |
                |________ Dog
                |
                |________ Wolf


The 'Mammal' class has a pure virtual method called 'Speak', and the
'Wolf' class overrides this method to print to the screen "Howl".

Now let's say during the execution of the program, we want to change how
Wolves work. Let's say we want them to say "Ow Ow Owwww" instead of "Howl".
So we want to edit the v-table for 'Wolf' so that the pointer for the
'Speak' method points to some other method.

I propose that the syntax for making this change to the v-table for 'Wolf'
would be something like as follows on Line #13:

01: #include <iostream> // cout, endl
02: #include "animals.hpp" // Mammal, Cow, Sheep, Canine, etc.
03:
04: void New_Way_Of_Speaking(Wolf *const this)
05: {
06: std::cout << "Ow Ow Owwww" << std::endl;
07: }
08:
09: int main(void)
10: {
11: Wolf my_wolf;
12:
13: virtual Wolf::Speak = New_Way_Of_Speaking;
14: }

The replacement method must be either:
    (A) Another method belonging to the "Wolf" class with the same signature
        (or a method with the same signature belonging to a base class
         of 'Wolf')
    (B) A free-standing function (or lambda) with the same signature except
        there is one more parameter inserted at the beginning, "Wolf
*const this".

In a multi-threaded program, the pointers in the V-table will have to be
atomic.

I have already written a sample program to show this in action. You can
compile the following program for Linux with the GNU compiler (g++) as
follows:

    g++ -o program source.cpp

The program asks the user to select 'Decimal' or 'Hexadecimal' and then
starts printing numbers to the screen. However it then starts alternating
between decimal and hexadecimal as the V-Table is altered at runtime.
This program works fine with the optimisation level set to (-O1) however
it malfunctions if you set it to (-02) or (-03), presumably because there
is caching of the v-table (I can't think of any other reason why).

On the Linux operating system, the V-Tables are stored in readonly memory,
and so my code makes that area of memory writeable at runtime. The
same can be done
on MS-Windows using the "VirtualProtect" function in the Win32 API.

Here's the sample program:

#include <cassert> // assert
#include <cstddef> // size_t
#include <chrono> // milliseconds
#include <thread> // this_thread::sleep_for
#include <iostream> // cout, cin, endl, flush
#include <ios> // dec, hex

struct NumberPrinter {
    unsigned counter = 0u;
    virtual void Print(void) = 0;
};

struct DecimalNumberPrinter : NumberPrinter {
    void Print(void) override;
};

struct HexadecimalNumberPrinter : NumberPrinter {
    void Print(void) override;
};

void DecimalNumberPrinter ::Print(void) { std::cout <<
std::dec << counter++ << std::endl; }
void HexadecimalNumberPrinter::Print(void) { std::cout << "0x" <<
std::hex << counter++ << std::endl; }

// The function declared on the next line is defined below main
bool Replace_Pointer_In_VTable(void *pvtable, void (*before)(void),
void (*after)(void));

int main(void)
{
    using std::cout;
    using std::cin;
    using std::endl;

    NumberPrinter *p = nullptr;

    cout << "Enter 1 for Decimal, or 2 for Hexadecimal: " << std::flush;

    unsigned choice;
    cin >> choice;

    if ( 2u == choice )
    {
        p = new HexadecimalNumberPrinter;
    }
    else
    {
        p = new DecimalNumberPrinter;
    }

    static DecimalNumberPrinter const objD; // This object is
needed just to peruse its vtable pointer
    static HexadecimalNumberPrinter const objH; // This object is
needed just to peruse its vtable pointer

    void (*const address_of_dec_func)(void) =
reinterpret_cast<void(*)(void)>(&DecimalNumberPrinter ::Print);
    void (*const address_of_hex_func)(void) =
reinterpret_cast<void(*)(void)>(&HexadecimalNumberPrinter::Print);

    for ( bool alternator = false; /* ever */; alternator = !alternator )
    {
        // The loop on the next line prints 5 numbers in half a second
        for ( unsigned counter = 0u ; counter != 5u ; ++counter )
        {
            p->Print();

            std::this_thread::sleep_for(std::chrono::milliseconds(100u));
        }

        void (*const before)(void) = (alternator ? address_of_hex_func
: address_of_dec_func);
        void (*const after )(void) = (alternator ? address_of_dec_func
: address_of_hex_func);

        // Pointer to vtable is at the beginning of the object -- so
make sure object is big enough
        assert ( sizeof(objD) >= sizeof(void*) );
        assert ( sizeof(objH) >= sizeof(void*) );

        Replace_Pointer_In_VTable(*static_cast<void**>(const_cast<void*>(static_cast<void
const *>(&objD))), before, after );
        Replace_Pointer_In_VTable(*static_cast<void**>(const_cast<void*>(static_cast<void
const *>(&objH))), after, before);
    }
}

/* In the two operating systems, MS-Windows and Linux, the area of memory
 * in which the V-table is stored is readonly. So we must set the write
 * permissions on that area of memory first. In Linux we do this with
 * the two functions 'sysconf' and 'mprotect'. In MS-Windows we can use
 * the function 'VirtualProtect' in the Win32 API.
 */
extern "C" long sysconf(int name);
extern "C" int mprotect(void *addr, std::size_t len, int prot);

void Set_Writeability_Of_Memory(void (**const p)(void), bool const writeable)
{
    std::uintptr_t const page_size = sysconf( 30 /*_SC_PAGE_SIZE*/);

    union {
        void *p_start_of_page;
        std::uintptr_t i_start_of_page;
    };

    p_start_of_page = p;

    i_start_of_page -= (i_start_of_page % page_size);
    mprotect(p_start_of_page, page_size, 1u /*PROT_READ*/ | (writeable
? 2u /*PROT_WRITE*/ : 0u));
}

bool Replace_Pointer_In_VTable(void *const address_of_vtable, void
(*const before)(void), void (*const after)(void))
{
    void (**funcptr)(void) = static_cast<void(**)(void)>(address_of_vtable);

    unsigned const how_many_pointers_to_try = 5u;

    for ( unsigned i = 0; i != how_many_pointers_to_try; ++i )
    {
        if ( before == *funcptr )
        {
            Set_Writeability_Of_Memory(funcptr, true);
            *funcptr = after;
            Set_Writeability_Of_Memory(funcptr, false);
            return true;
        }
    }

    assert( nullptr == "Cannot find entry in V-table" );
}

Received on 2022-07-30 18:56:13