Author - Andy Weinstein,Modifier - Ender
1. Introduction
This article discusses some issues that a typical Windows C++ programmer will encounter when approaching Symbian OS for the first time. Our experience in developing for three successive versions of Symbian OS has given us considerable perspective on what can be difficult when working in this otherwise rich and stable environment. While one reason for Symbian's success may be the desire of many mobile phone manufacturers not to be tied to Microsoft, the other reason is that Symbian has put together a lightweight, elegant system that succeeds in providing a very impressive range of functionality. Here are some pointers to help ease the transition to successful Symbian OS application development.
2. Documentation and resources
The first thing that average Windows programmers will notice about Symbian OS is that the SDK documentation is not quite at Microsoft's high level of polish. While it's been getting more complete, some API classes still have no documentation. For example, the class CEikRichTextEditor has no independent entry in the documentation. The class CRichText, which contains most of the relevant functionality, is documented, but you might not know that when you first set out to work with the control (see our comments about Object Structure, below). Instead of investing mammoth efforts in completing the documentation, earlier releases relied on the fact that Symbian provided generous amounts of source code (although not the full operating system) and example code. Programmers did not need to guess the behaviour of many APIs - they could simply look at the implementation.
In case that was not sufficient, Symbian also provided source code for utility applications like Word. The combination of API and example source was enough to give most developers all they needed to proceed. From Symbian OS v6.0, 95% of the source code for Symbian OS was made available to members of their Platinum Partner Program (((http://www.symbian.com/partners/part-platnm.html))), which requires an additional licence for which a fee is payable. One consequence of this move is that some of the complete source present on older SDKs is now no longer shipped as standard. Instead, Symbian and/or Nokia have gone the route of providing even more extensive example code, while continuing to improve documentation. For example, the documentation that comes with Series 60 has a separate explanatory entry called "How to manipulate rich text" (though still no entry for CEikRichTextEditor).
There are plenty of other useful, well-written "How to" articles in the documentation. In Series 60, the synergy between documentation and example code is explicit - there are example apps for each family of Avkon UI constructs which are referred to directly by the documentation. A Windows programmer will also have to get used to the relative scarcity of available external resources, although this is improving. 'Professional Symbian Programming' (PSP) contains a lot of useful information, but it isn't particularly helpful as a quick reference, and it was written in the days of Symbian OS v5 - a new version of the
book is expected early in 2003. PSP is not out-of-date yet, but things have changed, especially at the UI level. A newer book, 'Wireless Java for Symbian Devices' is perhaps more up to date, but isn't going to help much with the C++ SDK. Also available are 'Symbian OS Communications Programming' and 'Programming for Series 60 and Symbian OS' (Nov 2002).
More details can be found here: http://www.symbian.com/books/index.html.
On-line documentation and question-answering resources do exist to help fill in the blanks. Symbian itself runs the Symbian Developer Network, a net resource for forums, FAQs, examples, and other useful information. It starts at http://www.symbian.com/developer. Nokia also has a parallel program called Forum Nokia, which is located at http://www.forum.nokia.com. It is not always clear where the right place to look is, and there is no guarantee of getting an answer when you post a question - you're dependent on the goodwill of your fellow developers. Symbian and Nokia personnel do sometimes chime in, but if you want guaranteed answers from the folks who know, you need to sign up for one of the programs, which costs money. For Symbian, check out Partner Programs at http://www.symbian.com/partners/part-servs.html and for Nokia, try Forum Nokia (www.forum.nokia.com), and then look under Developer Support, Technical Case Solving. But first try the free resources: there's plenty of useful, current information and resources available, including timely SDK updates.
3. Object structure
One aspect of Symbian OS that takes some getting used to is its very strong object structure. For example, a list box isn't one object - it's four: the list box object, the model, the view, and the drawer. The division of functionality between them is fairly predictable and logical. The edit control is considerably more complex. At first glance, it seems to have only two main objects: the UI control and a text object which it owns which handles text formatting. But manipulating the formatting brings in further objects for character formatting and paragraph formatting. These, in turn, make use of separate mask
objects for indicating to the API calls which aspects of the formatting you wish to address on any given call. So code which is building text that alternates between bold and not bold, and which also wants to affect line spacing, needs to make use of CEikRichTextEditor, CRichText, TCharFormat, TCharFormatMask, CParaFormat, and CparaFormatMask! Here's a code fragment that does just that:
TCharFormat defaultFormat;
TCharFormatMask formatMask;
formatMask.SetAttrib(EAttFontStrokeWeight);
CRichText* text = iDisplay->RichText();
text->Reset();
for (int i = 0; i < 10; ++i)
{
TPtrC boldText = getBoldPiece;
TPtrC plainText = getRomanPiece;
...
TInt insertPos = text->DocumentLength();
text->SetInsertCharFormatL(*iCharFormat, formatMask,
insertPos);
text->InsertL(insertPos, boldText);
text->CancelInsertCharFormat();
insertPos = text->DocumentLength();
text->SetInsertCharFormatL(defaultFormat, formatMask,
insertPos);
text->InsertL(insertPos, plainText);
text->CancelInsertCharFormat();
}
CParaFormat paraFormat;
TParaFormatMask paraFormatMask;
iDisplay->RichText()->GetParaFormatL(¶Format, paraFormatMask,
0, iDisplay->TextLength());
paraFormatMask.ClearAll();
paraFormatMask.SetAttrib(EAttLineSpacing);
paraFormatMask.SetAttrib(EAttLineSpacingControl);
paraFormat.iLineSpacingControl =
CParaFormat::ELineSpacingExactlyInTwips;
CGraphicsDevice* screenDevice = iEikonEnv->ScreenDevice();
TInt paraDelta;
...
TInt lineHeight = screenDevice->VerticalPixelsToTwips(
iRegularFont->HeightInPixels() + paraDelta);
paraFormat.iLineSpacingInTwips = lineHeight;
iDisplay->RichText()->
ApplyParaFormatL(¶Format, paraFormatMask,
0, iDisplay->TextLength());
But that's not all - there is another little world of text view objects, specifically CTextView and CTextLayout, together with their helper objects. When we wanted to position the scroll point in the edit control so that the last line of text would be one line above the bottom of the edit control, we had to learn that CTextView existed, how to get one, and about CTextView::SetViewL and TViewYPosQualifier::SetHotSpot and TViewYPosQualifier::SetMakeLineFullyVisible. Here's how it looks:
TInt yPos = iDisplay->TextView()->ViewRect().iBr.iY;
TViewYPosQualifier yPosQualifier;
yPosQualifier.SetHotSpot(TViewYPosQualifier::EFViewBottomOfLine);
yPosQualifier.SetMakeLineFullyVisible(
TViewYPosQualifier::EFViewForceLineFullyVisible);
iDisplay->TextView()->SetViewL(
iDisplay->TextLength(),
yPos,
yPosQualifier,
CTextView::EFViewDiscardAllFormat);
Who would have imagined? One of our staff simply couldn't believe that this was the only flicker-free way of doing the job, but he proved it by trying all the various alternatives which suggested themselves, based on the many (and varied) APIs available in the various classes. It's important to understand that the kind of complexity we're describing is a reflection of the richness of functionality
provided by Symbian OS. Once you learn the territory, you'll appreciate how sensible the Symbian OS Object Structure is, and soon you'll find yourself able to predict where the functionality you want resides. Time after time, we found that our initial perplexity was replaced by aesthetic appreciation.
4. Strings
Strings. Aaah, strings in Symbian. There is no question that Symbian's implementation of Strings is well thought out, robust, and economical. There is also no question that it is a royal pain. Strings are implemented using what Symbian calls "Descriptors," and the following classes are involved: TDesC, TBufCBase, TDes, TPtrC, TBufC, HBufC, TBuf and TPtr. And that doesn't include literals, handled by TLitC, which is not, strictly speaking, a type of descriptor. I have also not yet mentioned Unicode, though that's taken care of pretty transparently. This set of classes lets you use the precise minimum amount of memory necessary for storing different kinds of strings, while allowing them to interoperate cleanly. But, it's really no fun having to think about which one to use every time, and there is no avoiding it because different APIs may expect or return different types. Here's a trivial example that puts a name into a boilerplate message:
_LIT(KBoilerplate, "Hello there, %S");
TPtrC name =
GetPointerIntoNameDescriptorWithoutAllocatingAnyMemory();
TPtr finishedProduct =
HBufC::NewLC(KBoilerplate().Length() + name.Length())->Des();
finishedProduct.Format(KBoilerplate(), &name);
There is no class provided which is the equivalent of an MFC CString or a Java String - a fully dynamic string for all seasons. The Java split between String and StringBuffer can give only the faintest flavour of what's going on here. To Symbian's credit, they do document all of this well, but it's documentation that you'd really prefer not to have to read so often. Symbian was thinking about limited resource machines when they wrote this, and there is no doubt that for some applications on some platforms it is necessary. But for now, a Symbian OS C++ programmer is going to feel like early IBM PC programmers, dealing with Intel's segmented architecture while the 68000 with its big, flat address space was making Apple look like a much cooler alternative.
5. Cleanup stack and error handling
The Symbian OS error handling framework, and principally the cleanup stack, is an element that will be unfamiliar to Windows programmers. Like Descriptors, it's elegant, and Symbian claims that is much more efficient than C++ exception handling. There is a good discussion of this in the document 'Coding idioms for Symbian OS', available at http://www.symbian.com. But, what is almost certainly "the right thing" at the system level ends up slowing down application writing. Symbian designed this framework for applications that need to run for a long time under limited resources, and there's a price paid for that robustness in application development time. The use of a TRY/CATCH type of framework is certainly welcome and straightforward, but the need to constantly push certain types of allocated memory onto the cleanup stack and pop them off cleanup stack at the right time means that you're bound to get it wrong once in a while. When this happens, you'll spend time tracking down the asymmetry which has crept into your code.
As an example of how a programmer unfamiliar with Descriptors and the Cleanup Stack can get into trouble, let's simply recall our string example:
_LIT(KBoilerplate, "Hello there, %S");
TPtrC name =
GetPointerIntoNameDescriptorWithoutAllocatingAnyMemory();
TPtr finishedProduct =
HBufC::NewLC(KBoilerplate().Length() + name.Length())->Des();
finishedProduct.Format(KBoilerplate(), &name);
This piece of code actually does put memory on the cleanup stack, but a cursory look at the datatypes of the variables will not tell you that. That's because we've used the convenient idiom of defining a TPtr to hold finishedProduct and assigning it the value of HBufC::NewLC(...)->Des(). An HBufC is a sign of memory allocated on the heap, but if we only are looking on the left, we might miss the fact that we had taken the shortcut of using Des() as soon as we had allocated. NewLC also puts the allocated memory directly onto the cleanup stack. Once we're done with finishedProduct, we'd better call to CleanupStack::Pop(), or we'll have an imbalance on our cleanup stack. The problem is, we might not detect that imbalance until later, when it causes an error. There are more enjoyable things than tracking down these kinds of asymmetries.
6. Application framework
While Symbian OS does provide a set of classes for building applications, there is very little framework code for building application screens that aren't dialog boxes. Things like handling focus changes have to be done 'manually'. Again, the examples are certainly adequate for understanding how to do this, but the fact that they have to be taken care of every time a screen is written is time-consuming and bug-prone. Starting in Symbian OS v6.1, Symbian OS added a very basic mechanism for generically switching between screens via unique IDs, but there's much more to be done.
Here's a simple example at the most basic level: there is no API to add a control to a screen. Instead, your view class has two functions called CountComponentControls and ComponentControl, where the latter must return a pointer to a UI control for every integer value from 0 below the number returned by the former function. To add a control, aside from constructing it, sizing it, and activating it in the proper places, you must increment, in your code, the number returned by CountComponentControls, and add another case to the switch statement in the function ComponentControl.
Symbian OS does provide a perfectly reasonable way of constructing dialogs from a resource file. When you declare your dialog in a resource file, you do not need to address any of these issues directly. But because dialogs only allow for laying out the controls in a single vertical column, they are not usually adequate for the complex screens of more sophisticated applications.
7. Windowing issues
Symbian OS discourages applications from using 'window-owning' controls. That is to say, a control doesn't usually actually have a window of its own, (in the sense of a graphic object managed by a window system enforcing z-order and clipping). Because such objects use system resources, the application is instead supposed to simply implement a proper drawing function that will respect the boundaries of the rectangle in which it has been positioned. This can be unintuitive to the developer used to having a 'real' window for every control, but it's not too hard to get the hang of this approach.
While the standard Symbian OS UI control set is rich and robust, there are occasional glitches. In one instance, we had an edit box with a scroll bar and wanted to be able to put another control in place of it temporarily. It was inadequate to simply make the edit box invisible. Some of the contents of the edit box, including the scroll bar, still 'showed through' in various interesting but unacceptable ways. We think that this may be due to the complex object structure of the edit box mentioned above; it seems that implementing invisibility in such a distributed structure is non-trivial. We eventually got around the problem by repositioning the control offscreen.
Along the way, we explored some other possibilities and learned about things like the control stack and what Symbian calls Popout Windows, which later came in handy for implementing a popout choice list activated by a button click. Here's how:
iPopout = new (ELeave) CEikColumnListBox;
iPopout->ConstructL(NULL,CEikListBox::EPopout);
iPopout->CreateScrollBarFrameL();
iPopout->ScrollBarFrame()->SetScrollBarVisibilityL(
CEikScrollBarFrame::EOff,CEikScrollBarFrame::EAuto);
iPopout->SetObserver(this);
TSize popoutSize = TSize(width, height);
TPoint popoutOrigin = TPoint(topLeft, bottomRight);
iPopout->Model()->SetItemTextArray(iArray);
iPopout->Model()->SetOwnershipType(ELbmDoesNotOwnItemArray);
iPopout->ItemDrawer()>ColumnData()>
SetColumnWidthPixelL(0, firstColWidth);
iPopout->ItemDrawer()>ColumnData()>
SetColumnWidthPixelL(1, secondColWidth);
iPopout->SetExtent(popoutOrigin, popoutSize);
iPopout->SetCurrentItemIndex(0);
iEikonEnv->AddDialogLikeControlToStackL(iPopout);
iPopout->ActivateL();
iPopout->DrawNow();
An early and interesting learning experience had to do with writing the code to refresh and redisplay the contents of a list box. The refresh code was complex, and it was not trivial to keep track of all insertions and deletions. At any rate, we wanted to have code which, at the end of the refresh process, would redisplay properly regardless of what had been refreshed, while not losing scroll position and selection or creating unnecessary flashing or blinking.
Symbian OS provides, for its base list box class, the functions HandleItemRemovalL and HandleItemAdditionL. There is also a function for setting the current item SetCurrentItemIndex. But in Symbian OS v5, there was no documentation of how these functions worked. We had to study the source code to determine that HandleItemAdditionL does an implicit DrawNow, whereas HandleItemRemovalL does not
redraw. It also had to be understood from reading the code for Reset, called by HandleItemRemovalL, that the current item index needed to be refreshed after calling HandleItemRemovalL. The asymmetry in handling redraw still seems strange and potentially problematic. But the good news is that in the current Symbian OS documentation, all of this is now documented clearly and explicitly right where you'd expect to find it.
As with other aspects of Symbian that we've touched on, in the area of graphics some amount of application coding overhead is added on in order to keep overall system overhead low. What Symbian does provide is elegant and mostly consistent, and is now documented well enough to make it relatively straightforward to produce smooth graphic behavior.
8. Messages, asynchronous services, and active objects
While MFC has done a nice job hiding the Windows messaging paradigm from novices, advanced Windows programmers are well aware of the message loop and dispatch mechanisms that drive their applications. In Symbian OS, there is an entirely different and quite powerful mechanism called active objects driving applications. It works closely with the Symbian OS client-server architecture for providing all kinds of system services and is also available to programmers for creating clean, standard interfaces to any asynchronous services they might need to create for their application.
To be very brief: a CActiveScheduler fills the role of a Windows message loop, providing cooperative multi-tasking within a given thread, and CActive (an active object) fills the role of a message handler. In the Symbian UI framework, this is hidden from the programmer even better than MFC hides messages: the equivalents of incoming messages are always dispatched by the framework to pre-defined methods which can be overridden by the application - for example, OfferKeyEventL (for handling keyboard input). A timer is an example of a simple system service that uses the active object framework. Instead of the Windows style of calling StartTimer which causes WM_TIMER messages to be sent, in Symbian OS a CTimer object interacts with a system timer service, RTimer, and its RunL function is called according to the intervals specified via its API. The programmer derives an object from CTimer and overrides the RunL method.
Other asynchronous system services such as the Camera on the Nokia 7650 are made available in a similar way - CCameraManager defines an active object for requesting camera operations and being informed of their progress. The actual asynchronous services are provided by RCameraServ, but the typical programmer is shielded from having to deal with any client-server or explicit inter-process communication issues. When using active objects, it's important to keep in mind the fairly simple rules for when an active will be run by the scheduler. That is, when will CActiveScheduler call the RunL method of a CActive object?
- the CActive object must be 'active'. This is achieved by calling Cactive::SetActive, which is usually done by the active object itself in response to a request from the application to do something
- the status of the CActive object, held in the member variable iStatus, must not be KRequestPending. That value indicates that the object is still waiting for the service to complete. The variable iStatus is usually set to KRequestPending by the service provider at the time a service request is made. The variable iStatus is subsequently updated by the server provider when it has terminated
- A signal needs to be received by the CActiveScheduler - the signal is sent by the asynchronous service when it completes
A stray signal can occur if an asynchronous process signals your application that it has completed, but there is no CActive object ready to run. This can happen in several circumstances
- you forgot to make your active object 'active' by calling CActive::SetActive
- the service provider, when signaling the termination of the operation, forgot to set the iStatus flag to a value that isn't KRequestPending. This can only happen if you are writing the service provider, which usually isn't the case - typical applications will use system-defined services
- the service provider sends more than one termination signal for the same operation.
The third scenario can occur in the case where a service provider has not been written carefully. For example, say a service has already completed and signaled the client of that fact. Then the client, just before receiving this information, sends a request that the service be cancelled. The service provider must ignore this request, which is now irrelevant. What if the service provider does not ignore this request, but instead responds to it with an additional signal indicating that it has terminated? In such a case, the additional signal will end up being a stray signal, and cause an application error. This can be difficult to debug, because the stray signal is detected by CActiveScheduler, with no indication of what CActive object is responsible.
A CActive object must ensure proper termination of the service associated with it in all circumstances. It should typically have a call to CActive::Cancel in its destructor. If Cancel isn't called, and the object is deleted while a request is still pending, an error will be raised. In addition, each CActive object must implement CActive::DoCancel in a way that ensures that any service which was requested by this object will be cancelled, otherwise CActive::Cancel will hang waiting for the termination signal from the service provider. Both of these kinds of errors can be hard to detect, simply because they will only be observed when there is an outstanding request.
Conclusion
We hope that this article has been helpful in both setting expectations and offering helpful hints to Windows programmers getting started with Symbian OS. We're looking forward to watching how future Symbian OS releases evolve as this new market grows, and new programming paradigms develop with it.
Andy Weinstein, andyw@degel.com, is a Senior Applications Analyst at Degel Software Ltd., the "Software SWAT Team" - a consultancy of highly experienced software professionals covering a broad range of technologies, with a recent emphasis on handheld platforms. For more information, see http://www.degel.com.
Published with permission of Degel Software Ltd. © Degel Software Ltd. 2002.
Symbian Ltd makes no warranty about the suitability of the information contained in this document for any purpose. The views contained in this document are those of the writer and Symbian Ltd accepts no responsibility for any statement contained herein.
Symbian licenses, develops and supports Symbian OS, the platform for next-generation data-enabled mobile phones. Symbian is headquartered in London, with offices worldwide. For more information see the Symbian website, http://www.symbian.com/.
Trademarks and copyright
'Symbian', 'Symbian OS' and other associated Symbian marks are all trademarks of Symbian Ltd. Symbian acknowledges the trademark rights of all third parties referred to in this material. © Copyright Symbian Ltd 2002. All rights reserved. No part of this material may be reproduced without the express written permission of Symbian Ltd.