Namespaces

Variants
Actions

Please note that as of October 24, 2014, the Nokia Developer Wiki will no longer be accepting user contributions, including new entries, edits and comments, as we begin transitioning to our new home, in the Windows Phone Development Wiki. We plan to move over the majority of the existing entries over the next few weeks. Thanks for all your past and future contributions.

Fundamentals of Symbian C++/Symbian C++ Miscellany

From Wiki
Jump to: navigation, search
Article Metadata
Article
Created: hamishwillee (10 Jan 2011)
Last edited: hamishwillee (23 Jul 2012)

This section discusses assorted miscellaneous features of the Symbian C++ platform: DLLs, UIDs, process global data (known as writeable static data/WSD), executables, panics, assertions and templates.

Contents

About UIDs

A UID is a 32-bit value used for many different purposes within the Symbian platform, one of which is to identify a file type, with a unique combination of up to three UIDs being used to identify a binary executable file:

UID1

  • This is the system-level identifier, which distinguishes between EXEs and DLLs. The value is built into code by the build tools depending on the keyword used with the targettype identifier in the MMP file (see here for more information).
  • For example, the targettype specified for a shared library is DLL. This means that UID1 is set by the build tools to be KDynamicLibraryUid (0x10000079). An application has UID1 set to be KExecutableImageUid (0x1000007a) by specifying a targettype EXE.

UID2

  • This UID is used to differentiate between shared library and polymorphic interface DLLs. For shared libraries, UID2 is KSharedLibraryUid (0x1000008d) but the value for polymorphic DLLs varies depending on their plug-in type (for example, the socket server protocol module UID2 value is 0x1000004A).
  • UID2 is not relevant for targettype EXE and can simply be set to 0 or omitted. Note that you will sometimes see the UID2 of an application set to 0x100039CE – this value is used because historically applications were polymorphic DLLs and the application framework used the UID2 value to identify them. While this causes no harm, because applications are executables, it is better to use 0 or omit the value altogether.

UID3

The third UID value identifies a file uniquely. To ensure that each binary has a different value, Symbian manages UID allocation through a central database. You must register with Symbian Signed to request UIDs for use as UID3 values in production code, although you can assign yourself a value from the development range for test code.

The choice of UID range to use for your application is discussed later but we list the full range below for completeness. UIDs are allocated on request by Symbian Signed (www.symbiansigned.com).

UID Range Intended Use Type
0x00000000 KNullUID Protected
0x00000001 – 0x0FFFFFFF Reserved Protected
0x10000000 – 0x1FFFFFFF Legacy allocated UIDs, not for V9 Protected
0x20000000 – 0x2FFFFFFF UID3/SID range Protected
0x30000000 – 0x6FFFFFFF Reserved Protected
0x70000000 – 0x7FFFFFFF Vendor IDs Protected
0x80000000 – 0x9FFFFFFF Reserved Unprotected
0xA0000000 – 0xAFFFFFFF UID3/SID range Unprotected
0xB0000000 – 0xE0FFFFFF Reserved Unprotected
0xE1000000 – 0xEFFFFFFF Development/testing range Unprotected
0xF0000000 – 0xFFFFFFFF Legacy UID compatibility range Unprotected


Exporting Functions From a DLL

A shared library DLL provides access to its APIs by exporting its functions so that separate executables (that is, DLL or EXE code compiled into a separate binary component) can call them. This makes the functions 'public' to other modules by creating a .lib/.dso file, which contains the export table to be linked against by the calling code.

Every function to be exported should be marked in the class definition in the header file with the macro IMPORT_C. The client code will include the header file, so they are effectively 'importing' each function into their code module when they call it. The corresponding function implementation in the .cpp file should be prefixed with the EXPORT_C macro. For example:

class CMyExample : public CSomeBase
{
public:
 
IMPORT_C static CMyExample* NewL();
 
public:
 
IMPORT_C void Foo();
 
...
 
};
 
EXPORT_C CMyExample* CMyExample::NewL()
 
{...}
 
