Using Qt and Symbian C++ code together in an application requires the use of good object-oriented software design principles.
This document assumes you have some basic knowledge of Symbian C++ programming. Essential information about Symbian C++ can be found in Introduction to Symbian C++ development.
Many of the device services provided on Symbian are available to your Qt applications through the Qt Mobility APIs and so some of your applications may not need to contain any Symbian C++ code. However, some Symbian device services and features are not accessible by using Qt and Qt Mobility APIs. To access these, you must use native Symbian C++ code. This document describes a design pattern for using Qt and Symbian C++ code together in a Qt application.
The essential rule is: do not mix Qt and Symbian C++ code in the same source file. The programming idioms for Qt and Symbian C++ are very different and you should not mix them. Ideally, write your application modules using Qt, and use native Symbian C++ code only where necessary. Provide clean interfaces between your Qt code and your Symbian C++ code.
If Qt does not provide an API for a feature or service that is available on a specific platform, the recommended way to access that service is to write a generic, cross-platform wrapper API and provide a private platform specific implementation of that API for each target platform. By isolating platform-specific code behind an interface, you can prevent widespread platform dependencies in your code. This facilitates porting applications that use the feature between different platforms. Using this approach, you can write most of your application code in the easy-to-use Qt programming idioms, and confine the platform specific code to individual code modules. This is the technique that Qt uses to abstract the underlying operating system so that developers do not need to be aware of each platform's native programming idioms, APIs, installation mechanisms, build systems and limitations.
There are a number of different design patterns you could use to structure your code. In this document we discuss the PIMPL (Pointer to Implementation) design pattern, in which platform-specific implementations are hidden behind a private pointer in a public API. PIMPL has a number of different names in design pattern literature, including "Handle-Body", "Compiler Firewall", "the Bridge", "Opaque Pointers", and "Cheshire Cat".
Note: You can use other design
patterns and techniques in your code. For example, in Qt most of the QPixmap
API is implemented in a common source file, but QPixmap::grabWindow()
is implemented in platform-specific
source files. This approach is acceptable provided the platform-specific
implementation is not exposed by the public Qt API.
Accessing device-specific features on Symbian is not simply a matter of using standard C++ APIs and idioms to call device-specific APIs. Symbian's native programming language is Symbian C++, a variant of C++ that evolved specifically to address the needs of resource constrained devices. Symbian C++ uses programming idioms and frameworks that promote robust and memory efficient code, but sometimes at the expense of ease of use. Symbian C++ has its own exception handling mechanism, and omits standard C and C++ libraries that were not available when it was first created, or were considered too resource-intensive.
PIMPL is a variant
of the Handle-Body design pattern, in which a public API class contains
a pointer to a private implementation class. The private implementation
class is forward declared (instead of using #include
) in the header file, and is therefore hidden to clients of the public
API.
If the implementation class must call methods in the public class we can pass a pointer or reference to the public class in the implementation class's constructor.
If the implementation class
needs to call private methods on the public class (for example, in
order to emit signals to public API clients) you can make the implementation
class a friend of the public class. The class diagram shown below
illustrates the relationships between classes, showing a public API
class QMyClass
which owns a private implementation
class QMyClassPrivate
.
Figure: Public API and private implementation classes.
The implementation of QMyClass
creates and
destroys the private implementation QMyClassPrivate
. In order to do this, QMyClass
must have the definition
of QMyClassPrivate
and so the QMyClass
source file must #include
the QMyClassPrivate
header files for the different target platforms. This can be achieved
by using platform specific #define
statements to
support conditional compilation for different platforms. Qt defines Q_OS_SYMBIAN
(in qglobal.h
) which can
be used in your application to conditionally compile Symbian-specific
code.
// In fictional myclass.cpp ... #ifdef Q_OS_SYMBIAN #include "qmyclass_symbian.h" // Symbian definition of private class #else #include "qmyclass_stub.h" // Stub for all other platforms #endif ...
The definition and implementation of your private class implementations are almost entirely up to you. The only real constraint is that you must use the same private implementation class name for all of your target platforms (otherwise you would need to include separate forward definitions and friend declarations in your public API header file for each platform). Also, typically all of your private classes for the different target platforms should expose the same function names to the public API class – this simplifies the implementation of the public class.
// Implementation of a public class slot mySlot() is defined on all // private implementation classes for different target platforms and // so can be called in the same way from the QMyClass public API class. void QMyClass::mySlot() { d_ptr->mySlot(); }
A private implementation class can derive from any
base class. Although it is common for implementation classes to be
derived from QObject
, this is not a requirement (although
it is necessary if you want your implementation to have signals or
slots). Qt code often uses QObjectPrivate
for private
implementations. This is not part of the public Qt APIs and you should
not use it in your own application code.
For private implementations written in Symbian C++ there are two basic class designs:
Private implementation class is-a CBase
.
Private implementation class uses-a CBase
and
implements a callback interface.
The following diagram shows a private implementation class
that is-a CActive
(CActive
derives
from CBase
).
Figure: Private implementation class is-a CActive
The following diagram shows a private implementation class
that uses-a CBase
and implements a callback interface.
Figure: Private implementation class uses-a CBase
The “is a CBase
” design requires less programming
effort, because you do not need an additional level of indirection
to create and use the Symbian class, or to get notification of asynchronous
function call completions through a callback interface. This is a
good design choice if you only need to use asynchronous function calls
onto a single Symbian C++ class, in which case you can often make
the private class a CActive
(which derives from CBase
). One disadvantage of the “is a CBase
” design is that code to construct the private implementation class
is a little more complex and it may be more difficult to conceptually
separate Qt and Symbian C++ code. The Dialpad example code, which
is described in the following sections uses a private implementation
class derived from CActive
.
Using the “has
a CBase
” design results in a cleaner separation between
the Qt and Symbian programming idioms. It is also a better solution
if your implementation needs to use several Symbian classes. The Callback
APIs section in Converting Active Objects to Signals & Slots explains
this design pattern in more detail.
The public Telephony
class from file telephony.h
is shown below. It has a pointer to the friend private implementation
class TelephonyPrivate
defined in files telephony_symbian.h
(for Symbian) and telephony_stub.h
(a dummy implementation).
The two private classes share the same name and have a superset
of the public class slots and methods. Note that they do not re-implement
the public API class's signals: they can cause the public API class
to emit signals by calling emit
on the public class
pointer they get on construction.
/* * Copyright (c) 2010 Nokia Corporation. */ #ifndef TELEPHONY_H #define TELEPHONY_H #include <QObject> // Forward declaration of the implementation class. class TelephonyPrivate; // An interface to start and stop a phone call // and notify clients of the call status. class Telephony : public QObject { Q_OBJECT public: explicit Telephony(QObject *parent = 0); ~Telephony(); // An enum for returning error codes to clients of this interface. // This enum is not fully defined in this demonstration code. // Your own interface should define some meaningful errors. enum TelephonyErrors {TelephonyNotSupported, TelephonyError, TelephonyError2, TelephonyError3}; signals: // Emitted when the request for the call is first made. void callDialling(QString number); // Emitted when telephone call is connected. void callConnected(); // Emitted when telephone call is disconnected. void callDisconnected(); // Emitted if some kind of error occurs. void error(Telephony::TelephonyErrors error); public slots: // Starts a call using number. void startCall(QString number); // Ends the current call. void endCall(); private: // The class that does the work. TelephonyPrivate* d_ptr; // Make TelephonyPrivate a friend so it can emit // signals using its pointer to its parent Telephony object. friend class TelephonyPrivate; }; #endif // TELEPHONY_H
The platform-specific inclusion and compilation of source files is controlled through the project file.
# From Dialpad.pro example # Needs to be defined for Symbian DEFINES += NETWORKACCESS # Common header and sources for all build targets HEADERS += telephony.h SOURCES += main.cpp \ telephony.cpp \ # Symbian specific values symbian { TARGET.UID3 = 0xE53F898F TARGET.CAPABILITY += NetworkServices HEADERS += telephony_symbian.h SOURCES += telephony_symbian.cpp LIBS += -letel3rdparty } else { # Stub sources and headers for platforms other than Symbian HEADERS += telephony_stub.h SOURCES += telephony_stub.cpp } ...The platform-specific sections also list the platform libraries the code must link with, in this case
etel3rdparty.dll
.A Symbian implementation must specify the capabilities needed
by the library or executable. The Dialpad project file specifies NetworkServices
in order to allow the Dialpad application
to make a phone call.
The public class implementation needs the private class header in order to construct the private class. The private class header is conditionally included based on the current platform.
// In telephony.cpp // Conditional include for the target platform. #ifdef Q_OS_SYMBIAN #include "telephony_symbian.h" // Symbian definition of private implementation class. #else #include "telephony_stub.h" // Stub class for all other platforms. #endif
The public class creates an instance of the private implementation class in its constructor as shown below:
// Constructor. Telephony::Telephony(QObject *parent) : QObject(parent) { #ifdef Q_OS_SYMBIAN // Symbian private class implementation. // This code can generate a Symbian Leave. // If it does, convert it into a throw. QT_TRAP_THROWING(d_ptr = TelephonyPrivate::NewL(this)); #else // Stub class implementation d_ptr = new TelephonyPrivate(this); #endif }
Because the private class is a Symbian CBase
-derived class, we use platform-specific construction
in the public class implementation. This is acceptable because the
Symbian class implementation is not visible in the public API. As
a general rule, however, it is better to put as much of the platform-specific
implementation as possible into the private class.
Function NewL()
is a standard Symbian static factory class for creating
new objects. It is implemented by TelephonyPrivate
to ensure that the object is either fully allocated, or is properly
cleaned up in the event of a problem. Because NewL()
can leave, it must be wrapped in a QT_TRAP_THROWING
macro to convert the leave into a standard C++ throw. The document Exceptions &
Error Handling explains how to create exception handling barriers
between Symbian and Qt code.
If you implement the private class
using the “has-a CBase
” design instead, constructing
the implementation class from the public class is simpler, and there
is no need for conditional macros.
// Public class Telephony::Telephony(QObject *parent) : QObject(parent) { d_ptr = new TelephonyPrivate(this); } //Private class TelephonyPrivate::TelephonyPrivate(QObject *parent) : d_ptr(parent) { // symbianMember is a CBase derived member variable in TelephonyPrivate. QT_TRAP_THROWING(symbianMember = CTelephonyManager::NewL(this)); }
There are other strategies for allocating the Symbian object in your code. Whatever method you use, the important things to remember are:
Symbian C classes have their new
function
overloaded from CBase
and it does not throw
exceptions. If you construct your private implementation class using new
, you must use the q_check_ptr()
function
to check the returned pointer and throw if it is Null
.
This is important if a CBase-derived object is not initialized
until it is first used. You can construct the object using new
, but you must check the pointer before you use it for
object initialization and throw if it is NULL.
It is your responsibility to ensure that the object is properly cleaned up if its construction fails.
This is important because it is not always obvious whether cleanup will occur properly, particularly when mixing Qt and Symbian C++ idioms. Consider the following code:
Telephony::Telephony(QObject *parent) : QObject(parent) { d_ptr = q_check_ptr(new TelephonyPrivate(this)); //from Qt 4.6 #ifdef Q_OS_SYMBIAN QT_TRAP_THROWING(d_ptr->ConstructL()); #endif }
If ConstructL()
leaves, the macro
throws. Following normal C++ rules, the memory for Telephony
is freed but the destructor is not called. Therefore any partially
allocated objects in TelephonyPrivate
will leak memory.
For this to work correctly d_ptr
would need to be
a smart pointer (QScopedPointer
).
The public class must delete the private implementation in its destructor.
// Public slot - starts a call using number. void Telephony::startCall(QString number) { d_ptr->startCall(number); }Note that we have not created any barrier between Qt and Symbian C++ Exception handling mechanisms in the above code. This is because we know from the implementation that
startCall()
cannot
Leave. This method can throw, which is acceptable because it is never
called from Symbian code. The Symbian platform TelephonyPrivate
class implementation is shown below. It is a Symbian active object. As it is a Symbian C++ class it is important to comply with the Symbian Coding Standards, in particular:
Construct the class using leave-safe Symbian mechanisms.
If using multiple inheritance, derive from the CBase
derived classes first, and only otherwise derive mixin (interface)
M classes.
Caution: Never derive from both CBase and QObject
as the new
used for construction is undetermined.
/* * Copyright (c) 2010 Nokia Corporation. */ #ifndef TELEPHONY_SYMBIAN_H #define TELEPHONY_SYMBIAN_H #include "telephony.h" #include <e32base.h> #include <etel3rdparty.h> // The Symbian implementation of Telephony interface. // Starts and stops a phone call on Symbian // and notify clients of the call status. class TelephonyPrivate : public CActive { public: // Symbian style static constructor static TelephonyPrivate* NewL(Telephony *aPublicAPI); ~TelephonyPrivate(); // Start a call using number void startCall(QString number); // End the current call void endCall(); public: // From CActive // Cancel an outstanding request void DoCancel(); // Called when an asynchronous service request completes void RunL(); private: // Constructor is private - NewL must be used to create objects TelephonyPrivate(Telephony* publicInterface); // Symbian second stage constructor void ConstructL(); // Convert from Symbian error code to interface Telephony::TelephonyErrors enum void ConvertErrorL(int err); // The Symbian telephony API class for making phone calls CTelephony* iTelephony; // Pointer to the public interface object that owns this private implementation Telephony* iPublic; // ID of the current call - used to end the call CTelephony::TCallId iCallId; // Status of the call CTelephony::TCallStatusV1 iCallStatus; CTelephony::TCallStatusV1Pckg iCallStatusPckg; // An enum for tracking the connection state of this object enum TCallStates {EIdle, EDialling, EConnected, EDisconnecting}; TCallStates iCallState; }; #endif // TELEPHONY_SYMBIAN_H
A TelephonyPrivate
object has a pointer to its parent Telephony
that
it uses to emit signals to the Qt clients when the active object completes
or has an error.
The private class replicates the public class's
API, reducing the conditional code in the public class implementation.
As it derives from CActive
it must implement its
base class virtual methods RunL()
and DoCancel()
to handle completion and cancellation of the active object.
The class has a private data member, iCallId
,
to store the ID of the current call made using the Symbian CTelephony
class . A pointer to the CTelephony
class is held in the iTelephony
member variable.
As the private implementation is a Symbian class it is constructed using the Symbian Two-phase construction idiom.
The public static NewL()
factory function uses a leaving constructor to create
the object, pushes it on the CleanupStack, initializes it using the leaving second phase constructor, pops
it off the stack and returns it to the public class.
TelephonyPrivate* TelephonyPrivate::NewL(Telephony *aPublicAPI) { TelephonyPrivate* self = new (ELeave) TelephonyPrivate(aPublicAPI); CleanupStack::PushL(self); self->ConstructL(); CleanupStack::Pop(self); return self; }
As part of its construction, the private object receives a pointer to the public class object. This pointer is used to emit signals from the public class.
The implementation of the private class's methods is dependent on the functionality required. The most important thing to keep in mind is correct inter-working of the platform exception handling systems, as discussed in Exception handling mechanisms .
Note also the use of the method void ConvertErrorL(int err)
to convert Symbian global errors
into errors defined by the public class API.
Handling of the RunL()
and DoCancel()
methods for this
example is discussed in Converting Active
Objects to Signals and Slots.
Wherever possible, keep public APIs free of platform-specific members and functions. Exceptions to this rule are very rare.
QPixmap
provides helper functions to
convert to and from a CFbsBitmap
: CFbsBitmap * QPixmap::toSymbianCFbsBitmap () const
static QPixmap QPixmap::fromSymbianCFbsBitmap ( CFbsBitmap
* bitmap )
Similarly, it is sometimes necessary to reveal platform-specific
types and publicly include platform headers in order to map them to
generic typedefs. This can be seen in QProcess
where Q_PID
is a typedef to a Symbian TProcessId
.
QPixmap
declaration: #if defined(Q_OS_SYMBIAN) CFbsBitmap *toSymbianCFbsBitmap() const; static QPixmap fromSymbianCFbsBitmap(CFbsBitmap *bitmap); #endif
Some of the material in this topic is based with permission on a Symbian Foundation wiki article Apps:Using Qt and Symbian C++ Together . The version used was that available at Symbian Foundation on 3 November, 2010. The content in this page is licensed under the Creative Commons Attribution-Share Alike 2.0 UK: England & Wales License (http://creativecommons.org/licenses/by-sa/2.0/uk).