Roman Budzianowski 3/12/91 Directly-Mapped C++ interface to Xt toolkit intrinsics, with test implementation of Athena Widgets. Internal Project Athena MIT report - not for distribution Contents: 1. Direct interface vs. wrapper approach 2. Advantages of the direct interface 2.1. Subclassing widgets 3. Problems with direct interface 3.1. Memory management issues 3.2 Destroying widgets 3.3 Var args 4. New resources for C++ subclasses 5. New action routines and translations 6. Implementation Appendix A. Example 1 Appendix B. Example 2 Appendix C. Example 3 Appendix D. Reference manual 1. Direct interface vs. wrapper approach The Xt toolkit is the most mature of the X based toolkits and it has gained a wide commercial acceptance. The most known of the commercial packages is the Motif tool set. An Xt based implementation of the Open Look toolkit is also available, The C++ on the other hand is gaining popularity as the primary tool for object oriented development. Until a mature C++ based X toolkit is available, there is a need to access the Xt functionality from C++. One popular approach is based on the idea of "wrappers", C++ classes built around Xt widgets. One can build a generic class which provides an interface to any type of widget, or one class per widget type providing more specialized interface. In any case each classe contains a pointer to a widget. The application programmer accesses the toolkit indirectly through the wrapper class. The directly-mapped interface, the idea is due to Charles Haynes, is based on a different idea. It draws from the fact, that the C++ classes (structures) are compatible with C structs. The interface doesn't use the C include files, instead it defines a set of classes in the C++ world which directly map into the structs in the C world. Because of that all operations on a widget performed by the intrinsics are directly reflected in the C++ world and vice versa. The C++ classes have direct access to the widget structure. The reason for building these interfaces, instead of just using X and Xt in C++ is the desire to program in C++ style. 2. Advantages of the direct interface Because there is no indirection involved, there is some gain in performence. The major advantage though, is that the return values of the Xt functions are direct pointers to C++ objects, so there is no need to perform a reverse mapping from the C world to the C++ world. In principle the direct interface can be implemented inline, that is with only a set of h files. This implementation has a small additional object code which has to be linked in addition to the normal X, Xt and widget libraries. It provides some new functionality. It may be removed in the future if it's deemed unnecessary. Direct access to the widget structure allows for efficient implementation of various additional functionality, e.g. read access to certain resources bypassing the usual GetValues mechanism. At the same time the internals of the widgets are protected from the application code by the private/protected mechanism of the C++ language. 2.1. Subclassing widgets Very attractive feature of the direct interface is that the widgets can be subclassed and still remain legitimate widgets from the intrinsic's point of view. In this way the application can attach new semantics to the widgets. The widget pointer passed to a callback or an action is actually the pointer to the new C++ class, so all the new class functionality can be accessed. The traditional method uses the closure argument to the callback or a global variable ( in the case of an action the only way). Attaching new semantics directly to the widget is a more elegant style of programming. Note that in the wrapper approach, the subclassing of the wrapper clases is of course also possible, and an Xt interface can be provided that accepts and returns the wrappers in lieu of widgets. The reverse mapping though would require maintenace of a hash table of all widgets. Since callbacks and actions are called by the intrinsics, the application would have to explicitely invoke the reverse mapping functionality. There is no way, currently, to create new Xt classes in C++. New widgets have to be written in C. The new subclasses described above cannot change, in a substancial way, the graphical semantics associated with the widgets. One limited method to do that is by writing new actions and changing the default translation table in the constructor. It works fine if the new class is intended just for one application, but is not as general a solution as a new Xt widget class, since other users can modify the translations in the resource file. This modification will be performed by the intrinsics before the one in the constructor (the constructor has to create the widget first). Direct interface to the Xt class mechanism is possible too. It would allow the user to create totally new Xt classes, by subclassing the existing ones and changing class methods. 3. Problems with direct interface In general, the direct interface is more "intimately" based on the intrinsic's and widgets' implementaions. As a consequence, the maintenance requires more effort. The wrapper approach, especially the generic wrapper, may not require any changes, in principle, as long as the exported Xt and widget's interface doesn't change (resources and extern functions). The classes in the direct interface (including the subclasses) cannot use the virtual function mechanism of C++, because the virtual table changes the layout of the structure, making it incompatible with C. There are limits to how much the interface can be made to be in C++ style. The callbacks and actions still have to be written as regular C-style functions. The widget pointers in the callbacks and actions have to be (down)casted to the appropriate widget type from Core*, which breaks the safe type checking mechanism of C++. This limitation applies equally to any approach to building a C++ interface to the Xt toolkit. 3.1. Memory management issues The intrinsics like to their own memory management. There is no easy way to accomodate this design from the C++ point of view. The placement syntax of the new operator in C++ is very inconvenient in this case (imagine: LabelWidget lw = new(XtCreateWidget(,,,,)) LabelWidget(,,,)). Some resources have to be set during the creation time so there is no way to avoid passing multiple arguments to the XtCreate routine. Overloading new and calling XtCreate from there doesn't help either, because we still have to pass the arguments to the operator new, and the syntax is equally inconvenient. Besides one wouldn't be able to create widgets on the stack. The only nice solution is to call XtCreate from the constructor, but by then the memory for the C++ object is already allocated. The C++ 2.0 standard made the assignment to 'this' obsolete, so we cannot use the Xt allocated memory. The solution is to provide an interface to the XtCreate routines, which accepts a placement argument. Currently there is a separate object file replicating the intrinsics functionality, but eventually we would only need to reorganize the intrinsics implementation and export the placement XtCreateP routines, e.g. XtCreateWidgetP(void* p, char* name, Widget parent, XtArg* args, Cardinal count). Note that this doesn't require any modifications to the semantics of the intrinsic's interface. The same approach is needed for the AppContext structure. 3.2 Destroying widgets The intrinsics use a two phase destroy algorithm, to allow calls to XtDestroyWidget from callbacks and actions. It relies on the intrinsics having a handle to a valid memory for some time after the XtDestroy has been called. The natural place in our interface to call XtDestroy is in the destructor of the widget classes. However, the operator delete is called immediately after the destructor and the memory is release leaving the intrinsics with a handle to the invalid memory. The solution is to overload the operator delete to be a noop leaving to the intrinsics the actual freeing of the memory. Creation of the widget objects on the stack works too. It is safe to assume that there must be an event dispatch loop in the lexical scope of such an widget. Upon leaving the lexical block, the widget can be destroyed completely, as no outside references are possible. !?!?!?!?!?!?!?!?!? this is a week point now; it seems to work without any special action (like calling Phase2Destroy), but I am not sure why. !?!?!?!?!?!?!?!?!? 3.3 Var args Using varargs, implemented in the R4 release of the Xt toolkit, is far more natural than the original XtArg method, especially in the C++ world. However vararg interface cannot be implemented inline and is one reason for the need of the additional object file in this interface previously mentioned. One, very crude approach is to define the relevant functions as accepting, say, 20 resource/value pairs with 19 pairs defaulted to NULL, using C++ default mechanism. An attractive aspect of this solution from the user point of view is that she doesn't have to end the list of arguments with NULL, as is needed for varargs. The disadvantage is that it is less general : vararg functions accept another format, the resources which require conversion. 4. New resources for C++ subclasses 5. New action routines and translations 6. Implementation The implementation consists of a set of h files: Intrinsic.h and one file per class. The classes are organized as follows: class ObjectRec {}; // Object.h class RectObjRec : public ObjectRec {}; // RectObj.h class CompositeObjRec : public RectObjRec {}; // CompObj.h class WindowObjRec : public RectObjRec {}; // WindowObj.h class Core : public WindowObjRec {}; // Core.h typedef Core* Widget; class CompositeWidget : public Core {}; // Composite.h class ConstraintWidget : public CompositeWidget {}; // Constraint.h class ShellWidget : public CompositeWidget {}; // Shell.h class OverrideShellWidget : public ShellWidget {}; class WMShellWidget : public ShellWidget {}; class VendorShellWidget : public WMShellWidget {}; //Vendor.h class TransientShellWidget : public VendorShellWidget{}; // Shell.h class TopLevelShellWidget : public VendorShellWidget {}; class ApplicationShellWidget : public TopLevelShellWidget {}; class ApplicationContext {} typedef ApplicationContext* XtAppContext; The Athena widgets are implemented as subclasses of Core or ConstraintWidget. For example the LabelWidget: extern WidgetClass labelWidgetClass; class LabelWidget : public SimpleWidget { protected: /* resources */ Pixel foreground; XFontStruct *font; char* _label; XtJustify justify; Dimension internal_width; Dimension internal_height; Pixmap pixmap; Boolean resize; Pixmap left_bitmap; private: /* private state */ GC normal_GC; GC gray_GC; Pixmap stipple; Position label_x; Position label_y; Dimension label_width; Dimension label_height; Dimension label_len; int lbm_y; /* where in label */ unsigned int lbm_width, lbm_height; /* size of pixmap */ public: const char* label() { return _label; } Constructors(Label,label){} // no semicolon }; The Constructors macro implements the necessary constructors, and can be used by any widget class implementation. It takes two arguments, both the name of the widget class without the suffix WidgetClass. One argument is lower case, one upper case. The macro expands into the following three constructors: LabelWidget() {}; // needed for subclassing //accepts pointer to parent LabelWidget(String name, Widget parent, ArgList args = NULL, Cardinal numArgs = 0) { CreateWidget(name, parent, labelWidgetClass, args, numArgs); } // accepts reference to parent LableWidget(String name, Core& parent, ArgList args = NULL, CardinalnumArgs = 0) { CreateWidget(name, &parent, labelWidgetClass, args, numArgs); } They will be changed to varargs in the future. Appendix A. Example 1 The example below illustrates the building of a simple application, the use of callbacks, actions, dialogs, nested dispatch loops. Note the use of ExitLoop method of the ApplicationContext to exit the last active MainLoop, instead of a global variable. Exiting the main loop in this manner is important if widgets are allocated on the stack. This ensures that the destructors are run before exiting the program, which may be important. The toolkit is initialized automatically when the Application context is created. // Example of using directly-mapped C++ interface to the Xt toolkit // rjb #include #include #include #include #include #include #include #include #include #include void Done(Core* , XtPointer , XtPointer); void EditWidget(Core*, XEvent*, String*, Cardinal*); void UserDone(Core* , XtPointer , XtPointer); void QueryUser(Core*, XEvent*, String*, Cardinal*); class Interactor :public TransientShellWidget { private: DialogWidget *dialog; public: Interactor(Widget parent) :TransientShellWidget("interactor", parent) { dialog = new DialogWidget("dialog",this); dialog->Manage(); dialog->AddButton("OK", Done, (XtPointer)this); this->Realize(); } ~Interactor() { delete dialog; } }; void Dummy(Core* c, XEvent*, String* args, Cardinal* num) { int d1[100]; d1[0] = 5; cerr << "In Dummy: " << args[0] << "\n"; } XtActionsRec actions[] = { {"QueryUser", QueryUser }, {"EditWidget", EditWidget }, {"Dummy", Dummy} }; void DoEdit(Core* c, XtPointer d, XtPointer) { Interactor *i = (Interactor*)c; char* name = i->dialog->GetValueString(); Core* w = (Core*)d; w->SetValues(XtNlabel,(XtArgVal)name); delete i; } void EditWidget(Core* c, XEvent*, String*, Cardinal*) { Interactor *i = new Interactor(c); i->AddCallback(XtNpopdownCallback,DoEdit,(XtPointer)c); Position x_root, y_root; c->TranslateCoords(c->Width(),c->Height(),x_root,y_root); i->SetValues(XtNx,x_root,XtNy,y_root); i->Popup(XtGrabNonexclusive); } void UserDone(Core* c, XtPointer, XtPointer) { c->GetAppContext()->ExitLoop(); } void QueryUser(Core* c, XEvent*, String*, Cardinal*) { Interactor i(c); i.AddCallback(XtNpopdownCallback,UserDone,(XtPointer)c); Position x_root, y_root; c->TranslateCoords(c->Width(),c->Height(),x_root,y_root); i.SetValues(XtNx,x_root,XtNy,y_root); i.Popup(XtGrabNonexclusive); i.GetAppContext()->MainLoop(); char* name = i.dialog->GetValueString(); c->SetValues(XtNlabel,(XtArgVal)name); } void Done(Core*, XtPointer d, XtPointer) { TransientShellWidget *w = (TransientShellWidget *)d; w->Popdown(); } void QuitCB(Core* w , XtPointer, XtPointer) { w->GetAppContext()->ExitLoop(); } void BeepCB(Core* w , XtPointer d, XtPointer) { TransientShellWidget *ts = (TransientShellWidget *)d; Dimension height = w->Height(); Dimension width = w->Width(); Position x = w->X(); Position y = w->Y(); Position x_root, y_root; w->TranslateCoords(width/2,height/2,x_root,y_root); ts->SetValues(XtNx,x_root,XtNy,y_root); ts->Popup(XtGrabNonexclusive); cerr << "x= " << x << " y= " << y << "\n "; cerr << "width= " << width << " height= " << height << "\n"; XBell(w->XtDisplay(),50); } main(unsigned int argc, char** argv) { ApplicationContext appContext; appContext.AddActions(actions,XtNumber(actions)); Display *dpy = appContext.OpenDisplay(NULL, argv[0], "Eureka", NULL,0, &argc, argv); ApplicationShellWidget aw( argv[0], "Eureka", dpy); FormWidget form("form", aw); form.Manage(); CommandWidget quit("quit", form); quit.AddCallback(XtNcallback, QuitCB, (XtPointer)NULL); quit.Manage(); TransientShellWidget ds("dshell",aw); DialogWidget dw("dialog", ds); dw.Manage(); dw.AddButton("OK", Done, (XtPointer)&ds); ds.Realize(); CommandWidget beep("beep", form); beep.AddCallback(XtNcallback, BeepCB, (XtPointer)&ds); beep.Manage(); ClockWidget clock("clock", form); clock.Manage(); aw.Realize(); appContext.MainLoop(); }