Chapter 3. Symbian Platform Fundamentals

C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do, it blows your whole leg off.

Bjarne Stroustrup

The aim of this chapter is to introduce you to the fundamentals of Symbian C++ and to show you basic guidelines to make your existing C++ code safe on this platform. Chapter 4 describes how you can use standard C and C++ libraries on the Symbian platform, including libc, STL and the Boost libraries, and this chapter discusses native Symbian C++ idioms. The aim is to cover the fundamentals that you need to work effectively on the platform and make you familiar with the terminology used by Symbian programmers, so you can both 'talk the talk' and 'walk the walk.'

These are the parts of Symbian C++ that you can't live without, or won't want to, if you aim to write efficient and memory-safe C++ code for Symbian devices. There are entire books about Symbian C++, so this chapter is necessarily a digest of the topics essential for getting up to speed to port code to the Symbian platform. If you need or want to know more about each topic, we recommend you check out the Fundamentals of Symbian C++ wiki pages on developer.symbian.org/wiki/index.php.

In the Beginning

When Symbian OS was designed in the mid-1990s, the compiler and tool chain available was not fully up to date with the evolving C++ standard. For example, exceptions were not supported initially and later, when the compiler was updated, the code generated when exception handling was enabled was found to add substantially to the size of the binaries and to the run-time RAM overheads, regardless of whether or not exceptions were actually thrown.

The API interfaces in Symbian C++ are based around standard Symbian notation; casing, prefixes, suffixes and names all convey information to the developer about what the function or variable does and how it performs. This design decision was taken because, when Symbian C++ was being developed, the C++ standard was not finalized and many of the features, such as exceptions, had a high implementation cost at run time, making it unsuitable for mobile development. Existing operating systems also had a high performance penalty when translated to mobile devices so additional constructs, such as active objects and the cleanup stack, were added to make the operating system perform in a more efficient manner and improve the battery life on the device.

This chapter helps developers understand where Symbian C++ differs from ANSI C++ and where Symbian C++ data structures and classes differ from the C++ library classes and structures. The features covered include resource management, strings, error handling and Symbianspecific features. This helps developers to understand how to develop and build applications that are well behaved and easily maintainable.

Naming Guidelines and Code Conventions

Symbian has very specific coding conventions that are context aware and help developers quickly understand what the function's, variable's or class's behavior will be at run time. This helps code be maintainable and understandable; failing to adhere to these conventions make it hard for developers and Symbian tools, such as LeaveScan and CodeScanner, to identify at compilation time issues such as resource leaks which can lead to serious code defects. The Symbian C++ coding standards are mandatory for submissions to the Symbian Foundation codeline.

Capitalization Guidelines

The first letter of class names is capitalized:

class TColor;

The words making up class or function names are adjoining, with the first letter of each word capitalized. Classes and functions have their initial letter capitalized. Function parameters and local, global and member variables have a lower-case first letter. Apart from the first letter of each word, the rest of each word is given in lower case, including acronyms. For example:

void CalculateScore(TInt aCorrectAnswers,
                    TInt aQuestionsAnswered);
class CActiveScheduler;
TInt localVariable;       // The first letter is not capitalized.
CShape* iShape;
class CBbc;     // Acronyms are not usually written in upper case.

Naming Guidelines: Prefixes and Suffixes

Member variables are prefixed with a lower case i which stands for instance:

TInt iCount;
CBackground* iBitmap;

Parameters are prefixed with a lower case a which stands for argument. We do not use an for arguments that start with a vowel:

void ExampleFunction(TBool aExampleBool, const TDesC& aName);

(Note that we write TBool aExampleBool rather than TBoolanExampleBool.)

Local variables have no prefix:

TInt localVariable;
CMyClass* ptr = NULL;

Constants are prefixed with K:

const TInt KMaxFilenameLength = 256;
#define KMaxFilenameLength 256

A trailing L at the end of a function name indicates that the function may leave. This is discussed further in Section 3.5.1.

void AllocL();

A trailing C on a function name indicates that the function returns a pointer that has been pushed onto the cleanup stack, which is described in more detail in Section 3.5.2:

CShapeShifter* NewLC();

Class Names

The class-naming guidelines make the creation, use and destruction of objects more straightforward so that, when writing code, the required behavior of a class should be matched to the Symbian class characteristics. Programmers using an unfamiliar class can be confident in how to instantiate an object, use it and destroy it without untoward side effects, such as leaking memory.

To enable the types to be easily distinguished, Symbian uses a simple naming convention that prefixes the class name with a letter (usually T, C, R or M).

T Classes

  • T classes are simple classes that behave much like the C++ built-in types.

  • T classes do not need an explicit destructor because they do not contain any member data that requires cleanup or which itself has a destructor.

  • T classes contain all their data internally and have no pointers, references or handles to data, unless that data is owned by another object responsible for its cleanup.

  • Individually declared T class objects are usually stack-based but they can also be created on the heap.

  • Although the data contained in these classes is simple, some T classes can themselves have fairly complex APIs, such as the descriptor base classes TDesC and TDes (see Section 3.4.4).

  • In other cases, a T class is simply a C-style struct consisting only of public data. Likewise, enumerations are simple types and so are also prefixed with T. Enumeration members are prefixed with E. For example:

enum TWeekdays { EMonday, ETuesday, ...};

C Classes

  • C classes are only ever allocated on the heap.

  • C classes may contain and own objects and pointers to other objects.

  • Unlike T classes, C classes have an explicit destructor to clean up member variables.

  • For Symbian memory management to work correctly, C classes must ultimately derive from the class CBase (defined in the Symbian header file e32base.h). This class has three characteristics that are inherited by every C class:

    • Safe destruction: CBase has a virtual destructor, so a CBase-derived object is destroyed through the overloaded destructor.

    • Zero initialization: CBase overloads operator new to zero-initialize an object when it is allocated on the heap. This means that all member data in a CBase-derived object is zero-filled when it is created (so this does not need to be done explicitly in the constructor).

    • Private copy constructor and assignment operators: CBase classes declare these to prevent calling code from accidentally performing invalid copy operations.

  • On instantiation, a C class typically needs to call code which may fail, for example, it may try to allocate memory when insufficient memory is available. There is an inherent risk of a memory leak at this point, so C classes use an idiom called two-phase construction to avoid it. I'll give more details on this in Section 3.5.4.

R Classes

  • An R class holds an external resource handle, for example a handle to a server session.

  • R classes are often small and usually contain no other member data besides the resource handle.

  • There is no RBase class similar to the CBase class.

  • A typical R class has a simple constructor and an initialization method that must be called after construction to set up the associated class and store its handle as a member variable of the R class object.

  • While a C class directly allocates resources (for example, memory), R classes cause the indirect allocation of resources. For example, in order to open a file, RFile::Open() does not itself open the file; instead it has a handle to a resource opened by the file server.

  • R classes may exist as class members or as local stack-based variables. They may be placed on the heap, but it is unconventional to do so, and creates additional responsibilities if they need to be made 'leave safe', which is something we describe in Section 3.5.4.

  • An R class also has a method that must be called on clean up to release the resource associated with the handle. Although in theory the cleanup function can be named anything, by convention it is almost always called Close(). A common mistake when using R classes is to forget to call Close() or to assume that there is a destructor that cleans up the resource.

M Classes

  • M classes are used to define interface classes. The M prefix stands for mixin.

  • On the Symbian platform, M classes are often used to define callback interfaces or observer classes.

  • The only form of multiple inheritance traditionally allowed on the Symbian platform is that involving multiple M classes. The reason for this is covered in more detail in Section 3.8.

  • An M class is an abstract interface class that declares pure virtual functions and has no member data.

  • Since an M class is never instantiated and has no member data, there is no need for an M class to have a constructor.

  • An M class may occasionally have non-pure virtual functions.

  • When a class inherits from a CBase class (or one derived from it) and one or more M classes it is always necessary to put the CBase-derived class first to emphasize the primary inheritance tree. This is because of the way the cleanup stack works (see Section 3.5.2). A detailed discussion can be found in the May 2008 Code Clinic article at developer.symbian.org/wiki/index.php/Mixin_Inheritance_and_the_Cleanup_Stack.

    class CCat : public CBase, public MDomesticAnimal

    and not

    class CCat : public MDomesticAnimal, public CBase

Static Classes

Some Symbian classes contain only static member functions. The classes themselves cannot be instantiated; their functions must instead be called using the scope-resolution operator. For example:

// Suspend the current thread for 1000 microseconds.
User::After(1000);

Static classes typically provide utility functionality where the functions are collected together within a class for convenience, in a similar way to the way in which a namespace might be used (these classes were created before the use of namespaces was standardized).

The naming convention for these classes is not to prefix with a significant letter; examples are the User, Math and Mem classes.

Data Handling

Simple Data Types

The Symbian platform defines a set of fundamental types which, for compiler independence, should be used instead of the built-in C++ types when calling native Symbian APIs. They are provided as a set of typedefs (within the file e32def.h) as shown in Table 3.1.

Collection Classes

Data collections such as dynamic arrays are fraught with complication in Symbian C++. There have been a number of different generations of container class provided over the years, and the addition of Open C/C++ has at last provided the ability to use STL collection classes.

For instance, you can choose to use std::vector to hold a Symbian platform class, provided you handle any possible exception accordingly:

std::vector<TTime> table;
try {
  TTime t = table.at(1);
  }
