Journal Articles
Browse in : |
All
> Journals
> CVu
> 122
(18)
All > Topics > Programming (877) Any of these categories - All of these categories |
Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.
Title: Adding Polymorphic Classes to the Anthem 'threads' Model
Author: Administrator
Date: 03 March 2000 13:15:35 +00:00 or Fri, 03 March 2000 13:15:35 +00:00
Summary:
Body:
In my previous CAUGers article, "Adding 'threads' to Anthem" [Goodliffe] I presented a somewhat devious method of adding a background processing thread to what was previously a single threaded co-operatively multitasking RISC OS application. This method is not a shining example of best-practice program design. OK, it is a bit of a hack that allows you to do something that you ordinarily cannot do in RISC OS. However, its product is a stable and very effective method of scheduling a background thread that can interact with the desktop application thread.
A number of implementation issues were not addressed by that article. I present here some further information to aid those interested in attempting a similar modification to their programs.
Before I continue, a quick recap would be in order. As well as refreshing the CAUGers readers' memories this will perhaps benefit the general C Vu readership who were unable to read the previous article.
Anthem is a MIDI sequencer application, which is somewhat akin to a word processor for music. The application UI code is based in part on the TSE sequencer engine library. This workhorse code performs MIDI playback. Both are written in C++.
RISC OS currently does not have a built-in application-level threading model. RISC OS desktop applications are co-operatively multitasked: they must explicitly yield the right to execute, no other application will be able to pre-empt them.
The Anthem application was originally designed around a simple single-threaded architecture. This meant that if another application hogged control of the co-operative multitasking, MIDI playback could break up. Adding a second 'thread' to produce MIDI output in the background was a later architectural decision to rectify this problem.
Under RISC OS one can build relocatable modules - code blocks that provide services to augment the basic operating system facilities. These blocks of code are capable of being entered on timer call-backs whereas normal application code is not. These call-backs can pre-empt application execution.
The threading solution described in [Goodliffe] involves building two versions of the sequencer engine code (based on a single source tree). One code build is targeted at the application level, one build at relocatable module level. The two versions run at the same time, tied up in such a way that they share data in a common memory pool (a RISC OS dynamic area).
The module version performs MIDI output on periodic call-backs. We provide a simple and sufficient locking mechanism to prevent one 'thread' from interfering with another. Using this mechanism it is possible to provide background playback whilst the desktop application is running and still be able to manipulate the song data. The playback no longer breaks up when desktop response is poor.
OK, so I made that term up myself. It sounds complicated and disgusting. That is because the concept it describes is. I alluded to these vtables at the end of the previous article and now we shall discover what on earth they are and why we need them.
The problem revolves around the existence of polymorphic classes, i.e. classes with virtual member functions. The vtable is the most common compiler implementation of the virtual function mechanism. It is essentially a jump table that is associated with an object of a polymorphic class. It contains a list of pointers to member functions, identifying each particular implementation of a given virtual function. This is a somewhat terse description, for more information you may wish to have a look at item 24 of More Effective C++ [Meyers].
The TSE sequencer engine library contains one polymorphic class hierarchy. This presents a problem to our dual build (application/module) approach. The vtable is effectively data associated with an object. So this data is shared between the application and module builds of the library. Here is our problem: which set of function pointers should be placed in the table?
If the vtable contains pointers to the application-build code then the program will crash and burn. When the module code is executing, it will call one of the virtual functions, look up the particular implementation in the shared vtable, and call into application code. Now if the application is currently paged in then all will go swimmingly. The problem is that the application will quite likely be paged out since another application currently has the (co-operatively managed) application execution privilege. The module code will branch somewhere into that random application. Bang. This is a pleasant way to bring our somewhat unprotected operating system to a blazing halt. Literally.
On the other hand if the vtable contains pointers to the module code then things will also not go according to plan. Although the module code will never be paged out as in the previous case, a module build and application build differ slightly - notably in their stack handling semantics. Explosions can happen this way too.
The solution? We hand-roll our own vtables. The faint of heart give up here whilst the daring continue. As I stressed in the previous article this is, in reality, a piece of blatant hackery that has been used to good effect in this situation. However, I would not recommend this technique as a starting point for the design of a new software system. Now I have made my disclaimer, I shall continue...
Let us say that my polymorphic base class originally looked like this (it is a simplification since there is no private data, but it shows the shape of what I am doing):
class Base { public: Base(); virtual ~Base() = 0; virtual void a() = 0; virtual int b(int param) = 0; }; class Derived : public Base { public: Derived(); virtual ~Derived(); virtual void a(); virtual int b(int param); };
Now under the new regime we will hand roll our own vtable and use it manually, thus:
struct Base_vtbl_type { void (*constructor)(); void (*destructor )(); void (*a )(); int (*b )(int param); }; class Base { public: Base(); ~Base(); void initialise(); // patch up vtables - more later void a() { (TSE_MODULEBUILD) ? vtbl_mod->a() : vtbl_app->a(); } int b(int param) { return (TSE_MODULEBUILD) ? vtbl_mod->b(param) : vtbl_app->b(param); } private: Base_vtbl_type *vtbl_mod; // vtable for module use Base_vtbl_type *vtbl_app; // vtable for app use };
In the above code we have defined a compile time flag, TSE_MODULEBUILD, that is set if we are compiling the code for a module, and not set if this is an application build. Here is the hint at how to use our hand-rolled vtables - the Base class is now just acting as the spring board into the appropriate piece of code (which will be the version located in the appropriate address space for the caller).
So how are we going to set up these two tables? First we need to know how we would implement Derived under the new regime. Rather than define Derived::b(int param) and et al, we instead define the following simple functions:
void Derived_constructor() { } void Derived_destructor() { } void Derived_a() { } void Derived_b(int param){ }
But what about object member data that would be passed in via the implicit this pointer? We will come to that later.
Next we define a pseudo-vtable for this pseudo class:
static Base_vtbl_type Derived_vtbl { Derived_constructor, Derived_destructor, Derived_a, Derived_b };
Based on some implementation-choosing magic, the Base class' constructor will patch up the function pointers appropriately. This will first be done in the application code, since that is where the Base object is created.
Base::Base(){ initialise(); } Base::~Base() { // if this is entered in module code we are // in error vtbl_app->destructor(); } void Base::initialise() { int impl = get_impl_type_from_ether(); Base_vtbl_type *vtbl = 0; switch(impl) { case Base_impl_Derived: vtbl = &Derived_vtbl; break; // other impls here } if (!vtbl) ... // panic! vtbl->constructor(); if (TSE_MODULEBUILD) vtbl_mod = vtbl; else vtbl_app = vtbl; }
This Base constructor will patch up the vtable appropriately for the application code by calling Base::initialise(). Once this is done by the application it is important that the 'registration' call of the module build of the code (this is the call that patches up the appropriate data pointers between the two code versions) also calls its version of Base::initiliase(). Following this we have two complimentary versions of our hand rolled vtable set up and ready to be used.
I'll admit it's not pleasant, but it works.
So what happens to the object's private data?
In my particular case, this was pretty easy to deal with. Naturally, each 'pseudo-object' needed some local object storage. However it is part of the TSE library design that the class is a singleton, i.e. there will only ever be one object of a class derived from Base. I can therefore use a static data pointer in the implementation file to suffice.
It is possible to make this more general, and I leave that as an exercise for the reader.
We have seen how to implement virtual functions on top of the trickery used to create a background thread.
Hopefully in implementing our own form of vtables we now also have a clearer idea of what the compiler is doing underneath us when we type make. This is a valuable lesson that we should hopefully never have to use in the same way again.
Again, I must stress that I have presented this here for your interest and not as a shining example of how to perform pseudo-threading in RISC OS. This is part of a technique - not the technique.
If any other readers have experiences of attempting a similar feat then please share them with the CAUGers readership. Anthem is available from R-Comp Interactive. See www.rcomp.co.uk for details.
Notes:
More fields may be available via dynamicdata ..