Embedded Linux Journal
Porting Programs from X to Nano-X: Introduction to Microwindows Programming, Part III
by Gregory Haerr, CEO, Century Software
Chief Maintainer, The Microwindows Project
Nowadays, it seems that when I've got an idea for a new program I want to write for Linux, if I search the net, someone's already published an open source version of it. Such was the case recently when I thought it'd really be fun to have a graphical solitaire game for Microwindows. Entering Solitaire+Linux into the search engine gave me quite a few different implementations of Klondike, as well as an old favorite, FreeCell. Most of them were written for the X Window System environment, to be played on the Linux desktop. I figured that if I could find a well-written program based only on Xlib, it would be pretty straightforward to port it to the Nano-X API of Microwindows. And what do you know - third from the top of the list appeared xfreecell, a small, well-written freecell implementation based on Xlib. Porting xfreecell to Nano-X, and going over the similarities and differences between the Xlib API and Nano-X API will be the subject of this month's column. The freecell source code is available at ftp://microwindows.org/pub/microwindows/nxfreecell-0.1.tar.gz.
Portability concerns
Of course, there are quite a few concerns that need to be thought about when considering bringing over a program built for the desktop to an embedded environment. The program's overall structure, suitability for touch-screen rather than mouse input, size, graphical widget set used and external libraries required are some of the first things to look at. Most graphical programs written for X are either based on the low-level client graphics library, Xlib, or written on top of a much larger graphical applications toolkit like GTK+, Qt, or FLTK. For the utmost smallest size, writing directly to the graphical windowing system's library will usually produce the smallest and quickest code, although there are tradeoffs. The low-level client libraries are exactly that: low level. Programs gain access direct to the drawing primitives that the windowing system provides. These include support for creating windows, drawing points, lines, polygons and filling areas. The windowing system handles drawing clipped to a given window, and manages the window stacking view for the user. However, neither X nor Nano-X support pre-built low-level widgets that many desktop programs require, like edit boxes, pushbuttons, and the like. All of these have to be built on top of the low-level library, or a higher level applications toolkit be used.
Xfreecell uses only low-level Xlib calls for all its drawing functions. The images for the playing cards are all specified as monochrome bitmaps in X11 format. These can be converted to Nano-X format with a little effort. Xfreecell has a few dialog boxes and data input controls, but these are written directly on top of Xlib. The remaining portions of xfreecell handles event input, which is handled similarly between X and Nano-X.
Porting programs from X to Nano-X
Graphical windowing environments usually differ in their basic approach to how an application must be structured to work with the windowing environment. The design of Nano-X very closely follows the windowing and graphics drawing abstractions put forth by X. Although the Nano-X API isn't a reimplementation of X, most parameters to drawing functions have the same meanings. This approach was designed for a reason: if the API were exactly the same, then a re-implementation of X wouldn't likely be much smaller, since most of the X server's code is there for a reason. In addition, folks are already working on making X smaller. The Nano-X implementation uses the same window and graphics abstractions as X, but redesigns certain areas that are overly complex in X, like the color model, color maps, and visuals. The result is a system that has many of the same capabilities as X, and uses the same programming approach, but is a lot less complex and quite a bit smaller.
The XtoNX.h header file
In porting xfreecell, I decided that I would try leaving as much of xfreecell unmodified as possible, rather than changing the names of each drawing function, for instance. The result was the creation of the XtoNX.h header file - a set of C macros that greatly speed the process of porting a program from X to Nano-X. I'll go through each of the major sections of the header file as I explain the issues I found when porting xfreecell.
The first step was to produce a set of typedefs for making each of the X window, pixmap, region and other ID's compatible with Nano-X. With these declarations, various structures and data types used by X are made compatible without further modification, since windows, pixmaps, graphics contexts, regions and cursors are already abstractly compatible.
typedef GR_WINDOW_ID Window;
typedef GR_WINDOW_ID Pixmap;
typedef GR_GC_ID GC;
typedef GR_FONT_ID Font;
typedef GR_CURSOR_ID Cursor;
typedef GR_REGION_ID Region;
typedef GR_POINT XPoint;
typedef GR_RECT XRectangle;
typedef unsigned long Time;
In order to ease porting, event and font structure typedefs were also created, although their member names aren't completely compatible. In this way, the compiler will accept the variable declarations, and later flag errors when member names are referenced incompatibily:
typedef GR_EVENT XEvent;
typedef GR_FONT_INFO XFontStruct;
The next set of statements deal with the Display structure, which is allocated on the client side upon activating a connection with the server. Creating this structure for Nano-X allows us to use many of the X Display macros for accessing various connection and display characteristics.
/* static display structed initialized by XOpenDisplay()*/
typedef struct {
GR_COORD display_width;
GR_COORD display_height;
int display_bpp;
} Display;
static Display _display;
#define DisplayWidth(d,s) (_display.display_width)
#define DisplayHeight(d,s) (_display.display_height)
#define DefaultDepth(d,s) (_display.display_bpp)
#define DefaultScreen(d) 0
#define RootWindow(d,s) GR_ROOT_WINDOW_ID
#define BlackPixel(d,s) BLACK
#define WhitePixel(d,s) WHITE
#define False 0
#define True 1
Event handling is also very similar between the two graphical windowing environments, although the structure member names can't always be made compatible. Because of this, certain portions of the event handling code will have to be changed when porting to Nano-X. However, most of the events themselves are very similar, so we can add macros that define the correlation between similar events:
#define EnterWindowMask GR_EVENT_MASK_MOUSE_ENTER
#define LeaveWindowMask GR_EVENT_MASK_MOUSE_EXIT
#define ButtonPressMask GR_EVENT_MASK_BUTTON_DOWN
#define ButtonReleaseMask GR_EVENT_MASK_BUTTON_UP
#define KeyPressMask GR_EVENT_MASK_KEY_DOWN
#define KeyReleaseMask GR_EVENT_MASK_KEY_UP
#define ExposureMask GR_EVENT_MASK_EXPOSURE
#define SubstructureNotifyMask GR_EVENT_MASK_CHLD_UPDATE
#define EnterNotify GR_EVENT_TYPE_MOUSE_ENTER
#define LeaveNotify GR_EVENT_TYPE_MOUSE_EXIT
#define ButtonPress GR_EVENT_TYPE_BUTTON_DOWN
#define ButtonRelease GR_EVENT_TYPE_BUTTON_UP
#define KeyPress GR_EVENT_TYPE_KEY_DOWN
#define KeyRelease GR_EVENT_TYPE_KEY_UP
#define Expose GR_EVENT_TYPE_EXPOSURE
#define ConfigureNotify GR_EVENT_TYPE_CHLD_UPDATE
Window management and graphics display functions
The mechanisms for window creation, destruction, mapping, unmapping, raising, lowering, moving and resizing are mostly compatible, so a group of macros was created for these. However, the XOpenDisplay function needs to set up some global client data. In order to accomplish this, a STATIC_FUNCTIONS define was created that declares the static client XOpenDisplay function.
/* client-statically declared functions*/
#define STATIC_FUNCTIONS \
\
Display * \
XOpenDisplay(char *name) \
{ \
GR_SCREEN_INFO sinfo; \
\
if (GrOpen() < 0) \
return NULL; \
\
GrGetScreenInfo(&sinfo); \
_display.display_width = sinfo.cols; \
_display.display_height = sinfo.rows; \
_display.display_bpp = sinfo.bpp; \
\
return &_display; \
}
#define XCreateSimpleWindow(d,p,x,y,w,h,bw,bordc,backc) \
GrNewWindow(p,x,y,w,h,bw,backc,bordc)
#define XCreateWindow(d,p,x,y,w,h,bw,depth,cl,vis,vm,attr) \
GrNewWindow(p,x,y,w,h,bw,WHITE,BLACK)
#define XDestroyWindow(d,w) GrDestroyWindow(w)
#define XReparentWindow(d,w,p,x,y) GrReparentWindow(w,p,x,y)
#define XMapWindow(d,w) GrMapWindow(w)
#define XUnmapWindow(d,w) GrUnmapWindow(w)
#define XClearWindow(d,w) GrClearArea(w,0,0,0,0,GR_FALSE)
#define XClearArea(d,w,X,Y,W,H,e) GrClearArea(w,X,Y,W,H,e)
#define XRaiseWindow(d,w) GrRaiseWindow(w)
#define XLowerWindow(d,w) GrLowerWindow(w)
#define XMoveWindow(d,w,x,y) GrMoveWindow(w,x,y)
#define XResizeWindow(d,w,W,H) GrResizeWindow(w,W,H)
Finally, a set of defines maps the low-level drawing functions from X to Nano-X:
#define XCreateGC(d,a,b,c) GrNewGC()
#define XSetFunction(d,g,f) GrSetGCMode(g,f)
#define GXxor GR_MODE_XOR
#define XSetForeground(d,g,c) GrSetGCForeground(g,c)
#define XSetWindowBackgroundPixmap(d,w,p) \
GrSetBackgroundPixmap(w,p,GR_BACKGROUND_TILE)
#define XSetWindowBackground(d,w,c) \ GrSetWindowBackgroundColor(w,c)
#define XSetWindowBorderWidth(d,w,bw) \ GrSetWindowBorderSize(w,bw)
#define XSetWindowBorder(d,w,c) GrSetWindowBorderColor(w,c)
#define XStoreName(d,w,n) GrSetWindowTitle(w,n)
#define XSetIconName(d,w,n)
#define XDrawLines(d,w,g,ar,cnt,B) GrDrawLines(w,g,ar,cnt)
#define XFillRectangle(d,w,g,x,y,W,H) GrFillRect(w,g,x,y,W,H)
#define XDrawString(d,w,g,x,y,s,c) \ GrText(w,g,x,y,(void*)s,c,GR_TFASCII)
#define XSetFont(d,g,f) GrSetGCFont(g,f)
#define XLoadFont(d,f) GrCreateFont(0, 0, 0)
#define XDefineCursor(d,w,c) GrSetWindowCursor(w,c)
#define XUndefineCursor(d,w) GrSetWindowCursor(w,0)
#define XCreateRegion() GrNewRegion()
#define XUnionRectWithRegion(rect,sr,dr) GrUnionRectWithRegion(dr,rect)
#define XRectInRegion(r,x,y,w,h) GrRectInRegion(r,x,y,w,h)
#define XSubtractRegion(sra,srb,dr) GrSubtractRegion(dr,sra,srb)
#define XDestroyRegion(r) GrDestroyRegion(r)
#define XQueryTree(d,w,r,p,c,nc) GrQueryTree(w,p,c,nc)
#define XFree(p) free(p)
Converting X bitmaps
Monochrome images in X and Nano-X can be specified as an array of bits, where each bit can be displayed in either the current foreground or background color. Xfreecell uses this mechanism to display each playing card. A monochrome bitmap is used to create a background pixmap for a window, and each playing card is created as a window using the automatic background pixmap feature to display the card image. The following call is used to associate a background pixmap with a window:
GR_WINDOW_ID pixid = GrNewPixmap(width, height, NULL);
GrSetBackgroundPixmap(wid, pixid, flags);
The flags argument specifies how the pixmap is positioned in the background of the window. The following values are valid for the flag argument:
#define GR_BACKGROUND_TILE 0 /* Tile across the window */
#define GR_BACKGROUND_CENTER 1 /* Draw in center of window */
#define GR_BACKGROUND_TOPLEFT 2 /* Draw at top left of window */
#define GR_BACKGROUND_STRETCH 4 /* Stretch image to fit window*/
#define GR_BACKGROUND_TRANS 8 /* Don't fill in gaps */
Using these functions and the GR_BACKGROUND_TILE flag, a pixmap representing each playing card is created and associated with a window. The only problem is, the GrNewPixmap function creates a blank pixmap, and there isn't a Nano-X function to initialize a pixmap from a bitmap array. In order to solve this, I created a couple of new functions that allow bitmap data to be converted to Nano-X format, and then used to initialize a pixmap: GrNewBitmapFromData and GrNewPixmapFromData.
The GrNewBitmapFromData function allocates a GR_BITMAP-compatible array of bits from a passed bit array. In the last article, we showed how Nano-X supports images, and how the GrBitmap function will draw a GR_BITMAP array of bits onto a window or pixmap using the current foreground and background colors. The GrNewBitmapFromData function is required since X and Nano-X don't store monochrome image bits in the same format. X stores monochrome images in a bit-reversed and byte-swapped format as compared to Nano-X. In addition, Nano-X requires the GR_BITMAP array to be short-word padded, where X allows no padding. Here's how this function can be used to convert X-style bitmap data for Nano-X:
GR_BITMAP *imagebits = GrNewBitmapFromData(width, height,
bits_width, bits_height, bits,
GR_BMDATA_BYTEREVERSE|GR_BMDATA_BYTESWAP);
The width and height parameters specify the output width and height in pixels, while the bits_width, bits_height and bits parameters specify the input width, height and bit array. After creating a compatible bitmap array, the following new function uses it to create and initialize a pixmap:
GR_WINDOW_ID pixid = GrNewPixmapFromData(width, height,
foreground, background, bits, flags);
The width and height parameters specify the input and output pixmap width and height. The foreground and background arguments specify the colors to use for the 1 and 0 bits, repectively.
Manually converting code
After removing the #include <X11.h> defines and replacing them with #include <XtoNX.h>, most of the laborious conversion from X to Nano-X is done. I built the XtoNX.h header file to convert xfreecell, but there are quite a few more similar defines that might need to be added for helping convert the program you're working on. More defines can be added for the unreferenced X functions you encounter. However, there will still be a number of lines of code that can't be changed automatically with #defines. Some examples of this are covered below, and include the event model, window manager communication, and the color model.
The event model used by X, while similar to Nano-X, uses different member names, and will need manual conversion. Xfreecell uses the .xany union to get a window id from an event structure. Nano-X currently requires that the requested event type be known. Thus, event processing code that looks like:
XEvent ev;
if (ev.xany.window == BUTTONID) ...
will need to be changed to:
if (ev.button.wid == BUTTONID) ...
X and Nano-X use completely different mechanisms to communicate with the window manager. Nano-X is considerably less complex, and uses the GR_WM_FLAGS_* and GR_WM_PROPS_* values in conjunction with GrGetWMProperties, GrSetWMProperties and GrNewWindowEx. In many cases, the X window management code can be commented out without much effect. Since the window manager flags are passed to Nano-X in the GrNewWindowEx call, in most cases specifying the flags while replacing the XCreateWindow call will solve the problem. See the March/April issue for complete information on window manager style bits.
The final area that differs greatly between X and Nano-X is the color model. The design of X allows user programs to take full advantage of widely differing hardware implementations for color representation. Microwindows and Nano-X, though, are designed to take advantage of the newer-style framebuffer access that's built into the Linux kernel. With Nano-X, colors are always specified as RGB values, regardless of the underlying hardware. Nano-X handles the conversion whether running on 8 bit-per-pixel paletized displays, or 16 or 32 bpp truecolor displays. While this sometimes costs in performance, it usually doesn't matter since embedded displays are quite a bit smaller than the displays used on desktops today. Also, X keeps a textfile database of a huge number of named colors and values, where Nano-X either uses color schemes or direct RGB specification. Because of this, most color code will have to be hand-converted. There's a considerable amount of code the X programmer has to write in order to convert from RGB or named color values to the currently used Visual, which requires hardware-dependent color values. You might find code like the following to obtain a color value:
Colormap cmap = DefaultColormap(display, 0);
XColor c0, c1;
XAllocNamedColor(display, cmap, "red", &c1, &c0);
return c1.pixel;
This can all be replaced in Nano-X by:
#define MWINCLUDECOLORS
#include <nano-X.h>
return RED;
If the color value is not already specified, then use the GR_RGB define to specify the color:
GR_RGB(255, 0, 0)
Conclusion
Converting programs built for one graphical windowing system to another can be quite complicated, unless the systems are based around the same abstract model for windowing and graphics. The X and Nano-X windowing systems share the same abstraction for window creation, manipulation and drawing, except for the color model which is significantly easier to program for using Nano-X. Because of this, a macro header file can be used to lessen the porting effort from X to Nano-X. Using the XtoNX.h header file allowed porting xfreecell to Nano-X in a day or so, rather than starting from scratch. This brought another fun application to Microwindows, and leveraged the open source work of others. Stay tuned and try porting some applications yourself, it's fun and rewarding to create applications for embedded Linux devices!