catch (std::out_of_range e)
  {  // Handle exception  }

Table 3.1. Fundamental Symbian platform types

Symbian typedef

C++ type

Description

[a]

TInt

signed int

In general, the non-specific TInt or TUint types should be used, corresponding to signed and unsigned 32-bit integers respectively, unless there is a specific reason to use an 8- or 16-bit variant.

TInt8

signed char

 

TInt16

short int

 

TInt32

long int

 

TUint

unsigned int

 

TUint8

unsigned char

 

TUint16

unsigned short int

 

TUint32

unsigned long int

 

TInt64

long long

These use the available native 64-bit support.

 

TUint64

unsigned long long

 

TReal32

float

Use of floating-point numbers should generally be avoided unless it is a natural part of the problem specification. Many Symbian devices do not have a hardware floating-point unit and their floating-point performance is much slower than integer performance.

 

TReal64

double

Most serious floating-point calculations require double precision. All standard math functions (see the Math class) take double-precision arguments. Single precision should only be used where space and performance are at a premium and when their limited precision is acceptable.

 

TReal

double

 

TAny

void

TAny* is a pointer to something – type unspecified – andis usedinpreferenceto void*. TAny is not equivalent to void[a].

 
  

Thus, we write

 
  

void TypicalFunction(TAny* aPointerParameter);

 
  

not

 
  

void TypicalFunction(void* aPointerParameter);

 
  

TAny TypicalFunction(TAny* aPointerParameter);

 

TBool

int

This type should be used for Booleans. For historical reasons, the Symbian TBool type is equivalent to int (ETrue = 1 and EFalse = 0). Since C++ interprets any non-zero value as true, direct comparison with ETrue should not be made.

 

[a] Always use void when a method has no return type.

However, if you want or need to use native container classes on the Symbian platform, you should use RArray<class T> and RPointerArray<class T>, found in e32cmn.h. These classes are the most efficient native dynamic containers and provide type safety and bounds-checked access, plus a range of methods for sorting and searching.

RArray<class T> comprises a simple array of elements of type T, which must be of the same type. The class is a thin template specialization of class RArrayBase (see e32cmn.h). The elements stored in RArray<class T> are typically T class objects[56] or simple types, enums or constants. RPointerArray<class T> is a thin template class deriving from RPointerArrayBase. It comprises a simple array of pointer elements that address objects, such as C class or M class objects, stored on the heap.

The API methods for adding elements to and removing elements from these arrays are relatively straightforward. They are documented in the Symbian Developer Library and in the Fundamentals of Symbian C++ wiki pages on developer.symbian.org. However, the following limitations should be noted:

  • The size of an array element is bounded. The current implementation for RArray imposes an upper limit of 640 bytes; a USER 129 panic arises if an element is larger than 640bytes (or is zero or negative in size). In cases where the element size is greater than 640 bytes, it is better to store the elements on the heap and use an RPointerArray<class T>.

  • There is no segmented-memory implementation for RArray<class T> or RPointerArray<class T>; both classes use a contiguous flat buffer rather than segmented memory layout. Contiguous memory blocks are typically used when high-speed pointer lookup is an important consideration and when resizing is expected to be infrequent. Segmented buffers are preferable for large amounts of data and if the container is expected to resize frequently, or where a number of elements may be inserted into or deleted. This is because, if a single flat buffer is used, numerous reallocations may result in heap thrashing and copying, because each allocation takes a new, larger memory block and then copies the data from the original location to the new one. If you think you need a segmented buffer array, you need to consider using one of the CArrayXSeg classes.

  • When porting existing code, you should be aware of using RArray with classes that rely on non-trivial copy constructors and assignment operators, as RArray performs shallow copy over its items.

  • For performance reasons, RArray<class T> is optimized to make best use of the ARM processor and stores objects with word (four-byte) alignment. This means that, even if the object is less than four bytes in size, it is stored in a four-byte element. Some member functions do not work when RArray<class T> is used for classes that are not word-aligned and an unhandled exception may occur on hardware that enforces strict alignment. The functions affected are:

    • the constructor RArray(TInt, T*, TInt)

    • Append(const T&)

    • Insert(const T&, TInt)

    • operator[], if the returned pointer is used to iterate through the container as for a C array.

For a thorough discussion of RArray, please refer to developer.symbian.org/wiki/index.php/Advanced_RArray.

The Symbian platform provides a number of legacy dynamic array classes with names prefixed by CArray, such as CArrayFixFlat <class T>, CArrayFixSeg<class T> and CArrayVarSeg<class T> (found in e32base.h) but you should not use these unless you have a fair degree of Symbian C++ expertise and understand why you need to use them. To attain this level of knowledge, we recommend you study an intermediate or advanced level title on Symbian C++.[57]

Native Data Collection Classes

Linked Lists

Class TDblQue<class T> can be used to create a double-linked list of objects of type T. Objects of type T must define a member variable of type TDblQueLink (to contain the forward and backward linked-list pointers). When the TDblQue is constructed, you must specify the offset of the TDblQueLink member variable in the constructor using the _FOFF macro. Please see the Symbian Developer Library documentation for further information and examples of the use of these classes.

Circular Buffers

CCirBuf<class T> can be used to create a circular buffer of objects of type T. Before adding anything to the circular buffer, SetLengthL() must be called to set the maximum length of the buffer. If the buffer fills up, the next addition returns 0, indicating that the data cannot be added.

Maps and Sets

RHashSet is a templated class that implements an unordered extensional set of objects of type T using a probe-sequence hash table. RHashMap is a templated class that implements an associative array with key type K and value type V, using a probe-sequence hash table. See the Symbian Developer Library documentation for further information and examples of how to use these classes.[58]

String Handling: Descriptors

In Symbian C++, the string classes are known as 'descriptors' because objects of each class are self-describing. A descriptor object holds information that describes it fully in terms of its length and layout. That is, it holds the length of the string of data it represents as well as its type, which identifies the underlying memory layout of the descriptor data. The rationale is that descriptors are thus protected against buffer overrun and don't rely on NULL terminators, which reduces the risk of off-by-one errors. This is all very sensible, but descriptors have something of a bad reputation among Symbian programmers because they take some time to get used to. There are a lot of different classes to become familiar with and their use is somewhat contorted by memory management issues.

Most Symbian C++ APIs use descriptors, so it's impossible to avoid using them completely if you intend to use the native APIs directly. However, with the advent of Open C/C++, RGA and Qt for S60, as described in Chapters 4 and 6, there are alternatives to descriptors if you don't need to use the native APIs, and you can skip ahead to Section 3.5. However, some knowledge of descriptors is handy should you need to discuss them seriously with a Symbian C++ evangelist or debug code that uses them.

Memory Management

A descriptor object does not dynamically manage the memory used to store its data. Any descriptor method that causes data modification first checks that the maximum length allocated for it can accommodate the modified data. If it can not, it does not re-allocate memory for the operation but instead panics (USER 11) to indicate that a programming error has occurred (see Section 3.5.5 for more about panics). In the event of such a panic, it can be assumed that no descriptor data was modified. Before calling a modifiable descriptor method that may expand the descriptor, you must ensure that there is sufficient memory available for it to succeed.

Character Size

The Symbian platform is built by default to support wide character sets with 16-bit characters (UTF-16 strings). The character width of descriptor classes can be identified from their names. If the class name ends in 8 (e.g. TPtr8) it has narrow (8-bit) characters, while a descriptor class name ending with 16 (e.g. TPtr16) manipulates 16-bit character strings.

There is also a set of 'neutral' classes that have no number in their name (e.g. TPtr). The neutral classes are implicitly wide, 16-bit strings. The neutral classes were defined for source compatibility purposes to ease a previous switch between narrow and wide builds of the Symbian platform and, although today the Symbian platform is always built with wide characters, it is recommended practice to use the neutral descriptor classes where the character width does not need to be stated explicitly. If nothing else, it makes your code more readable.

However, there are occasions when you are dealing with text of a specific character size. For example, you may be working with 8-bit text (e.g. an ASCII text file or Internet email) or simply using the RFile::Read() and RFile::Write() methods, which are specified for binary data and require explicit use of the 8-bit descriptor classes. When working with 16-bit text, you should indicate the fact explicitly by using the wide strings.

Descriptor Data: Text or Binary?

Descriptors are strings and can contain text data. However, they can also be used to manipulate binary data, because they don't rely on a NULL terminating character to determine their length, since it is instead built into the descriptor object. To work with binary data, the 8-bit descriptor classes should be used, with TInt8 and TUint8 to reference individual bytes.

The unification of binary and string-handling APIs makes their use easier for programmers – for example, the APIs to read from and write to a file all take an 8-bit descriptor, regardless of whether the file contains human-readable strings or binary data.

Symbian C++ Descriptor Classes

Figure 3.1 shows the inheritance hierarchy of the descriptor classes.

Descriptor class inheritance hierarchy showing modifiable and non-modifiable descriptors

Figure 3.1. Descriptor class inheritance hierarchy showing modifiable and non-modifiable descriptors

TDesC and TDes are 'abstract' descriptor base classes. They are not abstract in the strictest C++ meaning of the word, in that they do not consist solely of pure virtual functions (in fact, they have no virtual functions at all), but they are abstract in the sense that they are not meant to be instantiated, having only protected constructors.

Concrete descriptors come in two basic layouts: pointer descriptors, in which the descriptor holds a pointer to the location of a character string stored elsewhere; and buffer descriptors, where the data forms part of the descriptor.

Baes Classes: TDesC and TDes

Apart from the literal descriptors, all of the descriptor classes derive from the base class TDesC. The class determines the fundamental layout of every descriptor type: the first 4 bytes always hold the length of the data the descriptor currently contains. In fact, only 28 of the available 32 bits are used to hold the length of the descriptor data (the other four bits are reserved for another purpose, which we'll discuss shortly). This means that the maximum length of a descriptor is limited to 228 bytes (256 MB) which should still be more than sufficient for a single string.

Modifiable descriptor types all derive from the base class TDes, which is itself a subclass of TDesC. TDes has an additional member variable to store the maximum length of data allowed for the current memory allocated to the descriptor. Figure 3.2 shows the memory layouts of TDesC and TDes.

Memory layouts of TDesC and TDes

Figure 3.2. Memory layouts of TDesC and TDes

The Length() method of TDesC returns the length of the descriptor. This method is never overridden by its subclasses since it is equally valid for all types of descriptor. The MaxLength() method of TDes returns the maximum length the data value can occupy. Like the Length() method of TDesC, it is not overridden by derived classes.

Access to descriptor data is different depending on the implementation of the derived descriptor classes. The top 4 bits of the 4 bytes that store the length of the descriptor object are used to indicate the type of descriptor. When a descriptor operation needs the correct address in memory for the beginning of the descriptor data, it uses the Ptr() method of the base class, TDesC, which looks up the type of descriptor and returns the correct address for the beginning of the data.

TDesC::Ptr() uses a switch statement to identify the type of descriptor and thus establish where the data is located. This means that the TDesC base class has hard-coded knowledge of the memory layout of all its subclasses and you can't create your own descriptor class deriving from TDesC without having access to the base class code. The number of possible different types is limited to 24 (16) since there are only four bits in which to store the descriptor type. There are currently five memory layout types and it seems unlikely that Symbian will need to extend the range beyond the limit of 16.

Using the Length() and Ptr() methods, the TDesC base class can implement the operations required to manipulate a constant string object, such as data access, comparison and search (all of which are documented in full in the Symbian Developer Library found in your chosen SDK or online at developer.symbian.org). The derived classes all inherit these methods and, in consequence, all constant descriptor manipulation is implemented by TDesC, regardless of the type of the descriptor.

TDes defines a range of methods to manipulate modifiable string data, including those to append, fill and format the descriptor data. Again, all the manipulation code is implemented by TDes and inherited by the derived classes.

Pointer Descriptors: TPtrC and TPtr

The string data of a pointer descriptor is separate from the descriptor object and can be stored in ROM, on the heap or on the stack, as long as it is in memory that can be addressed by the process in which the pointer descriptor is declared. The memory that holds the data is not 'owned' by the descriptor and pointer descriptors are agnostic about where the memory they point to is actually stored.

In a non-modifiable pointer descriptor (TPtrC), the data can be accessed but not modified: that is, the data in the descriptor is constant. All the non-modifying operations defined in the TDesC base class are accessible to objects of type TPtrC.

The modifiable TPtr class can be used for access to and modification of a character string or binary data. All the modifiable and non-modifiable base-class operations of TDes and TDesC, respectively, may be performed on a TPtr.

Stack-based Buffer Descriptors: TBufC and TBuf

Stack-based buffer descriptors may be modifiable or non-modifiable. The string data forms part of the descriptor object, located after the length word in a non-modifiable TBufC descriptor and after the maximum-length word in a modifiable TBuf buffer descriptor.

These descriptors are useful for fixed-size, relatively small strings, since they are stack-based. They may be considered equivalent to const char[] or char[] in C, but with the benefit of overflow checking.

TBufC<n> is a thin template class which uses an integer value to determine the size of the data area allocated for the buffer descriptor object. The class is non-modifiable: because it derives from TDesC, it supplies no methods for modification of the descriptor data. However, the contents of a TBufC<n> descriptor are, indirectly, modifiable. We'll discuss how to do this shortly.

The TBuf<n> class for modifiable buffer data is a thin template class, the integer value determining the maximum allowed length of the buffer. It derives from TBufBase, which itself derives from TDes, thus inheriting the full range of descriptor operations in TDes and TDesC.

Non-modifiable Dynamic Descriptors: HBufC

Heap-based descriptors can be used for string data that cannot be placed on the stack because it is too big or because its size is not known at compile time. This descriptor class can be used where data allocated by malloc() would be used in C.

The HBufC8 and HBufC16 classes (and the neutral version HBufC, which is typedefd to HBufC16) export a number of static factory functions to create the buffer on the heap. These methods follow the two-phase construction model described in Section 3.5.4. There are no public constructors and all heap buffers must be constructed using one of the HBufC factory methods (or by using one of the Alloc() or AllocL() methods of the TDesC class, which spawn an HBufC copy of any existing descriptor).

As the 'C' suffixed to the class name indicates, these descriptors are not modifiable, although, in common with the stack-based non-modifiable buffer descriptor class, TBufC, the contents of the descriptor can be modified indirectly.

HBufC descriptors can be created dynamically at the size needed, but they are not automatically resized if additional memory is required. The buffer must have sufficient memory available for a modification operation, such as an append, to succeed or a USER 11 panic will occur.

Modifiable Dynamic Descriptors: RBuf

Class RBuf is another dynamic descriptor class. It behaves like HBufC in that the maximum length required can be specified dynamically, but RBuf descriptors hide the storage of the descriptor data from the developer.

Upon instantiation, which is usually on the stack, an RBuf object can allocate its own buffer (on the heap) or take ownership of preallocated heap memory or a pre-existing heap descriptor. The behavior is transparent and there is no need to know whether a specific RBuf object is represented internally as a type 2 or a type 4 pointer descriptor.

RBuf is derived from TDes, soan RBuf object can easily be modified and passed to any function where a TDesC or TDes parameter is specified. Calling the descriptor operations is straightforward because they correspond to the usual base-class methods of TDes and TDesC16.

The class is not named HBuf because, unlike HBufC, objects of this type are not themselves directly created on the heap. It is instead an R class, because it manages a heap-based resource and is responsible for freeing the memory at clean-up time. RBuf is a simpler class to use than HBufC if you need a dynamically allocated buffer to hold data that changes frequently. HBufC is preferable when a dynamically allocated descriptor is needed to hold data that doesn't change, that is, if no modifiable access to the data is required.

Literal Descriptors

Literal descriptors are somewhat different from the other descriptor types. They are equivalent to static const char[] in C and can be built into program binaries in ROM because they are constant. There is a set of macros defined in e32def.h which can be used to define Symbian platform literals of two types: _LIT and _L. Neither literal type derives from the TDesC base class, however they are designed to make conversion between literal and non-literal descriptors easy.

_LIT macro

The _LIT macro is preferred for Symbian platform literals, since it is more efficient. It must be declared in advance of its use. For example, the Symbian platform defines null descriptor literals to represent a blank string. Three variants of the null descriptor are defined in e32std.h, as follows:

// Build independent:
_LIT(KNullDesC," ");
// 8-bit for narrow strings:
_LIT8(KNullDesC8," ");
// 16-bit for UTF-16 strings:
_LIT16(KNullDesC16," ");

KNullDesC and its cohorts can be used as a constant descriptor.

When creating your own literals, try not to declare them in a header file, unless they need to be shared. This is to avoid unnecessary bloat: all CPP files that include the header generate a copy of the literal regardless of whether it is used.

_L macro

Use of the _L macro is now deprecated in production code, though it may still be used in test code (where memory use is less critical). The advantage of using _L (or the explicit forms _L8 and _L16) is that it can be used in place of a TPtrC without having to declare it separately from where it is used:

User::Panic(_L("telephony.dll"), KErrNotSupported);

In the example above, the string ("telephony.dll") is built into the program binary as a basic, NULL-terminated string. When the code executes, each instance of _L results in the construction of a temporary TPtrC, which requires setting the pointer, the length and the descriptor type; this is an overhead in terms of inline constructor code which may bloat binaries where many string literals are used. This is why the _LIT macro is considered preferable.

Descriptor Indigestion?

Table 3.2 summarizes the information this chapter has provided about the Symbian C++ descriptor types, and gives the C string equivalent, where appropriate.

Descriptors as Function Parameters

As I described earlier, the TDesC and TDes descriptor base classes provide and implement the APIs for all descriptor operations. This means that they can be used as function arguments and return types, allowing descriptors to be passed around in code without forcing a dependency on a particular type. An API client shouldn't be constrained to using a TBuf just because a particular function requires it, and function providers remain agnostic to the type of descriptor passed to them. Unless a function takes or returns ownership, it shouldn't even need to specify whether a descriptor is stack- or heap-based. When defining functions, the abstract base classes should always be used as parameters or return values.

For efficiency, descriptor parameters should be passed by reference, either as const TDesC&, for constant descriptors, or as TDes&, when modifiable. You should not define functions that take descriptor parameters by value (which is what happens if you omit the '&' symbol after the class name). This is because the TDes and TDesC base classes do not contain any string data. If you pass them as parameters by value, you statically bind the base class descriptor types at compile time. The code compiles but no valid data is associated with the descriptor parameter because the base class objects do not have storage for string data. For example:

Table 3.2. Symbian C++ descriptor classes

Name[a]

Modifiable

Approximate C string equivalent

Type

Notes

TDesC

No

n/a

Not instantiable

Base class for all descriptors (except literals)

TDes

Yes

n/a

Not instantiable

Base class for all modifiable descriptors

TPtrC

No

const char* (doesn't own the data)

Pointer

Data stored separately from the descriptor object, which is agnostic to its location

TPtr

Yes

char* (doesn't own the data)

Pointer

Data stored separately from the descriptor object, which is agnostic to its location

TBufC

Indirectly

const char []

Stack buffer

Thin template with size fixed at compile time

TBuf

Yes

char []

Stack buffer

Thin template with size fixed at compile time

HBufC

Indirectly

const char* (owns the data)

Heap buffer

Used for dynamic data storage where modification is infrequent

RBuf

Yes

char* (owns the data)

Heap buffer

Used for modifiable dynamic data storage

TLitC

No

Static const char []

Literal

Built into ROM

[a] Note that use of C-suffixed classes on Symbian does not enforce them to be const in C++; you still need to specify the const modifier, for example, const TDesC&.

void Foo(const TDesC aString); // error, should be const TDesC&
_LIT(KHello, "Hello");
TBuf<5> data(KHello());
Foo(data);
void Foo(const TDesC aString)
  {
  TBufC<10> buffer(aString);  // buffer contains random data
  ...
  }

As an example of how to use descriptor parameters, consider the system class RFile, which defines straightforward file read and write methods as follows:

IMPORT_C TInt Write(const TDesC8& aDes);
IMPORT_C TInt Read(TDes8& aDes) const;

For both methods, the input descriptor is explicitly 8 bit to allow for both string and binary data to be used within a file. The descriptor to write to the file is a constant reference to a non-modifiable descriptor, while to read from the file requires a modifiable descriptor. The maximum length of the modifiable descriptor determines how much file data can be read into it, so the file server doesn't need to be passed a separate parameter to describe the length of the available space. The file server fills the descriptor unless the content of the file is shorter than the maximum length, in which case it writes what is available into the descriptor. The resultant length of the descriptor thus reflects the amount of data written into it, so the caller does not need to pass a separate parameter to determine the length of data returned.

When calling a function which receives a modifiable descriptor, you don't need to know whether your descriptor has sufficient memory allocated to it, since the descriptor methods themselves perform bounds checking and panic if the operation would overflow the descriptor. Of course, you may not always want a panic, which is somewhat terminal. Check the documentation for any API you use to determine what happens when you pass a descriptor to receive data, in case what you pass is not large enough.

Using the Descriptor APIs

The descriptor API methods supplied by TDesC and TDes are fully documented in the Symbian Developer Library and in the Symbian Cookbook article found on the Symbian Developer wiki at developer.symbian.org/wiki/index.php/Descriptors_Cookbook.

Remember however that objects of type TDesC and TDes cannot be instantiated directly because their constructors are protected. It is the derived descriptor types that are actually instantiated and used.

Which Descriptor Class Should I Use?

Figure 3.3 summarizes the factors to bear in mind when deciding on the type of descriptor to use. If you need a constant descriptor then your choice is between a literal, a TPtrC, a TBufC or an HBufC. If the descriptor must be modifiable, the choice is between a TPtr, a Tbuf and an RBuf.

Choosing a descriptor class

Figure 3.3. Choosing a descriptor class

To conclude this section on descriptors, the following discussion concentrates on some of the trickier areas of descriptor manipulation.

The Difference Between Length() and Size()

The Size() method returns the size of the descriptor in bytes. The Length() method returns the number of characters it contains. For 8-bit descriptors, the length is the same as the size, but when the descriptor has 16-bit-wide characters, Size() returns a value double that of Length() for neutral and explicitly wide descriptors.

MaxLength() and Length Modification Methods

The MaxLength() method of TDes returns the maximum length of a modifiable descriptor value. Like the Length() method of TDesC, it is not overridden by the derived classes. The SetMax() method doesn't change the maximum length of the descriptor (as one might expect); instead, it sets the current length of the descriptor to the maximum length allowed.

The SetLength() method can be used to adjust the descriptor length to any value between zero and its maximum length. The Zero() method sets the descriptor's length to zero.

Des() Method

The stack- and heap-based constant buffer descriptor classes, TBufC and HBufC, provide a method that returns a modifiable pointer descriptor that references the data represented by the buffer. So, while the content of a non-modifiable buffer descriptor cannot be altered directly, except by using the assignment operator to replace the data completely, it is possible to modify the data indirectly by calling Des() and then operating on the data via the pointer it returns. When the data is modified via the return value of Des(), the length members of both the pointer descriptor and the constant buffer descriptor are updated if necessary.

For TBufC:

_LIT8(KPalindrome, "Satan, oscillate my metallic sonatas");
TBufC8<40> buf(KPalindrome); // Constructed from literal descriptor
TPtr8 ptr(buf.Des()); // data is the string in buf, max length = 40
// Use ptr to replace contents of buf
ptr = (TText8*)"Do Geese see God?";
ASSERT(ptr.Length() == buf.Length());
_LIT8(KPalindrome2,
      "Are we not drawn onward, we few, drawn onward to new era?");
ptr = KPalindrome2; // Panic! KPalindrome2 exceeds max buf length

For HBufC:

// Allocate a heap descriptor of max length 20
HBufC* heapBuf = HBufC::NewLC(20);
TInt length = heapBuf->Length(); // Current length = 0
TPtr ptr(heapBuf->Des()); // Modification of the heap descriptor
_LIT(KPalindrome, "Do Geese see God?");
TBufC<20> stackBuf(KPalindrome);
ptr = stackBuf;        // Copies stackBuf contents into heapBuf
length = heapBuf->Length();    // new heapBuf length = 17

There is an overhead to calling Des() to create a TPtr object around a buffer descriptor's data, and it should be avoided when not strictly necessary. For example, if a modifiable dynamic descriptor is needed, it is better to use class RBuf. Another common inefficiency when working with HBufC is to use Des() to return a TDesC to pass into a function. The HBufC class derives from TDesC and an HBufC* pointer can simply be dereferenced when a reference to a non-modifiable descriptor (TDesC&) is required:

void Function(const TDesC& aText)
  { // Some code }
_LIT(KText, "Hello");
HBufC* text = KText().AllocL();  // Allocate HBufC from descriptor
Function(*text);                 // Prefer this way ...
Function(text->Des());           // ... instead of this

Overview

The discussion in this section should give you a good idea about the requirements at which you should aim when dealing with standard C/C++ code. When it comes to string-handling functions, you should be aware of code that can potentially introduce overflow and similar problems due to the lack of proper boundary checking. Examples of these functions are strcmp(), strcat(), and memcpy(). Classes that handle these issues transparently should be always preferred, such as std::string or Qt's QString.

The Symbian platform is unforgiving when it comes to resource leaking as well. A good way to ensure your code is sufficiently robust is extensive use of appropriate testing tools[59] in the original platform before attempting the port. This can improve the reliability of your code and thus make the port easier to work with on the Symbian platform.

Error Handling and Memory Management

Leaves and Exceptions

When Symbian OS was designed in the mid-1990s, the compiler and tool chain available did not support C++ exceptions, since they had only recently become part of the C++ standard. An alternative to conventional, but rather awkward, error-checking of function return values was needed. This is why 'leaves'[60] were developed – as a simple, effective and lightweight exception-handling mechanism. You'll encounter lots of 'leaving code' when working on the Symbian platform. You need to know how to recognize code that leaves and how to use it efficiently and safely, since it's possible to leak memory inadvertently in the event of a leave.

What Is a Leave?

A leave is used to throw an error result back up the call stack to a point at which it can be caught and handled. In Symbian terminology, the catch block is known as a trap handler and is created by using a TRAP() macro. Like a standard C++ exception, the stack is unwound by the leave, and, as long as the leave is trapped, it does not terminate the thread. A leave propagates a single integer value, a 'leave code', to the trap handler and it represents the error that has occurred. I talk a little more about the implementation of leaves after I discuss the cleanup stack in Section 3.5.2.

A typical leaving function is one that performs an operation that is not guaranteed to succeed, such as a memory allocation that may fail if there is insufficient memory available (low-memory conditions are a reasonably common occurrence on a mobile device, although memory is not as limited these days as it was when Symbian OS was originally designed). When testing any code that may leave, you should test both paths of execution, that is, for a successful call and for a call that leaves as a result of each of the exceptional conditions you expect to handle (low-memory conditions, failure to write to a file, etc.). The Symbian platform provides macros, such as __UHEAP_SETFAIL to simulate out-of-memory (OOM) conditions, which are described further in the Symbian Developer Library.

How to Identify a Leaving Function

As I mentioned in Section 3.2.2, if a function may leave, its name must be suffixed with 'L'. You must use this rule: of all Symbian C++ naming guidelines it is probably the most important. If you don't name a leaving function accordingly, callers of your code may not defend themselves against a leave and may potentially leak memory.

The L suffix is not checked during compilation, so occasionally you may forget to append it to a function name or you may later add leaving code to a previously non-leaving function. Fortunately, Carbide.c++ provides a useful plug-in called LeaveScan, which you should run regularly against your source code. It checks that all functions that have the potential to leave are named according to the naming guidelines.

Since leaving functions have a leave code, they do not also need to return an error value. Every error that occurs in a leaving function should be passed out as a leave; if the function does not leave it is deemed to have succeeded and returns normally. Generally, leaving functions should just return void unless they use the return value for a pointer or reference to a resource allocated by the function.

Some examples of leaving function declarations are as follows:

void InitializeL();
static CTestClass* NewL();
RClangerHandle& CloneHandleL();

What Causes a Leave?

A function may leave if it:

  • Calls one of the system functions that initiate a leave, such as User::LeaveIfError() or User::Leave(). User::Leave-IfError() tests an integer parameter passed into it and causes a leave (using the integer value as a leave code) if the value is less than zero, for example, one of the KErrXXX error constants defined in e32err.h.

    User::LeaveIfError() is useful for turning a traditional C-style function that returns a standard error result into one that leaves with that value. User::Leave() doesn't carry out any value checking and simply leaves with the integer value passed into it as a leave code.

  • Uses the overloaded form of operator new which takes ELeave as a parameter and leaves if the memory allocation fails. This is discussed shortly.

  • Calls other functions that may leave because the code uses one of the previous two approaches or calls code that does so. When you call a function with the potential to leave, your code also then has the potential to leave, unless you catch the leave by surrounding any calls to leaving code with the TRAP() macro, which we discuss later. The following example shows a function with the potential to leave in three locations:

/*static*/ CClanger* CClanger::NewL()
  {
  CClanger* self = new(ELeave) CClanger; // Leaves if OOM
  CleanupStack::PushL(self);             // May leave
  self->ConstructL();                 // May leave
  CleanupStack::Pop(self);
  return self;
  }

Note that the code uses the Symbian cleanup stack, which makes heap-based objects leave-safe. It is discussed in more detail in Section 3.5.2. The code is also an example of the two-phase construction idiom discussed in Section 3.5.4.

Symbian Leaves and C++ Exceptions

Although standard C++ exception handling could not be used in early versions of Symbian OS, support for C++ exception handling was introduced in Symbian OS v9.1. From Symbian OS v9.1, the operating system defines a flag called __LEAVE_EQUALS_THROW__. This means that User::Leave() and TRAP are implemented in terms of C++ exceptions. When User::Leave() is called, an exception of type XLeaveException is thrown (this is a simple type that wraps a single TInt, which is the leave code that was passed to User::Leave()).

Whenever a C++ exception is thrown, memory must be allocated for the associated exception object. Nested exceptions occur when an exception is thrown while another is already being handled. For the second exception, a separate exception object is required, and if a further exception occurs while handling that one, another exception is required, and so on. If the number of levels of nesting of exception was unbounded, so would be the number of allocations of exception objects. This is unacceptable on the Symbian platform, given that memory resources are finite and exceptions are likely to occur specifically because of out-of-memory conditions. Therefore, nested exceptions are not supported by the Symbian platform, and only a single exception can be thrown at any one time. On hardware builds, if another exception occurs while the first is being handled, abort() is called to terminate the thread.[61]

By restricting the memory requirement to a single XLeaveException exception object, memory can be pre-allocated to ensure that the object can always be created. In fact, when an exception occurs, if there is enough space on the heap, the exception object is allocated there. If not, the XLeaveException object is created using pre-allocated space on the stack.

Legacy Implementation of User::Leave()

If you are working on a version of Symbian OS pre-v9.1, using a build of the Symbian platform where __LEAVE_EQUALS_THROW__ is not defined, or on a platform where exception support is not available, then the implementation of User::Leave() does not use exceptions. When a leave occurs, objects on the stack are simply de-allocated and their destructors are not called because the leaving mechanism does not emulate standard C++ throw semantics.[62] This legacy implementation is why classes intended to be used on the stack (T classes) are not expected to define an explicit destructor nor own resources that need to be cleaned up.

Heap Allocation Using new(ELeave)

Let's take a closer look at the use of new(ELeave) to allocate an object on the heap, which leaves if the memory is unavailable. If the allocation occurs successfully, no leave occurs and the returned pointer may be used without a further test that the allocation was successful as would otherwise be required. We have already seen it used in the code for CClanger::NewL(), shown previously:

/*static*/ CClanger* CClanger::NewL()
  {
  CClanger* self = new(ELeave) CClanger; // Leaves if OOM
  // ...
  }

The above code is preferable to the following code, which requires an additional check to verify that the clanger pointer has been initialized:

/*static*/ CClanger* CClanger::NewL()
  {
  CClanger* self = new CClanger;
  if (self)
    {
    CleanupStack::PushL(self);
    self->ConstructL();
    CleanupStack::Pop(self);
    }
  else
    User::Leave(KErrNoMemory);
return self;
  }

The new(ELeave) expression allocates the necessary memory by means of the new operator, passing in the size of the memory required. It then initializes the object accordingly.

The Symbian platform overloads the global operator new to take a TLeave parameter in addition to the size parameter, but the TLeave parameter is only used to differentiate this form of operator new from the non-leaving version. The overload calls a heap allocation function that leaves if there is insufficient heap memory available:

// From e32const.h
enum TLeave {ELeave};
// From e32std.h
inline TAny* operator new(TUint aSize, TLeave);
// e32std.inl
inline TAny* operator new(TUint aSize, TLeave)
                           // Call an allocation method that
                           // leaves if memory available &lt; aSize
  { return User::AllocL(aSize); }

Note that the standard C++ operator new is as follows:

void* operator new(std::size_t size) throw(std::bad_alloc);

The exception specifier shows that it throws a std::bad_alloc exception if there is insufficient memory for an allocation. Since the Symbian platform does not do this by default, it makes it impossible to mix calls that rely on the behavior of the Symbian operator new with calls to the standard C++ operator new in the same link unit. This is because the linker has to pick a single definition of the symbol to resolve against. If it picks the Symbian version, it breaks code that relies on the behavior of the standard C++ operator new and, if it picks the C++ version, it breaks Symbian code. You must separate code that uses the Symbian operator new into different link units from code which relies on more standard semantics.

Trapping a Leave Using TRAP and TRAPD

The Symbian platform provides two macros, TRAP and TRAPD, to trap a leave. The macros differ only in that TRAPD declares a variable in which the leave error code is returned, while the program code itself must declare a variable before calling TRAP. Thus the following statement:

TRAPD(result, MayLeaveL());
if (KErrNone != result)
  { // Handle error }

is equivalent to:

TInt result;
TRAP(result, MayLeaveL());
if (KErrNone != result)
  { // Handle error }

If a leave occurs inside the MayLeaveL() function, which is executed inside the harness, the program control returns immediately to the trap harness macro. The variable result contains the error code associated with the leave (i.e. that passed as a parameter to the User::Leave() system function) or is KErrNone if no leave occurred.

Any functions called by MayLeaveL() are executed within the trap harness, and so on recursively, and any leave that occurs during the execution of MayLeaveL() is trapped, returning the error code into result. Alternatively, TRAP macros can be nested to catch and handle leaves at different levels of the code, where they can best be dealt with. I'll discuss the run-time cost of using trap harnesses shortly, but if you find yourself using TRAP macros several times in one function, or nesting a series of them, you may want to consider whether you can omit trapping all the leaving functions except at the top level or change the layout of the code. Also note that you must not call a function with an LC suffix from inside a trap harness. The following code would leave an object on the cleanup stack if no failure arises, causing an E32USER-CBase 71:

TRAPD(error, NewLC());

For example, there may be a good reason why the following function must not leave but needs to call a number of functions which may leave. At first sight, it might seem straightforward enough simply to put each call in a trap harness.

TInt MyNonLeavingFunction()
  {
  TRAPD(result, FunctionMayLeaveL());
  if (KErrNone == result)
    TRAP(result, AnotherFunctionWhichMayLeaveL());
  if (KErrNone == result)
    {
    TRAP(result, PotentialLeaverL());
    // Handle any error if necessary
    }
  return (result);
  }

Prior to Symbian OS v9.1, use of the TRAP macro had an impact in terms of executable size and execution speed, and over-use of the trap handler was considered to be an expensive way of managing leaves. Since Symbian OS v9.1, the run-time overhead is much reduced, but you should still aim for simple code, rather than nesting multiple trap handlers 'just because you can'. It often makes sense to allow leaves to propagate to higher-level code or to combine leaving functions, as follows:

TInt MyNonLeavingFunction()
  {
  TRAPD(error,
    FunctionMayLeaveL();
    AnotherFunctionWhichMayLeaveL();
    PotentialLeaverL();
  );
  return error;
  }

Every program (even a simple 'hello world' application) must have at least one TRAP, if only at the topmost level, to catch any leaves that are not trapped elsewhere. If you are a GUI application developer you don't need to worry about this, though, because the Symbian application framework provides a TRAP.

Cleanup Stack

Consider the following example of a call to a leaving function:

void UnsafeFunctionL()
  {
  // Allocates test on the heap
  CTestClass* test = CTestClass::NewL();
  test->FunctionMayLeaveL();  // Unsafe endash a potential memory leak!
  delete test;
  }

Memory is allocated on the heap in the call to CTestClass::NewL() but the subsequent call to UnsafeFunctionL() may leave. Should a leave occur, the memory pointed to by test is not deallocated (it is said to be 'orphaned') and the function has the potential to leak memory.

The Symbian cleanup stack is designed to address this problem and make heap-based objects referenced by local pointers safe against being orphaned in the event of a leave. This is more simply described as making them 'leave-safe'.

Which Class Types Risk Becoming Orphaned?

  • T class objects are usually stack-based and stack objects cannot be orphaned. However, if you create a T class object on the heap, the pointer that references it must be leave-safe.

  • C class objects are always created on the heap, so they are never leave-safe unless they are accessible for destruction after a leave occurs, for example, if they are stored as the member variables of an object that can be accessed after the leave.

  • R class objects are generally not leave-safe since the resources they own must be freed in the event of a leave (through a call to the appropriate Close() or Release() function). If there is no accessible object to make this call after a leave, the resource is orphaned.

Using the Cleanup Stack

The cleanup stack is accessed through the static member functions of class CleanupStack, defined in e32base.h:

class CleanupStack
  {
public:
  IMPORT_C static void PushL(TAny* aPtr);
  IMPORT_C static void PushL(CBase* aPtr);
  IMPORT_C static void PushL(TCleanupItem anItem);
  IMPORT_C static void Pop();
  IMPORT_C static void Pop(TInt aCount);
  IMPORT_C static void PopAndDestroy();
  IMPORT_C static void PopAndDestroy(TInt aCount);
  IMPORT_C static void Check(TAny* aExpectedItem);
  inline static void Pop(TAny* aExpectedItem);
  inline static void Pop(TInt aCount, TAny* aLastExpectedItem);
  inline static void PopAndDestroy(TAny* aExpectedItem);
  inline static void PopAndDestroy(TInt aCount,
                                   TAny* aLastExpectedItem);
  };

Objects that are not otherwise leave-safe should be placed on the cleanup stack before calling code that may leave. This ensures they are destroyed correctly. If a leave occurs, the cleanup stack manages the deallocation of all objects that have been placed upon it (within the current enclosing TRAP).

The following code illustrates a leave-safe version of the Unsafe-FunctionL() example shown previously:

void SafeFunctionL()
  {
  CTestClass* test = CTestClass::NewL();
  // Push onto the cleanup stack before calling leaving function
  CleanupStack::PushL(test);
  test->FunctionMayLeaveL();
  // Pop from cleanup stack
  CleanupStack::Pop(test);
  delete test;
  }

If FunctionMayLeaveL() leaves, the test object is destroyed by the cleanup stack as part of leave processing, which we'll discuss shortly. If FunctionMayLeaveL() does not leave, the code continues, pops the pointer from the cleanup stack and calls delete on it. (This code could equally well be replaced by a single call to Cleanup-Stack::PopAndDestroy(test) which performs both the pop and a call to the destructor in one step.)

Why Is PushL() a Leaving Function?

PushL() is a leaving function because it may allocate memory to store the cleanup pointer and this allocation may fail in low-memory situations. However, you don't need to worry that the object you are pushing onto the cleanup stack will be orphaned if a leave does occur. The cleanup stack is created with at least one spare slot. When you call PushL(), the pointer you pass is first added to the vacant slot and, if there are no more vacant slots available, the cleanup stack code then attempts to allocate a slot for the next PushL() operation. If that allocation fails, a leave occurs, but your pointer has already been stored safely and the object it refers to is safely cleaned up.

In fact, the cleanup stack code is more efficient than my simplistic explanation; it allocates four slots at a time, by default. In addition, it doesn't release slots when you Pop() objects out of them, so a PushL() frequently does not make any allocation. This can be useful in circumstances where you acquire more than one pointer that is not leave-safe to push onto the cleanup stack. You can use this knowledge to expand the cleanup stack to contain at least the number of slots you need, then create the objects and safely push them onto the vacant slots.

Popping Items off the Cleanup Stack

Objects are pushed onto and popped off the cleanup stack in strict order: a series of Pop() calls must occur in the reverse order of the PushL() calls. The following example illustrates this:

void ContrivedExampleL()
  {
  // Note that each object is pushed onto the cleanup stack
  // immediately it is allocated, in case the succeeding
  // allocation leaves.
  CSiamese* sealPoint = NewL(ESeal);
  CleanupStack::PushL(sealPoint);
  CSiamese* chocolatePoint = NewL(EChocolate);
  CleanupStack::PushL(chocolatePoint);
  CSiamese* violetPoint = NewL(EViolet);
  CleanupStack::PushL(violetPoint);
  CSiamese* bluePoint = NewL(EBlue);
  CleanupStack::PushL(bluePoint);
  sealPoint->CatchMouseL();
  // Other leaving function calls,
  // some of which use the cleanup stack
  ...
 // All calls have successfully completed:
  CleanupStack::PopAndDestroy(bluePoint);
  CleanupStack::PopAndDestroy(violetPoint);
  CleanupStack::PopAndDestroy(chocolatePoint);
  CleanupStack::PopAndDestroy(sealPoint);
  }

It is possible to Pop() or PopAndDestroy() one or more objects without naming them, but it's a good idea to name the objects as they are popped off. This makes your code clearer and helps you guard against a 'cleanup stack imbalance' bug, which can be difficult to find because it causes unexpected panics in code quite unrelated to where the error has occurred. If you explicitly pass the name of the pointer when you pop it off the cleanup stack, in debug builds a check is made to confirm that the pointer being popped off is indeed the one you expect it to be. Because it only occurs in debug builds, it has no impact on the speed of released code, although, if you do want to perform checking in a release build, you can use the CleanupStack::Check() function.

There are alternative overloads to Pop() and PopAndDestroy(). For the previous example, the preferred way to destroy the four objects altogether would be this:

// Destroy all, naming the last object
CleanupStack::PopAndDestroy(4, sealPoint);

If an object is pushed onto the cleanup stack in a function and remains on it when the function returns, we introduce a further naming convention: the function name must have the additional suffix 'C'. This indicates to the caller that, when the function returns successfully, the cleanup stack has additional objects upon it. This idiom is typically used by CBase-derived classes that define static functions to instantiate an instance of the class and leave it on the cleanup stack. The C suffix indicates to the caller of a function that it is not necessary to push any objects allocated by the function onto the cleanup stack.

The following code creates an object of type CSiamese and leaves it on the cleanup stack. This function is useful because the caller can instantiate CSiamese and immediately call a leaving function, without needing to push the allocated object back onto the cleanup stack:

CSiamese* CSiamese::NewLC(TPointColor aPointColour)
  {
  CSiamese* self = new(ELeave) CSiamese(aPointColour);
  CleanupStack::PushL(self);  // Make this leave-safe...
  self->ConstructL();
  return (self) // Leave self on the cleanup stack for
                // the caller to Pop()
  }

Member Variables and the Cleanup Stack

While heap variables referred to only by local variables may be orphaned, member variables do not suffer a similar fate (unless their destructor neglects to delete them when it is called at some later point). Thus the following code is safe:

void CTestClass::SafeFunctionL()
  {
  iMember = CClangerClass::NewL(); // Allocates a heap member
  FunctionMayLeaveL();             // Safe
  }

Even if a leave occurs in FunctionMayLeaveL(), iMember is stored safely as a pointer member variable, to be deleted later through the class destructor. If iMember was pushed onto the cleanup stack, it would be destroyed if a leave occurred, but later the CTestClass destructor would also attempt to destroy it. This could lead to a USER-42 double-deletion panic. It should never be possible for an object to be cleaned up more than once, so you should never push class member variables (objects prefixed by 'i', if the Symbian C++ naming convention is followed) onto the cleanup stack. That's where the naming convention comes in useful – if you ever find yourself writing or reviewing code that uses PushL(iSomething), your inner alarm bell should ring.

Using the Cleanup Stack with Non-CBase Classes

Up to this point, we've only really considered one of the overloads of the CleanupStack::PushL() function, PushL(CBase* aPtr), which takes a pointer to a CBase-derived object. When Cleanup-Stack::PopAndDestroy() is called for that object, or if leave processing occurs, the object is destroyed by invoking delete on the pointer, which calls the virtual destructor of the CBase-derived object.[63]

If an object does not derive from CBase, there are two alternative overloads of PushL() that may be used.

CleanupStack::PushL(TAny*)

The simplest overload, CleanupStack::PushL(TAny*), can be used to push any item onto the cleanup stack. Using this overload means that if the cleanup stack later comes to destroy the object, its heap memory is simply deallocated (by invoking User::Free()) and no destructors are called on it. For simple heap-based objects, such as T classes with no destructors, this is fine. But otherwise you may find the presence of this overload leads to unexpected behavior. If a class does not inherit from CBase, directly or indirectly, and you push a pointer to a heap-based object of the type onto the cleanup stack, then no destructors are called if a leave occurs. The object may not be cleaned up as you expect.

A common error is to define a C class and forget to derive it from CBase, causing the incorrect overload to be used. A memory leak occurs if the cleanup stack destroys it, as the destructor is not called and any resources owned are not freed.

So what's the use of the CleanupStack::PushL(TAny*) overload? As I've already implied, it is used when you push onto the cleanup stack a pointer to a heap-based object which does not have a destructor, for example, a T class object or a struct which has been allocated on the heap. Recall that, until Symbian OS v9.1, T classes were forbidden from having destructors, to make them leave-safe on the stack. They thus they had no requirement for cleanup beyond deallocation of the heap memory they occupy. You can give a T class a destructor these days and it is still leave-safe on the stack. But, as the previous discussion should have warned you, you can't expect it to be leave-safe on the heap if you use the cleanup stack and require a destructor call for correct cleanup.

When you push heap descriptor objects (of class HBufC, described in Chapters 5 and 6) onto the cleanup stack, the CleanupStack::PushL(TAny*) overload is used too. This is because HBufC objects are, in effect, heap-based T class objects. Objects of type HBufC require no destructor invocation, because they contain only plain, built-in-type data. The only cleanup necessary is that required to free the heap cells for the descriptor object.

CleanupStack::PushL(TCleanupItem)

CleanupStack::PushL(TCleanupItem) takes an object of type TCleanupItem, which allows for customized cleanup routines. A TCleanupItem object encapsulates a pointer to the object to be stored on the cleanup stack and a pointer to a function that provides cleanup for that object. The cleanup function can be a local function or a static method of a class. Leave processing, or a call to PopAndDestroy(), removes the object from the cleanup stack and calls the cleanup function provided.

You may be wondering how the cleanup stack discriminates between a TCleanupItem and objects pushed onto the cleanup stack using one of the other PushL() overloads. The answer is that it doesn't – all objects stored on the cleanup stack are actually of type TCleanupItem. The PushL() overloads that take CBase or TAny pointers construct a TCleanupItem implicitly and store it on the cleanup stack.

As I've already described, the cleanup function for the CBase* overload deletes the CBase-derived object through its virtual destructor. For the TAny* overload, the cleanup function calls User::Free(), which simply frees the allocated memory:

void CCleanup::PushL(TAny *aPtr)
  {
  PushL(TCleanupItem(User::Free, aPtr));
  }

The Symbian platform also provides three utility functions, each of which generates an object of type TCleanupItem and pushes it onto the cleanup stack. The functions use templates so the cleanup methods do not have to be static. These methods are particularly useful when making R classes leave-safe, since they call cleanup methods Release(), Delete() and Close(). For further information about Cleanup-ReleasePushL(), CleanupDeletePushL() and CleanupClosePushL(), please see the Symbian Developer Library documentation or the Fundamentals of Symbian C++ wiki pages on developer.symbian.org.

Leaves and the Cleanup Stack

We can now tie together the implementation of leaves (in terms of standard C++ exceptions) and the cleanup stack.[64] When a leave occurs:

  1. The cleanup stack 'unwinds' and each item on it is cleaned up (e.g. destructors are called for C classes, Close() is called on any R class object made leave-safe by a call to CleanupClosePushL(), etc.).

  2. An XLeaveException is created and thrown to represent the leave, resulting in the unwinding of the stack.

  3. The exception is caught by the trap handler.

During Step 1, the cleanup stack takes responsibility for cleaning up, for example, heap-based C class objects and calling Close() on R class objects. At this point, no exception has been raised for the leave and, in the cleanup initiated by the cleanup stack, it is acceptable to leave or otherwise throw an exception, because it won't be nested.

During Step 2, the normal stack unwinds and the destructors of stack-based objects are called. If a leave were to occur in one of those destructors, it would generate an exception that would be nested within the XLeaveException exception thrown for the original leave. On mobile device hardware, this would cause the thread to be terminated.

At Step 3, exception handling is complete. Another leave or exception may occur without being nested in the exception thrown in Step 2.

If you write a destructor that leaves, it does not terminate the thread if the destructor is called by the cleanup stack (for example, for C class objects). However, if a destructor is called as the normal stack unwinds, you must not allow leaves or exceptions, because they have the potential to cause a nested exception that terminates the thread when running on target hardware.

On the Symbian platform, it is actually quite rare for any stack-based object to have a destructor since, until Symbian OS v9.1, T classes could not guarantee that their destructor would be called (leaves did not unwind the stack before Symbian OS v9.1). Likewise, R classes typically perform cleanup in their Close() method. However, if you do have a stack-based object with a destructor, or use an auto_ptr style of programming, you must be aware of the limitation and avoid code that leaves or otherwise throws exceptions. For example, if a stack-based auto_ptr destructor cleans up a heap-based object, the destructor code it calls must not leave or throw an exception either.

Of course, it is questionable whether it is ever sensible to leave in a destructor, since this suggests that the cleanup code may not completely execute, with the potential to cause memory or resource leaks. Instead of leaving in a destructor, it's preferable to have a separate leaving function, which can be called before destruction, to perform actions that may fail and provide the caller an opportunity to deal with the problem before finally destroying the object. Typically these would be functions such as CommitL() or FreeResourceL() and they effectively form a 'two-phase destruction' pattern, analogous with two-phase construction. If you use the cleanup stack to destroy an object which requires two-phase destruction, you must ensure that you accommodate the separate cleanup function in the event of a leave.

Two-Phase Construction

Let's examine what happens when you create an object on the heap, by considering the following line of code that allocates an object of type CExample, and sets the value of the foo pointer accordingly:

CExample* foo = new CExample;

The code calls the new operator, which allocates a CExample object on the heap if there is sufficient memory available. Having done so, it calls the CExample constructor to initialize the object. You can use the cleanup stack to ensure that the heap object is correctly cleaned up in the event of a leave. But it is not possible to do so inside the new operator between allocation of the object and invocation of its constructor.

If the CExample constructor itself leaves, a C++ exception (XLeave-Exception) is thrown, as described in Section 3.5.1. The semantics of C++ exception handling ensure that the memory allocated for the CEx-ample object is cleaned up, so no memory leak occurs. However, since the exception occurs in the constructor, the destructor of CExample is not itself called. So any memory the constructor may have allocated before the leave occurred is orphaned, unless it is made leave safe.

Furthermore, consider the following:

CExample* foo = new(ELeave) CExample;

If a CExample object is successfully allocated but its constructor leaves, throwing an exception, because the Symbian platform doesn't define a placement delete to pair with, there is no compiler-generated cleanup code. So, when the exception occurs, there is no de-allocation code to free the CExample object back to the heap. If the object was instead allocated by a call to the non-leaving new, the same object is cleaned up. Take a look at the following code for illustration:

class CWidget : public CBase
  {
public:
CWidget() { throw std::exception(); }
  ~CWidget() {}
  ...
  };
// Calling code
LOCAL_C void MainL()
  {
  CWidget* widgetSafe = new CWidget;         // Memory cleaned up
  CWidget* widgetLeak = new(ELeave) CWidget; // Memory orphaned
  }

The memory allocated for widgetSafe is freed back to the heap when CWidget() leaves. In contrast, widgetLeak is not cleaned up when CWidget() leaves.

This is very important to keep in mind when porting code which uses smart pointers:

boost::scoped_ptr&lt;CWidget> ptr(new CWidget);           // OK
boost::scoped_ptr&lt;CWidget> ptr(new(ELeave) CWidget);   // Leaks

This example shows an artificial case, where an exception was thrown deliberately. But to initialize an object of a C class, it is quite common to need to write code that throws, say to allocate memory or read a configuration file, which may be missing or corrupt. It is under these circumstances that, to avoid the potential for memory leaks when construction fails, the two-phase construction idiom is used.

Two-phase construction of C class objects[65] is an idiom that you'll see used extensively in Symbian C++ code. It breaks the construction code into two parts, or phases :

  • A basic constructor that cannot leave, which is called by the new operator. The constructor may invoke functions that cannot leave or initialize member variables with default values or those supplied as arguments.

  • A class method (typically called ConstructL()). This method may be called separately once the object has been allocated and the constructor has executed. The object should first be pushed onto the cleanup stack to make it safe in the event of a leave. If a leave occurs, the cleanup stack calls the class destructor to free any resources that have been successfully allocated, and the memory allocated for the object is freed back to the heap.

The following code shows a simple, but not perfect, example of two-phase construction:

class CExample : public CBase
  {
public:
  CExample();    // Guaranteed not to leave
  ~CExample();   // Must cope with partially constructed objects
  void ConstructL(); // Second-phase construction code may leave
  ...
  };

The simple implementation shown here expects clients to call the second-phase construction function themselves, but a caller may forget to call the ConstructL() method after instantiating the object and, at the least, will find it a burden since it's not a standard C++ requirement. For this reason, it is preferable to make the call to the second-phase construction function within the class itself. A commonly used pattern in Symbian C++ code is to provide a static function that wraps both phases of construction, providing a simple and easily identifiable means to instantiate objects on the heap. The function is typically called NewL() and a NewLC() function is often provided too, which is identical except that it leaves the constructed object on the cleanup stack for convenience.[66]

class CExample : public CBase
  {
public:
  static CExample* NewL();
  static CExample* NewLC();
  ~CExample();   // Must cope with partially constructed objects
private:
  CExample();        // Guaranteed not to leave
  void ConstructL(); // Second-phase construction code may leave
  ...
  };

Note that the NewL() function is static, so you can call it without first having an existing instance of the class. The non-leaving constructors and second-phase ConstructL() functions have been made private[67] so a caller cannot instantiate objects of the class except through NewL().

Typical implementations of NewL() and NewLC() may be as follows:

CExample* CExample::NewLC()
  {
  CExample* self = new(ELeave) CExample; // First-phase construction
  CleanupStack::PushL(self);
  self->ConstructL();                    // Second-phase construction
  return (self);
  }
CExample* CExample::NewL()
  {
  CExample* self = NewLC();
  CleanupStack::Pop(self);
  return (self);
  }

Note that the NewL() function is implemented in terms of the NewLC() function rather than the other way around (which would be slightly less efficient since this would make an extra PushL() call on the cleanup stack). As a rule of thumb, NewL() should be called when initializing member variables, whereas NewLC() should be called when initializing local variables.

Each factory function returns a fully constructed object, or leaves if there is insufficient memory to allocate the object or if the second phase ConstructL() function leaves. If second-phase construction fails, the cleanup stack ensures both that the partially constructed object is destroyed and that the memory it occupies is returned to the heap.

If your class derives from a base class which also implements ConstructL(), you need to ensure that it is also called, if necessary, when objects of your class are constructed (C++ ensures that the simple first-phase constructors of your base classes are called). You should call the ConstructL() method of any base class explicitly (using the scope operator) in your own ConstructL() method, to ensure the base class object is fully constructed before proceeding to initialize your derived object.

If it is possible to PushL() a partially constructed object onto the cleanup stack in a NewL() function, why not do so at the beginning of a standard constructor, calling Pop() when construction is complete?

CWidget:: CWidget()
  {
  CleanupStack::PushL(this);
  ConstructL();              // Second-phase construction
  CleanupStack::Pop(this);
  }

At first sight, this may be tempting, since the single-phase construction I described as unsafe at the beginning of the chapter would now be leave-safe, as long as the object was pushed onto the cleanup stack before any leaving operations were called. But there are several arguments against this approach:

  • If the class has a deep inheritance hierarchy, each constructor with potential to leave would call PushL() and Pop(), which is less efficient than the single use of the cleanup stack in a separate factory function.

  • Another, more trivial, argument is that a C++ constructor cannot be marked with a suffixed L to indicate its potential to leave unless the class is itself named as such.

  • The most significant reason for preferring two-phase construction was described in the June 2008 Code Clinic article at developer.symbian.org/wiki/index.php/The_Implications_of_Leaving_in_a_Constructor and is summarized in the following paragraphs.

If ConstructL() leaves, the following sequence occurs:

  1. The cleanup stack 'unwinds' and destroys the object, explicitly calling CWidget::∼CWidget(), and then frees the memory allocated for the object.

  2. An XLeaveException exception is created and thrown to represent the leave, the normal stack is unwound and the exception is caught by the trap handler.

When an exception is thrown in a constructor, the destructor of that object is not called since the object has not yet been constructed. However, C++ guarantees to call the destructor of any fully-constructed superclasses in the event of an exception. Since the superclass parts of the object have been constructed, their destructors are called and the memory allocated for them is freed. So, if CWidget derives from CGadget, as shown below, the CGadget superclass destructor is called twice – first by the cleanup stack and then as a result of the exception. The second time the destructor is invoked, it is on something that has already been destroyed and freed back to the heap. If it attempts to access any member variables, it causes a memory fault, which results in a USER 42 panic.

class CGadget : public CBase
  {
public:
  CGadget();
  ~CGadget() {delete iBuf};
private:
  HBufC* iBuf;
  };
class CWidget : public CGadget
  {
public:
  CWidget();
  ~CWidget() {};
private:
  void InitializeL();
  };

The lesson to learn is that, if a class derives from other classes that have explicit and implemented destructors, you must defer calling any leaving code on construction until after the standard C++ constructor has completed. Or, put simply, just keep using two-phase construction to ensure leaving code is called after the constructor has fully executed. Double-deletion wasn't an issue before leaves were implemented in terms of C++ exceptions in Symbian OS v9.1, but fortunately the two-phase construction idiom has always been employed and encouraged for Symbian C classes, so construction code didn't need to be updated to account for the change in implementation.

Panics

On the Symbian platform, panics are used to highlight a programming error in the most noticeable way possible. When a thread is panicked, the code in it stops running to ensure that the developer is aware of the programming error and fixes the problem. You can't continue running the code until you fix the problem and stop the panic from occurring. Unlike a leave, a panic can't be trapped. The code terminates and there is no recovery.

When a panic occurs in the main thread of a process, the entire process in which the thread is running terminates. On phone hardware, a panic in an application's main thread unceremoniously closes the application without warning.[68]

On the emulator, in a debug build, you can choose to break into the code to debug the cause of the panic – this is known as just-intime debugging. If just-in-time debugging is enabled, a panic stops the emulator and enters the debugger to allow you to diagnose what caused the panic. The debugger is launched within the context of the function that called the panic, using __asm int 3. You can use the debugger to look through the call stack to see where the panic arose and examine the problem code.

A call to the static function User::Panic() panics the currently running thread. A thread may panic any thread in the same process by calling RThread::Panic(), but cannot panic threads in any other process. If a thread tries to panic a thread in another process, it is itself panicked with KERN-EXEC 46.[69]

When calling a Panic() method, you are required to pass in a descriptor to specify the 'category' of the panic and a TInt value to identify the panic (known as the panic 'reason'). You can use any length of panic category string that you wish, but it is truncated to 16 characters when it's reported. The panic category and reason can be useful for tracking down the cause of a panic, for example, if you don't have access to the code for debugging. Symbian publishes a set of panic values, listed by category, in the reference section of the Symbian Developer Library documentation. You may encounter the following typical values:

  • KERN-EXEC 3 is raised by an unhandled exception, such as an access violation, caused, for example, by dereferencing NULL or an invalid pointer. For example:

    TInt* myIntegerPtr; // Not initialized
    *myIntegerPtr = 5;  // Panics with KERN-EXEC 3

    Another reason for this panic is stack overflow. By default, the stack size assigned to a thread is 8 KB (which can be extended by means of epocstacksize up to 80 KB). This is a place where behavior differs between emulator and device. As a rule of thumb, if you get this panic on the device but it's not reproducible on the emulator, the stack has probably overflowed.

  • E32USER-CBASE 46 is raised by the active scheduler as a result of a stray signal (described further in Section 3.6.2).

  • E32USER-CBASE 90 is raised by the cleanup stack when the object specified to be popped off is not the next object on the stack (as discussed in Section 3.5.2).

  • USER 11 is raised when an operation to modify a 16-bit descriptor fails because the operation would cause the descriptor's data area to exceed its maximum allocated length (as described in Section 3.4.4).

  • ALLOC xxxxxxxx is raised in debug builds when the heap-checking macros (i.e. __UHEAP_MARK and __UHEAP_MARKEND) detect that more heap cells have been allocated than freed. This is discussed in Chapter 2.

This section is based on an extended discussion of panics, which can be found in the November 2008 Code Clinic article at developer.symbian.org/wiki/index.php/Symbian_Panics_Explained.

Event-Driven Programming

Consider what happens when program code makes a function call to request a service. The service can be performed either synchronously or asynchronously:

  • A synchronous function performs the service to completion and only then returns to its caller with an indication of its success or failure as a return value or as a leave.

  • An asynchronous function submits a request and returns to its caller, but completion of that request occurs some time later. On the Symbian platform, asynchronous functions can be identified as those taking a TRequestStatus reference parameter, which is used to receive the result (an error code) of the request when it completes.

Symbian OS, like other operating systems, uses event-driven code extensively, both at a high level (e.g., for user interaction) and at a lower, system level (e.g., for asynchronous communications input and output). To be responsive, the operating system must have an efficient event-handling model to handle each event as soon as possible after it occurs and, if more than one event occurs, in the most appropriate order. It is particularly important that user-generated events are handled rapidly to give feedback and a good user experience. Between events, the system should wait in a low-power state. This avoids constant polling, which can lead to significant power drain and should be avoided on a battery-powered device. Instead the software should allow the operating system to move to an idle mode while it waits for the next event to occur.

Threads and Active Objects

On the Symbian platform, threads are scheduled by the kernel, switching between them to give the impression that multiple threads are running concurrently. The kernel runs the highest-priority thread eligible and controls thread scheduling preemptively. This means that it allows threads to share system resources by time-slice division, but it preempts the running thread if another, higher-priority, thread becomes eligible to run.

A context switch occurs whenever the current thread is suspended (for example, if it becomes blocked, has reached the end of its time-slice, or a higher priority thread becomes ready to run) and another thread is made eligible by the kernel scheduler. The context switch incurs a run-time overhead in terms of the kernel scheduler and, if the original and replacing threads are executing in different processes, the memory management unit and hardware caches.

On the Symbian platform, an alternative to using multiple threads is to use active objects. A number of active objects can run in a single thread but, within that single thread, they exhibit non-preemptive multitasking: once an active object is running, it cannot be replaced until it has finished.[70]

A Symbian application or server often consists of a single, main event-handling thread. A set of active objects run in the thread. Each active object encapsulates an asynchronous service provider. To that provider, it requests an asynchronous service, waits while it is serviced, then handles the completion event. On the Symbian platform, the use of multiple active objects is often preferable to multiple threads:

  • A switch between active objects that run in the same thread incurs a lower overhead than a thread context switch. The difference in speed between a context switch between threads and transfer of control between active objects in the same thread can be a factor of 10 in favor of active objects. In addition, the space overhead for a thread can be around 4 KB kernel-side and 8 KB user-side for the program stack, while the size of an active object may be only a few hundred bytes, or less.

  • Apart from the run-time expense of a context switch, using preemptive multithreading for event handling can be inconvenient because of the need to protect shared objects with synchronization primitives such as mutexes or semaphores. Since active objects are non-preemptive within a single thread, there is no need for synchronization protection of shared resources.

  • Resource ownership is thread-relative by default on the Symbian platform. If a file is opened by the main thread it is not possible for a different thread in the process to use it without the handle being explicitly shared through a call to RSessionBase::Share() (and some Symbian platform servers do not support session sharing at all). Because of this restriction, it may be very difficult to use multiple threads as a viable method for event-driven multitasking. Active objects run in the same thread, which means that memory and resources may be shared more readily.

Working with Active Objects

A Symbian platform thread that uses active objects for event handling also has an 'active scheduler' (see CActiveScheduler in e32base.h) that passes event completion signals to their corresponding active objects. Each active object requests an asynchronous service, which generates an event signal some time after the request. The active scheduler detects those event signals, determines which active object is associated with each, and calls it to handle the event. While an active object is handling an event, other signals may be received by the scheduler, but it cannot schedule another active object to run until the currently running active object has finished its event-handling routine.

Construction

Classes deriving from CActive must call the protected constructor of the base class, passing in a value to set the priority of the active object. The priority value is used by the active scheduler to determine the order in which event signals are handled if multiple active objects are ready to be 'scheduled' at once. That is, while one event is being handled, it is quite possible that a number of additional events may complete. The scheduler must resolve which active object gets to run next. It does this by ordering the active objects using their priority values, handling them sequentially in order of the highest active object priority rather than in order of their completion.

This make sense if the system is to remain responsive – otherwise, an event of low priority that completed just before a more important one would lock out the higher-priority event for an undefined period, depending on how much code the low-priority active object executed to handle its associated event.

As you can see in e32base.h, a set of priority values is defined in the TPriority enumeration of class CActive. You use the priority value EPriorityStandard (=0) unless you have a good reason to do otherwise.

As part of construction, the active object code should call a static function on the active scheduler, CActiveScheduler::Add(). This adds the object to a list maintained by the active scheduler of event-handling active objects on that thread.

CMyActive::CMyActive()
: CActive(CActive::EPriorityStandard)
  {
  CActiveScheduler::Add(this);
  }

An active object typically owns an object to which it issues requests that complete asynchronously, generating an event. An object implementing such methods is usually known as an asynchronous service provider and may need to be initialized as part of construction. If the initialization can fail, you should perform it as part of the second-phase construction, as described before.

class CMyActive : public CActive
  {
private:
  CProvider* iProvider;
  // ...
  };

Submitting Requests

An active object class supplies public methods for callers to initiate requests. They submit requests to the asynchronous service provider associated with the active object using a well-established pattern, as follows:

  1. Request methods should check that there is no request already submitted before attempting to submit another. Depending on the implementation, the code may:

    • panic if a request has already been issued (if this scenario could only occur because of a programming error)

    • refuse to submit another request (if it is legitimate to attempt to make more than one request but once one is outstanding all others are ignored)

    • cancel the outstanding request and submit the new one.

  2. The active object should then issue the request to the asynchronous service provider, passing in its iStatus member variable as the TRequestStatus& parameter.

  3. If the request is submitted successfully, the request method then calls the SetActive() method of the CActive base class to indicate to the active scheduler that a request has been submitted and is currently outstanding. This call is not made until after the request has been submitted.

Event Handling

Each active object class must implement the pure virtual RunL() member method of the CActive base class. When a completion event occurs from the associated asynchronous service provider and the active scheduler selects the active object to handle the event, it calls RunL() on the active object. The RunL() function has a somewhat misleading name because the asynchronous function has already run. Perhaps a clearer name would be HandleEventL() or HandleCompletionL().

Typical implementations of RunL() determine whether the asynchronous request succeeded by inspecting the completion code in the TRequestStatus object (iStatus) of the active object, a 32-bit integer value. Depending on the result, RunL() usually either issues another request or notifies other objects in the system of the event's completion; however, the degree of complexity of code in RunL() can vary considerably.

void CMyActive::RunL()
  {
  // check the iStatus for possible errors and
  // resubmit the request if necessary
  }

Whatever it does, once RunL() is executing, it cannot be preempted by other active objects' event handlers. For this reason, the code should do what it needs to in as short a time as possible so that other events can be handled without delay.

Error Handling

CActive provides a virtual RunError() method which the active scheduler calls if a leave occurs in the RunL() method of the active object. The method takes the leave code as a parameter and returns an error code to indicate whether the leave has been handled. The default implementation does not handle the leave and simply returns the leave code passed to it. If the active object can handle any leaves occurring in RunL(), it should do so by overriding the default implementation and returning KErrNone to indicate that the leave has been handled.

If RunError() returns a value other than KErrNone, indicating that the leave has yet to be dealt with, the active scheduler calls its Error() function to handle it. The active scheduler does not have any contextual information about the active object with which to perform error handling. For this reason, it is generally preferable to manage error recovery within the RunError() method of the associated active object, where more context is usually available.

Cancellation

An active object must be able to cancel any outstanding asynchronous requests it has issued. The CActive base class implements a Cancel() method which calls the pure virtual DoCancel() method (which every derived active object class must implement) and waits for the request's early completion. Any implementation of DoCancel() should call the appropriate cancellation method on the asynchronous service provider. DoCancel() can also include other processing but should not carry out any lengthy operations. It's a good rule to restrict the method to cancellation and any necessary cleanup associated with the request, rather than implementing any sophisticated functionality. This is because a destructor should call Cancel() but it may have already cleaned up resources that DoCancel() requires. As was already explained, you should not allow code to leave in a destructor, so DoCancel() should not leave either.

void CMyActive::DoCancel()
  {
  iProvider->CancelAll();
  }

It isn't necessary to check whether a request is outstanding for the active object before calling Cancel(), because it is safe to do so even if it isn't currently active.

Destruction

The destructor of a CActive-derived class should always call Cancel() to terminate any outstanding requests as part of cleanup code. The CActive base class destructor is virtual and its implementation checks that the active object is not currently active. It panics if any request is outstanding, that is, if Cancel() has not been called. This catches any programming errors which could lead to the situation where a request completes after the active object to handle it has been destroyed. This would otherwise result in a 'stray signal', where the active scheduler cannot locate an active object to handle the event.

Having verified that the active object has no issued requests outstanding, the CActive destructor removes the active object from the active scheduler. The destructor code should also free all resources owned by the object, as usual, including any handle to the asynchronous service provider.

Writeable Static Data

When porting an existing project, you'll probably find yourself working with DLLs. Symbian OS – like most operating systems – supports static (LIB target) and dynamic (DLL target) libraries, but some extra care should be taken when using global variables in DLLs, as it can be expensive in terms of memory usage. This is because writeable static data (WSD) in a DLL typically consumes 4 KB of memory for each process into which the DLL is loaded.

When a process loads its first DLL containing WSD, it creates a single chunk to store the data. The minimum size of a chunk is 4 KB (the size of a page frame) and that amount of memory is consumed irrespective of how much static data is actually required. Any memory not used for WSD is wasted (however, if subsequent DLLs are loaded into the process that also use WSD, the same chunk can be used rather than separate 4 KB chunks for every DLL that uses WSD).

Since the memory is per process, the potential memory wastage is:

(4KB – WSD bytes) × number of client processes

If, for example, a DLL is used by four processes, that's potentially an 'invisible' cost of 16 KB (minus the memory occupied by the WSD itself).

There are a number of occasions where the benefits of using WSD in a DLL outweigh the disadvantages (for example, when porting code that uses WSD significantly, and for DLLs that are only ever loaded into one process). Note, however, that the current supported version of the GCC-E compiler has a defect such that DLLs with static data may cause a panic during loading.

Usage

There are occasions where you may find you use WSD inadvertently. Some Symbian C++ classes have a non-trivial constructor, which means that the objects must be constructed at run time. You may not think you are using WSD but, because an object isn't initialized until the constructor for the class has executed, it counts as modifiable rather than constant. Here are a few examples:

static const TPoint KGlobalStartingPoint(50, 50);
static const TChar KExclamation('!'),
static const TRgb KDefaultColour(0, 0, 0);

On most versions of the Symbian platform, if you attempt to build DLL code that uses WSD, deliberately or inadvertently, you get a error when you build the DLL for phone hardware. The error looks something like this:

ERROR: Dll 'TASKMANAGER[1000C001].DLL' has uninitialised data

A DLL that uses WSD will always build for the Microsoft Windows emulator. It is only when code is compiled for phone hardware that the use of WSD is flagged as an error. If you are sure you want to use WSD, you can explicitly enable it by adding the keyword EPOCALLOWDLLDATA to the DLL's MMP file.

However, if you want to avoid the potential of extra memory consumption and emulator testing limitations, there is an alternative mechanism available: thread local storage (TLS). TLS can be used in the implementation of Singleton in DLLs on all versions of the Symbian platform (it can also be used in EXEs if desired). TLS is a single, per-thread storage area, one machine word in size (32 bits on Symbian OS v9).[71]

Global Destructors

If your code uses global objects, you should be aware that current Symbian platform implementations do not invoke destructors of global objects upon program termination.[72]

This is something to keep in mind specially when owning global resources, though you should also be very careful when dealing with global constructors and destructors and the order in which they're invoked.

Multiple Inheritance

On the Symbian platform, the use of multiple interface class (M class) inheritance is the only form of multiple inheritance that is encouraged.

The following example illustrates a class which inherits from CBase and two mixin interfaces, MRadio and MClock. In this case, MClock is not a specialization of MRadio and the concrete class instead inherits from them separately and implements both interfaces:

class MRadio
  {
public:
  virtual void TuneL() = 0;
  };
class MClock
  {
public:
  virtual void CurrentTimeL(TTime& aTime) = 0;
  };
class CClockRadio : public CBase, public MRadio, public MClock
  {
public:
  // From MRadio
  void TuneL();
public:
  // From MClock
  void CurrentTimeL(TTime& aTime);
  // Other functions omitted for clarity
  };

For M class inheritance, various combinations of this sort are possible. However, other forms of multiple inheritance can introduce significant levels of complexity if the standard CBase class is used.[73] Multiple inheritance from two CBase-derived classes will cause problems of ambiguity at compile time, which can only be solved by using virtual inheritance:

class CClock : public CBase
  {...};
class CRadio : public CBase
  {...};
class CClockRadio : public CClock, public CRadio
  {...};
void TestMultipleInheritance()
  {
  // Does not compile, CClockRadio::new is ambiguous
  // Should it call CBase::new from CClock or CRadio?
  CDerived* derived = new (ELeave) CDerived;
  }

Summary

This chapter has served as a basic introduction to the Symbian platform, describing the essential class types and their lifetimes, as well as fundamental idioms to be aware of when using the Symbian APIs. Then the use of descriptors was discussed; together with the cleanup stack and active objects, they are one of the fundamental cornerstones of the native API.

Once you get familiar with the above topics, you'll be able to make use of the extensive services offered by the OS and start writing your own service providers.

Finally, and no less importantly, writeable static data (WSD) and multiple inheritance were covered. WSD has its place, particularly when porting code that makes use of global objects (in the Singleton pattern, for example) whilst multiple inheritance is required for one of the most widely used patterns in the Symbian platform: the Observer–Listener pattern.

Note that the information provided in this chapter has the sole aim of giving you a bird's-eye view of the platform. The next chapter focuses entirely on standard C/C++ programming and POSIX APIs within the Symbian platform.



[56] Note that the use of 'class T' in the class name is coincidental – it is the standard notation for a templatized class.

[57] We can recommend Stichbury, J. (2004) Symbian OS Explained. John Wiley & Sons.

[58] Such classes have only been publicly available since S60 3rd Edition FP2.

[59] On Linux, one such tool is Valgrind (see valgrind.org).

[60] 'Leaves' is as in the verb 'to leave'. A function that contains code which may leave is called a 'leaving function' while code that has executed along the path of a leave (say, as the result of an exceptional condition) can be said to have 'left'.

[61] Note that nested exceptions are supported on the Microsoft Windows emulator, as exceptions are implemented using Win32 structured exception handling. Therefore code that appears correct on the emulator may not function as intended when executed on target hardware.

[62] If you experiment and write a T class which has a destructor, create an object on the stack and then force the code to leave, you'll notice that the class destructor is called on Symbian OS v9.x platforms but not on Symbian OS v7.0s, for example.

[63] As specified by C++, the object is destroyed by calling destructor code in the most-derived class first, moving up the inheritance hierarchy calling destructors in turn and eventually calling the empty CBase destructor. It is to this end that CBase has a virtual destructor – so that C class objects can be placed on the cleanup stack and destroyed safely if a leave occurs.

[64] See developer.symbian.org/wiki/index.php/Leaves_&_The_Cleanup_Stack_(Fundamentals_of_Symbian_C++).

[65] Two-phase construction is typically only used for C classes, since T classes do not usually require complex construction code (because they do not contain heap-based member data) and R classes are usually created uninitialized, requiring their callers to call Connect() or Open() to associate the R class object with a particular resource.

[66] This is also known as the 'named constructor' idiom.

[67] If you intend your class to be subclassed, you should make the default constructor protected rather than private so the compiler may construct the deriving classes. The ConstructL() method should be private (or protected if it is to be called by derived classes) to prevent clients of the class from mistakenly calling it on an object which has already been fully constructed.

[68] When a panic occurs in a secondary thread, it is only that thread which closes, unless the thread has been set as process critical or process permanent, in which case the process also panics. A call to User::SetCritical() is necessary to set a thread to process critical or permanent. User::SetCritical() can also be used to set a thread as system critical or system permanent so that, when code runs on phone hardware, a panic in the thread deliberately reboots the phone. See the developer library documentation for further information about this call.

[69] There is an exception to this rule where a server thread uses the RMessagePtr2 API to panic a misbehaving client thread, for example, when a client passes a badly-formed request. Rather than attempt to read or write to a bad descriptor, the server protects itself and flags the problem by causing a panic in the client thread. Generally, a malformed client request occurs because of a programming error in client code, but this strategy also protects a server against more malicious 'denial-of-service' attacks in which a client may deliberately pass a badly-formed or unrecognized request to a server to try to crash it.

[70] Although the active objects within a thread are non-preemptive, the thread in which they run is scheduled preemptively.

[71] For a thorough discussion of this topic applied to the Singleton pattern, please refer to the April 2008 Code Clinic article at developer.symbian.org/wiki/index.php/WSD_and_the_Singleton_Pattern.

[72] See wiki.forum.nokia.com/index.php/KIS001019_-_Open_C++:_Destructor_of_a_global_object_is_not_called.

[73] See the May 2008 Code Clinic article at developer.symbian.org/wiki/index.php/Mixin_Inheritance_and_the_Cleanup_Stack.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.199.130