Descriptors are essential objects that are used throughout Symbian OS to store arbitrary data. While they are most commonly used for storing text, they can also store binary data and even serialized compound objects, since they do not rely on zero termination . They are designed for maximum efficiency, and they avoid the use of virtual functions with their associated memory and instruction overhead.
Because of these issues, they have an API and derivation structure that takes a little getting used to, but they do provide:
Runtime bounds checking.
An extensive and standard API for data and text management throughout Symbian OS.
Tight integration with the Symbian OS resource management paradigm.
Developers new to Symbian OS sometimes want to avoid using descriptors, and instead try to develop their own alternatives. This often leads to dangerously unsafe code (particularly regarding leave-safety), and since many Symbian OS APIs use descriptors, you cannot avoid using them at some point! |
Figure 3-2 illustrates the derivation structure of the nine main descriptor classes. Note that the only instantiable classes are the five shown on the bottom.
Table 3-2 describes each of these classes in a little more detail. It is worth noting that all of the descriptors detailed here actually come in two sizes: 8-bit (or narrow) and 16-bit (or Unicode). These descriptors may be explicitly sized by specifiying 8 or 16 after their name ”for example, TDesC8 or tdesC16 .
Since all modern Symbian OS platforms (including Series 60) are exclusively Unicode, tdesC may be assumed synonymous with the explicitly sized tdesC16 variant, but this second form may be used for clarity, if required.
tdesC8 -derived classes should be specified when manipulating binary data.
This table divides the descriptors into two natural groups : modifiable and nonmodifiable descriptor types. If a descriptor has a modifiable API, it means its contents can be altered ; for example, a character could be appended. A nonmodifiable API does not allow the descriptor content to be modified, although it can be reset. The Symbian OS convention is that nonmodifiable types should append a C (for "constant") to their basic name ”for example, TBufC .
Name | Instantiable | Modifiable API | Comments |
---|---|---|---|
tdesC | No | No | Base class of all descriptors, used for nonmodifiable argument passing and return type (as const TDesC& ). |
TBufCBase | No | No | Rarely used intermediate class. Listed here for completeness only. |
tdes | No | Yes | Base class for modifiable descriptors, used for modifiable argument passing (as tdes& ). |
TBufBase | No | Yes | Rarely used intermediate class. Listed here for completeness only. |
TBufC | Yes | No | Nonmodifiable buffer descriptor with templated size. Size and data buffer stored in a single allocation cell . |
HBufC | Yes | No | Nonmodifiable heap descriptor. Never instantiated on the stack. Size, maximum size and data buffer stored in a single heap cell. |
TPtrC | Yes | No | Nonmodifiable pointer descriptor. Size and data buffer pointer are stored separately to the data, which is owned elsewhere. |
TBuf | Yes | Yes | Modifiable buffer descriptor with templated maximum size. Size, maximum size and data buffer stored in a single allocation cell. |
TPtr | Yes | Yes | Modifiable pointer descriptor. Size, maximum size and data buffer pointer are stored separately to the data, which is owned elsewhere. |
All of the descriptors have an iLength member that stores the current length of the descriptor. This is how descriptors can function without the need for null termination. Modifiable API descriptors also have an iMaxLength member, since their length needs to be bounded.
Note that although they are "nonmodifiable," the contents of the instantiable classes HBufC and TBufC can actually be modified through use of a pointer descriptor. You will see how this is achieved later in this section.
Since all descriptor types are derived from the abstract base class tdesC , they share a nonmodifiable API. Some of the most useful members are mentioned here (for full details of the API, see tdesC16 in the SDK documentation):
Length() : Simply returns the number of characters stored in the descriptor.
Size() : The number of bytes occupied by the descriptor. For a 16-bit (Unicode) descriptor the size will be twice the length; for an 8-bit descriptor they will be the same.
Ptr() : Returns a const TUint* (for a 16-bit descriptor or a const TUint8* for an 8-bit descriptor) to the descriptor's data buffer.
Alloc () : Allocates and returns a new HBufC heap-based descriptor, containing a copy of the data in this descriptor. If the allocation fails, it returns NULL .
AllocL() : Performs the same task, but leaves with KErrNoMemory if the allocation should fail.
AllocLC() : Identical to AllocL() except that it leaves a pointer to the newly allocated object on the Cleanup Stack.
The use of Alloc() , AllocL() , and AllocLC() can have an impact on the use of the Cleanup Stack if they are used in conjunction with items on the Cleanup Stack ”see the entry on HBufC later. |
Left() , Mid() and Right() : Standard string-slicing functions that return a TPtrC to the relevant substring of the descriptor. Note they do not affect the descriptor itself, but simply return a pointer to the relevant substring in place .
Find() : Search for a descriptor's contents within another, returning the zero-based offset of the first occurrence, or KErrNotFound if not found. Note that searching always starts from the beginning of the descriptor; use Right() to get the remainder of the descriptor, then repeat the find for further occurrences if necessary.
operator<() , operator>() , operator==() and operator!=() : These operators are overloaded for text, according to the standard text comparison rules. They may, of course, be used on descriptors containing binary data, but the results may be unpredictable.
operator[]() : Allows the inspection of individual characters or data items at a zero-based index.
operator=() : It may come as a surprise that the assignment operator is available for supposedly nonmodifiable descriptors. One may argue that reassignment is not modification, since it is simply replacing the entire data with some more. TPtrC is an exception ”it has no overloaded assignment operator and may be considered genuinely immutable.
Any attempt to assign data that is longer than the original data length will result in an immediate panic. |
Remember that these APIs are in addition to the nonmodifiable APIs. A common feature of all modifiable APIs is that any attempt to increase the length of the descriptor beyond the maximum length will result in an immediate panic.
As before, the following list is not a full API description, but it covers those APIs considered the most useful.
MaxLength() : The maximum number of data items that can be stored in the descriptor. Any operation that would cause the length of the descriptor to exceed this value will result in a panic.
MaxSize() : The maximum number of bytes that the data buffer will occupy.
SetLength() , SetMax() and Zero() : Adjusts the length of a descriptor to the specified length, the value of MaxLength() and zero, respectively.
These functions do not result in changes to the data buffer. In particular this may mean that uninitialized data will be present if the length is increased . |
Append() : Appends data in the given descriptor to the end of the current descriptor. Note this function cannot leave, since the data buffer is already allocated for the descriptor. It will panic, though, if the data appended causes the maximum length to be exceeded.
Insert() : Inserts the given descriptor at the specified location within the original descriptor.
Delete() : Removes the given number of data items from the given location in the descriptor. Any subsequent data is shuffled down.
Format() : Formats a descriptor in a manner similar (but not identical) to sprintf , given a descriptor specifying a format string and then an appropriate number of arguments. Note that the format specifiers are mostly the same as for sprintf , but there are some differences. Check in the SDK documentation for further details.
Copy() : Copies data into a descriptor from a variety of sources: raw memory addresses, 8- and 16-bit descriptors. Copying from an 8-bit into a 16-bit descriptor will result in the addition of padding bytes to convert from ASCII to Unicode; the converse is also true, although Unicode values greater than 255 are converted to 1.
Although not strictly part of the descriptors API, the string literal is a useful tool to have when learning how to use descriptors. String literals are used throughout the example code as an easy method of including printable text in the program. Before introducing this technique, it is vital to stress that in any commercial-quality (UI) application, such text should be provided in resource files for ease of localization. Localization is covered in more detail in Chapter 2 and Chapter 4.
Literals are stored as objects of type TLitC . As with descriptors, TLitC8 and TLitC16 are available as explicitly sized alternatives.
String literals are constructed using the _LIT macro. The macro takes the name of the literal, and a pointer to the null- terminated string it should contain, usually supplied by simply providing the string in quotes. Here is a simple example:
_LIT(KTxtMyStringLiteral, "My string");
It is important to appreciate that literals are not actually descriptors themselves . However, they can be converted to descriptors in three ways: implicitly, using operator() , and using operator& .
Implicit conversion takes place courtesy of operator const TDesC16&() const (and its 8-bit equivalent), which is defined for literals. Because of this, any function call that will take a const TDesC& will also take a literal without needing any modification or explicit casting.
operator() is defined for situations when implicit casting is not enough. For example, in the following code it is necessary to know the length of the literal KTxtLineDelimiter . While a constant could be defined to contain this value, it is convenient to use operator() and then use tdesC::Length() :
// Increment the file position iFilePos += elementBuf16->Length() + KTxtLineDelimiter().Length();
Finally, if a descriptor pointer is required from a literal, operator & is overloaded to return a const TDesC* :
// Get a descriptor pointer from a literal. const TDesC* ptr = &KTxtMyStringLiteral;
If you look through some of the examples provided with the SDK, you will probably see literals constructed with the _L macro. This has now been deprecated, because it is less efficient than _LIT and also because there can be problems with scope. Always create literals using _LIT! |
The common APIs for descriptors were introduced earlier in this chapter; this section provides practical examples to illustrate their use and should help you to understand the fundamental techniques necessary for using descriptors.
HBufC descriptors, despite not conforming to standard naming conventions, are allocated on the heap (they are not derived from CBase and so have the unique starting letter, H for heap). Heap desciprtors are generally used in preference to all other descriptors in the following situations:
If the size of the descriptor is not known at compile time, or its size is not bounded by a small maximum value.
If the size of the descriptor is known to be large.
For example, in the CChemicalElement class in the Elements example application, there are two descriptors. One is used to store the name of the element, the other its chemical symbol . The name of the element can be relatively large ”a maximum of 13 characters = 26 bytes (remember it is a Unicode System) for the element Rutherfordium, and to use 13 characters for each name would waste an average of about 10 bytes per element ”using an HBufC is most efficient here. The longest chemical symbol is only three characters in comparison, and most of them are two characters in length ”only an average 2 bytes per element is wasted storing these in a TBuf<3> .
HBufC s are constructed in a number of ways: the simplest is to use HBufC::New() , HBufC::NewL() or HBufC::NewLC() as in the following example, which creates a new HBufC as long as elementBuf8 :
HBufC* elementBuf16 = HBufC::NewLC(elementBuf8.Length());
NewLC() is used here for a locally scoped pointer. NewL() could be used if use of the Cleanup Stack were unnecessary (assignment to member data), and New() could be used if leaving behavior is not required. New() returns NULL if allocation fails.
There are no constructors defined for HBufC , but a new one can be constructed from an existing descriptor using the tdesC::AllocL() function, as in the code below:
void CChemicalElement::ConstructL(const TDesC& aName) { iName = aName.AllocL(); }
Note that AllocL() is invoked on the original descriptor and returns a pointer to the new HBufC . Since CChemicalElement::ConstructL() is private, there is no danger of this function being called more than once (so deleting and assigning to NULL is unnecessary).
It may seem surprising that there is no HBuf corresponding to HBufC . Does this mean that you cannot modify a heap-allocated buffer? Well, by using the HBufC API, you cannot (other than complete reassignment). But there is another way of doing it ”the method HBufC::Des() returns a modifiable pointer descriptor ( TPtr ), pointing to the data owned by the HBufC . Since this pointer has a modifiable API, then the data owned by the HBufC can be modified. Here is an example:
HBufC* elementBuf16 = HBufC::NewLC(elementBuf8.Length()); TPtr elementPtr16 = elementBuf16->Des(); elementPtr16.Copy(elementBuf8);
The code segment is used to copy data from a narrow (8-bit) ASCII descriptor into a Unicode HBufC . First it creates a new HBufC elementBuf16 with same length (but not size ) as the original 8-bit descriptor elementBuf8 . It then uses the Des() method to get a TPtr , elementPtr16 , pointing to the data of elementBuf16 . Finally, it uses the Copy() method of TPtr to copy the data from elementBuf8 into elementBuf16 (to which elementPtr16 is pointing).
This particular tdes16 Copy() overload, which takes a tdesC8 , converts from 8- to 16-bit by adding zero padding ”effectively converting ASCII to Unicode. Note that the TDes8 Copy() overload that takes a tdesC16 does the opposite , stripping every other byte and effectively converting from Unicode to ASCII (so long as the Unicode characters are less than 255, otherwise they are set to 1).
One point worth stressing once more about TPtr objects (which applies to TPtrC s, too) ”they do not own the data that they point at . In the example above, the data to which elementPtr16 points is not deleted when elementPtr16 goes out of scope, nor is there any way of doing this with the TPtr API. The HBufC pointer elementBuf16 must itself be deleted. You can have as many TPtr s as you like pointing to the buffer of an HBufC .
HBufC provides another pair of APIs that are extremely useful: ReAlloc() and ReAllocL() . As their name suggests, they are used for reallocating an HBufC .
What does this mean? Basically, it constructs a new HBufC on the heap. Should that allocation fail, ReAlloc() returns zero, and ReAllocL() will leave with KErrNoMemory , but in both cases the original HBufC remains unchanged. Only if the allocation is successful will ReAlloc() (or ReAllocL() ) then copy the data into the new HBufC() and delete the original one automatically. A pointer to a new HBufC() is returned.
This poses a potentially nasty problem. Consider the following code example:
// Push onto Cleanup Stack HBufC* variableBuf = HBufC::NewLC(10); // Make some (potentially leaving) use of the original variableBuf... variableBuf = variableBuf->ReAllocL(20); // Make it bigger // The old variableBuf CleanupStack::Pop(); CleanupStack::Push(variableBuf); // The new one // Make some (potentially leaving) use of the new variableBuf... CleanupStack::PopAndDestroy(variableBuf);
What would happen if you did not pop the old variableBuf off the Cleanup Stack and push the new one on? Well, at very least the CleanupStack::PopAndDestroy() would panic, since the pointer passed in (the new value for variableBuf ) would not match the one on the Cleanup Stack. What would happen if the leaving code after the reallocation were to leave? In that case, as the Cleanup Stack was unwound, a pointer to the old variableBuf would be deleted. Unfortunately, this has already been deleted as the last ReAllocL() step, causing a disastrous double-deletion. Follow the steps above, ensuring that the old HBufC* is popped and the new one is pushed , and you will be safe from such catastrophes.
If you refer back to Table 3-2, you will see that the buffer descriptors TBuf and TBufC are templated with a size ”maximum size for a TBuf and actual size for a TBufC . The use of this templated size means that you must know the size of a TBufC, or the maximum size of a TBuf, at compile time.
The following code snippet shows how to declare a TBufC to hold three characters:
const TInt KMaxSymbolLength = 3; ... // Delare a 3-character TBufC TBufC<KMaxSymbolLength> iSymbol;
To demonstrate how you can use a TBuf , the NotifyElementLoaded() function, declared in CElementsEngine, will be discussed. The function is a callback to provide user feedback when an element has been loaded from file (more on files later). It uses a console object that allows descriptors to be printed:
_LIT(KTxtLoaded, "Loaded "); ... void CElementsEngine::NotifyElementLoaded( TInt aNewElementIndex) { // Start with KTxtLoaded... TBuf<KMaxFeedbackLen> loadedFeedbackString(KTxtLoaded); // ...append the name of the new element... loadedFeedbackString.Append( (iElementList->At(aNewElementIndex)).Name()); // ...append a new line... loadedFeedbackString.Append(KTxtNewLine); // ...and print to the console iConsole.Printf(loadedFeedbackString); }
First, the literal KTxtLoaded is defined using the macro _LIT() . It is set to contain the string " Loaded ". KTxtNewLine simply contains a " \n ".
The TBuf constructor is templated with the maximum size and can also take a descriptor (or literal via implicit casting in this case) to copy into the new TBuf . In this example a TBuf of maximum size KMaxFeedbackLen is constructed and initialized with the characters "Loaded ".
An element name is then appended to the descriptor buffer using the Append() function. The argument passed in may look slightly confusing, but essentially the element at a particular position ( aNewElementIndex ) in the array is located and the name of the element retrieved using CChemicalElement::Name() . The new line characters are also added to the buffer. Again implicit casting means you can just pass the string literal as the argument.
TBufC s, without a modifiable API, are generally used less frequently. Here is how the iSymbol member of CChemicalElement is declared:
TBufC<KMaxSymbolLength> iSymbol;
Remember that only the nonmodifiable APIs listed for tdesC are available for TBufC , but this includes operator = (as used in CChemicalElement::SetSymbol() above).
Descriptors are often passed into functions and returned by them. It is common to use references to the base classes: tdes for modifiable descriptors and TDesC for nonmodifiable descriptors. This allows the greatest flexibility in implementation, as your API is not limited to one concrete type. Here are some common examples:
/** * Setter function for the element's symbol. * param aSymbol The element's new symbol. */ void SetSymbol(const TDesC& aSymbol);
Note that const TDesC& is used to pass in a nonmodifiable descriptor. While tdesC& might seem appropriate, remember that it is possible to assign to a tdesC . Doing this within the body of such a function can mean the value of the descriptor is modified, and so developers are encouraged to use const TDesC& in such circumstances.
The implementation of the above function is shown below:
void CChemicalElement::SetSymbol(const TDesC& aSymbol) { iSymbol = aSymbol; }
If the data to be passed as an argument is stored in an HBufC , then programmers new to descriptors sometimes insist on doing this:
DescriptorMethod(myHBufC->Des());
This is both inefficient and unnecessary unless DescriptorMethod() takes a modifiable tdes& . If DescriptorMethod() takes a const TDesC& , then dereferencing the HBufC* is all that is required:
DescriptorMethod(*myHBufC);
Modifiable descriptors can be used to pass back descriptors as shown below. Note that the Symbian OS convention is to precede such function names with "Get":
/** * Getter function for the element's symbol * @param aSymbol the element's symbol */ void GetSymbol(TDes& aSymbol) const;
Here is the implementation, but note that this function will panic if aSymbol is too small to accommodate iSymbol :
void CChemicalElement::GetSymbol(TDes& aSymbol) const { aSymbol = iSymbol; }
It is usually more convenient to simply return (a reference to) the descriptor. This is most commonly achieved using a const TDesC& (note that "Get" should not be used in the function name in this instance):
/** * Getter function for the element's symbol * @return The element's symbol */ const TDesC& Symbol() const;
And the implementation is as simple as:
const TDesC& CChemicalElement::Symbol() const { return iSymbol; }
This is only possible if the access function is returning the whole member descriptor. Functions that return part of a member descriptor must return a pointer descriptor ”this allows you to return member data in place, with the efficiency that entails. Copying part of a descriptor to a temporary variable for returning is not only inefficient, but can also be dangerous ”do not return a reference to a temporary variable, and always check your compiler warnings!
A classic example of returning a pointer to only part of a member descriptor is TDesC::Left() . It returns a nonmodifiable pointer descriptor to represent the leftmost part of the data, as shown in Figure 3-3.
Note that the pointer descriptor must be returned by value. Also note that TPtrC is genuinely immutable ”there is no way to modify the data to which it points ”so it is not necessary to return a const TPtrC .
When writing your API, it is often worth considering the extra flexibility that returning a TPtrC offers. It allows you to return all or part of a member descriptor, and this means you could possibly change the implementation of your function while retaining the same API. Remember, however, that returning a TPtrC will be slightly less efficient (it contains a pointer to the data and its size), so consider both facts when making your design choice. |
Package descriptors are a useful way of packaging complex data into a descriptor (the SDK guide refers to them as package buffers , but this is something of a misnomer ”only one of them is actually a buffer with its own data). Once packaged, this data can then be passed to any function that takes a tdesC8 derived class. This technique is used within Symbian OS to pass structured data between clients and servers in a type-safe manner.
Note that package descriptors should not be considered a way of streaming arbitrary data either to memory or to a file. They cannot handle compound data, where one object owns another via a pointer. See the use of streams in the section Files, Streams and Stores later in this chapter for a more detailed explanation of how to do this properly.
Three package descriptor types are available in Symbian OS: TPckg , TPckgC and TPckgBuf . Table 3-3 provides some detail about each.
Name | Owns Data | Modifiable API | Comments |
---|---|---|---|
TPckg | No | Yes | TPtr8 derived package descriptor. Pointer points to the original data, and allows modification of this data. |
TPckgC | No | No | TPtrC8 derived package descriptor. Pointer points to the original data, but no modification of this data is permitted. |
TPckgBuf | Yes | Yes | TBuf8 derived package buffer. Buffer contains a copy of the original data, which may be modified. Such modification will have no effect on the original data. |
Here is a simple (but rather contrived) example that demonstrates the use of a TPckg to contain and modify a trect object:
TRect myRect(0, 0, 10, 20); // Construct the package descriptor TPckg<TRect> myRectPckg(myRect); // Note that operator() on myRectPckg will return a // modifiable TRect& to the rectangle // it points to, i.e. myRect TInt twenty = myRectPckg().Height(); myRectPckg().SetWidth(30); // Modify width TInt thirty = myRect.Width();
This example may appear somewhat pointless, but remember that package descriptors are really important during Client/Server communication ”this is discussed later, in the section Client/Server Architecture .
Declaring a package descriptor is simply a case of selecting the appropriate package descriptor variety and then specifying the data type it is to contain as the template parameter. All three package descriptors have constructors which take an instance of the templated data type. Only TPckgBuf has a default constructor that allows an uninitialized buffer to be constructed.
Note the use of operator() . For the modifiable package descriptor types these return modifiable references to the object in the buffer for TPckgBuf , or the object pointed to by TPckg . In the case of TPckgC a nonmodifiable reference is returned, so only const functions can be called on this reference.
The example above also stresses that TPckg objects do not own the data ”the original myRect object is modified by the line myRectPckg().SetWidth(30) . If a TPckgBuf were used instead, only the package buffer's copy of the data would be modified ” myRect would remain unchanged. If a TPckgC were used, this line would not compile, since an attempt is being made to call a non- const function ( SetWidth() ) on a const reference.