A personal look at the OS/2 API

From EDM2
Jump to: navigation, search

By Roger Orr

Introduction

I thought it might be interesting (and possibly a little controversial) to look at the application programming interface of OS/2 as a whole, and to highlight what I perceive as some of its strengths and weaknesses. There are two reasons to do this; in the first place a knowledge of some of the weaknesses of the API can help you to avoid common pitfalls when using it yourself, and in the second place the OS/2 API can provide a useful example of how to (and how not to) write programming interfaces for your own OS/2 applications.

Naming conventions

The first obvious feature of the OS/2 API is extensive use of common prefixes (e.g. Dos..., Win...) and of MixedCaseFunctionNames.

This at first strikes most 'C' programmers as an awful lot of typing - especially since 'C' appears to have been written by people who never use two letters when one letter (or better still a symbol!) will do. If you are used to the C runtime library with short function names (e.g. qsort, itoa) then functions with names like DosQueryFileInfo, PrfQueryProgramCategory are excessive.

However I quite like this convention (within reason) and I sometimes use it for my own programming - e.g. TrcOpen(), TrcPrintf() could be part of a library of trace functions. It enables you to clearly and obviously identify the component being called which, with over 1800 API calls now provided by OS/2 alone, is important unless you have a very good memory!

The convention can be taken to extremes - for example I personally do not find that the OS/2 2.x name DosQueryCurrentDisk is more comprehensible than the 1.x equivalent of DosQCurDisk; in fact I find the longer name makes code harder to read by burying the action in masses of verbiage.

Getting them all in

There are one of two APIs which I wish were implemented - other than the obvious DosFindFirstBug().

An example of such is DosQueryMaxFH(). You can find out how many file handles your process is allowed - but you have to do something like the following:

ULONG zero = 0; /* The function takes a pointer! (WHY ???) */
LONG MaxFH = 0;

rc = DosSetRelMaxFH( &zero, &MaxFH );

Why do I have to issue a 'set' of "zero more than I have now" in order to find out how many file handles I have now? I am also concerned lest at some point the function were to return an error as the amount to change by is zero.

[Still it is an improvement - under OS/2 1.x you had to find out by trying to open files until you got a failure!]

What I would like is, of course:

LONG MaxFH = 0;

rc = DosQueryMaxFH( &MaxFH );

Most of the OS/2 API is actually very well served - almost all 'set' functions have a corresponding 'query' function. It is a good idea to check the design of your own API to ensure that there are no obvious 'missing links'.

Return codes

The OS/2 API splits into two major groupings - functions which return an error number on failure (most of the so-called "base" API) and functions which return a specific value (usually 0 or -1) for failure (most of the "PM" API). Both methods have their strengths.

When programming to the base OS/2 API you don't have to think hard about return values - if the call returns an error code it failed. I typically use a single return code ('rc') inside a procedure and my code structure often looks like:

rc = DosOpenXXX();
if ( rc == 0 )
   {
     rc = DosYYY();
     .
     .
     .
     DosCloseXXX();
   }
return rc;

This method of handling return codes is easy to check since all functions return the same value for success. In addition the actual error codes are loosely tied to the OS/2 help system which means that if your procedure returns error 5 you can type "HELP 5" to obtain help on 'Access denied'.

It also makes for easier debugging - the error number is visible in AX on return from the function call even if the program doesn't explicitly contain any error checking.

Beware though - some API functions return an error code EVEN when they succeed. One of the commonest examples of this is ERROR_SMG_START_IN_BACKGROUND (error 457) returned when DosStartSession is called from a program which is not running in the foreground. Many programs treat this as an error and incorrectly infer that the session was not created. OS/2 provides a function (DosErrClass) to help classify error codes which can identify 'warning only' errors but I do not like errors which aren't errors!

By contract, the PM style of programming is that a large number of functions return a handle to something or a value (e.g. WinCreateWindow, GpiQueryAttrMode). This makes it easier to chain functions together where the return value from one is an argument to another:

WinSendMsg( WinQueryWindow( hwnd, QW_PARENT ), ... )

It also makes the code clearer since it is much more obvious what is being returned by the function - where functions take a number of pointer parameters it is not clear from the code alone whether the parameter is an 'input' or an 'output' parameter.

If an error is returned then a bad handle or value is returned. Unfortunately this means that the actual value returned varies depending on the function used. For example GpiCreatePS returns 0 on error, GpiQueryClipRegion returns -1 on error. In fact the confusion about the actual error return value is so great that PMGPI.H has a comment by the definition of HRGN_ERROR stating this is the error return code from GpiSetRegion, which is not the case! Obviously checking for the wrong error return value will make your code incorrect, but it is very hard to verify the code from a casual (or even not so casual) walk through.

