═══ 1. Title Page ═══ Welcome to EDM/2 - The Electronic OS/2 Developers Magazine! Portions copyright (c) by Steve Luzynski, Larry Salomon Jr. Volume 1, issue 5 Select this to go to the next section ═══ 2. Copyright Notice (and other Legal Stuff) ═══ The editors of this electronic magazine are Steve Luzynski and Larry Salomon, Jr. Portions of EDM/2 are copyrighted by the editors. This publication may be freely distributed in electronic form provided that all parts are present in their original unmodified form. A reasonable fee may be charged for the physical act of distribution; no fee may be charged for the publication itself. All articles are copyrighted by their authors. No part of any article may be reproduced without permission from the original author. Neither this publication nor the editors are affiliated with International Business Machines Corporation. OS/2 is a registered trademark of International Business Machines Corporation. Other trademarks are property of their respective owners. Any mention of a product in this publication does not constitute an endorsement or affiliation unless specifically stated in the text. Select this to go to the next section ═══ 3. From the Editors ═══ After a rather bumpy release of volume 1, issue 4, we have high hopes that things will go a bit smoother in the future. Things that are on the board, so to speak, are: Subscription service For the Internet-reachable community, we thought that it wouldn't be too difficult to write an auto-mailer, especially since the Unix mail program will accept everything necessary on the command line. Add a list of people to mail to, bake for 30 minutes, and voila! Unfortunately, things were not that easy; after a first attempt, a SunOS limit on the number of processes was reached since mail spawns a copy of sendmail and then exits. Tweeking the process somewhat (pun intended) yielded a subscription mailer script that builds the mail header with all of the addresses in it and calls sendmail once for each part of the file. Having verified it with a small file, we have decided to test it for the first time with this issue, so we are asking that you please check the (alphabetical) list to see if your userid is on it. If it is and you did not receive the magazine automagically, please send us email as soon as possible. Assuming all goes well, an article will be posted to comp.os.os2.programmer.misc describing the subscription process, so be patient. Of course, the magazine will still be placed on the nets as before, but adding this service will allow us to keep a more accurate size of our reader base as well as allow us to reach other networks (like MCIMail) and BBS's with Internet mail links. New look (somewhat) Beauty is in the eyes of the beholder, so we've made a few changes to the magazine format. Firstly, the Questions and Answers column is being revamped, starting with a name change. It will now be known as the Scratch Patch and will contain not only Questions and Answers, but also the following items: o Snippet(s) of the Month This is for those useful, modular, functions that have been written and found indispensable by their authors. All submissions are requested (though not required) to be one function whenever possible and thoroughly commented, including a prologue which explains the purpose and the parameters. o Documentation Chop Shop This is for documentation-related submissions dealing with inaccuracies, inconsistencies, and vaguries. All potential submitters should remember that the goal here is to correct and clarify whenever possible; so, while pointing out the "tar pit" is something to write about, how to grab the vine swinging overhead is much more helpful. o Want Ads As in the past, we always look forward to your comments. Of particular interest are the requests for specific topics. Since we do not pretend to know everyone on the Internet, any topics that look appetizing or that have received a number of requests will be noted in this section in hopes that someone reading it will be masochistic enough to want to write an article on one of them. Don't forget to read the Article Submission Guidelines!!! Any submissions should be received by the editors by no later than the 25th of the month. And as always, even though it seems redundant, please explicitly state your permission for us to use what you have sent. Secondly, for this and all subsequent issues, we will try to include a hypertext link at the end of each section which will take you to the next section, so you should only have to repeatedly press ENTER (with an occassional TAB) to read the entire magazine. Thirdly, an installation program will be included with each issue, starting with this one. This Rexx/2 command file searches your entire desktop for a folder called "EDM/2" and creates one on the desktop if it cannot find an existing one. After doing this, it creates a shadow object within that folder that is linked to the filename of the current issue as distributed in the zip file (so if you change the name, it will not work). Finally, it sets the icon of the file to a newspaper so that you can spot it in a crowd. INSTALL.CMD makes the following assumptions: o C: is the drive containing the desktop folder hierarchy o The desktop root folder is named "d:\OS!2 2.x Desktop" where "x" is the version of the operating system. Note that if you reset your desktop using the Ctrl-Alt-F1 key combination, the system does not overwrite your existing folder, meaning that the actual desktop folder has another name. To get around this, you will have to edit the installation file (search for WP_DESKTOP). Miscellanea Last month it seems that we forgot to include the CNR2.ZIP file that was supposed to accompany the "Programming the Container Control - Part 2" article, so we are including it in this issue's zip file. We apologize for the inconvenience. Also, I have been in contact with Gavin Baker, who was the "Introduction to PM" columnist and he expressed a desire to continue writing for EDM/2. Unfortunately, he was pressed for time and could not complete his column for this issue, so we all will be looking forward to that in the next issue. Finally, as an update, I have also been contacted by David Singer with whom I was able to work out the necessary kinks to make EDM/2 available inside of IBM on a regular basis. He will be placing the issues on his Gopher servers (one for internal use and one that is reachable via the Internet). We are very thankful for his "contribution to the cause" and hope that by his offer we can expand our audience (and thus the number of potential authors). For more information about reaching the Gopher servers, you can contact him at singer@almaden.ibm.com. Enjoy! The Editors Select this to go to the next section ═══ List of Subscription Testers ═══ If you are listed below and did not receive the magazine this month via the subscription mailer, please send mail to os2man@panix.com. aaa@atl.hp.com menieuwp@cs.vu.nl abaddon@camelot.bradley.edu mgrice@athena.mit.edu acmmdj@gsusgi2.gsu.edu mmc@ehabitat.demon.co.uk bab%se40@se01.wg2.waii.com morio@ma2s2.mathematik.uni-karlsruhe.de bachww@motcid.rtsg.mot.com mstaedt@va-klaus.va.fh-ulm.de beaucham@phy.ulaval.ca mullins@magnum.convex.com benji@lise.unit.no pft@master10.zfe.siemens.de bhenning@wimsey.com pleitner@cs.curtin.edu.au bjorn@ludd.luth.se rcs58639@zach.fit.edu c878109@id.dth.dk rdm@csn.org chandoni@husc.harvard.edu rickw@umr.edu coulman@skdad.usask.ca rik@sci.kun.nl d2henan@dtek.chalmers.se rm3@stc06.ctd.ornl.gov donsmith@vnet.ibm.com robert.mahoney@f347.n109.z1.fidonet.org dradhak@unx.ucc.okstate.edu rodrigc@ecf.toronto.edu duffy@theory.chem.ubc.ca roe2@midway.uchicago.edu evanc@carbon.isis.org rpr@oce.nl ghdai@vax1.umkc.edu satish.movva@uic.edu gilbert@yalevm.ycc.yale.edu schaefer@calle2.e.open.de gogol@diku.dk schrock@cps.msu.edu gt7027c@prism.gatech.edu shawnmac@traider.ersys.edmonton.ab.ca heederik@fwi.uva.nl slumos@peewee.cs.unlv.edu hepner@gourami.nosc.mil soh3@midway.uchicago.edu jarlehto@utu.fi spatel@cs.utexas.edu jgarzik@pantera.atl.ga.us stephen.drye@synapse.org jjs@iedv6.acd.com timur@seas.gwu.edu jlauro@umich.edu visser@sci.kun.nl jofried@fzi.de wayne@stidol.mtv.gtegsc.com johnh@meaddata.com we44478@is1.bfu.vub.ac.be kenton+@cmu.edu whitaker@kean.ucs.mun.ca kevin@elvis.wicat.com wjw@eb.ele.tue.nl kfischer@hurricane.seas.ucla.edu xtifr@netcom.com ═══ 4. This Issue's Features ═══ The following articles constitute this issue's features: o Development of a New Window Class - Part 2 o The Help Manager and Online Documentation o OS/2 Installable File Systems - Part 2 o Programming the Container Control - Part 3 o A Review of C++ Compilers ═══ 4.1. Development of a New Window Class - Part 2 ═══ Written by Larry Salomon, Jr. Select this to go to the next section ═══ 4.1.1. Recapitulation and Regurgitation ═══ 0 to 60 MPH in 1 Paragraph Last issue we discussed the functional and design considerations for the development of a loose-leaf paper control. Decided was the control that the application programmer should be able to exert on our window class through messages and the notifications that should be sent for different types of actions. Also, it was determined that window words would be needed to store instance data for each window of this class. Is There Anybody Out There? Aside from defining the interfaces to the users of this control, we can sit in our room with the door shut and - assuming someone slips a slice of pizza or two under the door every now and then - eventually we will have something useable. So where do we begin? Even though entering text is the primary function of this class, we have to know where to draw the text, so painting the control will be our first component to implement. Select this to go to the next section ═══ 4.1.2. Painting ═══ Painting is easier to implement if you can break it down into distinct sections; since we defined various components of the paper control last issue, we can use those as a starting point. ─┬── Border ├── Top margin ├┬─ Side margin │└─── Top, middle, and bottom holes └┬─ Paper body └─── One or more text lines Figure 1. Different parts of the paper control Above all it most be noted that we cannot make any assumptions about the size of the control, unless we force the control to follow sizing constraints (we will not). Our options, therefore, are to paint the control ignoring the size or to use an abstract coordinate system based on the size when painting begins. Obviously, the latter is the more desireable so this will be implemented. The breakdown of the painting as I chose it is as follows: o Paint the border, if it exists o Paint the lines, both horizontal and vertical o Paint the holes o Paint the title text o Paint the body text Paint the border Since some developers might not want a border, a paper style was added - PPS_BORDER - to allow them to control this. PM defines its window styles (that encompass all windows) to be in the upper word of a ULONG, so PPS_BORDER is defined to be x'0001'. hpsPaint=WinBeginPaint(hwndWnd,NULLHANDLE,&rclPaint); WinFillRect(hpsPaint,&rclPaint,CLR_WHITE); WinQueryWindowRect(hwndWnd,&rclWnd); //---------------------------------------------------------------- // Paint the border first // // +------------+ // +|-----------+| // || || // || || // || || // White -----> || || <----- Dark gray // || || // |+------------+ // +------------+ //---------------------------------------------------------------- if ((ulStyle & PPS_BORDER)!=0) { GpiSetColor(hpsPaint,CLR_WHITE); ptlPoint.x=rclWnd.xLeft+1; ptlPoint.y=rclWnd.yBottom; GpiMove(hpsPaint,&ptlPoint); ptlPoint.x=rclWnd.xRight; ptlPoint.y=rclWnd.yTop-1; GpiBox(hpsPaint,DRO_OUTLINE,&ptlPoint,0,0); GpiSetColor(hpsPaint,CLR_DARKGRAY); ptlPoint.x=rclWnd.xLeft; ptlPoint.y=rclWnd.yBottom+1; GpiMove(hpsPaint,&ptlPoint); ptlPoint.x=rclWnd.xRight-1; ptlPoint.y=rclWnd.yTop; GpiBox(hpsPaint,DRO_OUTLINE,&ptlPoint,0,0); WinInflateRect(ppidData->habAnchor,&rclWnd,-2,-2); } /* endif */ Paint the lines I don't know why, but it just seemed better sense to paint the vertical line first, which was pink, if memory serves me correctly. This is followed by the horizontal lines, which were of a cyan tint. Here, we added three new styles - holes left (PPS_HOLESLEFT x'0000'), holes right (PPS_HOLESRIGHT x'0002'), and no holes (PPS_HOLESNONE x'0004') - which we check when drawing the vertical line. //---------------------------------------------------------------- // Paint the vertical line. Check the window style to see what // side it is on. //---------------------------------------------------------------- if ((ulStyle & PPS_HOLESNONE)!=0) { ptlPoint.x=rclWnd.xLeft+ppidData->fmFont.lAveCharWidth*5; } else if ((ulStyle & PPS_HOLESRIGHT)!=0) { ptlPoint.x=rclWnd.xRight-ppidData->fmFont.lAveCharWidth*5; } else { ptlPoint.x=rclWnd.xLeft+ppidData->fmFont.lAveCharWidth*5; } /* endif */ ptlPoint.y=rclWnd.yBottom; GpiMove(hpsPaint,&ptlPoint); ptlPoint.y=rclWnd.yTop; GpiSetColor(hpsPaint,CLR_PINK); GpiLine(hpsPaint,&ptlPoint); //---------------------------------------------------------------- // Paint the horizontal lines. Our strategy is to query each // line rectangle, and draw a line along the top edge of this // rectangle. This means the bottom edge of the bottom line // will not get painted, so explicitly handle this case. //---------------------------------------------------------------- GpiSetColor(hpsPaint,CLR_DARKCYAN); for (sIndex=0; sIndexsMaxLines; sIndex++) { WinSendMsg(hwndWnd, PPM_QUERYLINERECT, MPFROMP(&rclLine), MPFROMSHORT(sIndex)); ptlPoint.x=rclWnd.xLeft; ptlPoint.y=rclLine.yTop; GpiMove(hpsPaint,&ptlPoint); ptlPoint.x=rclWnd.xRight; GpiLine(hpsPaint,&ptlPoint); } /* endfor */ ptlPoint.x=rclWnd.xLeft; ptlPoint.y=rclLine.yBottom-1; GpiMove(hpsPaint,&ptlPoint); ptlPoint.x=rclWnd.xRight; GpiLine(hpsPaint,&ptlPoint); Paint the holes In the future, we might want to support four- and seven- hole paper, so we have to query the number of holes. We then loop, drawing each hole, until done. //---------------------------------------------------------------- // Note that if PPS_HOLESNONE was specified, 0 is returned //---------------------------------------------------------------- usMaxHoles=SHORT1FROMMR(WinSendMsg(hwndWnd,PPM_QUERYNUMHOLES,0,0)); for (sIndex=0; sIndexfmFont.lAveCharWidth,0)); GpiSetColor(hpsPaint,CLR_DARKGRAY); GpiFullArc(hpsPaint, DRO_OUTLINE, MAKEFIXED(ppidData->fmFont.lAveCharWidth,0)); } /* endfor */ Paint the title text If there is any title text (set on the WinCreateWindow() call or via WinSetWindowText()), we draw it here. if (ppidData->pchTitle!=NULL) { WinSendMsg(hwndWnd, PPM_QUERYLINERECT, MPFROMP(&rclLine), MPFROMSHORT(0)); rclLine.yBottom=rclLine.yTop+1; rclLine.yTop=rclLine.yBottom+ppidData->fmFont.lMaxBaselineExt; WinDrawText(hpsPaint, -1, ppidData->pchTitle, &rclLine, ppidData->lForeClr, 0, DT_CENTER); } /* endif */ Paint the body text Finally, we paint the body text, using the PPM_CONVERTPOINTS message to convert the invalid rectangle from world to line coordinates. While this is the only optimization of the paint process, we could easily extend this to the other components of the control. WinSendMsg(hwndWnd, PPM_CONVERTPOINTS, MPFROMP(&rclPaint), MPFROMSHORT(2)); if (rclPaint.yTop<0) { rclPaint.yTop=0; } /* endif */ if ((rclPaint.yBottom>ppidData->sMaxLines-1) || (rclPaint.yBottom<0)) { rclPaint.yBottom=ppidData->sMaxLines-1; } /* endif */ while (rclPaint.yTop<=rclPaint.yBottom) { WinSendMsg(hwndWnd, PPM_QUERYLINERECT, MPFROMP(&rclLine), MPFROMSHORT(rclPaint.yTop)); if (strlen(ppidData->aachLines[rclPaint.yTop])>0) { WinDrawText(hpsPaint, -1, ppidData->aachLines[rclPaint.yTop], &rclLine, ppidData->lBackClr, 0, DT_LEFT|DT_BOTTOM); } /* endif */ rclPaint.yTop++; } /* endwhile */ WinEndPaint(hpsPaint); Select this to go to the next section ═══ 4.1.3. User Input ═══ By definition, a window can only receive input when it has the input focus. Well, this isn't entirely true, since a window receives mouse movement messages regardless of whom has the focus, with the exception of mouse capture, only if...All kidding aside, character keystrokes go to the window with the input focus and the system notifies a window when it receives and loses the focus, so we use this to implement keystroke processing. Our 1000-foot view shows an entryfield that belongs to us, created without ES_BORDER, which we position over the line in which keystrokes are entered. We let the entryfield handle the keyboard interface and we need only to initialize it with any text currently on the line, and query it when the line is to change. First, when can the line number be changed? The answer is ours to define. I chose to allow first button clicks to set an absolute line and the up and down arrows to change the line by one in either direction. Second, what happens when the line number changes? Derived from our 1000-foot view, we query the entryfield text and update our internal array of line text, determine the world coordinates of the new line, call WinSetWindowPos() to change the position of the entryfield to reflect this new position, and finally initialize the entryfield via WinSetWindowText() with the text of the new line as stored in our internal array. These two questions translate to the messages WM_BUTTON1DOWN and WM_CHAR. case WM_BUTTON1DOWN: { RECTL rclWnd; //---------------------------------------------------------------- // Before we change the line, update the text array from the // current line //---------------------------------------------------------------- if (ppidData->sLine>-1) { WinQueryWindowText(ppidData->hwndText, sizeof(ppidData->aachLines[ppidData->sLine]), ppidData->aachLines[ppidData->sLine]); } /* endif */ //---------------------------------------------------------------- // Query the line clicked on //---------------------------------------------------------------- rclWnd.xLeft=SHORT1FROMMP(mpParm1); rclWnd.yBottom=SHORT2FROMMP(mpParm1); WinSendMsg(hwndWnd, PPM_CONVERTPOINTS, MPFROMP(&rclWnd.xLeft), MPFROMSHORT(1)); //---------------------------------------------------------------- // If the place clicked on is one of the lines, set the new // entryfield position to that line //---------------------------------------------------------------- if (rclWnd.yBottom>-1) { WinSendMsg(hwndWnd,PPM_SETCURRENTLINE,MPFROMP(rclWnd.yBottom),0); WinShowWindow(ppidData->hwndText,TRUE); WinSetWindowText(ppidData->hwndText, ppidData->aachLines[ppidData->sLine]); } else { ppidData->sLine=-1; WinShowWindow(ppidData->hwndText,FALSE); } /* endif */ WinSetFocus(HWND_DESKTOP,hwndWnd); } break; case WM_CHAR: if ((CHARMSG(&ulMsg)->fs & (KC_VIRTUALKEY | KC_KEYUP))== KC_VIRTUALKEY) { switch (CHARMSG(&ulMsg)->vkey) { case VK_UP: { RECTL rclLine; //---------------------------------------------------------- // Remember, we can only go up if there is another line // above us //---------------------------------------------------------- if (ppidData->sLine>0) { WinQueryWindowText(ppidData->hwndText, sizeof(ppidData->aachLines[ppidData->sLine]), ppidData->aachLines[ppidData->sLine]); ppidData->sLine--; WinSendMsg(hwndWnd, PPM_QUERYLINERECT, MPFROMP(&rclLine), MPFROMSHORT(ppidData->sLine)); WinSetWindowPos(ppidData->hwndText, NULLHANDLE, rclLine.xLeft, rclLine.yBottom, 0, 0, SWP_MOVE); WinSetWindowText(ppidData->hwndText, ppidData->aachLines[ppidData->sLine]); //------------------------------------------------------- // We only invalidate the line we left because the // entryfield paints itself //------------------------------------------------------- WinSendMsg(hwndWnd, PPM_QUERYLINERECT, MPFROMP(&rclLine), MPFROMSHORT(ppidData->sLine+1)); WinInvalidateRect(hwndWnd,&rclLine,TRUE); WinUpdateWindow(hwndWnd); WinSendMsg(ppidData->hwndOwner, WM_CONTROL, MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_UP), MPFROMSHORT(ppidData->sLine)); } else if (ppidData->sLine==0) { WinSendMsg(ppidData->hwndOwner, WM_CONTROL, MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_BEGINPAGE), MPFROMSHORT(ppidData->sLine)); } /* endif */ } break; //---------------------------------------------------------------- // We treat newline and enter the same as down arrow //---------------------------------------------------------------- case VK_DOWN: case VK_NEWLINE: case VK_ENTER: { RECTL rclLine; //---------------------------------------------------------- // Remember, we can only go down if there is another line // below us //---------------------------------------------------------- if ((ppidData->sLine>-1) && (ppidData->sLinesMaxLines-1)) { WinQueryWindowText(ppidData->hwndText, sizeof(ppidData->aachLines[ppidData->sLine]), ppidData->aachLines[ppidData->sLine]); ppidData->sLine++; WinSendMsg(hwndWnd, PPM_QUERYLINERECT, MPFROMP(&rclLine), MPFROMSHORT(ppidData->sLine)); WinSetWindowPos(ppidData->hwndText, NULLHANDLE, rclLine.xLeft, rclLine.yBottom, 0, 0, SWP_MOVE); WinSetWindowText(ppidData->hwndText, ppidData->aachLines[ppidData->sLine]); //------------------------------------------------------- // We only invalidate the line we left because the // entryfield paints itself //------------------------------------------------------- WinSendMsg(hwndWnd, PPM_QUERYLINERECT, MPFROMP(&rclLine), MPFROMSHORT(ppidData->sLine-1)); WinInvalidateRect(hwndWnd,&rclLine,TRUE); WinUpdateWindow(hwndWnd); WinSendMsg(ppidData->hwndOwner, WM_CONTROL, MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_DOWN), MPFROMSHORT(ppidData->sLine)); } else if (ppidData->sLine==ppidData->sMaxLines-1) { WinSendMsg(ppidData->hwndOwner, WM_CONTROL, MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_ENDPAGE), MPFROMSHORT(ppidData->sLine)); } /* endif */ } break; default: return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } /* endswitch */ } else { return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } /* endif */ break; Notice the notifications PPN_UP, PPN_DOWN, PPN_BEGINPAGE and PPN_ENDPAGE. Time Out You will not notice this now, but if you click on the paper control, the line number is supposed to change. Why do we not get a flashing cursor where the "invisible" entryfield exists? The answer is in the way the system handles focus changing. When the input focus window changes, a number of messages are sent as a result to both the window losing the focus and to the window receiving the focus. While this messages are being processed, the system considers no one to have the focus, so any attempt to change the focus via WinSetFocus() or WinFocusChange() will have no effect because the system will "overwrite" the focus change as it completes its processing. The result of this gibberish is that, if we are clicked on, we want the entryfield to receive the focus, but since we need to do some processing, we cannot just call WinSetFocus(HWND_DESKTOP,ppidData->hwndText) since we will never receive any notification. The result of that gibberish is that we need to get the focus and then somehow pass the focus on to the entryfield. Since we cannot change the focus while the system changes the focus, we need a little hocus-pocus to achieve this. #define PRVM_SETFOCUS (WM_USER) case WM_SETFOCUS: WinPostMsg(hwndWnd,PRVM_SETFOCUS,mpParm1,mpParm2); break; case PRVM_SETFOCUS: if (SHORT1FROMMP(mpParm2)) { if (ppidData->sLine>-1) { WinShowWindow(ppidData->hwndText,TRUE); WinFocusChange(HWND_DESKTOP, ppidData->hwndText, FC_NOLOSEACTIVE|FC_NOLOSEFOCUS|FC_NOLOSESELECTION); } /* endif */ WinSendMsg(ppidData->hwndOwner, WM_CONTROL, MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_SETFOCUS), 0); } else { //---------------------------------------------------------------- // If we're losing the focus, update the text array, but leave the // entryfield text alone. //---------------------------------------------------------------- if (ppidData->sLine>-1) { WinQueryWindowText(ppidData->hwndText, sizeof(ppidData->aachLines[ppidData->sLine]), ppidData->aachLines[ppidData->sLine]); WinShowWindow(ppidData->hwndText,FALSE); } /* endif */ WinSendMsg(ppidData->hwndOwner, WM_CONTROL, MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID), PPN_KILLFOCUS), 0); } /* endif */ break; In the code above, a couple of things need to be noted: o The hocus-pocus is in the call to WinPostMsg(). While it is not guaranteed, it is highly likely that our message will get processed after the system finished the focus change. This will allow us to do any processing necessary and then change the focus again to the entryfield. o The focus change added two new notifications - PPN_SETFOCUS and PPN_KILLFOCUS. These have the same semantics as the corresponding entryfield notifications EN_SETFOCUS and EN_KILLFOCUS. o The call to WinFocusChange() specifies through the use of the FC_* constants that the window with the focus (that's us) should not receive any notification that it is losing the focus. This is needed so that we don't send the PPN_KILLFOCUS notification. Select this to go to the next section ═══ 4.1.4. Owner Notifications ═══ Last issue, we defined a list of four events that should result in a notification to the owner window. The system allows us to define any notification code as a USHORT that we feel is necessary, since there is no "standard list" of notifications present for all window classes (versus window styles, where there is a common set of styles). The cursor moves up or down This is implemented in the processing for WM_CHAR. The code consists of the following call to WinSendMsg(). WinSendMsg(ppidData->hwndOwner, WM_CONTROL, MPFROM2SHORT(WinQueryWindowUShort(hwndWnd,QWS_ID),PPN_UP), MPFROMSHORT(ppidData->sLine)); Substitute PPN_DOWN for PPN_UP as needed. Note that if the cursor is on line 1/ppidData->sMaxLines and the user presses the up/down arrow, we instead send a PPN_BEGINPAGE/PPN_ENDPAGE notification to let the application know that the page boundary was reached. Any mouse button is clicked or double-clicked This is implemented in the processing for the various WM_BUTTONxUP and WM_BUTTONxDBLCLK messages. The code is the same as for the PPN_UP/PPN_DOWN notifications with the notification code changed as appropriate. Note that we could use the second half of mpParm2 to include which button was pressed, as a convenience to the user. The system-defined sequence for displaying the context menu is pressed Again, we use the same call to WinSendMsg(), this time from the WM_CONTEXTMENU message. Help is requested And finally one more WinSendMsg() from the WM_HELP message. That was easy, wasn't it? Select this to go to the next section ═══ 4.1.5. Presentation Parameters ═══ Processing these is probably the most interesting, because...well, because the ability for the user to change the color and font of a window was new with OS/2 2.0. [Get on soapbox] "I remember the days when you didn't have to deal with these dad-burned paramitization presentations or whatever the hell they're called."[Get off soapbox]. Because the user has the ability, via the color and font palettes, to change your presentation parameters, you can no longer avoid them when developing a new window class. Changing a presentation parameter is done for you by the system, providing you use a micro-cache'd presentation space in all of your drawing operations (obtained through WinBeginPaint() with NULLHANDLE specified as the second parameter, or through WinGetPS()). If you have your own screen presentation space which you specify in the call to WinBeginPaint, you will need to intercept the WM_PRESPARAMCHANGED message. Another time this message is needed is if you need the size of the current font for other processing or will unconditionally set the color to some color but want to restore it for other operations. Gee, aren't those familiar operations? We intercept this message and if a color was changed, we query the new color value and store that in our instance data. If the font changed, we re-query the FONTMETRICS structure values so that we can base our line size on the height of the font. case WM_PRESPARAMCHANGED: switch (LONGFROMMP(mpParm1)) { case PP_FOREGROUNDCOLOR: case PP_FOREGROUNDCOLORINDEX: { ULONG ulId; LONG lColor; HPS hpsWnd; SHORT sIndex; RECTL rclLine; WinQueryPresParam(hwndWnd, PP_FOREGROUNDCOLORINDEX, LONGFROMMP(mpParm1), &ulId, sizeof(lColor), &lColor, QPF_NOINHERIT); if (ulId==PP_FOREGROUNDCOLOR) { hpsWnd=WinGetPS(hwndWnd); lColor=GpiQueryColorIndex(hpsWnd,0,lColor); WinReleasePS(hpsWnd); } /* endif */ ppidData->lForeClr=lColor; for (sIndex=0; sIndexsMaxLines; sIndex++) { if (ppidData->aachLines[sIndex][0]!=0) { WinSendMsg(hwndWnd, PPM_QUERYLINERECT, MPFROMP(&rclLine), MPFROMSHORT(sIndex)); WinInvalidateRect(hwndWnd,NULL,TRUE); } /* endif */ } /* endfor */ WinUpdateWindow(hwndWnd); } break; case PP_BACKGROUNDCOLOR: case PP_BACKGROUNDCOLORINDEX: { ULONG ulId; LONG lColor; HPS hpsWnd; SHORT sIndex; RECTL rclLine; WinQueryPresParam(hwndWnd, PP_BACKGROUNDCOLORINDEX, LONGFROMMP(mpParm1), &ulId, sizeof(lColor), &lColor, QPF_NOINHERIT); if (ulId==PP_BACKGROUNDCOLOR) { hpsWnd=WinGetPS(hwndWnd); lColor=GpiQueryColorIndex(hpsWnd,0,lColor); WinReleasePS(hpsWnd); } /* endif */ ppidData->lBackClr=lColor; for (sIndex=0; sIndexsMaxLines; sIndex++) { if (ppidData->aachLines[sIndex][0]!=0) { WinSendMsg(hwndWnd, PPM_QUERYLINERECT, MPFROMP(&rclLine), MPFROMSHORT(sIndex)); WinInvalidateRect(hwndWnd,NULL,TRUE); } /* endif */ } /* endfor */ WinUpdateWindow(hwndWnd); } break; case PP_FONTNAMESIZE: case PP_FONTHANDLE: { HPS hpsWnd; RECTL rclWnd; hpsWnd=WinGetPS(hwndWnd); GpiQueryFontMetrics(hpsWnd,sizeof(FONTMETRICS),&ppidData->fmFont); WinReleasePS(hpsWnd); WinQueryWindowRect(hwndWnd,&rclWnd); WinSendMsg(hwndWnd, WM_SIZE, 0, MPFROM2SHORT((SHORT)rclWnd.xRight, (SHORT)rclWnd.yTop)); WinInvalidateRect(hwndWnd,NULL,TRUE); WinUpdateWindow(hwndWnd); } break; default: return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } /* endswitch */ break; Again, there are other (better) ways of implementing things. Here, we could query the RGB value of the changed color and use that in our painting to get the most accuracy. This exercise is left up to the programmer. While it doesn't really belong here, the window text processing has nowhere else to go so it is discussed here. The system's interface to a window class for items like this is through the WM_SETWINDOWPARAMS and WM_QUERYWINDOWPARAMS messages. The former is sent when a window parameter changes and the latter is sent to query the data from the window. A window parameter is either the window text or the control data (which is specific to a window class) and both are bundled in the WNDPARAMS structure. typedef struct _WNDPARAMS { ULONG fsStatus; ULONG cchText; PSZ pszText; ULONG cbPresParams; PVOID pPresParams; ULONG cbCtlData; PVOID pCtlData; } WNDPARAMS, *PWNDPARAMS; fsStatus For queries by the system, this specifies (as a combination of WPM_* constants) the parameters to query. cchText This specifies the size of the data pointed to by pszText pszText This points to the window text, terminated by a NULL character cbPresParams This specifies the size of the data pointed to by pPresParams pPresParams This points to the presentation parameters cbCtlData This specifies the size of the data pointed to by pCtlData pCtlData This points to the control data Since we are interested in the text only, we check the fsStatus field for the WPM_TEXT constant explicitly and act accordingly. The resulting code is shown below: case WM_QUERYWINDOWPARAMS: { PWNDPARAMS pwpParms; pwpParms=(PWNDPARAMS)PVOIDFROMMP(mpParm1); if ((pwpParms->fsStatus & WPM_TEXT)!=0) { pwpParms->pszText[0]=0; strncat(pwpParms->pszText,ppidData->pchTitle,pwpParms->cchText); WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); return MRFROMSHORT(TRUE); } /* endif */ return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } case WM_SETWINDOWPARAMS: { BOOL bProcessed; PWNDPARAMS pwpParms; bProcessed=FALSE; pwpParms=(PWNDPARAMS)PVOIDFROMMP(mpParm1); if ((pwpParms->fsStatus & WPM_TEXT)!=0) { if (ppidData->pchTitle!=NULL) { free(ppidData->pchTitle); ppidData->pchTitle=NULL; } /* endif */ if ((pwpParms->pszText!=NULL) && (strlen(pwpParms->pszText)>0)) { ppidData->pchTitle=malloc(strlen(pwpParms->pszText)+1); strcpy(ppidData->pchTitle,pwpParms->pszText); } /* endif */ bProcessed=TRUE; } /* endif */ if (bProcessed) { WinInvalidateRect(hwndWnd,NULL,TRUE); WinUpdateWindow(hwndWnd); WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); return MRFROMSHORT(TRUE); } else { return WinDefWindowProc(hwndWnd,ulMsg,mpParm1,mpParm2); } /* endif */ } Select this to go to the next section ═══ 4.1.6. Summary ═══ Although a lot of code was presented here, all of it is really only the extension of our thoughts from last issue when this new class was being designed. It cannot be stressed enough the importance of a good design; no one expects you to get it right the first time, but if you think most of it through the rest becomes significantly easier. As a final note, all coordinates that we discussed here were (if not already) mapped to line numbers via the message PPM_CONVERTPOINTS. This allows us to change the size of each line in one place only. This logic was applied in other places (PPM_QUERYNUMHOLES) to make our maintenance easier and goes way back to Computer Science 101 when code modularity was discussed. Select this to go to the next section ═══ 4.2. The Help Manager and Online Documentation ═══ Written by Larry Salomon, Jr. Select this to go to the next section ═══ 4.2.1. Introduction ═══ More times than I care to remember, I have been asked "How do I add online help to my applications?" or "How can I write online books?" While the latter takes less time to answer, I often myself talking for 45 minutes or so on everything from what an GML language is to the sequence of events that happen when a user presses the F1 key. Finally, today I got tired of repeating myself (even as much as I love to talk) so I decided to write the article that has been promised for so long. Hopefully after reading this, you should be able to do the following: o Briefly explain what an GML language is and what markup is o List the three components needed when adding context-sensitive help to a PM application and what each component's purpose is o Describe the sequence of events that occur when a user presses F1 o Detail the minimum markup required to create a valid online help file and to create a valid online book file o Be able to write the markup for headings, paragraphs, lists (all five types), emphasis, hypertext links, and graphics. o Explain some of the limitations of the IPFC compiler Select this to go to the next section ═══ 4.2.2. The Basics ═══ GML is an acronym for generalized markup language and describes a group of languages which perform operations on blocks of text. Typically, these languages are used for output formatting but are not limited to this arena. The language consists of control words (called tags) interspersed within blocks of text; these tags have the form ─────────────────────────────────────────────────────────────────────────────── :tag [attribute[=value] [attribute[=value] [...]]].[text] ─────────────────────────────────────────────────────────────────────────────── Figure 1. GML tag syntax The attributes shown above vary from tag to tag and may not exist at all. Likewise, each attribute may or may not accept a value; consult the "Information Presentation Facility" reference guide which comes with the 2.x toolkit for a complete list of the attributes for each tag. Frequently, a tag is used to mark the beginning of a change in formatting and has a corresponding end tag to signify the end of that change. The end tag often is the same as the begin tag prefixed with an "e". Thus, you use :hp2. to begin emphasis level 2 and :ehp2. to end it. The term markup is used to describe a combination of tags and text in a GML file. Application components There are three help-related components to any PM application, listed below. o Source code - calls to and from the Help Manager from your application o Resources - relationships between focus windows and help panels o Panel definitions - GML source describing the help panels' appearance Select this to go to the next section ═══ 4.2.3. Source code ═══ This consists of the various calls to Help Manager functions from within your PM application. The bare minimum exists in your main() function and creates an instance of the Help Manager: ─────────────────────────────────────────────────────────────────────────────── #define INCL_WINHELP : INT main(USHORT usArgs,PCHAR apchArgs[]) { : HELPINIT hiHelp; habAnchor=WinInitialize(0); hmqQueue=WinCreateMsgQueue(habAnchor,0); : : /* Create frame window */ : if (hwndFrame!=NULLHANDLE) { : : /* Initialize HELPINIT structure */ : hwndHelp=WinCreateHelpInstance(habAnchor,&hiHelp); WinAssociateHelpInstance(hwndHelp,hwndFrame); : : /* Message loop in here somewhere */ : WinDestroyHelpInstance(hwndHelp); WinDestroyWindow(hwndFrame); } /* endif */ WinDestroyMsgQueue(hmqQueue); WinTerminate(habAnchor); } ─────────────────────────────────────────────────────────────────────────────── Figure 2. Help Manager initialization The HELPINIT structure is a rather large conglomeration of configurable items. ─────────────────────────────────────────────────────────────────────────────── typedef struct _HELPINIT { ULONG cb; ULONG ulReturnCode; PSZ pszTutorialName; PHELPTABLE phtHelpTable; HMODULE hmodHelpTableModule; HMODULE hmodAccelActionBarModule; ULONG idAccelTable; ULONG idActionBar; PSZ pszHelpWindowTitle; ULONG fShowPanelId; PSZ pszHelpLibraryName; } HELPINIT, *PHELPINIT; ─────────────────────────────────────────────────────────────────────────────── Figure 3. HELPINIT structure cb Specifies the size of the structure in bytes ulReturnCode On exit, contains any error codes returned from the Help Manager pszTutorialName Points to the string specifying the default tutorial name. If NULL, there is no default tutorial or it is specified in the panel definition file. phtHelpTable Points to the HELPTABLE to use, or specifies the resource id of the HELPTABLE to be loaded from the resource file. If this specifies the resource id, it is contained in the low word and the high word must be 0xFFFF. hmodHelpTableModule Specifies the handle of the module from whence the HELPTABLE is to be loaded. idAccelTable Specifies the resource id of the accelerator table to be used. A value of 0 specifies that the default is to be used. idActionBar Specifies the resource id of the action bar to be used. A value of 0 specifies that the default is to be used. hmodAccelActionBarModule Specifies the handle of the module from whence the accelerator table and action bar are to be loaded. pszHelpWindowTitle Points to the help window title text. fShowPanelId Specifies whether or not to show the panel identifier (if present). Valid values are CMIC_SHOW_PANEL_ID and CMIC_HIDE_PANEL_ID. pszHelpLibraryName Points to the filename containing the compiler panel definitions. It needs to be noted that even though a valid window handle is returned from WinCreateHelpInstance(), an error might have occurred whose value is specified in the ulReturnCode field of the HELPINIT structure. Messages There will be times when you will want to send messages to the Help Manager and when messages will be received. The four most frequent messages sent to the Help Manager are listed below: HM_HELP_INDEX Causes the help index to be displayed HM_EXT_HELP Causes the extended (general) help panel defined for the active window to be displayed (described more later) HM_DISPLAY_HELP Causes the panel specified by name (in mpParm1) or resource id (in mpParm2) to be displayed. Specifying NULL and 0 for these values cause the "Using Help" panel to be displayed. HM_KEYS_HELP Causes the keys help panel to be displayed. Since the active key set is dependent on the current state of the application, this cannot be statically defined in the resource tables. Instead, the Help Manager responds by sending your application an HM_QUERY_KEYS_HELP message to get the resource id of the keys help panel. The following three messages sent to your application are probably the most widely used: HM_ERROR Sent whenever an error occurs between the time F1 is pressed and the help operation ends. The error code is specified in mpParm1 HM_HELPSUBITEM_UNDEFINED Sent whenever help was requested but no entry in the HELPSUBTABLE was found. HM_INFORM Sent whenever a link with the inform attribute is encountered. Select this to go to the next section ═══ 4.2.4. Resources ═══ To make the connection between a window and a help panel, two new resource types were added to PM's resource file definition - HELPTABLEs and HELPSUBTABLEs. Together, they specify an array of variable length lists that map a window resource id to a help panel resource id. ─────────────────────────────────────────────────────────────────────────────── HELPTABLE RES_CLIENT { HELPITEM RES_CLIENT, SUBHELP_CLIENT, GENHELP_CLIENT HELPITEM RES_DIALOG1, SUBHELP_DIALOG1, GENHELP_DIALOG1 HELPITEM RES_DIALOG2, SUBHELP_DIALOG2, GENHELP_DIALOG2 : HELPITEM RES_DIALOGn, SUBHELP_DIALOGn, GENHELP_DIALOGn } HELPSUBTABLE SUBHELP_CLIENT { HELPSUBITEM WID_WINDOW1, HID_PANEL1 HELPSUBITEM WID_WINDOW2, HID_PANEL2 : HELPSUBITEM WID_WINDOWn, HID_PANELn } HELPSUBTABLE SUBHELP_DIALOG1 { HELPSUBITEM WID_WINDOW1, HID_PANEL1 HELPSUBITEM WID_WINDOW2, HID_PANEL2 : HELPSUBITEM WID_WINDOWn, HID_PANELn } HELPSUBTABLE SUBHELP_DIALOG2 { HELPSUBITEM WID_WINDOW1, HID_PANEL1 HELPSUBITEM WID_WINDOW2, HID_PANEL2 : HELPSUBITEM WID_WINDOWn, HID_PANELn } HELPSUBTABLE SUBHELP_DIALOG3 { HELPSUBITEM WID_WINDOW1, HID_PANEL1 HELPSUBITEM WID_WINDOW2, HID_PANEL2 : HELPSUBITEM WID_WINDOWn, HID_PANELn } ─────────────────────────────────────────────────────────────────────────────── Figure 4. Help Manager resource structures Each HELPITEM specifies the resource id of an active window, the id of the HELPSUBTABLE associated with this window, and the resource id of the General Help panel associated with this window. Each HELPSUBITEM specifies a focus window resource id (WID_*) and a corresponding help panel resource id (HID_*). What are the rules that the Help Manager uses to get from F1 to a help panel id? To answer that question, we need to know the sequence of events that occur when a user presses F1. Below, we assume that the application is help-enabled. 1. The user presses F1 2. The Help Manager queries the active window resource id (ID1) and the focus window id (ID2). 3. ID1 is used in a lookup of the HELPTABLE to determine the HELPSUBTABLE to use. 4. ID2 is used in a lookup of the HELPSUBTABLE to find the resource id of the help panel to display. 5. The Help Manager displays the panel. There are two obvious error conditions: 1) there is no HELPSUBTABLE for the active window and 2) there is no HELPSUBITEM for the focus window. The former is resolved by examining each window in the parent window chain until a window that does have a HELPSUBTABLE is found and then the process continues as normal. If this still yields nothing, the owner window chain is searched and then if nothing still, an error message is sent to the active window. The latter is resolved by first sending an HM_HELPSUBITEM_UNDEFINED message to attempt to alleviate the situation. If the application returns FALSE the general help panel specified on the HELPITEM statement is displayed. Select this to go to the next section ═══ 4.2.5. Panel definitions ═══ This is, by far, the most time-consuming portion of help-enabling. Not only do you have to write the text to be displayed, but you must also be aware of formatting options and what effect they have on the output. At a minimum, you must have the following to create a valid online help file: ─────────────────────────────────────────────────────────────────────────────── :userdoc. :h1.Heading 1 :p.Paragraph 1 :euserdoc. ─────────────────────────────────────────────────────────────────────────────── Figure 5. Minimum GML markup The tags used above are described below: :userdoc. Specifies the beginning of a user document. :h1. Specifies a heading level 1. :p. Specifies a new paragraph. :euserdoc. Specifies the end of the user document. Online book writers must also specify a :title.Document title after the :userdoc. tag. As you can imagine, this file doesn't do much. In fact, it does nothing since as you can see nowhere are an help panel resource ids specified (even though you don't know how to specify them). For that matter, what defines a panel? A panel is a block of formatted text beginning with a heading level and ending with the beginning of the next panel displayed in the table of contents or the end of the document body. (the back matter of a document can contain an index.) Heading levels are specifies after the h and can be in the range 1-9; by default, only heading levels 1-3 are displayed in the table of contents, but this is configurable using the :docprof. tag. A heading may also have attributes; these are the res, id, name, and hide attributes. ─────────────────────────────────────────────────────────────────────────────── :hn [res=value][id=value][name=value][hide]. ─────────────────────────────────────────────────────────────────────────────── Figure 6. Heading tag syntax res=value This specifies a numeric resource id for the panel and is used in the HELPSUBITEM definitions and can be used to specify the target of a hypertext link. id=value This specifies an alphanumeric id that can be used to specify the target of a hypertext link. name=value This specifies an alphanumeric id that can be referenced by an application. hide This specifies that, regardless of the heading level, the panel should not show up in the table of contents. This is useful for hypertext links. Headings must have data associated with them, i.e. you cannot have an :h1. tag immediately followed by an :h2. tag. Also, heading levels other than 1 that are ordinally higher than their predecessors must have the next cardinal value. Thus, the first example is valid while the second is not: ─────────────────────────────────────────────────────────────────────────────── :h1.Heading 1 :p.Blah blah blah :h2.Heading 2 :p.Blah blah blah :h1.Heading 1 :p.Blah blah blah :h1.Heading 1 :p.Blah blah blah :h3.Heading 3 :p.Blah blah blah :h1.Heading 1 :p.Blah blah blah ─────────────────────────────────────────────────────────────────────────────── Figure 7. Heading examples This does not apply to headings that have a lower value than their predecessors. Paragraphs The most frequently used tag is probably the :p. tag, used to begin a new paragraph. It should be noted that some constructs implictly begin on a new paragraph, so this is not needed. A paragraph is denoted by ending the current line, inserting a blank line, and continuing on the next line. The current indentation level (modified by lists, etc.) is not changed. ─────────────────────────────────────────────────────────────────────────────── :p. ─────────────────────────────────────────────────────────────────────────────── Figure 8. Paragraph tag syntax A word here should be mentioned about symbols. What happens when you want to put a colon (:) in your panels? How is the compiler going to be able to differentiate between a colon as part of the text or as the beginning of a tag. GML defines a syntax for symbols such that they begin with an ampersand (&) and end with a period with a symbol name in the middle. Thus, a colon is &colon., and ampersand is &., etc.. Some other commonly used symbols appear below: &lbrk. Left bracket ([) &rbrk. Right bracket (]) &lbrc. Left brace ({) &rbrc. Right brace (}) &vbar. Vertical bar (|) &apos. Apostrophe (') &bsl. Backslash (\) &odq. Open double quote (") &cdq. Close double quote (") &osq. Open single quote (`) &csq. Close single quote (') You should use the symbol syntax instead of the actual characters whenever possible to ease the process of translating your IPF source to other languages, should that happen, since the compiler defines the code point to which each symbol is mapped according to the codepage in effect. Lists IPF (Information Presentation Facility - the language definition) defines five types of lists - simple, unordered, ordered, definition, and parameter. All lists consist of a begin tag, one or more list items, and an end tag. Definition and parameter list items are unique in that they consist of two parts. The begin tags/end tags are :sl./:esl., :ul./:eul., :ol./:eol., :dl./:edl., and :parml./:eparml. for simple, unordered, ordered, definition, and parameter lists respectively. List items for the first three types are specified using the :li. tag. Definition list terms are specified using the :dt. tag, and the corresponding definitions using the :dd. tag. Parameter list items are specifies using the :pt. tag and - like the definition lists - each item has a corresponding definition specified using the :pd. tag. ─────────────────────────────────────────────────────────────────────────────── :sl [compact]. :li.text :esl. :ul [compact]. :li.text :eul. :ol [compact]. :li.text :eol. :dl [tsize=value][compact][break={ all | none | fit }]. [:dthd.text] [:ddhd.text] :dt.text :dd.text :edl. :parml [tsize=value][compact][break={ all | none | fit }]. :pt.text :pd.text :eparml. ─────────────────────────────────────────────────────────────────────────────── Figure 9. List tags syntax Below are some examples of lists. ─────────────────────────────────────────────────────────────────────────────── :sl. :li.Simple list item 1 :li.Simple list item 2 :li.Simple list item 3 :esl. :ul. :li.Unordered list item 1 :li.Unordered list item 2 :li.Unordered list item 3 :eul. :ol. :li.Ordered list item 1 :li.Ordered list item 2 :li.Ordered list item 3 :eol. :dl. :dt.Term 1 :dd.Definition 1 :dt.Term 2 :dd.Definition 2 :dt.Term 3 :dd.Definition 3 :edl. :parml. :pt.Parameter 1 :pd.Definition 1 :pt.Parameter 2 :pd.Definition 2 :pt.Parameter 3 :pd.Definition 3 :eparml. ─────────────────────────────────────────────────────────────────────────────── Figure 10. Examples of list markup The above are formatted as: Simple list item 1 Simple list item 2 Simple list item 3 o Unordered list item 1 o Unordered list item 2 o Unordered list item 3 1. Ordered list item 1 2. Ordered list item 2 3. Ordered list item 3 Term 1 Definition 1 Term 2 Definition 2 Term 3 Definition 3 Parameter 1 Definition 1 Parameter 2 Definition 2 Parameter 3 Definition 3 All lists accept the attribute compact which specifies that items should not be separated by blank lines. Definition and parameter lists also accept the attributes tsize=value and break=[all | fit | none]. tsize specifies the width of the term column in units of the average character width of the current font. break specifies what is to be done when the term exceeds the column width; the default is none and starts the definition after the term preceeded by a space. all specifies that all definitions begin on a new line indented by the term column width. fit specifies that definitions whose terms exceed the column width begin on a new line as in all. Emphasis Emphasis markups consist of a begin and end tag and have the form :hpn. There are different emphasis levels, ranging from 1 to 9, which is specified as n. ─────────────────────────────────────────────────────────────────────────────── :hpn. ─────────────────────────────────────────────────────────────────────────────── Figure 11. Emphases tag syntax Level Meaning 1 Italic 2 Bold 3 Italicized bold 4 Alternate color 1 5 Underlined 6 Underlined italic 7 Underlined bold 8 Alternate color 2 9 Alternate color 3 Emphasis markup has no attributes. Hypertext Ever since online documentation became all-the-rage, one of the greatest advantages that it touted was the elimination of the phrase "For more information, see page...". Hypertext - as it was termed - is the ability to jump from one point to another by performing some action (usually a click of the mouse) on a hot-link; these hot-links are usually a word or phrase that has more, related information associated with it that the user will supposedly want to read eventually but without cluttering up the topic already being read. Hypertext in IPF markup is accomplished using the :link. tag and its corresponding end tag. ─────────────────────────────────────────────────────────────────────────────── :link reftype={ fn | hd | launch | inform } [res=value][refid=value] [object='value'][data='value'][x=value y=value cx=value cy=value]. :elink. ─────────────────────────────────────────────────────────────────────────────── Figure 12. Hypertext tag syntax The type of the link destination is specified by the reftype parameter. It can have one of the following values: fn Footnote. The footnote must have an id attribute which is specified in the refid parameter. hd Panel. The panel must have either an id or a resource id specified using the id and res attributes, respectively. This identifier is specifies in the refid and res attributes of the link tag. launch Application. The application, whose full path, name, and extension must be specified in the object attribute is executed. Any command line parameters may be specified in the data attribute. inform Message. This is applicable to online help only. The active window is sent an HM_INFORM message with the value of the res attribute passed in mpParm1. Hypertext links can be used to allow access to panels displayed elsewhere or not displayed at all; for example, suppose that you are on heading level 3 and you have some indirectly related information that you want to avoid cluttering the panel with. You cannot make it a heading level 4 because it won't show up in the table of contents. Linking makes a lot of sense here; make the target panel heading hidden and level 1 (to avoid nonsensical error messages from the compiler) and then link it to allow the user to read the information only if desired. Graphics Graphics can be included in your documents also; both OS/2 bitmaps and metafiles are supported. I have found it useful to use a screen capture program to export a bitmap of my application to a file, annotate it with a graphical editor, and then include it in the online help to label the items of interest. The tag that is used is the :artwork.. tag. ─────────────────────────────────────────────────────────────────────────────── :artwork name='filename' [runin][linkfile='filename'] [align={ left | center | right }] ─────────────────────────────────────────────────────────────────────────────── Figure 12. Hypertext tag syntax name specifies the filename containing the bitmap or metafile. runin specifies that the graphic does not force an newline before and after the graphic. align specifies the justification of the graphic. linkfile is for graphical linking (called hypergraphics) and will not be discussed. Select this to go to the next section ═══ 4.2.6. The Extra Mile ═══ Okay, so you've completely enabled your application to use online help...or have you? Actually, with the exception of rare applications, there is one more area that needs to be covered and that is message box help. Message boxes are modal dialogs that contain information for the user - error messages, usually. However, there is only so much that you can say in a message box; so, there is a style that you can specify on the call to WinMessageBox() that says to add a Help pushbutton. Fine, so you have a Help pushbutton. The user selects it. Nothing happens. What goes on under the covers? Hooks What goes on is that the message box receives the WM_HELP message and, because it can't determine the window that called WinMessageBox() (for more information on why this is so, read the documentation for WinLoadDlg() and pay attention to the part about determining the real owner of a dialog), it sends the message to the help hook. What is a help hook? Well, a hook in general is a function that gets called by an application (or, in this case, the system) when specific types of events occur; thus, a help hook is a function that receives notification of help-related events. ─────────────────────────────────────────────────────────────────────────────── BOOL EXPENTRY helpHook(HAB habAnchor, SHORT sMode, SHORT sTopic, SHORT sSubTopic, PRECTL prclPosition); ─────────────────────────────────────────────────────────────────────────────── Figure 13. Help hook function prototype habAnchor Specifies the anchor block of the thread to receive the message sMode Specifies the context of the help request. The values and the contexts are: HLPM_FRAME When the parent of the focus window is an FID_CLIENT window. sTopic is the frame window id, sSubTopic is the focus window id, and prclPosition points to the screen coordinates of the focus window. HLPM_WINDOW When the parent of the focus window is not an FID_CLIENT window. sTopic is the focus window's parent id, sSubTopic and prclPosition have the same meaning as HLPM_FRAME. HLPM_MENU When the application is in menu mode. sTopic contains the active action bar item id, sSubTopic contains the id of the menu item with the cursor, or -1 if the action bar has the cursor. In message boxes, the Help pushbutton is not defined with the BS_NOPOINTERFOCUS style, so it has the focus after it is selected. According to the rules, the help hook should get HLPM_WINDOW for the mode and the various identifiers in the other parameters. However, this is not the case; the help hook does indeed receive HLPM_WINDOW as the mode, but the sTopic parameter is the number specified as the fifth parameter to WinMessageBox() (the help button identifier). sSubTopic always contains the value 1. I have not verified if prclPosition points to the screen coordinates of the focus window. Before we can utilitize this information, we need to install the help hook using the WinSetHook() function. (Don't forget to uninstall it before the application exits using the WinReleaseHook() function.) Since the Help Manager works by installing its own help hook, and since WinSetHook() puts the hook at the beginning of the hook chain, you need to remember two things: 1) install your hook after calling WinCreateHelpInstance(), and 2) always return FALSE to insure that the next hook in the chain is called. As an ending note, someone sent me a while back, detailed instructions on a short cut which - if I remember correctly - eliminated the need for help hooks to provide message box help. Unfortunately, I lost these notes. Select this to go to the next section ═══ 4.2.7. Epilogue ═══ Now that we have all of the necessary information, we can start developing our online help and documents. To compile your IPF source, the Developer's Toolkit contains the IPFC compiler. It takes an IPF source file as input and compiles it to a HLP binary file in the same directory. For online documents, you need to specify the /INF switch which instead produces an INF binary file in the same directory. You will probably notice some limitations with the compiler. o There is no symbolic substition a la the C preprocessor o Numbers can only be expressed in decimal o All artwork must be in the same directory as the source The first one got so frustrating that I wrote my own IPFC preprocessor which processes C-style include files and allows you to substitute the #define macros in your IPF source as though it were a symbol. This preprocessor is included, as well as the accompanying User's Guide (in INF format, of course). The latter two problems could also be resolved by adding functionality to the preprocessor, but those features were never implemented. Select this to go to the next section ═══ 4.2.8. Summary ═══ Whew! A lot of information was presented here. Hopefully, you should be able to reread the objectives of this article and know the answers to the implied questions therein. The importance of online help cannot be understressed because as computer systems and applications become more complex, the more difficult it becomes to remember every feature that is provided. Online documentation is also important in that it provides a good vehicle for information dissemination and saves trees. Select this to go to the next section ═══ 4.3. OS/2 Installable File Systems - Part 2 ═══ Written by Andre Asselin Select this to go to the next section ═══ 4.3.1. Introduction ═══ Last time I went over some of the background information needed to write an IFS. In this article, I'll continue on and examine a framework to write a split ring 0/ring 3 IFS. This month I'm going to limit myself to just the code that does initialization and communication between ring 0 and ring 3; it's complicated enough to warrant a full article of it own. The things we will cover are: o How an IFS initializes o How to communicate between ring 0 and ring 3 o How a request to the IFS gets handed up to the control program This article is going to assume that you're familiar with the concepts of programming at ring 0. If this seems a little scary, take heart - the next article will finally get down to implementing the actual FS_* calls, and will concentrate mostly on ring 3 code. Select this to go to the next section ═══ 4.3.2. Project Layout ═══ The source code for the project is divided up into two directories, RING0 and RING3 (included as IFSR0.ZIP and IFSR3.ZIP - Editor); RING0 holds the ring 0 source, and RING3 hold the ring 3 source. As I mentioned last time, all IFS's must be 16-bit code, so for the source in the RING0 directory, I'm using Borland C++ 3.1. The ring 3 side is 32-bit code, however, so for it I'm using Borland C++ for OS/2. I haven't tried this code on any other compilers, but it should be easily portable. One thing to note is that I'm compiling the code in C++ mode and using the C++ extensions for one line comments and anonymous unions (see for example R0R3SHAR.H). I also use one Borland #pragma for forcing enumerations to be 16 bits. With a few modifications, this source should work with any ANSI C compiler. The contents of the RING0 directory are: C0.ASM Stripped down Borland C++ startup code FSD.H FS_* call prototypes and data structures FSH.H FSH_* call prototypes and data structures FSHELPER.LIB Import library for file system helpers OS216.H Header file for 16-bit OS/2 Dos APIs R0.CFG Borland C++ configuration file R0.DEF Definition file for the linker R0.MAK Make file for the IFS R0COMM.C Routines to communicate with the control program R0DEVHLP.ASM DevHlp interface routines R0DEVHLP.H Header file for DevHlp interface routines R0GLOBAL.C Global variable definitions R0GLOBAL.H Global variable declarations R0INC.H Main include file R0R3SHAR.H Shared data structures between the IFS and control program R0STRUCT.H IFS private structures R0STUBS.C FS_* stub routines The contents of the RING3 directory are: FSATT.C Sample attach program FSD.H FS_ call prototypes and data structures R0R3SHAR.H Shared data structures between the IFS and control program R3.CFG Borland C++ configuration file R3.MAK Make file for the control program R3COMM.C Routines to communicate with the IFS R3GLOBAL.H Global variable declarations R3INC.H Main include file R3STUBS.C FS_* stub routines The two directories are laid out pretty similarly. Some notes on the files: o The Rx.MAK file is the make file for the directory, and also generates the Rx.CFG file, which contains all the compiler switches for the compiler. o RxGLOBAL.H contains declarations for all of the global variables used. The IFS side also has R0GLOBAL.C which defines all of the global variables. The control program side doesn't have a corresponding R3GLOBAL.C because it only has one global variable that I thought was better put in R3STUBS.C. o FSD.H and FSH.H are slightly modified versions of the files by the same name distributed with the IFS documentation. They are modified slightly to accommodate C++ and some clashes in naming conventions. o R0R3SHAR.H contains definitions for the data structures that the IFS and control program share between themselves. This file is a bit tricky to write because you have to get the same results no matter if you compile with a 16-bit or 32-bit compiler. This means for example, that you have to explicitly declare all your int's long or short so that both compilers do the same thing. o In order to call the DevHlp's from C, we need to write interface functions for them. Files RING0\R0DEVHLP.H and RING0\R0DEVHLP.ASM contain the prototypes and assembler functions for the DevHlp's that are used in the code. The functions use the same parameter ordering as the ones in Steve Mastrianni's book "Writing OS/2 2.0 Device Drivers in C", although some of the typedefs and semantics are a bit different. I'd like to thank him for graciously allowing me to use the same parameter layouts. Select this to go to the next section ═══ 4.3.3. Communicating Between Ring 0 and Ring 3 ═══ The easiest and fastest way for ring 0 and ring 3 code to communicate is through shared memory. The way I implemented it is to have the control program allocate two buffers when it initializes: one buffer is used to hold all the parameters for a given operation, and the other serves as a data buffer to hold data for operations like FS_WRITE. After allocating the buffers, it makes a special call to the IFS, which sets up a GDT alias for itself (we need to use GDT selectors because the IFS can be called in the context of any process). In more detail, what we do is: When the IFS loads Call the AllocGDTSelector DevHlp to allocate two GDT selectors. These will be the selectors used by the IFS to get access to the control program's two buffers. We allocate them now because GDT selectors can only be allocated at initialization time. When the control program loads o Call DosAllocMem() to allocate memory for the two buffers to communicate between the IFS and the control program. o Call the IFS via DosFSCtl() and pass the addresses of the two buffers. o The IFS calls the VMLock DevHlp to lock the buffers permanently into memory (we need to do this because the next call requires it). o Call the LinToGDTSelector DevHlp to map the memory to the GDT selectors we allocated when the IFS loaded. This isn't the only way we could've implemented this. We could've had the IFS allocate the memory, for example, instead of the control program. It really comes down to personal preference, because either works just as well. Select this to go to the next section ═══ 4.3.4. A Shared Memory Protocol ═══ Once the buffers are allocated and accessible to both the ring 0 and ring 3 code, we need to set up some kind of protocol for its use. The control program needs to know when a valid operation is in the buffers and ready to be performed. The IFS needs to know when the buffers are in use, and when the buffers contain the results of a completed operation. Again, there are several ways to implement this. The method I chose involves using semaphores and captive threads. After the control program allocates the buffers and does any other initialization, it calls the IFS through DosFSCtl(). The IFS sets up the ring 0 GDT aliases for the buffers, and then suspends the control program's thread by making it wait on a semaphore (thus capturing it). To the control program, it just looks like it made a system call that is taking a very long time. When a request comes in to the IFS on another thread, it places the parameters and data into the two buffers and releases the semaphore that the control program's thread is blocked on. When that thread starts running again, the IFS returns from the DosFSCtl() call to the control program, where it executes the operation and places the results back into the buffer. It then calls the IFS again, which blocks the control program on the semaphore and starts the whole process over again. The advantage of this approach is that whenever the control program is running, it is guaranteed to have a valid operation in the buffer waiting to be executed. Thus you never have to worry about semaphores in the control program. This is especially nice because 16-bit and 32-bit semaphores are incompatible. Select this to go to the next section ═══ 4.3.5. The Semaphores ═══ Even though the control program doesn't have to worry about semaphores, the IFS certainly does, and in a big way. It has to worry about serializing all the requests it gets, and handling things like the control program unexpectedly terminating. To do this, we employ four semaphores: ┌─────────────────────────┬─────────────────────────┬─────────────────────────┐ │Name │Mnemonic │States │ ├─────────────────────────┼─────────────────────────┼─────────────────────────┤ │CPAttached │Control Program Attached │-1 = never attached, 0 = │ │ │ │not currently attached, 1│ │ │ │= attached │ ├─────────────────────────┼─────────────────────────┼─────────────────────────┤ │BufLock │Shared buffers are locked│Clear = buffers not │ │ │ │locked, Set = buffers are│ │ │ │locked │ ├─────────────────────────┼─────────────────────────┼─────────────────────────┤ │CmdReady │Command ready to execute │Clear = command ready, │ │ │ │Set = command not ready │ ├─────────────────────────┼─────────────────────────┼─────────────────────────┤ │CmdComplete │Command is complete │Clear = command is │ │ │ │complete, Set = command │ │ │ │not complete │ └─────────────────────────┴─────────────────────────┴─────────────────────────┘ CPAttached is used to indicate whether the control program is currently attached to the IFS. A value of -1 indicates that it has never attached to the IFS, 0 means it currently is not attached, but has been in the past, and 1 means it currently is attached. This semaphore is unique in that it is not a system semaphore, but an int that is being used as a semaphore. The reason we need to implement it this way will become clear when we start discussing the code. BufLock is used to serialize requests to the IFS. Whenever the IFS gets a request, the request thread blocks on this semaphore until it's clear, at which time it knows that its OK to use the shared buffers to initiate the next operation. CmdReady is the semaphore used to tell the control program that a request is in the shared buffers and is ready to execute. The control program thread blocks on this semaphore; a request thread clears this semaphore when a request is ready. CmdComplete indicates to the request thread that the command it initiated is complete and that the results are in the shared buffers. It is cleared by the control program thread when it calls back into the IFS after it completes an operation. Select this to go to the next section ═══ 4.3.6. IFS Initialization ═══ When OS/2 is booting and finds an IFS= line in the CONFIG.SYS, it will check that the file specified is a valid DLL and that it exports all of the required entry points for IFS's. If it is not a valid IFS, OS/2 will put up a message and refuse to load it. If the IFS is valid, OS/2 will load it into global system memory and then initialize it by calling FS_INIT (note that if the IFS has a LibInit routine, it will be ignored). RING0\R0COMM.C contains the code for the FS_INIT routine. Just like device drivers, IFS's get initialized in ring 3. Because of the special state of the system, an IFS can make calls to a limited set of Dos APIs (see table 1 for a list of which ones are allowed). It can also call any of the DevHlp routines that are valid at initialization time, but it cannot call any of the file system helpers. FS_INIT gets passed a pointer to the parameters on the IFS= line and a pointer to the DevHlp entry point. The third parameter is used to communicate between the IFS and the system's mini-IFS; we can safely ignore it. The first thing our IFS does is call DosPutMessage() to put up a sign-on message (it's a good idea to put up a message like this while you are still debugging the IFS, but you should take it out in release versions). After the sign-on banner is printed, we call a special routine to initialize the C runtime environment. This is a stripped down version of the startup code that comes with Borland C++; all it does is zero the BSS area and call any #pragma startup routines. Strictly speaking, it is probably not necessary. Next we save any parameters that were on the IFS= line in a global buffer and save the address of the DevHlp entry point. Note that contrary to what the IFS reference says, we have to check the szParm pointer before using it because it will be NULL if there are no parameters. The reference leads you to believe that it will point to an empty string, but that isn't true. Next we allocate a small block of memory in the system portion of the linear address space with the VMAlloc DevHlp (the system portion is global to all processes, just like GDT selectors). This memory will be used to hold the two lock handles that are created by the VMLock DevHlp when we lock down the memory that is shared between the control program and the IFS. We have to allocate the lock handles in the linear address range because VMLock can only put its lock handles there. Since our code is 16-bit, the compiler doesn't know what a linear address is. We deal with them by creating a new typedef, LINADDR, which is just an unsigned long. Next we also allocate two GDT selectors to alias the shared memory on the ring 0 side. This is done here because according to the PDD reference, you can only allocate GDT selectors at initialization time (in fact, if you do it after initialization, it still works, but why take the chance, right ?). We then create pointers out of the GDT selectors and assign them to the two global variables used to access the shared buffers. Note that at this point, no memory is allocated! We have our pointers set up, but if we were to try and access them, we'd get a TRAP D. We must wait for the control program to start and call the IFS before we can put memory behind those GDT selectors. After that's done, we set CPAttached to -1, which says that the control program has never attached to the IFS. We'll see later why its important to distinguish between when it has never attached, and when it has attached but then detached. Select this to go to the next section ═══ 4.3.7. Control Program Flow ═══ RING3\R3COMM.C contains the code to startup the control program. It first prints a banner, just like the IFS, and then allocates and commits memory for the two buffers. Once that is done, it puts the pointers to the two blocks of memory in the structure that is passed to the IFS for initialization. Before we call the IFS, though, we make a copy of the file system name in a temporary buffer. The DosFSCtl() call can use three different methods to figure out which IFS to call; we want to use the method where we specify the IFS's name. To do that we have to make a temporary copy of the IFS name because DosFSCtl could modify the buffer that contains the IFS name. Once all the preparations are made, the control program calls the IFS to initialize. To the control program it's really no big deal - just one DosFSCtl() call. When the DosFSCtl() returns, it will either be because there was an initialization error, or there was an operation waiting in the shared buffers to be executed. If an error occurred, we just terminate the control program (perhaps a more user friendly error message should be printed, but after all, this is just a framework). If it returned because an operation is ready, we enter the dispatch loop. The dispatch loop figures out what operation was requested, and calls that routine to execute it. Right now we only support the attach routine (which is actually just a stub that returns NO_ERROR). If it gets a request for an operation it doesn't understand, it prints an error message and returns ERROR_NOT_SUPPORTED to the IFS. Once the operation has been executed, we again copy the IFS name into a temporary buffer and make a DosFSCtl() call to indicate that this operation is complete, the results are in the shared buffer, and we're ready for the next request. When that DosFSCtl() returns, another operation will be waiting in the shared buffer. Select this to go to the next section ═══ 4.3.8. Ring 0 Side of Control Program Initialization ═══ As mentioned above, the ring 3 side of the control program initialization is very simple. The ring 0 side is a little more complicated, though. FS_FSCTL in RING0\R0COMM.C contains the code for the initialization. FS_FSCTL is used to provide an architected way to add IFS specific calls (sort of like the IOCTL interface for devices). There are three standard calls, which we just ignore for now. To those we add two new calls, FSCTL_FUNC_INIT and FSCTL_FUNC_NEXT. FSCTL_FUNC_INIT is called by the control program when it initializes. FSCTL_FUNC_NEXT is called when the control program has completed an operation and its ready for the next one. When FSCTL_FUNC_INIT is called, the first thing we do is check to see if the control program is already attached. If it is, we return an error code (this scenario could happen if the user tries to start a second copy of the control program). If the control program isn't already running, we wait until the BufLock semaphore is cleared. We do this because theoretically, we could run into the following situation: a request comes into the IFS and it starts servicing it. The control program is then detached, and then a new copy is run and tries to attach. The IFS is still in the middle of trying to service that request, however, and hasn't yet noticed the control program detached in the first place. It could be really bad if that ever did happen because the shared buffers would be corrupted, so we explicitly wait until the BufLock semaphore is clear, meaning that there are no threads using the shared buffers. We have to surround this with a check to see if the control program has ever been attached, because if it hasn't, the BufLock semaphore will not be initialized. Next we verify that the buffer that was passed to us is the proper size and that it is addressable. We have to check addressability on everything that is passed in from a ring 3 program because if it is not addressable, we bring down the whole entire system. Once addressability has been verified, we lock down the operation parameter area, and put the returned lock into the memory we allocated at FS_INIT time. Once that is done, we map the memory to the GDT selector that we allocated at FS_INIT time. We then do the same for the data buffer. Once these operations are complete, the memory can be shared between the IFS and the control program. Once that is complete, we clear the BufLock semaphore to initialize the semaphore that indicates that the shared buffer is not being used by anyone. We then get the process ID of the control program. This is used by the FS_EXIT routine. FS_EXIT is called whenever any process terminates. We have it check the process ID of the process that is terminating against the process ID of the control program, so that if the control program unexpectedly terminates, we detach it properly. After all that initialization is completed, CPAttached is set to 1 to indicate that the control program is attached. We then fall through to FSCTL_FUNC_NEXT. Since this function will be called every time an operation is completed, we first ensure that the control program is attached. If it's not, we return an error code. If it is attached, we first set the CmdReady semaphore to indicate that a command is no longer in the shared buffers (instead, results are in the buffers). We then clear CmdComplete to unblock the requesting thread (letting it know that its results are waiting). We then wait on the CmdReady semaphore, which will be cleared when a new operation is put into the shared buffers. At any time, any of the semaphore calls could return ERROR_INTERRUPT if the user is trying to kill the control program. If that occurs, we detach the control program before returning the error code. To detach the control program, we have to first set CPAttached to 0. We have to do it first to avoid possible deadlocks. We then unlock the shared memory buffers; if we don't do this, the control program will appear to die, but you will never be able to get rid of its window. Finally, we clear the CmdComplete semaphore so that if there is a request in progress, the requesting thread will unblock. Select this to go to the next section ═══ 4.3.9. An Example Call: Attaching a Drive ═══ Before you can use a drive managed by your IFS, you have to attach it. This creates an association between a drive letter and the IFS. RING3\FSATT.C contains an example program that attaches a drive. It is basically a front end to the DosFSAttach() and DosQueryFSAttach() calls. With a little help from the Control Program Programming Reference, you should be able to figure it out easily. The part that needs more explaining is the ring 0 side of the interface. When you issue a DosFSAttach() or DosQueryFSAttach(), the file system router calls the IFS's FS_ATTACH entry point (this can be found in RING0\R0STUBS.C). This code is basically a prototype for all of the FS_* calls that the IFS handles. It serializes access to the control program, does some preliminary validation of the parameters, sets up the argument block and passes it to the control program, waits until the control program executes the operation, and then returns the results of the operation. Once the details of this call are understood, all the others can be written pretty easily. The first thing FS_ATTACH does is check to see if the control program is attached; if it isn't, it immediately returns an error code. If the control program is attached, it waits until it can get access to the shared buffers. It is possible to time out waiting for this access; if we do, we return an ERROR_NOT_READY return code to the caller. Once we have access to the shared buffers, we wait until the control program completes the last operation it started. We have to do this because it is possible for a thread to give the control program a request to service, and then time out waiting for it to complete it. We could then have another thread come along and try to start a new request, but if the control program hasn't finished the last one yet, the shared buffers will get trashed because the IFS will be trying to put a new operation in them, and the control program will be trying to put the results of the last operation in them. Therefore we must wait until the control program has finished the last operation. Once those verifications are completed, we check to make sure we can access the buffer that was passed in. For an attach or detach request, all we have to do is check for readability, but for the query attach request, we have to check writability. We then check that the control program is still attached. This check is crucial because during any of those semaphore or FSH_PROBEBUF calls we could've blocked, and the control program could've terminated. If it did, the shared buffers are no longer valid, and if we try to access them we will trap. It's for this reason that the CPAttached semaphore is an int and not a system semaphore - the semaphore calls don't guarantee that they won't block (i.e. they could block). To make absolutely sure, the only thing we can rely on is a semaphore implemented as an int (it's probably worthwhile to refresh your memory here that ring 0 code will never be multitasked - you have to explicitly give up the CPU). Once we have verified that the control program is still attached, and thus our shared buffers are still valid, we setup the shared buffers with the operation's parameters. You can refer to R0R3SHAR.H (in either RING0 or RING3) for the data structure used. After that's complete, we clear the CmdReady semaphore to unblock the control program and indicate to it that a request is ready to be executed. We then block on CmdComplete waiting for the control program to execute our request. We specify a time-out to the wait so that we never get hung up on a faulty control program (if you never want to time out, you can change the value of MAXCPRESWAIT to -1). If we should time out, we release our hold on the shared buffer by clearing BufLock, and then return ERROR_NOT_READY. After the wait returns and we check for a time out, we also check to make sure the control program is still attached. It is possible that while the control program was executing our request that it terminated (maybe we had a bug that caused it to trap). If so, the shared buffers are no longer accessible, so we return an error code to the caller. If all went well, we copy the results out of the result buffers. Note that while we are doing this, we can't do anything that could cause us to yield because the control program could terminate during that time. After we copy the results out, we free up out hold on the shared buffers by clearing the BufLock semaphore and then return the error code that the control program told us to return. Select this to go to the next section ═══ 4.3.10. And That's About It ═══ That about covers the communications between the ring 0 and ring 3 sides of an IFS. If you're daring, you now have all the basics to forge ahead and begin implementing this type of IFS. If this still seems a little scary, don't worry - in the next article I'll fill in all the rest of the routines to give you a true skeleton to work with, and start discussing how to implement the FS_* calls. I will also provide a state diagram that shows all of the various states the system can be in, along with the states of the semaphores, to show that no deadlocks will occur in the IFS no matter what happens (this is actually very important because a deadlock is extremely difficult to track down, so you're better off investing time up front making sure they will never occur than beating your head against a wall later trying to track one down). I'd like to thank everyone who has written to encourage me to continue the series or with ideas for topics you'd like me to cover. Since the only pay I receive is your feedback, I hope you'll continue to write. Select this to go to the next section ═══ Dos APIs Callable at Initialization Time ═══ The following Dos APIs are callable by the IFS at initialization time: o DosBeep o DosChgFilePtr o DosClose o DosDelete o DosDevConfig o DosFindClose o DosFindFirst o DosFindNext o DosGetEnv o DosGetInfoSeg o DosGetMessage o DosOpen o DosPutMessage o DosQCurDir o DosQCurDisk o DosQFileInfo o DosQSysInfo o DosRead o DosWrite ═══ 4.4. Programming the Container Control - Part 3 ═══ Written by Larry Salomon, Jr. Select this to go to the next section ═══ 4.4.1. Back at the Batcave ═══ Last month we continued our disection of the container control and how to use it. The tree view was added to our list of conquests, and we started developing a sample application which we will continue to use. This month, we will add more meat to the bones of our skeleton by learning about the detail view and direct editing, among other things. Select this to go to the next section ═══ 4.4.2. Detail View ═══ Back in the first installment of this series, the detail view was described in the following manner. Each object is represented as a detailed description of the object. The information displayed is defined by the application. While I realize that did not say much, it served to illustrate that the detail view is the most flexible of the views, in terms of what can be displayed. It should be logical then to assume that this means yet more setup on the part of the application. What is the Detail View? To be precise, the detail view is a matrix view of the contents of a container, where each row in the matrix is a separate object and each column is an attribute (called a field) of every object to be displayed. Since the objects are already added using the CM_ALLOCRECORD/CM_INSERTRECORD messages, the columns must be added; this is done using the CM_ALLOCDETAILFIELDINFO/CM_INSERTDETAILFIELDINFO messages. As with its record-oriented counterpart, the CM_ALLOCDETAILFIELDINFO accepts the number of fields to allocate memory for and returns a pointer to the first FIELDINFO structure in the linked-list. typedef struct _FIELDINFO { ULONG cb; ULONG flData; ULONG flTitle; PVOID pTitleData; ULONG offStruct; PVOID pUserData; struct _FIELDINFO *pNextFieldInfo; ULONG cxWidth; } FIELDINFO, *PFIELDINFO; Figure 1. The FIELDINFO structure. cb specifies the size of the structure in bytes. flData specifies flags (CFA_* constants) for the field, especially the datatype. flTitle specifies flags (CFA_* constants) for the column title. pTitleData points to a string used for the column heading (can be NULL). offStruct specifies the offset of the data in the container record to be formatted according to its datatype. The FIELDOFFSET macro (defined in the Toolkit) is very helpful in initializing this field. When the datatype is CFA_STRING, the field in the container record is expected to be a pointer to the string and not the string itself. For example, typedef struct _MYCNRREC { MINIRECORDCORE mrcCore; CHAR achText[256]; ULONG ulNumSold; float fGrossIncome; float fNetIncome; float fTotalCost; float fNetProfit; CHAR achProdName[256]; PCHAR pchProdName; } MYCNRREC, *PMYCNRREC; we would specify FIELDOFFSET(MYCNRREC,pchProdName) instead of FIELDOFFSET(MYCNRREC,achProdName). The reason for this will be clear when we discuss direct editing. pUserData points to any application-defined data for the field. pNextFieldInfo points to the next FIELDINFO structure. This is initialized by the CM_ALLOCDETAILFIELDINFO message. cxWidth specifies the width in pixels of the field. This is initialized by the CM_ALLOCDETAILFIELDINFO message to 0, indicating that the field should be wide enough to show the widest value. If the default is not used and the data is too wide to fit, it is truncated when displayed. The flData field is initialized using one or more CFA_* constants: Data type CFA_BITMAPORICON offStruct "points" to the handle of a bitmap or icon, depending on whether or not CA_DRAWICON or CA_DRAWBITMAP was specified in the flWindowAttr field in the CM_SETCNRINFO message (CA_DRAWICON is the default if not explicitly changed by the application). CFA_STRING offStruct "points" to a pointer to the string to be displayed. Only data of this type can be directly edited. CFA_ULONG offStruct "points" to an unsigned long integer. CFA_DATE offStruct "points" to a CDATE structure. CFA_TIME offStruct "points" to a CTIME structure. For the latter three, NLS support is provided automatically by the container. You should note that there is no support for short integers, since they map directly to long integers with no loss in precision, nor is there support for floating point (none of PM uses floating point, so why should they start now). The latter means that you have to also have a string representation of the number (and creates all kinds of headaches if you will allow editing of the value). Alignment CFA_LEFT Align the data to the left CFA_CENTER Horizontally center the data CFA_RIGHT Align the data to the right CFA_TOP Align the data to the top CFA_VCENTER Vertically center the data CFA_BOTTOM Align the data to the bottom Miscellaneous CFA_SEPARATOR Displays a vertical separator to the right of the field CFA_HORZSEPARATOR Displays a horizontal separator underneath the column heading CFA_OWNER Enables ownerdraw for this field CFA_INVISIBLE Prevents display of this column CFA_FIREADONLY Prevents direct editing of the data if CFA_STRING is the datatype The flTitle field is initialized using one or more of the alignment fields and/or one or both of the following Miscellaneous CFA_BITMAPORICON pTitleData is the handle of a bitmap or icon, depending on whether or not CA_DRAWICON or CA_DRAWBITMAP was specified in the flWindowAttr field in the CM_SETCNRINFO message (CA_DRAWICON is the default if not explicitly changed by the application). If this is not specified, pTitleData is expected to point to character data. CFA_FITITLEREADONLY the text of the title is not directly editable. What's Next? Once you have initialized all of the FIELDINFO structures, you can "insert" them into the container using the CM_INSERTDETAILFIELDINFO message. Again using the parallel of the CM_INSERTRECORD message, it expects a pointer to the first FIELDINFO structure as well as a pointer to a FIELDINFOINSERT structure. typedef struct _FIELDINFOINSERT { ULONG cb; PFIELDINFO pFieldInfoOrder; ULONG fInvalidateFieldInfo; ULONG cFieldInfoInsert; } FIELDINFOINSERT, *PFIELDINFOINSERT; Figure 2. The FIELDINFOINSERT structure. cb specifies the size of the structure in bytes. pFieldInfoOrder specifies the FIELDINFO structure to be linked after, or CMA_FIRST or CMA_LAST to specify that these FIELDINFO structures should go to the head/tail of the list, respectively. fInvalidateFieldInfo specifies whether or not the fields are to be invalidated. cFieldInfoInsert specifies the number of FIELDINFO structures being inserted. Finally, changing the view to detail view is as simple as - you guessed it - sending the control a CM_SETCNRINFO message. CNRINFO ciInfo; ciInfo.flWindowAttr=CV_DETAIL; WinSendMsg(hwndCnr, CM_SETCNRINFO, MPFROMP(&ciInfo), MPFROMLONG(CMA_FLWINDOWATTR)); Figure 3. Changing to the detail view. Note that, even if you initialize the pTitleData field of the FIELDINFO structure to point to the column heading, the titles are not displayed unless you specify CA_DETAILSVIEWTITLES in the flWindowAttr field. Select this to go to the next section ═══ 4.4.3. Direct Editing ═══ Direct editing is accomplished by pressing the proper combination of keys and/or mouse buttons as defined in the "Mappings" page of the "Mouse" settings (in the "OS/2 System"/"System Setup" folder) while the mouse is over a directly-editable region. When this is done, a multi-line edit control appears and is initialized with the current text, in which you can make your changes; the enter key acts as a newline, while the pad enter key completes the editing operation and (normally) applies the changes. From a programming perspective, three notifications are sent to the application whenever direct-editing is requested by the user when over a non-read-only field ("field" is used here to mean any text string and not as it was defined in the discussion of the detail view) - CN_BEGINEDIT, CN_REALLOCPSZ, and CN_ENDEDIT (in that order). For all three, mpParm2 points to a CNREDITDATA structure which describes the state of the record being edited. The purpose of CN_BEGINEDIT and CN_ENDEDIT is to notify the user that editing is about to begin/end. However, only the CN_REALLOCPSZ is important, since the former two can be ignored while the latter cannot. typedef struct _CNREDITDATA { ULONG cb; HWND hwndCnr; PRECORDCORE pRecord; PFIELDINFO pFieldInfo; PSZ *ppszText; ULONG cbText; ULONG id; } CNREDITDATA; Figure 4. The CNREDITDATA structure. cb specifies the size of the structure in bytes. hwndCnr specifies the handle of the container. pRecord points to the record being edited. pFieldInfo points to the FIELDINFO structure describing the field being edited. ppszText points to the pointer to the text being edited. cbText specifies the size of the text. id specifies which part of the container contains the text to be edited and can be one of the following: CID_CNRTITLEWND, CID_LEFTDVWND, CID_RIGHTDVWND, CID_LEFTCOLTITLEWND, or CID_RIGHTCOLTITLEWND. The CN_REALLOCPSZ indicates that editing is about to end and that the application should allocate a new block of memory to contain the text. ppszText double-points to the old text and cbText specifies the length of the new text. If a new memory block is allocated, the pointer to the new memory block must be stored in ppszText. Returning TRUE from this notification indicates that ppszText points to a memory block sufficiently large enough to hold cbText bytes and that the container should copy the new text to this buffer. (I am not sure if cbText includes the null terminator - `\0') Returning FALSE indicates that the changes should not be copied and should be discarded. Select this to go to the next section ═══ 4.4.4. Altered States ═══ As defined by CUA '91 (I think), an object in general can be in one or more of five states (or none at all) - source, target, in-use, cursored, and selected. A container record stores information on its current state in the flRecordAttr (in both the RECORDCORE and MINIRECORDCORE structures) in the form of CRA_* constants. Setting the state, however, is not a simple matter of setting this field, since the container will have no way of knowing that you've changed the field. Instead, you send the container a CM_SETRECORDEMPHASIS message which updates this field in the record and updates the display of that record on the screen. Those who are "on the ball" will notice that there is no CRA_SOURCE constant defined in the 2.0 Toolkit. This was inadvertently left out and should be defined to be 0x00004000L in pmstddlg.h. So what do all of these states mean? CRA_CURSORED the record has the input focus. CRA_INUSE the record (and thus the object) is in use by the application. CRA_SELECTED the record is selected for later manipulation. CRA_SOURCE the record is a source for a direct-manipulation operation. CRA_TARGET the record is a target for a direct-manipulation operation. When you want to query all records with a particular emphasis type, you use the CM_QUERYRECORDEMPHASIS message. This returns the next record that has the specifies emphasis (or NULL if none exists). Popup Menus If you take a close look at the Workplace Shell, you will see all of these states used in one way or another. A more interesting use is in conjunction with popup menus; if the record underneath the mouse is not selected, it alone is given source emphasis. If it is selected, all records that are selected are given source emphasis. If no record is underneath the mouse, the container itself is given source emphasis. After the appropriate record states have been changed, WinPopupMenu() is called. Finally, the WM_MENUEND message is intercepted to "un-source" the records that were changed. Broken down into pseudo-code, this becomes: 1. Determine if the mouse is over a container record o If so, check the selection state - If the record is selected, add source emphasis to all selected records - If the record is not selected, give it source emphasis only o If not, select the enter container 2. Call WinPopupMenu() 3. Undo source emphasis changes While determining if the mouse is over a record is easy when processing the WM_CONTROL message, it is a bit more difficult when in the WM_CONTEXTMENU menu. The solution, it would appear from looking at our arsenal of messages that we can send to the container, would be to send the container a CM_QUERYRECORDFROMRECT message, specifying the mouse position as the rectangle to query. Looking a bit closer at the documentation reveals that the rectangle has to be specified in virtual coordinates. What??? Virtual Coordinates Okay, okay, everyone has probably heard of and is vaguely familiar with virtual coordinates, or else you would not be in PM programming to begin with. The container's notion of the origin in its coordinate system is somewhat awry, unfortunately, and this confuses things; the origin is defined to be the screen coordinate of the lower left corner of the container at the time the last CM_ARRANGE message was sent. So, you either have to keep track of when you send the container a CM_ARRANGE message and perform all sorts of hocus pocus to remember where the origin is supposed to be, or you can finish reading this sentence and discover that the documentation for CM_QUERYRECORDFROMRECT is flat-out wrong. The rectangle specified in this message is in window coordinates. Whew! That greatly simplifies things, except that when in detail view the record returned is the one above the one the mouse is over. Oh boy. Fortunately, we can calculate the height of a record using the CM_QUERYRECORDRECT message, which we use to adjust the mouse position before calling CM_QUERYRECORDFROMRECT. Now that we have the record underneath the mouse, we can check its selection state by examining the flRecordAttr field. If the record is selected, it is probably more efficient to use the CM_QUERYRECORDEMPHASIS message to get all selected records, but we already have this exquisite recursive search function, so I used that instead. Another example of poor documentation is in CM_SETRECORDEMPHASIS where it does not tell you that you can set the container's source emphasis by specifying NULL for the record. Finally, we call WinPopupMenu() and undo the source emphasis and - voila! - we're done. Select this to go to the next section ═══ 4.4.5. CNR3 - A Sample Application Revisited ═══ CNR3 builds upon CNR2 by adding detail view support, direct editing support, and "proper" popup menu support. As with part II, landmarks have been added to CNR3.C which are to point out things of interest. These landmarks are described below. Landmark 1 This is to point out the additions of the last four fields to the MYCNREDIT structure and the addition of the pmcrMenu field to the CLIENTDATA structure. Landmark 2 This points out the allocation, initialization, and insertion of the FIELDINFO structures. Landmark 3 This points out the new procedure for handling the context menu. Landmark 4 This points out the correction for the bug in the CM_QUERYRECORDFROMRECT message when in details view as described above. Landmark 5 This points out the processing of the CN_REALLOCPSZ notification. Landmark 6 This points out the addition of the detail view menu item. Select this to go to the next section ═══ 4.4.6. Summary ═══ This month we learned a lot of things, namely how to setup the details view, how direct editing is performed and what the container expects from the application with regards to this, and how selection states are set, queried, and used. We also saw how inadequate the documentation is when it contains so many examples of incorrect or incomplete information. Now you have enough information to use the container well. However, we're not done yet; next month, I will try to figure out some of the more advanced capabilities of the container such as record sharing and deltas. Stay tuned, same Bat-time, same Bat-channel! Select this to go to the next section ═══ 4.5. A Review of C++ Compilers ═══ Written by Gordon W. Zeglinski Select this to go to the next section ═══ 4.5.1. Introduction ═══ This article, examines various real-world aspects of the primary C++ compilers available for OS/2. The compilers included in this review are: Borland C++ for OS/2 version 1.0 Watcom C/C++ version 9.5 IBM Cset++ version 2.0 EMX/gcc version 0.8g I have omitted Zoretch C++ from this list because I do not have access to this compiler. The last time I looked at this compiler, it did not include an OS/2 debugger. These compilers will be compared on the bases of: o Time to compile the test code o Execution time of the test code o Size of the resultant .EXE file o Quality of bundled tools o Bugs found About the Tests The test code consists of a series of matrix objects developed by the author. These objects rely on both floating point operations and list manipulations (integer math, pointer dereferencing, etc.). The object hierarchy makes extensive use of virtual, pure virtual, and inline "functions". "Functions" in this case refer to both operators and functions. Select this to go to the next section ═══ 4.5.2. Compiler Overview ═══ In this section, I will examine the non-quantitative aspects of the various compilers, i.e. how easy they are to install, ease of use, and quality of the support tools. Given this, one should keep in mind that the following discussion is subjective and, as such, is the opinion of the author. Borland C++ for OS/2 version 1.0 This compiler is very similar to its windows counter part. Its IDE uses the same basic design and the windows version. The package comes with both a command line and PM-based compiler, a debugger, and documents on OS/2 programming in .INF format. Also, various on-line documents are provided to help use the the tools. I have found several bugs in the compiler. The following list is by no means extensive. 1. The editor in the IDE has a tendency to drop line feed characters every so often this leads to the occasional hard to find syntax error. 2. None of the PM functions are accessible using the help hotkeys from within the IDE. 3. The compiler has problems filling in virtual function tables under certain instances. It leaves them as NULL. The results of calling a function in this state is a protection violation error. 4. The debugger is next to unusable in my opinion. When debugging C++ programs (especially PM programs), one can expect to reboot their systems very often. This is due to OS/2's single threaded message queue and the inability to kill the debugger via repeated 's. 5. TLINK is unable to properly handle segments with IOPL permission, resulting is a protection violation. There was a work around for this problem, but I have not tried it because bug #3 prevents me from using this compiler in the application that needs IOPL priviledges. The Resource Workshop that ships with this version is similar to its Windows counterpart but lack much of its nice features. For instance, when editing menus one is simply typing a resource script into a large entry field control. The only resources that are edited visually are the dialog boxes, bitmaps and icons; however, the icons no longer work under OS/2 2.1 and the bitmap editor has some bugs in it that cause stray pixels to appear. The dialog editor is great, though, and is far better than the one IBM supplies with CSet++. This version lacks OWL and the profiler which ships with its DOS/Windows counterpart. On the positive side, this is a fast compiler. It compiles source files faster than any other compiler tested. Also, it seems that there has been some bugs fixed in its optimizer because code that would bomb under the DOS compiler when optimized now works under the OS/2 compiler. Tech support from Borland is available through Compuserve. I have heard that they no longer provide tech support to Internet mail addresses but cannot say if this is true. Past experience with them was a little disappointing. If you report a bug that has been fixed, they will tell you about a patch if it exists. Bug fixes integrated into the product, though, are usually done in a future version, which you have to purchase to get these fixes. Watcom C/C++ version 9.5 The Watcom compiler ships with a modified version of the OS/2 Toolkit version 2.0. It generates code for 32 bit DOS, Windows (with a DOS extender), NT and OS/2. If multi-platform code generation is a must for you, then this is the compiler you want. It includes a text mode debugger and profiler. Another plus for this package is that it is capable of optimizing for the Pentinum processor. Also included in the package is quite the hefty stack of documentation. However, in order to program in any of the above environments, one still has to buy the appropriate programming guides for the target operating systems. Shipped documentation includes: o WATCOM C/C++32. Optimizing User's Guide o WATCOM C/C++ Tools User's Guide o WATCOM VIDEO User's Guide o WATCOM VIDEO User's Guide Addendum o WATCOM Linker User's Guide o Supplement to WATCOM Linker User's Guide o WATCOM C Library Reference o WATCOM C++ Class Library Reference o WATCOM C++ Container Class Library Reference o WATCOM C Graphics Library Reference o The C++ Programming Language o WATCOM C Language Reference o WATCOM C/C++32 Commonly Asked Questions & Answers Although being a VIO mode app, the debugger is quite powerful. In addition to the typical features associated with a source-level debugger, it also allows debugger instructions to be executed at break points. However, there does not seem to be any C++ specific functionality, e.g. class browsers. The profiler included is also a VIO mode application and is divided into two separate parts: the sampler and sample displayer. The sampler uses the system clock to periodically interrupt the executing program and see where it is. It also allows the programmer to set "profile points" or "marks" within the source code, allowing it to be used as either an intrusive or non-intrusive profiler. Because the profiler does not use any device drivers, it is likely that under OS/2 the sample rate is only approximately 32 Hz, due to the fact that the profiler can only use the standard API calls to gain access to the timer interrupts. At any rate, the non-intrusiveness of the profiler is a negative point when it comes to profiling existing C++ code since in any OOP language, it is common to have many small functions that are frequently called. The only way to determine the impact of these functions is to place marks around them. For large amounts of code, this is undesirable because it has to be done manually. The sample displayer is pretty basic. It only gathers and displays time and frequency related data. One of the things which bothers me the most about the product is that the Watcom linker does not use standard .DEF files. In the few days that I have tested this package, the following bugs were found: 1. The compiler has problems dealing with the construct virtual operator=. I was able to work around this bug so that I could perform the benchmarks. 2. I could not get it to work properly with the Workframe version 1.1. This is probably due to the fact that I'm using the beta version of the Workframe from the last PDK until my copy of CSet++ arrives. Having found what I consider to be a serious bug (#1 above), I decided to give their tech support a try, provided through email. My first bug report was answered within 2 hours and subsequent email were all answered the same day. One day after sending them the code, I got a reply. Unfortunately for me, there is a grey area in the C++ "standards" that revolve around the use of virtual operator=(). Watcom follows the approach taken in MSC 7.0 which is different than just about every other C++ compiler. Watcom will be bringing this area to the attention of the ANSI C++ standards committee. IBM CSet++ version 2.0 Now that my copy of the GA version of CSet++ has arrived, I can compare it to the release version of the others and to the beta version some of you may have. The package includes the OS/2 toolkit, IBM's Workframe, and the CSet++ compiler. I'll concentrate on the compiler and it's support tools. The comparison of the GA to the BETA version of this compiler can be summarized in one sentence: the GA version compiles code about 2-3 times faster and it's tools are far more stable. One of the most important things (for some of my applications) about this compiler and the accompanying linker is that it is capable of interfacing with IOPL code segments. I have not tested Watcom's ability in this area. I also believe that the EMX package has input/output functions. Like Watcom, this compiler is compatible with version 3.0 of the C++ specifications, meaning that they support exceptions among other things. The Borland compiler does not and I'm not sure about the EMX package. The debugger is PM-based and has object browsing abilities built into it. It also has features which are specific to debugging PM-based programs (ie. monitoring message queues and such). My biggest problem with any PM-based debugger is that, if it is buggy, it can hang both the system queue and itself such that a reset is required. It does not appear to have the ability to program actions at breakpoints like the Watcom compiler does. After much use, the debugger performed almost flawlessly. I have found that it sometimes kills itself when the program monitor (a variable browser) is packed with variables. The best feature it has is its ability to save all of the settings and restore them when you resume debugging. These settings include breakpoints, user defined messages, etc. I have found a minor bug in its user defined messages - it did not properly display the message parameters after restarting the debugger although it did remember the messages I defined. The profiler is excellent, feature wise, and is PM-based. It is an intrusive analyzer that requires one to compile the code with both debugging info and profiling hooks turned on. The programmer can also insert profiling points within the code. The profiler is a full featured execution analyzer capable of measuring time, the number of times a profiler hook was called, and displaying a call tree. The time and frequency data can be displayed in a number of formats. The call tree displays which functions called who and the number of times these calls were made. It has problems, however; I have found that one of its display options will not function with a trace file I generated. Even worse is that some of its display modules do not support long filenames, which is unacceptable. The object browser is also quite useful; it allows a graphical and textual exploration of the classes used in the program. It displays the relationship between these objects by showing inheritance, who calls who, and class members. I have not used the PM object library that comes with this compiler because I am creating my own library. Others have complained that there is a lot of overhead in using this library, though, and that it takes along time to compile code that uses it. My biggest complaint about this product is its so-called documentation. I bought the 3.5" format package hoping to find tons of hard copy manuals. To my surprise, the hard copy documentation is very similar in size to the documentation that came with OS/2 2.0 GA. In several places, the hard copy documentation refers the reader to the on-line help. The only hard copy manuals with some thickness to them are: o Class Libraries Reference Summary o User Interface Class Library User's Guide (an IBM Red Book) o Programming Guide The rest of the manuals seem to have trivial content; if you were really stuck, you would typically be referred to the on-line help for the product or some other on-line file. There are also a few typo's in the manuals. Latest Bug Fixes My complaints about the lack of long filename support in EXTRA (the profiler) have been solved by applying CSD level 0001 to the package and then running the following little REXX program, which sets a flag in the EXTRA executables that allow them to see files with long names. Note: This REXX file assumes that the program EXEHDR.EXE is on the path (it is included with the toolkit). /* This exec addes the "newfiles" flag to the header of each of the Extra executables. This allows the user to use long names for trace files */ "EXEHDR /NEWFILES IXTRA.EXE" "EXEHDR /NEWFILES ITIME.EXE" "EXEHDR /NEWFILES IEXCDENS.EXE" "EXEHDR /NEWFILES ICALNEST.EXE" "EXEHDR /NEWFILES ISTATS.EXE" "EXEHDR /NEWFILES IDCGRAPH.EXE" EMX/gcc version 0.8g The EMX/GNU compiler package is a very impressive freeware compiler. The package includes the GNU gcc version 2.4.5 compiler and a debugger. A bunch of other Unix-like tools are also included. The debugger is a VIO program which is command line driven. I haven't used it for PM programming or interfacing with OS/2's API. The docs for the package are available via ftp from the primary OS/2 FTP sites. The reader can get these documents for themselves if they so desire. Select this to go to the next section ═══ 4.5.3. Summary of Features ═══ ┌────────────────────┬───────────────┬───────────────┬───────────────┬───────────────┐ │ │Borland C++ for│Watcom C/C++ │IBM CSet++ │EMX/gcc │ │ │OS/2 │ │ │ │ ├────────────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ │Multi-Platform │No │32 bit OS/2, │No │OS/2, DOS │ │ │ │DOS, Windows, │ │ │ │ │ │NT │ │ │ ├────────────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ │Debugger │PM │VIO │PM │VIO │ ├────────────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ │Profiler │No │VIO │PM │No │ ├────────────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ │C++ Level │2.1 │3.0 │3.0 │2.1(?) │ ├────────────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ │Precompiled Headers │Yes │No │Yes │No │ ├────────────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ │Tech Support │compuserve │Internet email │Internet email │No official │ │ │ │ │ │support │ └────────────────────┴───────────────┴───────────────┴───────────────┴───────────────┘ Select this to go to the next section ═══ 4.5.4. On with the Benchmarks ═══ The benchmarking was performed on a 486 DX/33-based machine with 16M of RAM. The test consist of two programs both are related to my work on object oriented matrix classes. One test (the full matrix) is mostly floating point while the other (the sparse matrix) is a combination of list manipulation and floating point. Listed are the times it took to compile the two first test both with and without optimizations enabled is measured, the times it took to execute the each test program, and the size of the resulting executables. Note: in the following charts, (opt) means that optimization was turned on Compile Times These times are measured with a stopwatch and represent the time it took NMAKE or MAKE (in the case of EMX) to create the executable. The source code consists of approximately 11 files for each test. Note: For each table below, all times are in seconds and all sizes are in bytes. ┌────────────────────┬────────┬────────┬────────┬────────┬────────────┬───────────┬───────────┬────────────┐ │ │Borland │Watcom │IBM │EMX/gcc │Borland(opt)│Watcom(opt)│IBM(opt) │EMX/gcc(opt)│ ├────────────────────┼────────┼────────┼────────┼────────┼────────────┼───────────┼───────────┼────────────┤ │Full Matrix │106 │240 │161 │270 │122 │259 │204 │288 │ └────────────────────┴────────┴────────┴────────┴────────┴────────────┴───────────┴───────────┴────────────┘ Executable size ┌────────────────────┬────────┬────────┬────────┬────────┬────────────┬───────────┬───────────┬────────────┐ │ │Borland │Watcom │IBM │EMX/gcc │Borland(opt)│Watcom(opt)│IBM(opt) │EMX/gcc(opt)│ ├────────────────────┼────────┼────────┼────────┼────────┼────────────┼───────────┼───────────┼────────────┤ │Full Matrix │86549 │137532 │147328 │176132 │83477 │145622 │125552 │118788 │ ├────────────────────┼────────┼────────┼────────┼────────┼────────────┼───────────┼───────────┼────────────┤ │Sparse Matrix │113173 │156712 │171344 │229380 │107541 │164775 │147104 │139628 │ └────────────────────┴────────┴────────┴────────┴────────┴────────────┴───────────┴───────────┴────────────┘ Execution Time The full test measures the time to LU decompose a 200 x 200 matrix. The sparse test measures the time to LU decompose and solve a 800 x 800 sparse matrix. ┌────────────────────┬────────┬────────┬────────┬────────┬────────────┬───────────┬───────────┬────────────┐ │ │Borland │Watcom │IBM │EMX/gcc │Borland(opt)│Watcom(opt)│IBM(opt) │EMX/gcc(opt)│ ├────────────────────┼────────┼────────┼────────┼────────┼────────────┼───────────┼───────────┼────────────┤ │Full Matrix │8 │8.33 │11 │11.67 │6.33 │6.00 │7.67 │5.33 │ ├────────────────────┼────────┼────────┼────────┼────────┼────────────┼───────────┼───────────┼────────────┤ │Sparse Matrix │75.5 │51.5 │102.5 │109 │67.5 │48.5 │50.5 │50.0 │ └────────────────────┴────────┴────────┴────────┴────────┴────────────┴───────────┴───────────┴────────────┘ I've noticed that while running the sparse test, the Watcom compiler required about 8M of RAM. All the other compilers produced executables that only required about 2M of RAM when executed, which requires further investigation. All of the compilers except Borland performed equally well in the sparse test. Select this to go to the next section ═══ 4.5.5. Closing Remarks ═══ When I began this series of tests, I was hoping that there would be a clear "winner." However, I do not think that one exists; each of the products have their own unique qualities which will appeal to different users. I strongly feel that for people looking simply for an OS/2 C++ compiler with excellent tools, CSet++ is the right choice. For others, I hope the guidelines below will help. As a side note, I am very impressed with the EMX/GCC package. It is as good as any of the others and costs nothing. If I didn't need a profiler and want precompiled header files, I'd definetly save some cash and use it. ┌────────────────────┬─────────────────────────────────────────────┐ │Desired Usage │Compiler(s) │ ├────────────────────┼─────────────────────────────────────────────┤ │Interfacing with │CSet++, Watcom C/C++ or EMX/GCC(?) │ │IOPL code │ │ ├────────────────────┼─────────────────────────────────────────────┤ │Floating Point │EMX/GCC, Watcom C/C++, or CSet++ │ │intensive code │ │ ├────────────────────┼─────────────────────────────────────────────┤ │PM programming │CSet++ or Borland (because of precompiled │ │ │header files). Any of the 4 are quite able in│ │ │this area. │ ├────────────────────┼─────────────────────────────────────────────┤ │C++ code with │ EMX/GCC or CSet++ (Watcom follows the MSC │ │working virtual │way of handling the operator=() ) │ │functions │ │ ├────────────────────┼─────────────────────────────────────────────┤ │Profiler │CSet++ or Watcom C/C++ │ ├────────────────────┼─────────────────────────────────────────────┤ │Environment like BC │Borland C++ for OS/2 │ │3.1 │ │ ├────────────────────┼─────────────────────────────────────────────┤ │ Really fast │ Borland C++ or CSet++ (CSet++ is slower but │ │compilation │it's debugger is better) │ └────────────────────┴─────────────────────────────────────────────┘ Select this to go to the next section ═══ 4.5.6. Editor's Notes ═══ After mentioning in comp.os.os2.programmer.misc that this issue would contain a comparison of C++ compilers, I received a request from Tim Francis in the C-Set++ compiler group to review the article pre-press. I gladly sent him a copy with the stipulation that the article would not be modified based on any comments he made, to which he agreed. However, I feel it necessary to put these comments here, as a side-note, with the intent of demonstrating the apparent customer-driven attitude of this group. As a disclaimer, while I do know some people on the compiler development team, I have never had any business dealings with them and am not doing this as a favor to them or because of any bias I have. Tim Francis writes: I didn't find anything really wrong in the compiler review. I thought C-Set++ came out looking quite good, actually. I would be interested in seeing the actual code used in the execution benchmarks - we were last in the opt/full matrix test, and 2nd last in the opt/sparse matrix test. I'm certainly not trying to dispute these numbers, but our benchmarks tend to place us a little higher in the competition than that. Our performance evaluation guy would really like a look at what's happening, to see if we can improve anything. The only other comment I have is in the summary of features, Tech support column. As documented in the C-Set++ package, we offer the following support: o Non-defect support, and informal (best effort) defect support - Compuserve - Internet - Talklink o Formal defect support - 1-800 number, available 24hrs/day, 7 days/week. Obviously we feel that the support we offer is a key component of the product, so if you mention the above somewhere in EDMI I'd appreciate it. For the purposes of completeness, I am also including the following from Ian Ameline: If possible, could you have those floating point tests run with /Gf+ turned on - it will result in *much* faster FP code by relaxing our strict interpretation of the IEEE standard. The other compilers all use the more relaxed interpretation - and this places us at a bit of a performance disadvantage compared to them, but we do produce results that are the same as any other IEEE 64 bit FP processor (Of course the Intel one is uses 80 bits of precision naturally, and if we try to conform to the 64 bit standards, we have to truncate the numbers each time they're stored to a variable. This truncation is expensive) Also, I'll make sure the long filename bug in Extra is fixed for CSD 2. Ian's comment about the compiler's strict interpretation of IEEE standards prompted me to request that the author use the /Gf+ option for the optimized part of the benchmarks (he was using /Gf); he did so and reported that /Gf and /Gf+ resulted in no difference in time (it appears that /Gf invokes the default which is /Gf+), while /Gf- resulted in an time increase of 1.5 seconds. Select this to go to the next section Select this to go to the next section ═══ 5. Columns ═══ The following columns can be found in this issue: o Scratch Patch ═══ 5.1. Scratch Patch ═══ Welcome to this month's "Scratch Patch"! Each month, I collect various items that fit into this column sent to me via email. The ones that I feel contribute the most to developers, whether in terms of information or as a nifty trick to tuck into your cap, get published in this column. To submit an item, send it via email to my address - os2man@panix.com - and be sure to grant permission to publish it (those that forget will not be considered for publication). This month, we have the following: o Questions and Answers o Snippet(s) of the Month o Documentation Chop Shop o Want Ads ═══ 5.1.1. Questions and Answers ═══ Brian Stark (stark@saturn.sdsu.edu) writes: While building entry fields I noticed the style option ES_AUTOTAB, which moves the cursor to the next "control window" automatically when the maximum number of characters are entered (Ref. OS/2 2.0 Technical Library, Programming Guide Volume II). My assumption is that this is done by sending a WM_CONTROL message to the current control windows owner. However I was unable to verify this in the document, or any other document. Currently I am only able to establish control of an entry field using the mouse, I would like to be able to use the arrow keys and the tab to move from field to field. Is the application responsible for this? If so, is there a document available that gives a clear description of this process, or am I just doing something wrong when I create the fields. After checking the toolkit header files (mainly pmwin.h) and not finding any notifications that would indicate what you are hoping (EN_* constants), a quick test application yielded that no WM_CONTROL messages are indeed sent that indicate the auto-tab feature has been invoked. However, the EN_KILLFOCUS and EN_SETFOCUS notifications are sent to the entryfield losing the focus and to the one receiving the focus, respectively. While these are not sent only when the auto-tab takes place (a mouse click in another entryfield will generate the same two notifications), a little thought and hocus-pocus will help you figure out how to do what you want to do. Regarding arrow keys and tabs, the system treats the keys in the following ways: Tab/Down arrow Moves the focus to the next control ("next" is defined using Z-order) with the WS_TABSTOP style. Backtab/Up arrow Moves the focus to the previous control ("previous" is defined using Z-order) with the WS_TABSTOP style. In entryfields, the left/right arrows maneuver the cursor within the control. Dominique Beauchamp (beaucham@phy.ulaval.ca) writes: What is the difference between a "string" and a "message" in a resource file? Each can be loaded with WinLoadString and WinLoadMessage but afterwards it seems we can use them the same way. If I want to program an error message box, should I use "string" or "message" to do it? (No, it's not obvious!) To be honest, often times I have asked the same question. Within PM, there seem to be a few items whose usefulness are questionable (this being one of them). To my knowledge, there is no difference between the two; one of the two might exist for historical reasons (SAA comes to mind), or there might be other logic at work here. In any case, I personally prefer WinLoadString since its name implies that it is used for more than just messages; whatever you choose, be consistent in your coding. ═══ 5.1.2. Snippet(s) of the Month ═══ Since I announced this new section this month, I would be expecting a lot if I wanted to put user-submissions here. Thus, here are some of my favorite subroutines: SHORT winDisplayMessage(HWND hwndParent, HWND hwndOwner, ULONG ulStyle, HMODULE hmDll, USHORT usId, USHORT usHelpId,...) //------------------------------------------------------------------------- // This function puts a message to the screen in PM mode (using WinMessageBox). // The title of the message box is assumed to have the message id usId|0x8000. // // Input: hwndParent - handle of the parent window // hwndOwner - handle of the owning window // ulStyle - specifies the WinMessageBox styles // hmDll - handle of the DLL containing the message // usId - specifies the id of the message to load // usHelpId - specifies the id of the corresponding help panel // Returns: TRUE if successful, FALSE otherwise //------------------------------------------------------------------------- { CHAR achMsg[1024]; CHAR achTitle[256]; va_list vlArgs; CHAR achBuf[2048]; ULONG ulRc; if (ulStyle==0L) { ulStyle=MB_INFORMATION | MB_OK; } /* endif */ if ((ulStyle & MB_SYSTEMMODAL)==0) { ulStyle|=MB_APPLMODAL; } /* endif */ if (usHelpId!=0) { ulStyle|=MB_HELP; } /* endif */ ulStyle|=MB_MOVEABLE; //---------------------------------------------------------------------- // Load the message box text and title //---------------------------------------------------------------------- if (WinLoadString(NULLHANDLE, hmDll, usId, sizeof(achMsg), achMsg)==0) { return MBID_ERROR; } /* endif */ if (WinLoadString(NULLHANDLE, hmDll, usId | 0x8000, sizeof(achTitle), achTitle)==0) { return MBID_ERROR; } /* endif */ //---------------------------------------------------------------------- // Format the message and display it //---------------------------------------------------------------------- va_start(vlArgs,usHelpId); vsprintf(achBuf,achMsg,vlArgs); va_end(vlArgs); ulRc=WinMessageBox(hwndParent, hwndOwner, achBuf, achTitle, usHelpId, ulStyle); return ulRc; } VOID winCenterWindow(HWND hwndCenter) //------------------------------------------------------------------------- // This function centers the window within its parent // // Input: hwndCenter - handle of the window to center //------------------------------------------------------------------------- { SWP swpCenter; RECTL rclParent; WinQueryWindowPos(hwndCenter,&swpCenter); WinQueryWindowRect(WinQueryWindow(hwndCenter,QW_PARENT),&rclParent); swpCenter.x=(rclParent.xRight-swpCenter.cx)/2; swpCenter.y=(rclParent.yTop-swpCenter.cy)/2; WinSetWindowPos(hwndCenter,NULLHANDLE,swpCenter.x,swpCenter.y,0,0,SWP_MOVE); } ═══ 5.1.3. Documentation Chop Shop ═══ Problem with BN_PAINT I have a confession to make: I have yet to upgrade my machine at work to OS/2 2.1, so the following problem might have been fixed in 2.1. I will try to remember to check at home, but if anyone else knows the answer already I would appreciate email. The problem is with buttons created with the style BS_USERBUTTON; the documentation states that, when the button needs to be repainted, you will receive a BN_PAINT notification and mpParm2 will point to a USERBUTTON structure which contains four fields: hwnd handle of the button window hps presentation space in which drawing should be performed fsState the current state of the button fsStateOld the previous state of the button According to the documentation, the fields fsState and fsStateOld can be one of three values - BDS_DEFAULT, BDS_HILITED, or BDS_DISABLED. When creating a 32-bit application utilizing "ownerdraw" buttons, this did not seem to work, so I added a few calls to fprintf() and below is what I got (the labelling of the events were added later): Upon window creation -------------------- Button state = 0x00000000 Button state (old) = 0x00040010 First down ---------- Button state = 0x00000100 Button state (old) = 0xD0DF032B First up -------- Button state = 0x01000000 Button state (old) = 0x01000100 Second down ----------- Button state = 0x00000100 Button state (old) = 0xD0DF032B Second up --------- Button state = 0x01000000 Button state (old) = 0x01000100 If you will accept the notion that my code is correct, you can see that the documentation appears to be completely wrong. I tried to reinterpret the values but still I ran into problems. Several calls to printf() later, a pattern emerged. I quickly followed my hunch and all of my problems went away. IBM defined the USERBUTTON structure incorrectly! The fsState and fsStateOld fields which are defined as ULONG's should be USHORT's instead. That simplified the problem to having to undefine BDS_DEFAULT (0x0400) and redefining it as 0x0000. Workaround The workaround should be obvious - either change your toolkit header files or define your own structure and redefine the BDS_DEFAULT constant. The former is preferred since you will not have to "kludge" every program that utilizes user-buttons to get this to work. ═══ 5.1.4. Want Ads ═══ My apologies for all of you who have sent requests for other topics that I did not place below. My memory is getting fragile in my old age. (*grin*) These seem to be good topics to write on; I have tried to assign some weighting on the "hotness" ("heat" just doesn't convey the idea properly, so I made up a new word) of the topic, so feel free to write on the really hot ones. Anything on Rexx/2 (hot) - many people have requested more articles on Rexx/2. I, for one, would like to see how to write external functions encased in DLL's, but other topics include interfacing Rexx/2 with C (as in a macro language), writing "Enhanced Editor" macros in Rexx/2, and interfacing with the Workplace Shell from Rexx/2. Using Input Hooks (hot) - this is a complicated topic which is brought up frequently in the comp.os.os2.programmer.misc newsgroup. Hit testing (warm) - one reader noted that the Jigsaw sample in both the IBM and Borland toolkits (are they not the same?) perform there own correlation and wondered why? Charles Petzold, in his OS/2 book "Programming the OS/2 Presentation Manager" briefly describes correlation and hit-testing, but does not go into any detail nor does it describe the Gpi functions used for this purpose. Animation (warm) - a few readers expressed an interest in the various animation techniques that can be applied to PM applications. The ultimate article, in my opinion, would be one that develops a sprite library a la the Commodore 64's (and Amiga's?) built-in routines, since this is probably the hardest component of any good animation sequence. Client/Server (warm) - using either named pipes (with or without a network) or sockets, client/server programming is all-the-rage these days. Some time ago, I started development on a post-office and a named-pipe implementation of FTP; maybe I will get time to finish them and will write articles on them. Multiple Threads in a PM application (warm) - this is another complicated topic which is brought up from time to time in the comp.os.os2.programmer.misc newsgroup. While various solutions to the dilemma of communication without global variables have been discussed, it would be nice to see (one or more of) them in a more permanent place than a news server. Select this to go to the next section ═══ 6. Future Attractions ═══ As always, we are always looking for (new) authors. If you have a topic about which you would like to write, send a brief description of the topic electronically to any of the editors, whose addresses are listed below, by the 15th of the month in which your article will appear. This alerts us that you will be sending an article so that we can plan the issue layout accordingly. After you have done this, get the latest copy of the Article Submission Guidelines from ftp.cdrom.com in the /pub/os2/2_x/program/newsltr directory. (the file is artsub.zip) The completed text of your article should be sent to us no later than the last day of the month; any articles received after that time may be pushed to the next issue. The editor's can be reached at the following email addresses: o Steve Luzynski - sal8@po.cwru.edu (Internet), 72677,2140 (Compuserve). o Larry Salomon - os2man@panix.com (Internet). Since Steve is incapacitated at the moment, Larry is the preferred contact at this time. Select this to go to the next section ═══ 7. Contributors to this Issue ═══ The following people contributed to this issue in one form or another (in alphabetical order): o Andre Asselin o Larry Salomon, Jr. o Gordon Zeglinski o Network distributors ═══ 7.1. Andre Asselin ═══ Andre Asselin recently graduated Cum Laude from Rensselaer Polytechnic Institute with a Bachelor of Science degree in Computer Science. He has worked with OS/2 since version 1.3, and also has extensive experience with MS-DOS and Microsoft Windows. He currently works in IBM's OS/2 TCP/IP Development group in Raleigh NC, where his responsibilities include the NFS client, a remote file system implemented as an IFS. Andre is also a member of Alpha Sigma Phi Fraternity, and enjoys hockey, soccer, and a good science fiction novel. He can be reached via email at asselin@vnet.ibm.com or on CompuServe at 71075,133. ═══ 7.2. Larry Salomon, Jr. ═══ Larry Salomon wrote his first Presentation Manager application for OS/2 version 1.1 in 1989. Since that time, he has written numerous VIO and PM applications, including the Scramble applet included with OS/2 and the I-Brow/Magnify/Screen Capture trio included with the IBM Professional Developers Kit CD-ROM currently being distributed by IBM. Currently, he works for International Masters Publishers in Stamford, Connecticut and resides in Bellerose, New York with his wife Lisa. Larry can be reached electronically via the Internet at os2man@panix.com. ═══ 7.3. Gordon Zeglinski ═══ Gordon Zeglinski is a freelance programmer/consultant who received his Master's degree in Mechanical Engineering with a thesis on C++ sparse matrix objects. He has been programming in C++ for 6 years and also has a strong background in FORTRAN. He started developing OS/2 applications with version 2.0 . His current projects include a client/server communications program that utilitizes OS/2's features and is soon to enter beta testing. Additionally, he is involved in the development of a "real-time" automated vehicle based on OS/2 and using C++ in which he does device driver development and designs the applications that comprise the control logic and user interface. He can be reached via the Internet at zeglins@cc.umanitoba.ca. ═══ 7.4. Network distributors ═══ These people are part of our distribution system to provide EDM/2 on networks other than the Internet. Their desire to help provide others access to this magazine is voluntary and we appreciate them a lot! o Paul Hethman (hethman@cs.utk.edu) - Compuserve o David Singer (singer@almaden.ibm.com) - IBM Internal ═══ ═══ Precompiled headers are very useful when including OS2.H. Precompiled header files can greatly decrease the amount of time necessary to compile files that include many and/or large header files. However, not all compilers are equal in this respect. Borland compiles all headers into one large file. This probably make it a bit faster to access than multiple files. But, when one header changes, all headers must be recompiled and stored in the large file. IBM's CSet++ on the other hand uses one precompiled file for each header. Thus, when you change one of your header files, only that file has to be recompiled. Also, the method employed by CSet++ allows the same precompiled headers to be used by multiple projects, unlike the method used by Borland. ═══ ═══ The following command line options were used with Borland's compiler: -I.. -If:\bcos2\include -ff -G -4 -O2it -vi -c -D_USE_POST_FIX_ ═══ ═══ The following command line options were used with IBM's compiler: /I.. /Tdp /J- /Si- /O+ /Oi+ /Os+ /W1 /Gf /Gi /G4 /Gx+ /C /d_USE_POST_FIX_ ═══ ═══ The following command line options were used with Watcom's compiler: /i=.. /i=. /i=f:\watcom\H /i=f:\TOOLKIT\C\OS2H /mf /4r /bt=os2 /sg /d_USE_POST_FIX_ /oneatx /zp4 ═══ ═══ The following command line options were used with GNU's EMX compiler: -c -O2 -m486 -I.. ═══ ═══ The following command line options were used with Borland's compiler: -I.. -If:\bcos2\include -ff -G -4 -vi -c -D_USE_POST_FIX_ ═══ ═══ The following command line options were used with IBM's compiler: /I.. /Tdp /J- /Si- /W1 /Gf /Gi /G4 /Gx+ /C /d_USE_POST_FIX_ ═══ ═══ The following command line options were used with Watcom's compiler: /i=.. /i=. /i=f:\watcom\H /i=f:\TOOLKIT\C\OS2H /mf /4r /bt=os2 /sg /d_USE_POST_FIX_ /zp4 ═══ ═══ The following command line options were used with GNU's EMX compiler: -c -m486 -I..