- •Contents
- •1.1 Fundamental Types
- •1.2 T Classes
- •1.3 C Classes
- •1.4 R Classes
- •1.5 M Classes
- •1.6 Static Classes
- •1.7 Buyer Beware
- •1.8 Summary
- •2 Leaves: Symbian OS Exceptions
- •2.1 Leaving Functions
- •2.3 Constructors and Destructors
- •2.4 Working with Leaving Functions
- •2.5 Trapping a Leave Using TRAP and TRAPD
- •2.6 LeaveScan
- •2.7 Summary
- •3 The Cleanup Stack
- •3.1 Using the Cleanup Stack
- •3.2 How Does the Cleanup Stack Work?
- •3.4 Using TCleanupItem for Customized Cleanup
- •3.5 Portability
- •3.6 An Incidental Note on the Use of Casts
- •3.7 Summary
- •5 Descriptors: Symbian OS Strings
- •5.3 Pointer Descriptors
- •5.4 Stack-Based Buffer Descriptors
- •5.5 Heap-Based Buffer Descriptors
- •5.6 Literal Descriptors
- •5.7 Summary
- •6 Good Descriptor Style
- •6.1 Descriptors as Parameters and Return Types
- •6.2 Common Descriptor Methods
- •6.3 The Use of HBufC Heap Descriptors
- •6.4 Externalizing and Internalizing Descriptors
- •6.5 The Overuse of TFileName
- •6.6 Useful Classes for Descriptor Manipulation
- •6.7 Summary
- •7 Dynamic Arrays and Buffers
- •7.1 CArrayX Classes
- •7.3 Why Use RArray Instead of CArrayX?
- •7.4 Dynamic Descriptor Arrays
- •7.5 Fixed-Length Arrays
- •7.6 Dynamic Buffers
- •7.7 Summary
- •8.1 Multitasking Basics
- •8.2 Event-Driven Multitasking
- •8.3 Working with Active Objects
- •8.4 Example Code
- •8.5 Threads Without an Active Scheduler
- •8.6 Application Code and Active Objects
- •8.7 Summary
- •9 Active Objects under the Hood
- •9.1 Active Object Basics
- •9.2 Responsibilities of an Active Object
- •9.3 Responsibilities of an Asynchronous Service Provider
- •9.4 Responsibilities of the Active Scheduler
- •9.5 Starting the Active Scheduler
- •9.6 Nesting the Active Scheduler
- •9.7 Extending the Active Scheduler
- •9.8 Cancellation
- •9.9 Request Completion
- •9.10 State Machines
- •9.11 Long-Running Tasks
- •9.14 Common Mistakes
- •9.15 Summary
- •10 Symbian OS Threads and Processes
- •10.2 Thread Priorities
- •10.3 Stopping a Running Thread
- •10.5 Exception Handling
- •10.6 Processes
- •10.7 Summary
- •11.2 How Do the Client and Server Fit Together?
- •11.3 How Do the Client and Server Communicate?
- •11.5 How Do Synchronous and Asynchronous Requests Differ?
- •11.6 How Is a Server Started?
- •11.7 How Many Connections Can a Client Have?
- •11.8 What Happens When a Client Disconnects?
- •11.9 What Happens If a Client Dies?
- •11.10 What Happens If a Server Dies?
- •11.15 How Many Outstanding Requests Can a Client Make to a Server?
- •11.16 Can Server Functionality Be Extended?
- •11.17 Example Code
- •11.18 Summary
- •12.2 Client Boilerplate Code
- •12.3 Starting the Server and Connecting to It from the Client
- •12.4 Server Startup Code
- •12.5 Server Classes
- •12.6 Server Shutdown
- •12.7 Accessing the Server
- •12.8 Summary
- •13 Binary Types
- •13.1 Symbian OS EXEs
- •13.2 Symbian OS DLLs
- •13.3 Writable Static Data
- •13.4 Thread-Local Storage
- •13.5 The DLL Loader
- •13.6 UIDs
- •13.8 Summary
- •14 ECOM
- •14.1 ECOM Architecture
- •14.2 Features of an ECOM Interface
- •14.3 Factory Methods
- •14.4 Implementing an ECOM Interface
- •14.5 Resource Files
- •14.6 Example Client Code
- •14.7 Summary
- •15 Panics
- •15.2 Good Panic Style
- •15.3 Symbian OS Panic Categories
- •15.4 Panicking Another Thread
- •15.5 Faults, Leaves and Panics
- •15.6 Summary
- •16 Bug Detection Using Assertions
- •16.3 Summary
- •17 Debug Macros and Test Classes
- •17.1 Heap-Checking Macros
- •17.2 Object Invariance Macros
- •17.3 Console Tests Using RTest
- •17.4 Summary
- •18 Compatibility
- •18.1 Forward and Backward Compatibility
- •18.2 Source Compatibility
- •18.3 Binary Compatibility
- •18.4 Preventing Compatibility Breaks
- •18.5 What Can I Change Without Breaking Binary Compatibility?
- •18.6 Best Practice: Planning for Future Changes
- •18.7 Compatibility and the Symbian OS Class Types
- •18.8 Summary
- •19 Thin Templates
- •20.1 Class Layout
- •20.3 Parameters and Return Values
- •20.4 Member Data and Functional Abstraction
- •20.5 Choosing Class, Method and Parameter Names
- •20.7 Summary
- •21 Good Code Style
- •21.1 Reduce the Size of Program Code
- •21.2 Use Heap Memory Carefully
- •21.3 Use Stack Memory Carefully
- •21.5 Optimize Late
- •21.6 Summary
- •Glossary
- •Bibliography and Online Resources
- •Index
198 |
THE CLIENT–SERVER FRAMEWORK IN PRACTICE |
Figure 12.1 illustrates the previous discussion and code sample.
|
CHerculesData |
iDes1 |
CHerculesData |
|
iDes1 |
|||
|
|
|
Hello |
|
|
|
Hello |
|
|
iDes1 |
|
|
iDes1 |
|
|
||
|
iDes2 |
|
iDes2 |
iDes2 |
|
iDes2 |
||
|
iVal = 5 |
|
iVal = 5 |
|
||||
|
|
World! |
World! |
|||||
|
|
|
|
|
||||
|
CHerculesData::MarshalDataL() |
|
CHerculesData::NewLC() |
|||||
|
(calls CHerculesData::ExternalizeL()) |
|
||||||
|
|
|
calls |
|
||||
|
|
|
|
|
CHerculesData::InternalizeL() |
|||
HBufC8* dataDes |
|
|
|
|
|
|
||
L |
|
|
|
L |
|
|
|
|
E |
|
|
RSessionBase::SendReceive() E |
|
|
|
||
N |
Hello |
World! |
5 |
N |
Hello |
World! |
5 |
|
G |
G |
|||||||
|
|
|
|
|
|
|||
T |
|
|
|
T |
|
|
|
|
H |
|
|
|
H |
|
|
|
|
|
iDes1 |
iDes2 |
iVal |
|
|
|
|
|
|
'Descriptorized' CHerculesData |
|
|
|
|
|||
|
|
|
CLIENT |
SERVER |
|
|
|
Figure 12.1 Marshaling CHerculesData from client to server within a descriptor
A client and server typically run in different processes. For this reason, server-side code should not attempt to access a clientside object directly through a C++ pointer passed from the client to server. Where necessary, objects must be ”descriptorized” for inter-process communication.
12.3Starting the Server and Connecting to It from the Client
The RHerculesSession constructor zeroes its iHandle value so it is clear that the RHerculesSession object is invalid and must first connect to the server by calling RSessionBase::CreateSession(). The client-side implementation of server access code typically wraps this call in a method called Connect(), e.g. RFs::Connect(), or occasionally an Open() method. If an attempt is made to submit a request to a server using a client session that has not yet connected to the server, a panic occurs (KERN-EXEC 0).
If the server is an essential system server, it is guaranteed to have been started by the system as Symbian OS starts and will always be running. The definition of a system server, for example, the file server, is that
STARTING THE SERVER AND CONNECTING TO IT FROM THE CLIENT 199
it is required by the system. If a system server panics for some reason, Symbian OS is forced to reboot and restart the server because it cannot function without it.
Client connection to a system server is straightforward because the server is already running. In effect, a Connect() method simply needs to call RSessionBase::CreateSession(). However, as I’ve described, the Hercules example server discussed in this chapter is not an essential system server; instead it is implemented as a transient server. If it is not already running when a client calls CreateSession(), that is if no other client is connected to it, the Connect() method of the client-side implementation must start the server process. The server runs in a separate process on phone hardware and Symbian OS processes run in separate threads within the single process of the Windows emulator.
Here is the implementation of Connect() for the Hercules server:
// The server’s identity within the client – server framework _LIT(KServerName,"HerculesServer");
EXPORT_C TInt RHerculesSession::Connect()
{
TInt retry=2; // A maximum of two iterations of the loop are required for (;;)
{
// Uses system-pool message slots
TInt r=CreateSession(KServerName,TVersion(1,0,0)); if ( (KErrNotFound!=r) && (KErrServerTerminated!=r) )
return (r);
if (--retry==0) return (r);
r=StartTheServer();
if ( (KErrNone!=r) && (KErrAlreadyExists!=r) ) return (r);
}
}
The method calls RSessionBase::CreateSession(), passing in the name of the server required. If the server is already running and can create a new client session, this call returns KErrNone and the function returns successfully, having connected a new session. However, if the server is not running, CreateSession() returns KErrNotFound. The Connect() method must check for this result and, under these circumstances, attempt to start the server. You’ll notice that the method also checks to see if KErrServerTerminated was returned instead of KErrNotFound, which indicates that the client connection request was submitted just as the server was shutting down. For either value,4 the server should be started (or restarted) by calling
4 If any other error value besides KErrNotFound or KErrTerminated is returned, Connect() returns it to the caller.
200 |
THE CLIENT–SERVER FRAMEWORK IN PRACTICE |
StartTheServer(). I’ll discuss this function shortly – if it returns KErrNone, the server has started successfully. The next iteration of the for loop submits another request to create a client session by calling
RSessionBase::CreateSession().
If some other client managed to start the server between this client’s failed attempt to create a session on it and the call to StartTheServer(), it returns KErrAlreadyExists. Under the circumstances, this is not an error,5 because the server is now running, even if the client didn’t start it. So, again, the next iteration of the loop can submit a request to create a new client session.
If the server fails to start or the client connection fails, an error is returned and the code breaks out of the loop, returning the error value to indicate that session creation failed.
StartTheServer() launches the server in a new thread on the Windows emulator, or in a new, separate process on hardware running Symbian OS. Because the server runs differently depending on whether it is running on the emulator or a phone, the client-side server startup code is quite complex. The code is implemented as follows (I’ve included a number of comments to make it clear what is going on and I’ll discuss the most important features below):
// Runs client-side and starts the separate server process/thread static TInt StartTheServer()
{
TRequestStatus start;
TServerStart starter(start);
// HerculesServer.exe or HerculesServer.dll _LIT(KServerBinaryName,"HerculesServer");
const TUid KServerUid3={0x01010101}; // Temporary UID const TUidType serverUid(KNullUid, KNullUid, KServerUid3);
#ifdef __WINS__ // On the Windows emulator the server is a DLL RLibrary lib;
TInt r=lib.Load(KServerBinaryName, serverUid); if (r!=KErrNone)
return (r);
//The entry point returns a TInt representing the thread
//function for the server
TLibraryFunction export1 = lib.Lookup(1);
TThreadFunction threadFunction =
reinterpret_cast<TThreadFunction>(export1()); TName name(KServerName); // Defined previously
// Randomness ensures a unique thread name name.AppendNum(Math::Random(), EHex);
5 The KErrAlreadyExists error code is returned because there must only be one copy of the server running on the system. Simultaneous attempts to launch two copies of the server process will be detected by the kernel when the second attempt is made to create a CServer object of the same name. It is this which fails with KErrAlreadyExists.
STARTING THE SERVER AND CONNECTING TO IT FROM THE CLIENT 201
// Now create the server thread
const TInt KMinServerHeapSize=0x1000; const TInt KMaxServerHeapSize=0x1000000;
RThread server;
r = server.Create(name, threadFunction, KDefaultStackSize, &starter, &lib, NULL,
KMinServerHeapSize, KMaxServerHeapSize, EOwnerProcess);
lib.Close(); // The server thread has a handle to the library now
#else |
// |
Phone hardware - comparatively easy – |
RProcess server; |
// |
just create a new process |
TInt r=server.Create(KServerBinaryName,starter.AsCommand(), serverUid);
#endif
if (KErrNone!=r) return r;
TRequestStatus threadDied; server.Logon(threadDied);
if (KRequestPending!=threadDied)
{// logon failed - server isn’t running yet // Manage the thread signal by consuming it User::WaitForRequest(threadDied);
server.Kill(0); // don’t try to start the server server.Close();
return threadDied.Int(); // return the error
}
//Start the server thread or process and wait for startup or thread
//death notification
//This code is identical for both the emulator thread
//and the phone hardware process
server.Resume(); User::WaitForRequest(start, threadDied); if (KRequestPending==start)
{// server died and signaled - startup still pending server.Close();
return threadDied.Int();
}
//The server started.
//Cancel the logon and consume the cancellation signal server.LogonCancel(threadDied);
server.Close(); // Don’t need this handle any more User::WaitForRequest(threadDied);
return (KErrNone);
}
As you can see, StartTheServer() is responsible for creating the new thread or process in which the server runs. Having done so, it waits for notification that the server thread or process has been initialized and returns the initialization result to its caller. You’ll notice that the client waits for server notification by calling User::WaitForRequest(), which means that the client thread is blocked until the server responds. For this reason, server initialization should be as rapid as possible,
202 |
THE CLIENT–SERVER FRAMEWORK IN PRACTICE |
performing only the actions necessary to initialize the server framework. Other initialization should be performed asynchronously within the server after it has been successfully launched.
On the emulator, the new thread is created with a unique name, to avoid receiving a KErrAlreadyExists error from the kernel if the server is restarted just after it has exited. The thread is assigned a standard stack size, minimum and maximum heap sizes and an RLibrary handle to the DLL in which the server code resides. The thread entry function is initialized to the first export function of the DLL, which will be discussed shortly when I come to describe the server-side startup code. The server thread is owned by the entire emulator process (as indicated by the EOwnerProcess parameter) rather than by the client thread that creates it. This means that the client thread can terminate but the server thread will continue to run, allowing other client threads to access the server without it dying unexpectedly.
On a real Symbian OS phone, the server is launched as a process, the code for which is significantly more straightforward than for emulator thread startup. Once the server thread or process has started, the client thread calls Logon() in case a failure occurs before server initialization function is complete. The Logon() call ensures that the client thread is notified if the server dies before the server startup TRequestStatus is signaled. This prevents the client thread from hanging indefinitely if server initialization fails. Once the server entry point signals the client and sets the value of start to something other than KRequestPending, the Logon() notification is canceled. I’ll discuss how the server-side code manages initialization later in this chapter.
The client thread passes a TRequestStatus to the server for notification when its initialization is complete. A TServerStart object (”starter”) is used to transfer the client’s TRequestStatus object (”start”) to the server. The TServerStart class is defined and implemented as follows:
class TServerStart
{
public: TServerStart() {};
TServerStart(TRequestStatus& aStatus);
TPtrC AsCommand() const;
TInt GetCommand(); void SignalL();
private: TThreadId iId;
TRequestStatus* iStatus; };
inline TServerStart::TServerStart(TRequestStatus& aStatus) :iId(RThread().Id()),iStatus(&aStatus){aStatus=KRequestPending;}