What is worse, some functions return 0 on error AND on other conditions - for example WinDrawText() returns the number of characters drawn, which may be zero if an empty string is being shown.

It is no wonder that very few programs check return codes in PM programs.

My personal preference when writing my own functions is to use the base OS/2 convention and to make API functions return a defined set of error values on failure and 0 on success. This encourages the writing of code to check return codes and makes debugging much easier.

Handle values

OS/2 is full of 'handles' - identifiers of various resources returned by OS/2 when something is created or first accessed and used on subsequent calls.

The idea is simple but there are a few points to be aware of.

In the first place, many handles are simple indices counting from 0 or 1. Please take care with these low numbers - for example if you write code which opens a file, performs some I/O and then closes the file take care about what you do if the file open fails, since if you close the file handle despite failing to open the file you may find you have closed, for example, file handle 0 which is stdin. Before you think you would never do anything so silly ... look at the example below.

HFILE hf = 0;
rc = DosOpen( pszFile, &hf, ... );
   if ( rc == 0 )
      {
       /* Do some work */
      }

/* Tidy up */
DosClose( hf );     /* <- Here you may close file handle 0 !!! */

In my opinion it is safer for your own API functions to ALWAYS set the return values to invalid values on failure to prevent this sort of bug, which can be very hard to track down. If DosOpen() set hf to -1 on failure, for example, then the subsequent DosClose would fail rather than closing stdin.

In the second place many of the components of OS/2 reuse handle values immediately they are freed up. This can cause problems, especially in a multi-threaded program.

One classic case of this is the DosAsyncTimer API, which returns a timer handle. The timer handle is invalidated by OS/2 when the timer expires, and can also be invalidated by the application program calling DosStopTimer.

In a multi-threaded environment if the application program calls DosStopTimer just AFTER the timer has elapsed it is possible that the timer handle has been reused by another thread in the program. This sort of bug is VERY hard to find since as there is only a narrow timing window the fault will occur sporadically and quite likely NOT when you are debugging.

Window handles use an elegant technique to avoid this pitfall - the handle is made up of two parts, one of which identifies the data item and the other part is a check value which must match the value contained in the data item. If an old window handle is used, the check word will have changed and PM can detect that the window handle is no longer valid.

OS/2 2.1 has attempted to add a similar check to the file system by providing a set of 'DosProtectXXX' functions which basically extend the file handle to a 64-bit number, 32-bits of which is a check word to detect invalid handles and so prevent accidental corruption of files by use of an old file handle.

If they'd done it right first time round this wouldn't be necessary - there is after all no WinProtectXXX API.