EXPORT_C void CMyExample::Foo()
 
{...}

The rules as to which functions should be exported are as follows:

  • Inline functions must never be exported, because there’s no need to do so. The IMPORT_C and EXPORT_C macros add functions to the export table to make them accessible to components linking against the library. However, the code of an inline function is, by definition, already accessible to callers, since it is declared within the header file (and the compiler interprets the inline directive by adding the code directly into the client code wherever it calls it).
  • Only functions which need to be used outside a DLL should be exported. The use of IMPORT_C and EXPORT_C adds an entry to the export table in the module definition (.def) file. If the function is private to the class and can never be accessed by client code, exporting it merely adds it to the export table unnecessarily.
  • All virtual functions, whether public, protected or private, should be exported, since they may be re-implemented by a derived class in another code module. Any class that has virtual functions must also export a constructor, even if it is empty.

Non-Shareable Classes

A class may be declared as non sharable using the NONSHARABLE_CLASS macro. Declaring a class as non sharable prevents the compiler from exporting compiler implementation-specific symbols, i.e. runtime type information and virtual tables. This prevents classes in other DLLs from being able to derive from it.

The following code fragment shows how a non-sharable class is declared.

NONSHARABLE_CLASS(CMyClass) : public CBase
{
public :
...
 
private :
...
 
}

Lookup By Ordinal and By Name

On the Symbian platform, the size of DLL program code is optimized to save ROM and RAM space. In most operating systems, to load a dynamic library, the entry points of a DLL can either be identified by string-matching their name (lookup by name) or by the order in which they are exported in the module definition file (lookup by ordinal).

Symbian C++ does not offer lookup by name, because this adds an overhead to the size of the DLL (storing the names of all the functions exported from the library wastes limited ROM and RAM space). Instead, Symbian C++ only uses link by ordinal. This has significant implications for binary compatibility; ordinals must not be changed between one release of a DLL and another. For example, code which links against a library and uses an exported function with a specific ordinal number in an early version of the library will either result in a panic or call a completely different function.

Binary compatibility is discussed further here

The one type of virtual function which should not be exported from a DLL is a pure virtual function, because there is generally no implementation code for a pure virtual function, so there is no code to export.

DLLs

Shared Libraries and Polymorphic Interface DLLs

Dynamic link libraries (DLLs) consist of a library of compiled C++ code that may be loaded into a running process in the context of an existing thread. In Symbian C++, there are two main types of DLL: shared library (static interface) DLLs and polymorphic interface (plug-in) DLLs.

A shared library DLL implements library code that may be used by multiple components of any type, that is, other libraries or EXEs. The filename extension of a shared library is .dll – examples of this type on Symbian C++ are the user library (EUser.dll) and the file system library (EFile.dll).

Symbian C++ uses link-by-ordinal.

A shared library exports API functions according to a module definition (.def) file. It may have any number of exported functions, each of which is an entry point into the DLL. A shared library releases a header file (.h) for other components to compile against, and an import library (.lib or .dso) to link against, in order to resolve the exported functions.

When executable code that uses the library runs, the loader component loads any shared library DLLs that it links to, and any further DLLs that those DLLs require, recursively, until all shared code needed by the executable is loaded.

The second type of DLL, a polymorphic interface DLL, implements an abstract interface that is usually defined separately, for example by a framework. It may have a .dll filename extension, but it can use a different extension to identify the nature of the DLL further, for example, .fsy for a file system plug-in, or .prt for a protocol module plug-in.

Polymorphic DLLs have a single entry-point 'gate' or 'factory' function, which instantiates the concrete class that implements the interface. Polymorphic interface DLLs are often used to provide a range of different implementations (plug-ins) of a single consistent interface. They are loaded dynamically, typically by a framework.

Plugins are implemented using ECOM. ECOM is a generic framework for specifying interfaces, and for finding and loading those plug-ins that implement them. It is possible to write a ‘proprietary’ type of polymorphic interface DLL but ECOM provides much functionality and a standardized way of implementing plug-in frameworks (code which uses a proprietary mechanism is likely to pre-date the introduction of ECOM into the Symbian C++ platform).

