Fundamentals of Symbian C++/Client-Server Framework
In Symbian OS, most system services, such as the file server, are implemented using the client-server framework. This article provides an overview of the client-server framework for users wishing to understand the architecture or to implement their own server.
We use a modified version of Central Repository as an example of a server. Central Repository provides support for persistent settings, such as network access points, that need to be shared across multiple applications.
Related settings are organized into groups called repositories. For example, the comms settings are stored together in a single repository, and the locale settings are stored in another repository.
Individual settings can be integers, strings or real numbers. They can be set and retrieved, and users can ask to be notified when another user changes them.
For more detail on Central Repository, refer to the Symbian Product Reference.
There are three typical motivations for implementing a service as a server rather than using a shared library:
- To manage access to shared system resources. For example:
- The file server manages access to the file system.
- The window server manages access to the screen.
- The telephony server manages access to the modem.
- To implement programs that run continually in the background and notify clients of events. For example:
- A service to notify clients about new SMS messages arriving on the phone
- A service to notify clients when peripherals such as headphones are detected.
- To define and enforce a Fundamentals of Symbian C++/Platform Security. Although servers do not have to run in a separate process to their clients, in practice they almost always do. Since the process boundary is the point where platform security checks can be enforced, a server running in its own process is able to implement a security policy controlling access to the resource. For example:
- The file server can enforce data caging rules.
- The telephony server can control access to the network.
- Central Repository can implement access control policies for the settings it manages.
Even if it does not run in a separate process to its clients, a server will always run in a separate thread, and all client–server communication takes place by messages that are mediated by the kernel.
The communication channel for passing messages between the client and server is known as a session. The client’s messages to the server are known as requests. Each request contains an identifier specifying the operation to be performed, and can also contain parameter data packaged up and passed to the server. Simple data can be passed with the request, while more complex data is accessed by the server using inter-thread data transfer.
To hide the details of the message passing and data packaging from application code, a typical server has associated client-side code to format requests and pass them to the server, via the kernel. This is usually provided in a separate library. For example, mytest.exe, an application that is a client of the Symbian platform file server (efile.exe), accesses the file system by linking against (efsrv.dll), which is the file server’s client-side implementation library. This is illustrated in Figure 4.9.
The diagram below shows a simplified version of a synchronous client-server call:
The client-side code exposes a meaningful interface to its users, packages up requests into client-server messages and sends them to the server.
The server-side code channels the request to the correct object, which executes and completes the request.
Because servers run in a different thread to their clients, servers operate as asynchronous service providers. In this case, the request from the client includes a TRequestStatus, which the server will complete when the operation has finished. Typically, the client will use an active object to handle completion of the request.
The diagram below shows a simplified version of an asynchronous client-server call. In this call, the client asks to be notified when a setting changes. It uses an active object to handle the request: the active object sends its as a parameter, the server just stores the message, and the call returns. Some time later, the setting changes, the server completes the outstanding request, and the client-side active object’s RunL() is called:
Server Startup and Shutdown
Servers are often started on demand: when the first client connects to them the connect code checks to see if the server is already running and, if it is not, starts it. Other servers are started by the system automatically on boot.
Some servers, called transient servers, run only while a client is connected to them; when the last client disconnects they shut down. Other servers are permanent and continue to run whether or not a client is connected. System servers must be running for the operating system to function properly: if a system server terminates the system will automatically reboot.
There are subtle complexities involved in writing server startup and shutdown code; although the client-server framework does not supply its own startup and shutdown code, Symbian provides template code (here) and developers are strongly encouraged to use it. Although the template code is specifically for transient servers, it is usable for permanent servers with only minor modifications (primarily to remove the shutdown code).
The main classes involved in the client-server framework are shown below:
The classes shown in black, along with the enumeration TServerRequest, are a modified version of the main classes implementing the client-server part of Symbian’s Central Repository component.
Central Repository is a server that provides support for settings that need to be shared across multiple applications.
Settings are organized into groups called repositories and each connection to a distinct repository is represented client-side as a separate subsession object.
Settings can be integer or string types, can be set to new values (SetInt(), SetString()), and retrieved either individually (GetInt(), GetString()) or as sets which match user-specified criteria (FindL()). Users can also listen for changes to particular settings (NotifyRequest()).
The TIpcArgs class is used to package the arguments to be sent to a server. A TIpcArgs object constitutes the payload for a client–server request; it can package up to four 32-bit arguments together with information about each argument’s type. It is also possible for the object to contain no arguments at all, for requests that have no associated data.
This is the base class for classes that own handles to other objects, generally those that are created within the kernel. It defines a disconnection method, Close().
This is the base class for client-side handles to servers. Most of its methods are protected, so are not called directly by users of the class, but rather provide the underlying implementation for the functions defined by its subclasses.
It defines methods to create new sessions: CreateSession, and to send messages to the server: SendReceive and Send.
The SendReceive() method is overloaded to handle synchronous and asynchronous requests.
The asynchronous request method takes a TRequestStatus& parameter and the client typically uses an active object to handle completion of the request.
The synchronous overload does not take a TRequestStatus parameter but the implementation of it creates a local TRequestStatus and waits for it to be completed by the server:
This means that the synchronous overload blocks the thread until the server has explicitly completed the request.
RSessionBase::Send() sends a message to the server but does not receive a reply (and in practice, this function is rarely used).
The Send() and SendReceive() methods take a 32-bit identifier to identify the client request and a TIpcArgs object containing the arguments to be passed. The identifier is taken from an enumeration, which is shared between client and server and effectively represents the contract between the two. For the Central Repository, the enumeration is called TServerRequest.
Some servers allow their session to be shared, so a session handle can be passed from one thread to another. To make a session sharable between multiple threads in a single process, ShareAuto() must be called on it. To make it sharable across a process boundary, ShareProtected() must be called on it.
The client-side representation of a server session, in this example RRepositorySession, is an R class deriving from RSessionBase.
It exports a function to initialize the session, which is conventionally called Connect() or Open(). If the server is not already running the implementation of Connect() starts it. Then it calls RSessionBase::CreateSession, passing in the name of the server it wants to connect to.
The fact that servers are addressed by name opens a potential security loophole in which one server could pose as a trusted system server by using its name; as long as the impersonator is started before the real system server, then clients will be fooled into connecting to it, rather than to the real system server. This is addressed in Symbian with a special capability ProtServ: programs with this capability are trusted not to spoof system servers and are allowed to register a name starting with !: then all system servers have such names, and untrusted programs cannot impersonate them.
The session class also wraps communication with the server and offers a more meaningful interface to its callers. For example, class RFs, which derives from RSessionBase, provides access to the file server and offers functions such as RFs::Delete() or RFs::GetDir(), which wrap raw calls to SendReceive, packaging up any arguments in a TIpcArgs structure and passing in the correct identifier for the function requested.
When a design does not make use of subsessions, this class usually represents the main interface to the server. When the design does use subsessions, the main session class is often primarily a factory for subsessions. This is the case for the Central Repository, as well as the file server and the socket server.
This is the base class for client-side handles to subsessions.
Creating a session has some overhead associated with it because of the objects that need to be created in the kernel and the server thread. A subsession is a mechanism that enables a single client to have multiple separate connections to a server without the overhead of creating multiple sessions. Effectively, a number of subsessions are able to piggy-back on a single session.
Many servers do not make use of subsessions: a good example of one that does is the file server, in which RFile is a subsession piggy-backing on the RFs connection. This means a single client can open many files in the context of a single client-server session.
In our example each connection a thread has with a distinct repository is represented as a separate subsession object. This means that if a single thread wants to open multiple repositories they can all share a single session.
The main client-side interface to a repository is defined in this class.
This class is a server handle to a message sent from a client. It defines:
- A Client() method to identify the client thread
- A Complete() method to signal to the client that the thread has completed
- Read() and Write() methods to read data from, and write data to, the client's address space.
This class derives from RMessagePtr2, and encapsulates a single request from a client. It defines:
- A Function() method that returns the opcode identifying the function requested
- Methods to retrieve each of the four 32-bit arguments that may have been supplied by the client either as integers or as TAny* pointers.
This dual representation of the arguments as integers and pointers enables the client to send more data than will fit into a 32-bit value by setting the argument to a pointer to a descriptor in the client’s address space. The server then extracts the argument as a pointer and uses the ReadL() method defined in RMessagePtr2 to read the data.
The main server class, CServer2, derives from CActive. Messages from clients are delivered to it in the form of RMessage2 objects, and when they are received its RunL method is called. RunL looks at the message’s Function() and:
- If it is a connect message, it creates a new CSession2 subclass by calling its pure virtual NewSessionL method.
- If it is a disconnect message, it destroys the corresponding CSession2 subclass.
- Otherwise, it sends the message to the ServiceL method for the corresponding CSession2.
EXPORT_C void CServer2::RunL()
TInt fn = Message().Function();
// Service the message
// Queue reception of next message if it hasn't already been done
CPolicyServer extends the basic server class to add support for a security policy, that is, the set of capabilities, secure identifiers or vendor identifiers that client processes must have to make particular requests. It is initialized with a TPolicy object defining the policy, which it uses to check all messages it receives.
The author of a server must provide a subclass of CPolicyServer or CServer2, which implements the factory function NewSessionL.
The server does not have to derive from CPolicyServer: if a security policy is not needed it can derive directly from CServer2.
Also, if the server needs a more flexible policy than that supported by CPolicyServer, it may derive from CServer2 and implement a policy from scratch, and this is what Central Repository does.
CSession2 is the server-side class corresponding to RSessionBase. For each initialized RSessionBase subclass client-side, there will be one CSession2 subclass server-side. The key function it declares is the pure virtual ServiceL.
The author of a server must supply a subclass of CSession2 that implements ServiceL. ServiceL is typically implemented as a switch statement, which examines the message identifier and dispatches the message to the appropriate method.
Once the method finishes, the session calls RMessagePtr2::Complete() to indicate to the client that the operation has finished. Only at this point is the client thread (or client active object if the request is asynchronous) eligible to run.
If a server supports subsessions, then for each initialized RSubSessionBase subclass client side, there will be a server-side class to which its requests are channelled. In the case of Central Repository, this is CServerSubSession, and requests from the client will eventually be sent here to be executed.
There is no client-server base class for server subsessions; the CObject framework is typically used instead. This makes implementing subsessions in the server relatively complicated.
If client and server are running in separate processes, parameter data can never be transferred using simple C++ pointers, because the server never has direct access to the client’s address space (or vice versa).
From Client to Server
The TIpcArgs structure passed from client to server contains four 32-bit values. If this is enough to contain the parameters then the data may be effectively passed by value and copied into the message that is received by the server. Otherwise, it is always passed by setting the argument to a pointer to a descriptor in the client address space, which the server accesses by calling RMessagePtr2::Read() or RMessagePtr2::ReadL().
If the client data is not in the form of a descriptor, but is a simple struct not containing any pointers, it can be converted to a descriptor type using the TPckg class.
If the client data contains pointers – for example, if it is a C class – then it must be explicitly serialized into a descriptor format, typically by using an ExternalizeL method. Then the server can read the descriptor data and re-create the object using the corresponding InternalizeL method.
The server must not assume that the client’s data is well-formed, and must explicitly validate it, otherwise a malicious client can crash the server.
From Server to Client
The client is never allowed to read data from the server's address space, so to pass data from server to client, the client must supply a pointer to a descriptor in the message it sends, and the server writes data into the descriptor using one of the RMessagePtr2::WriteL() methods.
Before any commands can be sent to the server, the client must make a connection to it.
RRepositorySession::Connect() calls to the base class CreateSession(), passing in the name of the server. If the server cannot be found, the method tries to start by calling StartServer() and retrying. It retries a limited number of times before giving up:
const TVersion KVersion(KServerMajorVersion, KServerMinorVersion, KServerBuildVersion);
TInt retry = 2;
TInt err = KErrGeneral;
TInt numMessageSlots = -1;
// Try to create a new session with the server.
err = CreateSession(KServerName, KVersion, numMessageSlots);
if((err != KErrNotFound) && (err != KErrServerTerminated))
// Server not running, try to start it.
break; // Failed.
err = StartServer();
if((err != KErrNone) && (err != KErrAlreadyExists))
break; // Launched server
The server’s RunL() is called. It examines the message and calls its Connect() method, which eventually calls its NewSessionL() method, which creates a new CServerSession object.
Finally, the server completes the client request.
Now the client can open a specific repository by calling RRepositorySubsession::Open(), and passing in the UID of the repository to open.
RRepositorySubsession::Open() calls the base class CreateSubSession():
TInt RRepositorySubSession::Open(RRepositorySession* aSession, TUid aUid)
return(CreateSubSession(*aSession, EInitialize, TIpcArgs(aUid.iUid));
The server’s RunL() is called. It examines the message and forwards it to the correct session object, which opens the repository file itself and creates a subsession object to handle client requests.
Finally, the server completes the client request.
A Simple Synchronous Accessor – GetInt
RRepositorySubSession::GetInt() supplies a UID for a setting as a TUint32 and the server writes the value back to the client. The client-side implementation creates a TPckg object to hold the returned integer and calls into the base class SendReceive supplying:
- The identifier for getting an integer value
- A TIpcArgs object containing the setting’s UID by value and a pointer to the TPckg object.
TInt RRepositorySubSession::GetInt(TUint32 aId, TInt& aVal)
return SendReceive(EGetInt, TIpcArgs(aId, &p));
The main server class CSessionManager forwards the message to the right session object, which in turn forwards it to the right subsession object. The subsession looks at the message identifier and calls its internal GetIntL() method.
GetIntL() reads the setting’s UID (the first argument in the message), checks that the client is allowed read access to it and, if it is, reads the value and writes it to the descriptor referenced by the second argument:
TInt CServerSubSession::GetIntL(RMessage2 & aMessage)
TUint32 key = aMessage.Int0();
if(KErrNone != CheckPolicy(aMessage,iRepository.GetReadAccessPolicy(key))
TInt error = iRepository.Get(key, val);
if (error == KErrNone)
Finally, the server completes the client request.
A Complex Accessor – FindL
Central Repository’s FindL function returns the collection of settings which match a pattern supplied by the user. It is more complex because the client code cannot predict how much data will be returned by the server, so it does not know how much space it should reserve.
First, the client-side FindL() method constructs an array large enough to contain KDefaultSettingsCount settings and sets it as the destination argument along with the pattern to match against.
The server forwards the message to the correct subsession, which retrieves the collection of matching settings, and writes the data back as follows:
- The first entry in the array is the total number of settings that matched. This is the minimum size the array needs to be.
- Subsequent entries are as many settings as will fit inside KDefaultSettingsCount-1.
Then the client-side checks the result and, if necessary, resizes the array and and reissues the request.
Thus, a single function may require two IPC round-trips, but this is transparent to the user.
Asynchronous Notification – NotifyRequest
Central Repository enables its clients to listen for changes to particular settings.
RRepositorySubSession::NotifyRequest() takes the ID of the setting we are interested in and a TRequestStatus, which typically belongs to an active object in the client thread:
void RRepositorySubSession::NotifyRequest(TUint32 aId, TRequestStatus& aStatus)
SendReceive(ENotifyRequest, TIpcArgs(aId), aStatus);
The server reads the setting ID and checks that the client is allowed to listen to it. If it is, the server adds the message to a list of pending requests. Unlike the other client-server calls, the server does not complete the client request.
TInt CServerSubSession::NotifyRequest(RMessage2& aMessage)
TUint32 key = aMessage.Int0();
if(KErrNone != CheckPolicy(aMessage,iRepository.GetReadAccessPolicy(key))
TInt error = iNotifier.AddRequest(key, aMessage);
When the setting changes, the server’s notifer goes through the list, calling RMessage2::Complete() on each message in the list that matches the value for the setting which changed.
This completes the TRequestStatus in the client, and the active object’s RunL() is called.