C++ Logo

std-proposals

Advanced search

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

From: Tom Honermann <tom_at_[hidden]>
Date: Tue, 2 Aug 2022 11:05:49 -0400
If you haven't already, I suggest spending some time investigating
Objective-C; it supports dynamic class types, meta classes, and the
ability to mutate class types at run-time similar to what you describe.
See here
<https://www.cocoawithlove.com/2010/01/what-is-meta-class-in-objective-c.html>
for a brief introduction. The run-time class design is pretty good once
you get used to the unfamiliar syntax and terminology.

As for C++, changing how existing C++ classes work is a non-starter.
What might be an option though is to add dynamic classes via an opt-in
syntax similar to that used to declare a class final. Something like:

class X dynamic { ... };

An implementation could then put the virtual table for such classes in
read-write memory and provide means to mutate them. Likewise, meta
classes could be synthesized by the compiler.

I think an extension like this would require very strong motivation for
the C++ committee to consider standardizing it. You might be able to
find good uses for dynamic classes by mining existing Objective-C
projects. Look for calls to the Objective-C run-time functions used to
create and modify classes to identify such projects. An implementation
and some motivating projects would almost certainly be required for the
committee to take a proposal seriously.

Tom.

On 7/30/22 2:56 PM, Frederick Virchanza Gotham via Std-Proposals wrote:
> 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-08-02 15:05:52