Exception Handling and Resource Management

This crucial section describes the exception handling methodology used by Symbian OS. C++ programmers may well be familiar with try , catch and tHRow for exception handling, and they may wonder why Symbian OS does not make use of this well- understood paradigm. The reason is mainly that the Symbian OS implementation is far more lightweight and appropriate to the constraints of small devices, but also that C++ exception handling was not fully supported by the target (GNU) complier when Symbian OS was first developed.

The Symbian OS implementation does have similarities. For example, it employs a trap harness , trAP , that can be compared to the try and catch elements of traditional C++, while leaving , a call to User ::Leave() , is the equivalent of throw. These concepts, and other exception handling techniques, will be explained in detail in this section.

Rigorous exception handling is central to the stability of Symbian OS ”it is closely integrated into resource management and also affects function naming conventions. It is incredibly important that applications do not leak memory, and so you must take steps to ensure that any application resources stored on the heap are cleaned up when an exception occurs. The Cleanup Stack is central to this methodology, as it offers a way to delete data that may otherwise be orphaned ”more about this later. Also of importance in this regard is the Symbian OS approach to object construction. The traditional C++ new() operator is generally not used. An overloaded version is provided for simple object construction, and a two-phase approach is taken when complex objects are involved. These techniques are employed to ensure that constructors do not leave.

An unfortunate consequence of the Symbian OS exception handling methodology is that it can sometimes be intimidating to programmers who are new to the platform. It forms such an integral part of the language that even writing a simple "Hello World" application can seem like a major task. However, you should not be discouraged, as most of the rules will soon become second nature and you will be happy in the knowledge that you are writing good code.

Exceptions, Leaves , Panics and Traps

In simple terms, an exception is a runtime error that is not the programmer's fault. As a programmer, you may like to think that no errors are your fault! However, common problems, such as array out of bounds errors, or the use of dangling pointers, are certainly the responsibility of the programmer. Other errors, such as a lack of memory, the inability to open a socket because of a dropped Internet connection, or the failure of a nonexistent file to open , cannot necessarily be attributed to bad programming ”they are due to conditions of the system that were not expected at the time of execution.

In other words, these errors occur because the system is in an unusual state. Sufficient memory should be available ”the Internet connection should not have failed ”the user should specify a file that does exist. The failures of these conditions are the exception rather than the rule, but they cannot be ignored just because they are unusual. So, although you cannot predict when any of these problems may occur, it is important that you consider the possibility that they could happen at some time and that you cater for them. Good software should be written to cope with all such exceptional circumstances, and cope gracefully, without compromising the user's data if it cannot proceed.

Contrast this with a panic . In Symbian OS, a panic is a programmatic error, which is the programmer's fault, such as an out-of-bounds array. Often the system will invoke a panic to indicate that something has gone seriously wrong and immediately terminate the offending application with a brief message, as shown in Figure 3-1.

Figure 3-1. A sample panic message.

Needless to say, thorough testing of your application should highlight any programmatic errors; therefore such panics should not occur in a released version of your application and so should never be visible to the end user!

Writing code to cope with every single exceptional circumstance that may occur would quickly become tiresome, and so the technique is to "throw" any such exception up the call stack to some centralized exception handler. In Symbian OS this is referred to as a "leave." Execution of a function continues under normal circumstances, but if anything untoward happens, then execution leaves the function and jumps back to the exception handler. This handler is known as a trap , or trap harness . Such traps should be few and far between ”trapping every possible leave where it occurs defeats the object of centralized exception handling and consumes additional system resources.