Writeable Static Data

Writeable Static Data Defined

Global writeable static data is any per-process modifiable variable which exists for the lifetime of the process. In practice, this means any globally scoped data declared outside of a function, struct or class, as well as function-scoped static variables.

The only global data that can be used within DLLs (assuming EPOCALLOWDLLDATA has not been defined within the MMP file) is constant global data of the built-in types, or of a class with no constructor. These definitions are acceptable:

static const TUid KUidFooDll = { 0xF000C001 };
 
static const TInt KMinimumPasswordLength = 6;

These cannot be used:

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

The last set of examples above cannot be used because they have non-trivial class constructors, which require the objects to be constructed at run-time. This means that, although the memory for the object is pre-allocated in code, it doesn’t actually become initialized and constant until after the constructor has run. Thus, at build time, each constitutes a non-constant global object and causes the build to fail for phone hardware.

The following object is also non-constant because, although the data pointed to by ptr is constant, the pointer itself is not constant:

// Writable static data!
static const TText* ptr = (const TText*)"data";

This can be corrected by making the pointer constant:

static const TText* const ptr = (const TText*)"data";

Symbian C++ supports global writeable static data in EXEs, but it is not recommended if code is going to be loaded into a lot of processes, as is the case with a shared DLL, (because this makes it expensive in terms of memory usage). WSD is generally acceptable for third-party code because most third-party code is only loaded into one process.

Further details on WSD, including workarounds, can be found at Symbian Platform Support for Writeable Static Data in DLLs.

In order to enable global writeable static data, the EPOCALLOWDLLDATA keyword must be added to the MMP file of a DLL. Where this is not used, the PETRAN build tool will return an error when the DLL code is built for the phone hardware.

Panics and Assertions

Symbian C++ leaves (see Leaves and the Cleanup Stack) occur when unexpected conditions occur that the code should be able to handle gracefully. Good examples are when there is insufficient heap memory for an allocation or when a communications link is dropped. Leaves are implemented in terms of standard C++ exceptions, and code implements a recovery strategy, and maybe rollback, by catching the leaves using trap harnesses (TRAPs).

Programming errors are bugs caused by incorrect assumptions, typographic or implementation errors. Examples include writing off the end of an array or making requests on a server in an incorrect sequence (such as trying to write to a file before opening it). Programming errors are persistent and predictable once the code is compiled (they happen deterministically). Assertions are used to check programming logic and highlight errors by terminating a thread using a panic. Unlike leaves, panics cannot ever be caught and handled gracefully – though it is possible to run code in a separate thread if deemed necessary and monitor it for panics, without bringing down the whole process. This is a good strategy in a robust plug-in system.

Panics

Assertions are implemented using panics. Panics are raised with a category and a panic number; the category identifies the system component which has raised the panic, and the number identifies the reason for the panic. An un-trapped exception in user code will also be converted into a panic when the system is unable to find a handler. Unlike a leave, a panic can’t be trapped because it terminates the thread. In fact, if a panic occurs in the main thread of a process, it terminates the process too. If the panic happens in a secondary thread, only that thread is terminated.

On phone hardware, a panic is typically seen as an Application closed message box. When debugging on the emulator builds, the panic breaks the code into the debugger by default, and so you can look at the call stack and diagnose the problem.

To cause a panic in the current thread, the Panic() method of class User can be used. Because of the restrictions of the secure platform, it is not possible to panic other threads in the system except those running in the same process. (One exception is made for server threads, which may panic badly behaved client threads by using the RMessagePtr2::Panic().) To panic another thread in the running process, your code should open an RThread handle and call RThread::Panic().

User::Panic() and RThread::Panic() take a string parameter, which is used to specify the reason for the panic, and an integer panic code, which can be any value, positive, zero or negative. The panic string should be short and descriptive. They are not intended for a user to see, only for you and other programmers during development.

