Mastering Symbian OS Descriptors
For Heaven's Sake, What Is It For?
Symbian OS was designed to be as efficient as possible to fit the lack of limited available resources on smartphones. This fact causes greater complexity as a price for better performance. Descriptors are just a very good example of this trade-off. An overall idea is pretty simple: to have good behaving and predictable classes for string-like data (you may think they are Symbian OS strings). No more buffer overflows and so forth. As a result, you have about a dozen classes that allow manipulating the data effectively in different circumstances.
Consider an inheritance tree for descriptors. The cornerstone of all the business is that the descriptor is a data structure that consists of two main parts: the length of the following buffer and data itself. Thus, you have a well-controlled object that performs all necessary checks to prevent such hazardous operations as out-of-boundary writes. There are few types of descriptions on Symbian OS; they differ in what is allowed to do with their data and where this data is located:
- Non-Modifiable and Modifiable Descriptors: TDesC and TDes
- Non-Modifiable and Modifiable Pointer Descriptors: TPtrC and TDes
- Non-Modifiable and Modifiable Stack-Based Buffer Descriptors: TBufC and TBuf
- Heap-Based Buffer Descriptors: HBufC
The relationships among various descriptor classes may be very confusing, so later sections provide a short overview of every type, along with simple examples of conversion between them.
Base Classes: Non-Modifiable And Modifiable Descriptors
At the very basis of the hierarchy tree stands TDesC, is a base class for all other types of descriptors (excluding literals). In fact, TDesC is a typedef that becomes a 16- or 8-bit class depending on build settings. The default is 16-bit, so you can specify an appropriate type as TDesC8 or TDesC16 when needed.
According to standard naming conventions, a "C" suffix shows that it represents an object whose content can't be changed. This single base class provides common functionality for all other derived classes. 4 bytes are used to hold the length of the data portion of the descriptor available via the Length() method, and access to actual data may be obtained via a Ptr() call. From the whole 32 bits of length data member, only 28 bits really hold data length. The top 4 bits keep the descriptor type; therefore, there is no need for virtual functions because the parent class always knows the current type.
To achieve maximal efficiency, it was decided that the vtable pointer would be undesirable overhead. Thus, TDesC has hardcoded knowledge about the memory layout of all its subclasses. Apart from it, TDesC implements a few common string operations, such as search and comparison. As a bottom line, all constant operations are performed regardless of the type of the given descriptor, by a common TDesC base code. Because TDesC is an abstract class, you can't instantiate it; nevertheless, you may use it as a function parameter or return type, thus allowing you to perform standard operations on constant content.
Next in kin, TDes, inherits from TDesC and implements additional methods to modify the data. Besides, TDes has a member for maximum length of currently allocated data. You easily can shrink and expand data up to maximum length limit.
As it was said above, TDes implements few methods for data modification such as Append, but it is important to keep in mind that none of them allocates memory. It means that if some operation exceeds maximum length of the descriptor, it results in panic. It is left totally for developer's responsibility to take care about sufficient memory allocated.
Copy & Co.: Basic Descriptor Operations
TDesC class contains a lot of methods for comparison, finding, and matching data. You can access a individual character within a descriptor by calling operator. You have few methods for making a comparison and finding which use locale-specific algorithms or folding (in other words, locale independent). Another interesting thing is Match functions that allow you to verify whether some descriptor matches a given pattern, which in turn may contain wildcard characters. And, of course, there are Left(), Right(), and Mid() functions to get you the desired part of the descriptor's data.
Stack-Based Buffer Descriptors
Now, turn to the descriptor classes that you will use in practice. There are really quite a few of them to name. You start from stack-based descriptors as a very simple case. Here, you have two templated classes—TBufC<N> and TBuf<N>—that inherit from TDesC and TDes respectively via a TBufBase pair of classes. These descriptors provide a fixed-size buffer to store their data, so they are useful for constant strings or binary data. You can treat them similarly to "char" declarations.
These descriptors can be instantiated in various ways:
TBufC<12> buf1; // empty _LIT(KBuf2,"Buffer2"); TBufC<12> buf2(KBuf2); // by literal TBufC<12> buf3(buf2); // by another TBufC descriptor TBufC8<12> buf4((TText8*)"Sample"); // by C-string TBuf<12> buf5(buf3.Des()); // by TDesC from non-modifiable // descriptorThe last line above demonstrates a common trick you use widely. The Des() and Ptr() methods help you perform required type conversions in most use-cases.
Well, up to here all is relatively easy, isn't it? Now, see how to modify your data. "Non-modifiable" TBufC's content may be altered in two ways: complete replacement or indirect modification via TPtr (which will be discussed in the very next section). The following snippet shows it in more details:
_LIT(KBuf2,"Buffer2"); TBufC<32> buf2(KBuf2); _LIT(KBuf2,"Buffer2"); TBufC<32> buf3(KBuf3); buf2 = buf3; _LIT(KNewValue, "New Value"); TPtr ptr = buf2.Des(); ptr = KNewValue;
TBufC/TBuf classes doesn't reallocate memory, so when you try to assign a value that exceeds the descriptor's size, it will panic.
Okay, now on to pointer descriptors. They work as you might expect from their name—their data is separated from the descriptor itself, pointing either to the heap, stack, or ROM. Again, pointer descriptors don't care about managing the memory they point to, so it's the complete responsibility of the application developer to deal with it.
Pointer descriptors come in pairs, so you have both non-modifiable and modifiable ones. The non-modifiable descriptor class, TPtrC, is quite similar to a "const char*" declaration; in other words, you can access the data but you can't modify it. TPtrC inherits from TDesC and adds its own member to point to the actual data. Thus, all operations available for TDesC can be performed on TPtrC as well. TPtrC also has a Set() method that allows you to re-assign your descriptor to point to different data. Similarly, TPtr inherits from TDes by adding Set() and operator= to set and modify descriptor data respectively. Hence, you can regard it as a kind of "char*". Here are a few typical usage examples:
_LIT(KOne,"BufC One"); TBufC bufC(KOne); TPtr ptr(bufC.Des()); TPtr ptr2(0,0); ptr2.Set(bufC.Des());
When you really need variable length descriptors, heap-based ones are at your disposal. The HBufC family of classes incapsulates such functionality for you. Inheriting indirectly from TDesC via TBufCBase16, HBufC allows you to increase or decrease data size at runtime. Due to the "C" suffix, data can be accessed but not modified. Nevertheless, you can replace it totally via one of the assignment operators. Data reallocation isn't done automatically, so you need to take care when you really want to reallocate HBufC's buffer with the ReAllocL() method.
Note: If you have gathered a pointer to HBufC data through its Des() method, it may become invalid after reallocation. Thus, it is safer to create a new TPtr object to ensure robustness.
Some trivial examples follow:
_LIT(KTestBuffer,"Heap Based"); HBufC *pHeap = HBufC::NewLC(32); TPtr ptr(pHeap->Des()); ptr = KTestBuffer; CleanupStack::PopAndDestroy();
Just as a good practice, the code snippet above creates pHeap via a HBufC::NewLC call. In other words, it places a newly created object on the cleanup stack to handle possible leaves gracefully.