I strongly recommend that if your own API makes use of handles to hide the implementation details from the user then you think very carefully about the issue of handle reuse, especially if you expect that your API will be used in a multi-threaded program (and if it won't be why are you using OS/2!).

Parameter passing

Some parts of OS/2 have gone through a number of iterations. For example OS/2 1.0 did not have presentation manager or extended file attributes; and so these features had to be added to the API set. Some of this is done by adding fresh functions to manage the new facilities; but some already existing APIs needed extending to cope with the new ideas.

Two different mechanisms were used when extending older API calls.

DosOpen had another parameter added (the address of an EAOP) and became DosOpen2. Of course DosOpen had to be retained for compatibility and so we then had two APIs where there was one before - confusing to new programmers. (Under OS/2 2.x DosOpen has changed again but merged with DosOpen2 so we now only have one API again!)

The DosStartSession API definition did not appear to change when PM appeared. How was this achieved? The function takes as first parameter a pointer to a structure (STARTDATA) with first data item a length. When PM functionality was added new items were added to the end of the old structure, and the length increased. Old programs had the old length and so this value could be used by the function to determine whether the new fields were supplied, and if not default values or behaviour could be supplied.

The main drawback with this approach is that recompiling an old program with the new header files could cause problems if the entire structure was not preset to a default value, usually zero, since extra fields were added which the original programmer did not know about, but the length value would be changed by recompiling since it would usually be set to sizeof(STARTDATA) in the code. This can mean that an old program works perfectly well but when you recompile with the later header files it stops working. This sort of trouble can take quite a while to track down!

It is, I believe, better to use a #defined constant for the 'level' of information contained in the buffer and so the programmer explicitly specifies what version of the API he is coding for.

Similarly when the system moved from a 16bit model to the current 32bit model a number of functions had to be changed and parameters changed from, for example, USHORT to ULONG.

Presentation manager avoided a lot of this sort of change since firstly it used non-size specific data types which could be widened in OS2.H and the change was transparent to the programmer, and secondly it was designed with 32bit operation in view from the outset.

It is always worth thinking ahead when writing your own API. What is the maximum size of the arguments likely to be now and in 5 years time? What are the likely extensions your users will require? Which of the current API functions will need changing if any of these extensions are implemented? As a general rule ALWAYS assume that your API will be extended and then you will be encouraged to save yourself work later!

As a simple example you may be writing an API which consists of open, read/write and close. The first version may be required only to support one open object at a time; but since an obvious extension to such an API is to support multiple open handles simultaneously it might well be prudent to design the API to make use of an explicit handle value. This value will be returned from the open call and passed to the subsequent calls - in the first implementation the value could even be ignored by these calls. If you provide an upgrade to a version supporting multiple handles you won't break any already written code; or even better if your API is packaged as a DLL previously compiled programs will run without recompiling.

Input or Output parameters

One or two of the OS/2 API functions use the SAME parameter for both input and output. This can be sensible, but in many cases is a nuisance. For example DosFindFirst/DosFindNext takes as one parameter (pcFileNames) the number of filenames to return on input and the number returned on output. This can lead to code which looks like this:

ULONG one = 1; /* Used for DosFindFirst */
       .
       .
       .
       rc = DosFindFirst( ... &one, ... );
       while ( rc == 0 )
           {
           .
           .
           .
           one = 1;  /* probably not needed but better be safe... */

           rc = DosFindNext( ..., &one );
           }

where the parameter 'one' is always set to one on input and is ignored on output.

I would prefer to pass a number for the input parameter and the address of a number for the output parameter to separate out the two uses of the current single parameter.

This is one case of a more general point about API functions - where an output parameter may not be used it is nice if you can omit it. Some of the OS/2 API functions allow this, some do not.

For example WinQueryWindowProcess takes two output parameters - a pointer to a process ID and a pointer to a thread ID. Either (or both!) can be omitted by setting the pointer to NULL if the value is not required.

However the function DosQueryHType also takes two output parameters - the type of the handle and the attributes of the handle; where the attributes are only meaningful if the handle type is a device. If you merely want to know whether, for example, stdin has been redirected to a file or pipe you may try:

DosQueryHType( 0, &ulType, NULL );

and if you do so your program will TRAP ! This seems to me a bit drastic - especially since the attributes are only relevant in only one of the possible values of handle type. This leads on to my final view on API functions...

Parameter Checking

In general, I hold that a trap is the fault of the owner of the actual code location at which the trap occurred.

You must expect programs using your API to pass you complete rubbish - and I hold that you must make a good attempt at checking this and returning an error code. 99% of traps in library code (after the initial development stage is passed) are caused by laziness - especially in failure to check input parameters for sensible values.

Even if you don't agree with this allocation of responsibility, please check the commonest case - being passed a NULL pointer or an invalid handle value.

Why do I hate traps inside API functions so much?

  1. A trap is usually fatal to your program - and the system cannot decide that what you were doing wasn't that important anyway and ignore the error since it does not operate at that level. It simply aborts your entire program - which a bit sad if the trap occurred when, for example, updating the time of day on the status screen of a multi-user database program!
  2. A trap is hard to debug - you usually DON'T have the source code to the library (!) and so it can be very hard to decide the cause of the trap. Most users will blame your 'rotten lousy no-good code'.
  3. If your API causes traps people don't trust it - well I don't anyway!

So, please, whenever you provide an API do sensible parameter checking and provide error return values which are, if possible, helpful in determining the problem. Who hasn't struggled with some of the more obscure OS/2 APIs to try and work out why error 87 (ERROR_INVALID_PARAMETER) was being returned - it is nice to get an error but please can someone tell me WHICH parameter was in error, and why!

Conclusion

I have looked briefly at some of the elements which go to make up an Application Programming Interface with particular reference to the OS/2 API.

I have touched on some of what I consider as the weak points of the OS/2 API, although I expect most of you will disagree with my analysis at some point.

In general I think the designers of OS/2 did a reasonably good job of providing a usable API, and it provides a useful role model for designing one's own interfaces; but I hope that you might be encouraged to think of ways to make your own APIs even better than OS/2's own API.

Roger Orr 22 Dec 1993