Symbian C++ has a set of well-documented panic categories (for example, KERN-EXEC, E32USER-CBASE, ALLOC, USER), the details of which can be found in the reference section of the Symbian Developer Library. Developers should choose meaningful names for their panics because it makes diagnosing problems a little easier.

The following example shows the use of a panic to highlight a programming error. Enough information is provided so a developer can understand the problem and work out how to fix it:

void CTest::GetData(TDes& aBuffer)
{
// Check that the buffer passed in is sufficiently large
// to contain the data – caller should have allocated KLengthNeeded.
 
if (aBuffer.Length()<=KLengthNeeded)
{
_LIT(KCTestGetData, "CTest::GetData");
User::Panic(KCTestGetData, KErrArgument);
}
else
...
 
}

In fact, this kind of checking should be carried out by using an assertion statement, as we’ll cover in the next section.

Assertions

Assertions are fatal errors that are not propagated but cause an immediate halt at the error location. Symbian OS provides the macros __ASSERT_DEBUG, ASSERT and __ASSERT_ALWAYS. Assertions are probably the most effective way to check for programming errors in user code, by forcing the program to fail at the point of the error. Typically, they are used to define pre- and post-conditions for code blocks.

There is an assertion macro for debug builds only (__ASSERT_DEBUG) and another for both debug and release builds (__ASSERT_ALWAYS). However, you should carefully consider the use of assertions in release builds because the checking will have an impact on the code size and execution speed. If the assertion fails, the code will terminate, potentially losing all user data, and the user will see an ugly Application closed message box. If you’ve tested your code in debug builds sufficiently, you shouldn’t need to include assertions in release builds.

Both assertion macros takes a statement as their first parameter. They evaluate this and, if it proves false, it uses the second parameter passed to flag up the failure. The assertion macros are unfortunately not hard-coded to panic, but they should be. The assertion should always terminate the code at the point of failure to help you, the developer, detect the problem where it occurs.

Here is an example of how to use the debug assertion macro:

void CTestClass::SetCount(TInt aCount)
{ // Check aCount >=0 because it shouldn’t be negative.
 
#ifdef _DEBUG
_LIT(KPanicDescriptor, "CTestClass::SetCount");
#endif
 
__ASSERT_DEBUG((aCount>=0),
 
User::Panic(KPanicDescriptor, KErrArgument));
 
... // Use aCount.
}

It is common to define a panic function and a set of specific panic enumerators that can be re-used by an entire class or application. For example, the following could be added to CTestClass:

enum TTestClassPanic
{
 
ESetCountInvalidArgument, // Invalid argument passed to SetCount().
 
... // Enum values for assertions in other
 
// CTestClass methods.
 
};
 
static void CTestClass::Panic(TTestClassPanic aCategory)
{
_LIT(KTestClassPanic, "CTestClass");
User::Panic(KTestClassPanic, aCategory);
}
 
The assertion can then be written as follows:
 
void CTestClass::SetCount(TInt aCount)
{
__ASSERT_DEBUG((aCount>=0), Panic(ESetCountInvalidArgument));
... // Use aCount.
}

By making sure that the panic string is clear and using unique and well-named/commented enumeration values for the panic code, developers using an API and receiving a panic can easily track down what the problem is when their code terminates unexpectedly during development.

In addition to __ASSERT_ALWAYS and __ASSERT_DEBUG, there is also the ASSERT macro, which is very useful for checking internal code logic. You don’t need to pass a descriptor or an error value, but simply pass in the statement for test. In debug builds, this is evaluated and a USER 0 panic is raised if the result is EFalse. It’s not helpful for external calling code to receive this panic, since it’s difficult to track down the reason, but it is useful when you’re writing code and want to check any errors as you test. For example, it can be used in a switch statement to ensure that the logic of a state machine is correct:

// State machine.
 
void TLongRunningCalculation::DoTaskStep()
{ // Do a short task step.
 
switch (iState)
{
case (EWaiting):
iState = EBeginState; break;
case (EBeginState):
iState = EIntermediateState; break;
case (EIntermediateState):
iState = EFinalState; break;
case (EFinalState):
iState = EWaiting; // Finished
break;
 
default:
ASSERT(EFalse); // Cause a panic! Should never get here.
}
}

