Ignore:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • doc/proposals/virtual.txt

    re1e4aa9 rba5131d  
    11Proposal for virtual functionality
    2 
    3 There are two types of virtual inheritance in this proposal, relaxed
    4 (implicit) and strict (explicit). Relaxed is the simpler case that uses the
    5 existing trait system with the addition of trait references and vtables.
    6 Strict adds some constraints and requires some additional notation but allows
    7 for down-casting.
    8 
    9 Relaxed Virtual Inheritance:
    102
    113Imagine the following code :
     
    2820void draw(line*);
    2921
    30 While all the members of this simple UI support drawing, creating a UI that
    31 easily supports both these UI requires some tedious boiler-plate code:
     22While all the members of this simple UI support drawing creating a UI that easily
     23supports both these UI requires some tedious boiler-plate code :
    3224
    3325enum type_t { text, line };
     
    4941}
    5042
    51 While this code will work as implemented, adding any new widgets or any new
    52 widget behaviors requires changing existing code to add the desired
    53 functionality. To ease this maintenance effort required CFA introduces the
    54 concept of trait references.
     43While this code will work as indented, adding any new widgets or any new widget behaviors
     44requires changing existing code to add the desired functionality. To ease this maintenance
     45effort required CFA introduces the concept of dynamic types, in a manner similar to C++.
    5546
    56 Using trait references to implement the above gives the following :
     47A simple usage of dynamic type with the previous example would look like :
    5748
    58 trait drawable objects[10];
     49drawable* objects[10];
    5950fill_objects(objects);
    6051
    6152while(running) {
    62       for(drawable object : objects) {
     53      for(drawable* object : objects) {
    6354            draw(object);
    6455      }
    6556}
    6657
    67 The keyword trait is optional (by the same rules as the struct keyword). This
    68 is not currently supported in CFA and the lookup is not possible to implement
    69 statically. Therefore we need to add a new feature to handle having dynamic
    70 lookups like this.
     58However, this is not currently do-able in the current CFA and furthermore is not
     59possible to implement statically. Therefore we need to add a new feature to handle
     60having dynamic types like this (That is types that are found dynamically not types
     61that change dynamically).
    7162
    72 What we really want to do is express the fact that calling draw() on a trait
    73 reference should find the underlying type of the given parameter and find how
    74 it implements the routine, as in the example with the enumeration and union.
     63C++ uses inheritance and virtual functions to find the
     64desired type dynamically. CFA takes inspiration from this solution.
    7565
    76 For instance specifying that the drawable trait reference looks up the type
    77 of the first argument to find the implementation would be :
     66What we really want to do is express the fact that calling draw() on a object
     67should find the dynamic type of the parameter before calling the routine, much like the
     68hand written example given above. We can express this by adding the virtual keyword on
     69the parameter of the constraints on our trait:
    7870
    7971trait drawable(otype T) {
     
    8173};
    8274
    83 This could be implied in simple cases like this one (single parameter on the
    84 trait and single generic parameter on the function). In more complex cases it
    85 would have to be explicitly given, or a strong convention would have to be
    86 enforced (e.g. implementation of trait functions is always drawn from the
    87 first polymorphic parameter).
     75This expresses the idea that drawable is similar to an abstract base class in C++ and
     76also gives meaning to trying to take a pointer of drawable. That is anything that can
     77be cast to a drawable pointer has the necessary information to call the draw routine on
     78that type. Before that drawable was only a abstract type while now it also points to a
     79piece of storage which specify which behavior the object will have at run time.
    8880
    89 Once a function in a trait has been marked as virtual it defines a new
    90 function that takes in that trait's reference and then dynamically calls the
    91 underlying type implementation. Hence a trait reference becomes a kind of
    92 abstract type, cannot be directly instantiated but can still be used.
     81This storage needs to be allocate somewhere. C++ just adds an invisible pointer at
     82the beginning of the struct but we can do something more explicit for users, actually
     83have a visible special field :
    9384
    94 One of the limitations of this design is that it does not support double
    95 dispatching, which concretely means traits cannot have routines with more than
    96 one virtual parameter. The program must have a single table to look up the
    97 function on. Using trait references with traits with more than one parameter
    98 is also restricted, initially forbidden, see extension.
     85struct text {
     86      char* text;
     87      vtable drawable;
     88};
    9989
    100 Extension: Multi-parameter Virtual Traits:
     90struct line{
     91      vtable drawable;
     92      vec2 start;
     93      vec2 end;
     94};
    10195
    102 This implementation can be extended to traits with multiple parameters if
    103 one is called out as being the virtual trait. For example :
     96With these semantics, adding a "vtable drawable" means that text pointers and line pointers are now
     97convertible to drawable pointers. This conversion will not necessarily be a type only change however, indeed,
     98the drawable pointer will point to the field "vtable drawable" not the head of the struct. However, since all
     99the types are known at compile time, converting pointers becomes a simple offset operations.
    104100
    105 trait iterator(otype T, dtype Item) {
    106         Maybe(Item) next(virtual T *);
     101The vtable field contains a pointer to a vtable which contains all the information needed for the caller
     102to find the function pointer of the desired behavior.
     103
     104One of the limitations of this design is that it does not support double dispatching, which
     105concretely means traits cannot have routines with more than one virtual parameter. This design
     106would have many ambiguities if it did support multiple virtual parameter. A futher limitation is
     107that traits over more than one type cannot have vtables meaningfully defined for them, as the
     108particular vtable to use would be a function of the other type(s) the trait is defined over.
     109
     110It is worth noting that the function pointers in these vtables are bound at object construction, rather than
     111function call-site, as in Cforall's existing polymorphic functions. As such, it is possible that two objects
     112with the same static type would have a different vtable (consider what happens if draw(line*) is overridden
     113between the definitions of two line objects). Given that the virtual drawable* erases static types though,
     114this should not be confusing in practice. A more distressing possibility is that of creating an object that
     115outlives the scope of one of the functions in its vtable. This is certainly a possible bug, but it is of a
     116type that C programmers are familiar with, and should be able to avoid by the usual methods.
     117
     118Extensibility.
     119
     120One of the obvious critics of this implementation is that it lacks extensibility for classes
     121that cannot be modified (ex: Linux C headers). However this solution can be extended to
     122allow more extensibility by adding "Fat pointers".
     123
     124Indeed, users could already "solve" this issue by writing their own fat pointers as such:
     125
     126trait MyContext(otype T) {
     127      void* get_stack(virtual T*)
     128};
     129
     130void* get_stack(ucontext_t *context);
     131
     132struct fat_ucontext_t {
     133      vtable MyContext;
     134      ucontext_t *context;
    107135}
    108136
    109 iterator(int) generators[10];
    110 
    111 Which creates a collection of iterators that produce integers, regardless of
    112 how those iterators are implemented. This may require a note that this trait
    113 is virtual on T and not Item, but noting it on the functions may be enough.
    114 
    115 
    116 Strict Virtual Inheritance:
    117 
    118 One powerful feature relaxed virtual does not support is the idea of down
    119 casting. Once something has been converted into a trait reference there is
    120 very little we can do to recover and of the type information, only the trait's
    121 required function implementations are kept.
    122 
    123 To allow down casting strict virtual requires that all traits and structures
    124 involved be organized into a tree. Each trait or struct must have a unique
    125 position on this tree (no multiple inheritance).
    126 
    127 This is declared as follows :
    128 
    129 trait error(otype T) virtual {
    130         const char * msg(T *);
     137//Tedious forwarding routine
     138void* get_stack(fat_ucontext_t *ptr) {
     139      return get_stack(ptr->context);
    131140}
    132141
    133 trait io_error(otype T) virtual error {
    134         FILE * src(T *);
    135 }
     142However, users would have to write all the virtual methods they want to override and make
     143them all simply forward to the existing method that takes the corresponding POCO(Plain Old C Object).
    136144
    137 struct eof_error virtual io_error {
    138         FILE * fd;
     145The alternative we propose is to use language level fat pointers :
     146
     147trait MyContext(otype T) {
     148      void* get_stack(virtual T*)
    139149};
    140150
    141 So the trait error is the head of a new tree and io_error is a child of it.
     151void* get_stack(ucontext_t *context);
    142152
    143 Also the parent trait is implicitly part of the assertions of the children,
    144 so all children implement the same operations as the parent. By the unique
    145 path down the tree, we can also uniquely order them so that a prefix of a
    146 child's vtable has the same format as its parent's.
     153//The type vptr(ucontext_t) all
     154vptr(ucontext_t) context;
    147155
    148 This gives us an important extra feature, runtime checking of the parent-child
    149 relationship with a C++ dynamic_cast like operation. Allowing checked
    150 conversions from trait references to more particular references, which works
    151 if the underlying type is, or is a child of, the new trait type.
     156These behave exactly as the previous example but all the forwarding routines are automatically generated.
    152157
    153 Extension: Multiple Parents
     158Bikeshedding.
    154159
    155 Although each trait/struct must have a unique position on each tree, it could
    156 have positions on multiple trees. All this requires is the ability to give
    157 multiple parents, as here :
     160It may be desirable to add fewer new keywords than discussed in this proposal; it is possible that "virtual"
     161could replace both "vtable" and "vptr" above with unambiguous contextual meaning. However, for purposes of
     162clarity in the design discussion it is beneficial to keep the keywords for separate concepts distinct.
    158163
    159 trait region(otype T) virtual drawable, collider;
    160 
    161 The restriction being, the parents must come from different trees. This
    162 object (and all of its children) can be cast to either tree. This is handled
    163 by generating a separate vtable for each tree the structure is in.
    164 
    165 Extension: Multi-parameter Strict Virtual
    166 
    167 If a trait has multiple parameters then one must be called out to be the one
    168 we generate separate vtables for, as in :
    169 
    170 trait example(otype T, otype U) virtual(T) ...
    171 
    172 This can generate a separate vtable for each U for which all the T+U
    173 implementations are provided. These are then separate nodes in the tree (or
    174 the root of different trees) as if each was created individually. Providing a
    175 single unique instance of these nodes would be the most difficult aspect of
    176 this extension, possibly intractable, though with sufficient hoisting and
    177 link-once duplication it may be possible.
    178 
    179 Example:
    180 
    181 trait argument(otype T) virtual {
    182         char short_name(virtual T *);
    183         bool is_set(virtual T *);
    184 };
    185 
    186 trait value_argument(otype T, otype U) virtual(T) argument {
    187         U get_value(virtual T *);
    188 };
    189 
    190 Extension: Structural Inheritance
    191 
    192 Currently traits must be the internal nodes and structs the leaf nodes.
    193 Structs could be made internal nodes as well, in which case the child structs
    194 would likely structurally inherit the fields of their parents.
    195 
    196 
    197 Storing the Virtual Lookup Table (vtable):
    198 
    199 We have so far been silent on how the vtable is created, stored and accessed.
    200 
    201 Creation happens at compile time. Function pointers are found by using the
    202 same best match rules as elsewhere (additional rules for defaults from the
    203 parent may or may not be required). For strict virtual this must happen at the
    204 global scope and forbidding static functions, to ensure that a single unique
    205 vtable is created. Similarly, there may have to be stricter matching rules
    206 for the functions that go into the vtable, possibly requiring an exact match.
    207 Relaxed virtual could relax both restrictions, if we allow different vtable
    208 at different conversion (struct to trait reference) sites. If it is allowed
    209 local functions being bound to a vtable could cause issues when they go out
    210 of scope, however this should follow the lifetime rules most C programs
    211 already follow implicitly.
    212 
    213 Most vtables should be stored statically, the only exception being some of
    214 the relaxed vtables that could have local function pointers. These may be able
    215 to be stack allocated. All vtables should be immutable and require no manual
    216 cleanup.
    217 
    218 Access has two main options:
    219 
    220 The first is through the use of fat pointers, or a tuple of pointers. When the
    221 object is converted to a trait reference, the pointers to its vtables are
    222 stored along side it.
    223 
    224 This allows for compatibility with existing structures (such as those imported
    225 from C) and is the default storage method unless a different one is given.
    226 
    227 The other is by inlining the vtable pointer as "intrusive vtables". This adds
    228 a field to the structure to the vtable. The trait reference then has a single
    229 pointer to this field, the vtable includes an offset to find the beginning of
    230 the structure again.
    231 
    232 This is used if you specify a vtable field in the structure. If given in the
    233 trait the vtable pointer in the trait reference can then become a single
    234 pointer to the vtable field and use that to recover the original object
    235 pointer as well as retrieve all operations.
    236 
    237 trait drawable(otype T) {
    238         vtable drawable;
    239 };
    240 
    241 struct line {
    242         vtable drawable;
    243         vec2 start;
    244         vec2 end;
    245 };
    246 
    247 This inline code allows trait references to be converted to plain pointers
    248 (although they still must be called specially). The vtable field may just be
    249 an opaque block of memory or it may allow user access to the vtable. If so
    250 then there should be some way to retrieve the type of the vtable, which will be
    251 autogenerated and often unique.
    252 
    253 
    254 Keyword Usage:
    255 
    256 It may be desirable to add fewer new keywords than discussed in this proposal.
    257 It is possible that "virtual" could replace both "vtable" above with
    258 unambiguous contextual meaning. However, for purposes of clarity in the design
    259 discussion it is beneficial to keep the keywords for separate concepts distinct.
    260 
    261 
    262 Trait References and Operations:
    263 
    264 sizeof(drawable) will return the size of the trait object itself. However :
    265 
    266 line a_line;
    267 drawable widget = a_line;
    268 sizeof(widget);
    269 
    270 Will instead return the sizeof the underlying object, although the trait must
    271 require that its implementation is sized for there to be a meaningful value
    272 to return. You may also get the size of the trait reference with
    273 
    274 sizeof(&widget);
    275 
    276 Calling free on a trait reference will free the memory for the object. It will
    277 leave the vtables alone, as those are (always?) statically allocated.
Note: See TracChangeset for help on using the changeset viewer.