Here is part of a function containing a trap harness:

 void DoMainL()    {    ...    TRAPD(error, DoExampleL()); // Perform example function    if (error)       {       console->Printf(KFormatTxtFailed, error);       }    else       {       console->Printf(KTxtOK);       }    ...    } 

The job of this function is simply to invoke the DoExampleL() function (shown later) within a trap harness, then handle any errors that may have occurred (by informing the user via the console). Note the form of the trap harness: TRAPD . The variable error is declared by the macro to be a TInt (and so should not have already been declared) that will contain the error code. The state of this error can be checked later. If all is well, a reassuring OK message can be displayed. Otherwise, the leave code is embedded in a warning message and output to the user in this example. It is naturally up to the programmer to decide what is to be done with errors at each trap harness, which may include displaying a warning message, attempting to take some remedial action or simply "throwing" the leave up to the next nested trap.

There is another form of trap harness: trAP (without the trailing " D "). This is almost identical but does not declare the error variable. Instead, it depends on the variable having been declared previously as a TInt , perhaps explicitly, or perhaps by a previous TRAPD macro. Remember, the " D " here stands for "declare."

Traps can be nested ”in other words, one TRAP may enclose a function that itself contains other traps. It is also common for the error-handling code following one TRAP to deal with just a few possible errors ”any others that occur can be thrown up to the next TRAP harness using User::Leave().

So how do the leaves occur in the first place? There are three basic ways:

  • Use of an explicit leave, in other words: User::Leave() , User::LeaveIfError(), User::LeaveNoMemory() , or User::LeaveIfNull() .

  • Use of the overloaded new (ELeave) operator.

  • Calling a leaving function.

The first of these is quite straightforward (similar to throw in regular C++ exception handling). User::Leave() simply leaves at that point. A single TInt argument is provided, which is the leave code (that would be passed to the trap harness via the error variable in the previous example). User::LeaveIfError() leaves if the value passed in is negative ”all error codes are less than zero ”otherwise it does nothing. The value returned to the trap harness will simply be the leave error code as before. This is a way of causing a function that returns an error code to leave instead ”simply place the non-leaving function inside a User::LeaveIfError() wrapper. The code below shows this:

 TInt foundAt = iElementArray.Find(example, aRelation); // Leave if an error (i.e. KErrNotFound) User::LeaveIfError(foundAt); 

If the Find() function returns a negative value (the only likely one is KErrNotFound , which equals “1 ), then User::LeaveIfError() will leave with this value ”otherwise the flow of control continues as normal onto the next line of code.

User::LeaveNoMemory() takes no argument and is effectively shorthand for User::Leave(KErrNoMemory) . Finally, User::LeaveIfNull() leaves with KErrNoMemory if the pointer it is passed is NULL .

The next two leaving scenarios are illustrated in the DoExampleL() function, shown here. This is the function that the DoMainL() function (shown earlier) calls within its trap harness:

 void DoExampleL()    {    // Construct and install the Active Scheduler    CActiveScheduler* scheduler = new (ELeave) CActiveScheduler;    CleanupStack::PushL(scheduler);    CActiveScheduler::Install(scheduler);    // Construct the new element engine    CElementsEngine* elementEngine = CElementsEngine::NewLC(*console);    // Issue the request...    elementEngine->LoadFromCsvFilesL();    // ...then start the scheduler    CActiveScheduler::Start();    CleanupStack::PopAndDestroy(2, scheduler);    } 

The first executable line shows the use of new (ELeave) to allocate memory for a CActiveScheduler instance. In Symbian OS, heap (dynamic memory) allocation of new simple objects should almost always be performed this way. new on its own is used only in certain very specific circumstances ”for example, when an instance of a new application is created before the framework is fully constructed . new on its own does not take advantage of the leaving mechanism and requires an explicit check of success each time.

The next line, and one or two others, show the third way of potentially causing a leave ”calling a potentially leaving function. All functions that could leave must end with a trailing L . CleanupStack::PushL() and LoadFromCsvFileL() are both leaving functions, as is DoExampleL() itself.

When writing a function, how can you tell if it is a leaving function? Simple: if any line can leave and that leave is not trapped locally, then it is a leaving function. So all functions that contain calls to User::Leave() , new (ELeave) or calls to other leaving functions are themselves leaving functions. Their name must have a trailing L .

Why the need for the trailing L ? It is to ensure that any caller of your function knows that a leave may occur. Partly this is to give them the option of trapping the leave should they wish to. More important are the issues associated with jumping out of a function should a leave happen, as discussed in the next subsection.

The compiler will not detect if a function without a trailing L could leave. However, the Leavescan and EpocCheck tools described in Chapter 13 can be used to eliminate this problem.

Leaving Issues and the Cleanup Stack

This subsection focuses on the precautions you need to take to ensure memory is not leaked when a function leaves. It introduces the Cleanup Stack and describes the essential role it plays in Symbian OS resource management.

To understand the problems that can be caused by a function leaving, consider the following (poorly implemented) function ”a slightly modified version of the DoExampleL() function seen previously:

 void IncorrectDoExampleL()    {    // Construct and install the Active Scheduler    CActiveScheduler* scheduler = new (ELeave) CActiveScheduler;    CActiveScheduler::Install(scheduler);    // Construct the new element engine    CElementsEngine* elementsEngine = CElementsEngine::NewL(*console);    // Issue the request...    elementsEngine->LoadFromCsvFilesL();    // ...then start the scheduler    CActiveScheduler::Start();    delete elementsEngine;    delete scheduler;    } 

The differences are subtle, but crucial. The references to the Cleanup Stack have been removed, and if you are very observant you will have noticed the missing " C " from the CElementsEngine::NewL() call. Two delete s have been added that were not there previously ”cleaning up the heap-allocated objects. On first inspection, all seems well, but consider what would happen if the LoadFromCsvFilesL() function were to leave:

As mentioned previously, the trap harness would catch this leave, but what does this mean in real terms (or processor instructions)? It means (among other things) that a long jump ( longjmp ) will be executed back to the TRAPD macro. As part of this, the stack will be "unwound," and all automatic variables ( scheduler and elementsEngine ) will be destroyed .

The vital point is while these variables will be destroyed, the objects which they point at will not be. The runtime environment is not clever enough to realize that these are locally scoped pointers ”when execution leaves before the two delete operations, the pointers drop out of scope and their contents are lost. There is now no way to deallocate the two heap-based objects, and a memory leak occurs.

"So what?" you may think. "It is only a few hundred bytes, and that can be reclaimed next time the system reboots."

Such leaks may be tolerable on machines with multi-gigabytes of virtual memory that are rebooted daily, but remember that Symbian OS runs on devices that may have only a couple of megabytes of RAM and may go months, or even years , between reboots. All leaks must be anticipated and avoided by design.

What is needed is a way of making some kind of "backup copy" of any locally scoped pointers, just in case a leave should occur. If a leave does occur, then all of these pointers should be automatically deleted to ensure that the heap cells they point to are not leaked. This is precisely what the Cleanup Stack is for.

The Cleanup Stack

The Cleanup Stack ( CleanupStack is defined in e32base.h ) is a special stack that is crucial to Symbian OS resource mangement. It is essentially a way of making sure that if a leave occurs, then all resources are cleaned up, so there are no memory leaks ”for example, heap cells that were pointed to by locally scoped variables are not orphaned.

The basic idea is that before you call a potentially leaving function, you use one of the PushL() methods to add any locally scoped pointers to the top of the stack. If the function leaves, then all pointers to objects placed on the stack since the last trAP will be removed and the objects they point to will be deleted. If the function does not leave, then you use one of the Pop() methods to remove the pointer from the stack yourself. As you would expect, the Cleanup Stack operates on a last-on, first-off basis.

You will see some practical examples of how to use the stack throughout this section, and the crucial points ("Resource Handling Rules") will be highlighted.

Resource Handling Rule 1

Any locally scoped pointer to a heap-allocated object must be pushed onto the Cleanup Stack if there is a risk of a leave occurring and there is no other reference to the object elsewhere.

Note that if there is no chance of a leave, then there is no reason to push such a pointer onto the Cleanup Stack ”there is no danger of the pointer going out of scope before it is deleted.

Also, and perhaps less obviously, objects owned elsewhere must not be pushed onto the Cleanup Stack.

Resource Handling Rule 2

Instance data (data owned by an instance of a class) must never be pushed onto the Cleanup Stack.

If a leave were to occur with the instance data on the Cleanup Stack, then the instance data would be deleted as the Cleanup Stack was unwound. This is fine until you consider that the owning object must also be on the Cleanup Stack somewhere ”when the owner gets deleted (as the Cleanup Stack unwinds further), its destructor will try to delete the same piece of instance data, leading to a double-deletion.

Remember that instance data in Symbian OS always has a prefixed " i ". So the following is always wrong :

 // This is always wrong! CleanupStack::PushL(iMemberPointer); // Never push instance data! 

Instance data is (and must always be) deleted in the destructor of the class that owns it. (Note that a class may maintain a pointer to data without actually owning it ”in this case deleting a pointer in a non-owning class would also cause a double-deletion! However, this is not a Symbian OS-specific issue.)

Once the leaving code has safely completed, there is no danger of the local pointer being lost, so it can safely be popped off the stack using CleanupStack::Pop() . The same is true if ownership is being transferred to another object, which is itself on the Cleanup Stack somewhere, as in this example:

 // Append new element to the element array AppendL(newElement); // Remove the element pointer from the cleanup stack, // since ownership has successfully passed to the array CleanupStack::Pop(newElement);   // Now owned by array 

Since the array in question is owned elsewhere, once the newElement has been appended, it is the responsibility of the array's owner to dispose of it. So if the AppendL() function was successful (in other words, it did not leave), then the new element must be popped off the Cleanup Stack to avoid the same double-deletion problem as highlighted previously. Notice that the newElement pointer is passed into the Pop() function as an argument. (Cleanup) Stack pop operations always remove items from the top, so technically the argument is not necessary. However, supplying the pointer is strongly encouraged , since, for a debug build, the system will check it against the address of the object being popped, and panic if they do not match ”remember that a panic indicates a programmer's error. This allows any Cleanup Stack imbalance to be tracked down quickly!

If you have more that one object on the Cleanup Stack that you wish to pop, you can use an overloaded version of the Pop() function. Pop(TInt account, TAny* aLastExpectedItem) takes an integer value to indicate how many objects should be popped, and a pointer to the last item you want to be popped.

Often you will wish to dispose of an object at the end of the function. You could Pop() followed by delete , but this is unnecessary, since Symbian OS provides PopAndDestroy() to do both operations atomically. You can see its use at the end of the original DoExample() function ”the 2 denotes the number of objects to be popped, and scheduler is the address of the last item to be popped. See the SDK documentation for details of the overloads for both Pop() and PopAndDestroy() . Here is the code again:

 void DoExampleL()    {    // Construct and install the Active Scheduler    CActiveScheduler* scheduler = new (ELeave) CActiveScheduler;    CleanupStack::PushL(scheduler);    CActiveScheduler::Install(scheduler);    // Construct the new element engine    CElementsEngine* elementEngine = CElementsEngine::NewLC(*console);    // Issue the request...    elementEngine->LoadFromCsvFilesL();    // ...then start the scheduler    CActiveScheduler::Start();    CleanupStack::PopAndDestroy(2, scheduler);    } 

How come there are two objects to be popped, when there appears to be only one PushL() ? Remember the trailing " C " that disappeared in the second implementation? The next subsection will explain its significance and address another pivotal Symbian OS concept: two-phase construction.

Two-phase Construction

So far you have learnt about the clean-up of objects owned by locally scoped pointers, which must always be pushed onto the Cleanup Stack whenever there is danger of a leave occurring. What about complex objects, when one object owns another via a heap pointer? As mentioned previously, such objects are usually CBase -derived. Consider, by way of an example, the engine class from the Elements application (the example used throughout this chapter) where the engine owns an array of elements. The declaration of the relevant data members looks like this:

 class CElementsEngine : public CBase    { private:    CElementList*                 iElementList;    CConsoleBase&                 iConsole;    }; 

CElementList is a CBase -derived class that represents a list of chemical elements. The destructor for the class looks like this:

 CElementsEngine::~CElementsEngine()    {    delete iElementList;    } 

What about the constructor? You might think this would suffice:

  CElementsEngine::CElementsEngine(CConsoleBase& aConsole)   : iConsole (aConsole)   {   iElementList = new (ELeave) CElementList();   }  

However, such constructors are not used in Symbian OS, and for good reason. To see why, you need to consider what would happen if the allocation of the new CElementList were to fail, causing a leave. Well, presumably this constructor would have been invoked elsewhere something like this:

  CElementsEngine* myEngine = new (ELeave) CElementsEngine(*console);  

Remember that this line does two important things: First, the new (ELeave) operator allocates memory for the new CElementsEngine instance (and all its nested data). Assume for the moment that this allocation is successful. Next it calls the C++ constructor shown above. If the allocation of the CElementList in the CElementsEngine constructor were to fail, say, due to running out of memory, there would be a problem. When this leave occurs, there is no pointer pointing to the area of memory successfully allocated for the CElementsEngine object, so this memory will be orphaned, and a memory leak will occur. Because of this possibility, in Symbian OS a C++ constructor must not leave. Also note that destructors must never leave and must never assume that full construction has occurred.

Resource Handling Rule 3:

Constructors and destructors must not leave, and destructors must not assume full construction.

So how can complex objects be constructed? The solution is to perform the construction in two phases .

The first phase is to do the normal, non-allocating construction in a normal C++ constructor. The real CElementsEngine constructor is shown below ”just as you might expect but with the construction of iElementList missing:

 CElementsEngine::CElementsEngine(CConsoleBase& aConsole) : iConsole(aConsole)    {    } 

The second, potentially leaving phase of construction is performed by the function ConstructL() , the name that is always given to such second-phase constructors (except those in abstract base classes, which are often called BaseConstructL() ). The relevant part of CElementsEngine::ConstructL() is shown below:

 void CElementsEngine::ConstructL()    {    iElementList = new (ELeave) CElementRArray;    } 

Note that CElementRArray is a specialization of CElementList .

So, is it always necessary to use two lines of code to perform the construction of a complex object in Symbian OS? No, because such classes will usually provide a static NewL() function. Here is a possible CElementsEngine::NewL() :

 CElementsEngine* CElementsEngine::NewL(CConsoleBase& aConsole)    {    CElementsEngine* self = new (ELeave) CElementsEngine(aConsole);    CleanupStack::PushL(self);    self->ConstructL();    CleanupStack::Pop(self);    return self;    } 

Consider what each line does. Firstly, the overloaded new operator allocates memory for a new CElementsEngine instance. Should this allocation fail, a leave will occur (and, as always, this will be caught by the last trAP , which will unwind the Cleanup Stack). If not, the non-leaving C++ constructor will do some initialization, but no allocation . The newly allocated and initialized CChemicalElement is assigned to the local pointer self .

Next you need to push this local pointer onto the Cleanup Stack, because you are about to call a leaving function. Note that CleanupStack::PushL() may itself leave, since it must allocate memory for a new stack cell. In fact it always has a "spare" stack cell , to which it assigns the pointer being pushed before allocating the new stack cell. This means that even if the call to PushL() leaves, the pointer being pushed is guaranteed to be on the stack before it leaves, and so is guaranteed to be cleaned up.

Once the pointer is safely on the stack, you call the ConstructL() to perform the potentially leaving allocation. If this does leave, the pointer to the new CElementsEngine is on the Cleanup Stack and so it will be deleted as the Cleanup Stack unwinds. This is why destructors must not assume full construction ”they may be invoked because ConstructL() left before construction was complete. However, because the overloaded new (ELeave) operator zero-initializes all member data, it is safe to delete this object, since the member pointer iElementList will have been set to NULL , and delete NULL does nothing.

Assuming ConstructL() did not leave, you now have a fully constructed CElementsEngine instance and can safely pop the pointer to it from the Cleanup Stack. Finally, you return the pointer to this new object.

This whole process works in much the same way as a normal C++ constructor but encapsulates the two-phase construction process in a leave-safe manner. Since NewL() is static, it can be invoked like this:

 CElementeEngine* myEngine = CElementsEngine::NewL(*console); 

However, you are now left with a locally scoped pointer ( myElement ) to some heap-allocated data. If you are going to call some leaving functions before deleting it, you should push it onto the Cleanup Stack for safety. Since this is such a common thing to do, most complex classes provide a NewLC() function in addition to the NewL() . The only difference is that the NewLC() function leaves a pointer to the newly constructed object on the Cleanup Stack. This is another Symbian OS naming convention ”a trailing " C " after a function name denotes that function leaves a pointer to the object it returns on the Cleanup Stack. You will see other examples of this later.

 CElementsEngine* CElementsEngine::NewLC(CConsoleBase& aConsole)    {    CElementsEngine* self = new (ELeave) CElementsEngine(aConsole);    CleanupStack::PushL(self);    self->ConstructL();    return self;    } 

If both functions are being implemented, the NewL() is best written in terms of NewLC() :

 CElementsEngine* CElementsEngine::NewL(CConsoleBase& aConsole)    {    CElementsEngine* self = CElementsEngine::NewLC(aConsole);    CleanupStack::Pop(self);    return self;    } 

Note that if you are providing NewL() and/or NewLC() , it is usually good practice to make the corresponding C++ constructor and ConstructL() private (or protected). This prevents the ConstructL() from being called more than once and overwriting pointers to previously allocated member data. Also, a private (or protected) C++ constructor prevents construction on the stack and therefore removes the potential use of an object that is only partially constructed.

Private constructors prevent derivation of your class ”if you wish to allow derivation, then make the constructors protected instead.

One final rule:

Resource Handling Rule 4:

After deleting a member pointer, always zero it before reallocation.

For instance, consider the CChemicalElement::SetNameL() function in the Elements application. This function is used to assign a name to a particular element. The element name is stored in an HBufC descriptor, iName . You will learn more about the HBufC descriptor later in the section of this chapter, but for now, all you need to know is that it is a heap-based class for holding a text string.

Here is the implementation of SetNameL() :

 void CChemicalElement::SetNameL(const TDesC& aName)    {    // First delete the old name    delete iName;    iName = NULL;   // In case the next line leaves!    iName = aName.AllocL();    } 

Since the iName member is a pointer to some heap-allocated data, the function must do two things:

  • Delete the old name.

  • Allocate memory for the new one.

Consider what might happen if the AllocL() failed. You have successfully deleted iName , but if you did not set it to NULL , it remains pointing to the old, deallocated iName . If the AllocL() leaves, the CChemicalElement instance will be destroyed, and its destructor will try to delete the nonzero iName pointer, resulting in a fatal double-deletion!

Symbian OS Construction Methods

So, in Symbian OS there are four potential ways of constructing a new heap-allocated instance of a class: new , new (ELeave) , NewL() and NewLC() . Following are some summary guidelines for when to use each:

  • new : Almost never used. One common exception is when constructing a new instance of an application, as the Cleanup Stack does not exist at that point. Creating an application is covered in Chapter 4.

  • new (ELeave) : Used when constructing a heap-allocated instance of a simple class, such as a T -class (normally stack allocated) or a C -class that does not own any heap-allocated data. Note that heap allocation of anything other than a C -class may require advanced use of the Cleanup Stack.

  • NewL() : Used when constructing a heap-allocated instance of a compound class and either the instance is assigned directly to a member pointer of another class or there is no danger of a leave before the object is deleted. (Use of the Cleanup Stack is not needed.)

  • NewLC() : Used when constructing a heap-allocated instance of a compound class that is to be assigned to a locally scoped pointer, where leaving functions will be called before the pointer is deleted. (Use of the Cleanup Stack is needed.)

Advanced Use of the Cleanup Stack

There are a few situations when conventional cleanup is not sufficient. The most common of these relate to heap allocation of non- CBase -derived objects, and the use of R -classes.

Non-CBase-Derived Heap-Allocated Objects

CleanupStack::PushL() has an overload that takes a TAny* (in other words, an untyped pointer), and any such object pushed onto the Cleanup Stack will be cleaned up by invoking User::Free() . While this will deallocate memory assigned to the object, it will not invoke the object's destructor . This is sufficient for T -classes, since they must not require destructors, but it may not suffice for other arbitrary objects. Such objects must make use of TCleanupItem ”see the SDK documentation for further details of how to use this class to clean up after arbitrary objects.

R Classes

It is often assumed that the Cleanup Stack is purely concerned with memory. While cleaning up memory is its most common use, it can in fact clean up any resource. Since resources are usually owned by R -class handles, which do not have a common base class, special functions are provided, depending on the cleanup function required. The functions CleanupClosePushL() , CleanupDeletePushL() and CleanupReleasePushL() push resources onto the Cleanup Stack and invoke a corresponding Close() , Delete() or Release() method on the given resource object when CleanupStack::PopAndDestroy() is called for that object. Further information can be found in the SDK documentation ”a simple example is shown below:

 RFs myFileSystemHandle; // Connect to the file system TInt error = myFileSystemHandle.Connect(); CleanupClosePushL(myFileSystemHandle);  // In case of a leave // Some leaving code // Clean up myFileSysteHandle by calling Close() CleanupStack::PopAndDestroy(); 

The function CleanupClosePushL() creates a TCleanupItem object that automatically invokes Close() on the myFileSystemHandle object when CleanupStack::PopAndDestroy() is called. Note that, since you do not have the address of this TCleanupItem , it is not possible to pass an address into PopAndDestroy() for checking.

Developing Series 60 Applications. A Guide for Symbian OS C++ Developers
Developing Series 60 Applications: A Guide for Symbian OS C++ Developers: A Guide for Symbian OS C++ Developers
ISBN: 0321227220
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net