Templates

C++ templates allow for code re-use, for example when implementing container classes such as dynamic arrays that may contain different types. The use of templates makes the code generic and re-usable.

template<class T>
 
class CDynamicArray : public CBase
{
 
public:
 
... // Functions omitted for clarity.
 
void Add(const T& aEntry);
T& operator[](TInt aIndex);
};

The use of templates is preferable to an implementation using void* arguments and casting because templated code can be checked for type safety at compile time. However, template code can lead to increased code size because separate code is generated for each templated function for each type used. For example, if an application uses the CDynamicArray class to store an array of HBufC* and a separate array of TUid values, there would be two copies of the Add() function generated, and two copies of operator[]. On a limited-resource system, templates can have a significant and undesirable impact on code size, and you should avoid using them except as Symbian C++ does, through the thin template pattern.

Thin Templates

The thin template pattern gives the benefits of type-safety, while reducing the amount of duplicated object code at compile time. This idiom is also known as template hoisting and Symbian C++ uses it in all its container classes, collections and buffers.

The thin template idiom implements the container using a generic base class with TAny* pointers. A templated class is then defined that uses private inheritance of the generic implementation. (If your C++ is a bit rusty, private inheritance allows implementation to be inherited, but the methods of the base class become private members. The deriving class does not inherit the interface of the base class so the generic implementation can only be used internally and not externally, which avoids accidental use of the non-type safe methods.)

Instead of the generic interface, the derived class presents a templated interface to its clients and implements it in-line by calling the private base class methods. The use of templates means that the class can be used with any type required and that it is type-safe at compile time. There are no additional runtime costs because the interfaces are defined in-line, so it will be as if the caller had used the base class directly. Only a single copy of the base class code is generated because it is not templated, so the code size remains small, regardless of the number of types that are used. This is ideal, if a bit confusing. Let’s look at an example to make it clearer.

A snippet of the Symbian C++ RArrayBase class and its deriving class RArray are shown below (the RArray class is discussed here). RArrayBase is the generic base class that implements the code logic for the array, but the code cannot be used directly because all its methods are protected.

class RArrayBase
{
 
protected:
 
IMPORT_C RArrayBase(TInt aEntrySize);
IMPORT_C RArrayBase(TInt aEntrySize,TAny* aEntries, TInt aCount);
IMPORT_C TAny* At(TInt aIndex) const;
IMPORT_C TInt Append(const TAny* aEntry);
IMPORT_C TInt Insert(const TAny* aEntry, TInt aPos);
...
};

The derived RArray class is templated, defining a clear, usable API for clients. The API is defined inline and uses the base class implementation which it privately inherits from the base class. Elements of the array are instances of the template class, as was described earlier in this chapter. Also RArray has a special template override for TInt which is a special-case optimization for 32-bit datatypes (one ARM word).

template <class T>
class RArray : private RArrayBase
{
public:
...
inline RArray();
inline const T& operator[](TInt aIndex) const;
inline T& operator[](TInt aIndex);
inline TInt Append(const T& aEntry);
inline TInt Insert(const T& aEntry, TInt aPos);
...
};
 
template <class T>
 
inline RArray<T>::RArray() : RArrayBase(sizeof(T))
{}
 
template <class T>
inline const T& RArray<T>::operator[](TInt aIndex) const
{return *(const T*)At(aIndex); }
 
template <class T>
inline T& RArray<T>::operator[](TInt aIndex)
{return *(T*)At(aIndex); }
 
template <class T>
inline TInt RArray<T>::Append(const T& aEntry)
{return RArrayBase::Append(&aEntry);}
 
template <class T>
inline TInt RArray<T>::Insert(const T& aEntry, TInt aPos)
{return RArrayBase::Insert(&aEntry,aPos);}
This page was last modified on 23 July 2012, at 07:48.
81 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×