| DCOP: Desktop COmmunications Protocol | |
| Preston Brown <[email protected]> | |
| October 14, 1999 | |
| Revised and extended by Matthias Ettrich <[email protected]> | |
| Mar 29, 2000 | |
| Extended with DCOP Signals by Waldo Bastian <[email protected]> | |
| Feb 19, 2001 | |
| Motivation and Background: | |
| -------------------------- | |
| The motivation behind building a protocol like DCOP is simple. For | |
| the past year, we have been attempting to enable interprocess | |
| communication between KDE applications. KDE already has an extremely | |
| simple IPC mechanism called KWMcom, which is (was!) used for communicating | |
| between the panel and the window manager for instance. It is about as | |
| simple as it gets, passing messages via X Atoms. For this reason it | |
| is limited in the size and complexity of the data that can be passed | |
| (X atoms must be small to remain efficient) and it also makes it so | |
| that X is required. CORBA was thought to be a more effective IPC/RPC | |
| solution. However, after a year of attempting to make heavy use of | |
| CORBA in KDE, we have realized that it is a bit slow and memory | |
| intensive for simple use. It also has no authentication available. | |
| What we really needed was an extremely simple protocol with basic | |
| authorization, along the lines of MIT-MAGIC-COOKIE, as used by X. It | |
| would not be able to do NEARLY what CORBA was able to do, but for the | |
| simple tasks required it would be sufficient. Some examples of such | |
| tasks might be an application sending a message to the panel saying, | |
| "I have started, stop displaying the 'application starting' wait | |
| state," or having a new application that starts query to see if any | |
| other applications of the same name are running. If they are, simply | |
| call a function on the remote application to create a new window, | |
| rather than starting a new process. | |
| Implementation: | |
| --------------- | |
| DCOP is a simple IPC/RPC mechanism built to operate over sockets. | |
| Either unix domain sockets or tcp/ip sockets are supported. DCOP is | |
| built on top of the Inter Client Exchange (ICE) protocol, which comes | |
| standard as a part of X11R6 and later. It also depends on Qt, but | |
| beyond that it does not require any other libraries. Because of this, | |
| it is extremely lightweight, enabling it to be linked into all KDE | |
| applications with low overhead. | |
| Model: | |
| ------ | |
| The model is simple. Each application using DCOP is a client. They | |
| communicate to each other through a DCOP server, which functions like | |
| a traffic director, dispatching messages/calls to the proper | |
| destinations. All clients are peers of each other. | |
| Two types of actions are possible with DCOP: "send and forget" | |
| messages, which do not block, and "calls," which block waiting for | |
| some data to be returned. | |
| Any data that will be sent is serialized (marshalled, for you CORBA | |
| types) using the built-in QDataStream operators available in all of | |
| the Qt classes. This is fast and easy. In fact it's so little work | |
| that you can easily write the marshalling code by hand. In addition, | |
| there's a simple IDL-like compiler available (dcopidl and dcopidl2cpp) | |
| that generates stubs and skeletons for you. Using the dcopidl compiler | |
| has the additional benefit of type safety. | |
| This HOWTO describes the manual method first and covers the dcopidl | |
| compiler later. | |
| Establishing the Connection: | |
| ---------------------------- | |
| KApplication has gained a method called "KApplication::dcopClient()" | |
| which returns a pointer to a DCOPClient instance. The first time this | |
| method is called, the client class will be created. DCOPClients have | |
| unique identifiers attached to them which are based on what | |
| KApplication::name() returns. In fact, if there is only a single | |
| instance of the program running, the appId will be equal to | |
| KApplication::name(). | |
| To actually enable DCOP communication to begin, you must use | |
| DCOPClient::attach(). This will attempt to attach to the DCOP server. | |
| If no server is found or there is any other type of error, attach() | |
| will return false. KApplication will catch a dcop signal and display an | |
| appropriate error message box in that case. | |
| After connecting with the server via DCOPClient::attach(), you need to | |
| register this appId with the server so it knows about you. Otherwise, | |
| you are communicating anonymously. Use the | |
| DCOPClient::registerAs(const QCString &name) to do so. In the simple | |
| case: | |
| /* | |
| * returns the appId that is actually registered, which _may_ be | |
| * different from what you passed | |
| */ | |
| appId = client->registerAs(kApp->name()); | |
| If you never retrieve the DCOPClient pointer from KApplication, the | |
| object will not be created and thus there will be no memory overhead. | |
| You may also detach from the server by calling DCOPClient::detach(). | |
| If you wish to attach again you will need to re-register as well. If | |
| you only wish to change the ID under which you are registered, simply | |
| call DCOPClient::registerAs() with the new name. | |
| KUniqueApplication automatically registers itself to DCOP. If you | |
| are using KUniqueApplication you should not attach or register | |
| yourself, this is already done. The appId is by definition | |
| equal to kapp->name(). You can retrieve the registered DCOP client | |
| by calling kapp->dcopClient(). | |
| Sending Data to a Remote Application: | |
| ------------------------------------- | |
| To actually communicate, you have one of two choices. You may either | |
| call the "send" or the "call" method. Both methods require three | |
| identification parameters: an application identifier, a remote object, | |
| a remote function. Sending is asynchronous (i.e. it returns immediately) | |
| and may or may not result in your own application being sent a message at | |
| some point in the future. Then "send" requires one and "call" requires | |
| two data parameters. | |
| The remote object must be specified as an object hierarchy. That is, | |
| if the toplevel object is called "fooObject" and has the child | |
| "barObject", you would reference this object as "fooObject/barObject". | |
| Functions must be described by a full function signature. If the | |
| remote function is called "doIt", and it takes an int, it would be | |
| described as "doIt(int)". Please note that the return type is not | |
| specified here, as it is not part of the function signature (or at | |
| least the C++ understanding of a function signature). You will get | |
| the return type of a function back as an extra parameter to | |
| DCOPClient::call(). See the section on call() for more details. | |
| In order to actually get the data to the remote client, it must be | |
| "serialized" via a QDataStream operating on a QByteArray. This is how | |
| the data parameter is "built". A few examples will make clear how this | |
| works. | |
| Say you want to call "doIt" as described above, and not block (or wait | |
| for a response). You will not receive the return value of the remotely | |
| called function, but you will not hang while the RPC is processed either. | |
| The return value of send() indicates whether DCOP communication succeeded | |
| or not. | |
| QByteArray data; | |
| QDataStream arg(data, IO_WriteOnly); | |
| arg << 5; | |
| if (!client->send("someAppId", "fooObject/barObject", "doIt(int)", | |
| data)) | |
| qDebug("there was some error using DCOP."); | |
| OK, now let's say we wanted to get the data back from the remotely | |
| called function. You have to execute a call() instead of a send(). | |
| The returned value will then be available in the data parameter "reply". | |
| The actual return value of call() is still whether or not DCOP | |
| communication was successful. | |
| QByteArray data, replyData; | |
| QCString replyType; | |
| QDataStream arg(data, IO_WriteOnly); | |
| arg << 5; | |
| if (!client->call("someAppId", "fooObject/barObject", "doIt(int)", | |
| data, replyType, replyData)) | |
| qDebug("there was some error using DCOP."); | |
| else { | |
| QDataStream reply(replyData, IO_ReadOnly); | |
| if (replyType == "QString") { | |
| QString result; | |
| reply >> result; | |
| print("the result is: %s",result.latin1()); | |
| } else | |
| qDebug("doIt returned an unexpected type of reply!"); | |
| } | |
| N.B.: You cannot call() a method belonging to an application which has | |
| registered with an unique numeric id appended to its textual name (see | |
| dcopclient.h for more info). In this case, DCOP would not know which | |
| application it should connect with to call the method. This is not an issue | |
| with send(), as you can broadcast to all applications that have registered | |
| with appname-<numeric_id> by using a wildcard (e.g. 'konsole-*'), which | |
| will send your signal to all applications called 'konsole'. | |
| Receiving Data via DCOP: | |
| ------------------------ | |
| Currently the only real way to receive data from DCOP is to multiply | |
| inherit from the normal class that you are inheriting (usually some | |
| sort of QWidget subclass or QObject) as well as the DCOPObject class. | |
| DCOPObject provides one very important method: DCOPObject::process(). | |
| This is a pure virtual method that you must implement in order to | |
| process DCOP messages that you receive. It takes a function | |
| signature, QByteArray of parameters, and a reference to a QByteArray | |
| for the reply data that you must fill in. | |
| Think of DCOPObject::process() as a sort of dispatch agent. In the | |
| future, there will probably be a precompiler for your sources to write | |
| this method for you. However, until that point you need to examine | |
| the incoming function signature and take action accordingly. Here is | |
| an example implementation. | |
| bool BarObject::process(const QCString &fun, const QByteArray &data, | |
| QCString &replyType, QByteArray &replyData) | |
| { | |
| if (fun == "doIt(int)") { | |
| QDataStream arg(data, IO_ReadOnly); | |
| int i; // parameter | |
| arg >> i; | |
| QString result = self->doIt (i); | |
| QDataStream reply(replyData, IO_WriteOnly); | |
| reply << result; | |
| replyType = "QString"; | |
| return true; | |
| } else { | |
| qDebug("unknown function call to BarObject::process()"); | |
| return false; | |
| } | |
| } | |
| Receiving Calls and processing them: | |
| ------------------------------------ | |
| If your applications is able to process incoming function calls | |
| right away the above code is all you need. When your application | |
| needs to do more complex tasks you might want to do the processing | |
| out of 'process' function call and send the result back later when | |
| it becomes available. | |
| For this you can ask your DCOPClient for a transactionId. You can | |
| then return from the 'process' function and when the result is | |
| available finish the transaction. In the mean time your application | |
| can receive incoming DCOP function calls from other clients. | |
| Such code could like this: | |
| bool BarObject::process(const QCString &fun, const QByteArray &data, | |
| QCString &, QByteArray &) | |
| { | |
| if (fun == "doIt(int)") { | |
| QDataStream arg(data, IO_ReadOnly); | |
| int i; // parameter | |
| arg >> i; | |
| QString result = self->doIt(i); | |
| DCOPClientTransaction *myTransaction; | |
| myTransaction = kapp->dcopClient()->beginTransaction(); | |
| // start processing... | |
| // Calls slotProcessingDone when finished. | |
| startProcessing( myTransaction, i); | |
| return true; | |
| } else { | |
| qDebug("unknown function call to BarObject::process()"); | |
| return false; | |
| } | |
| } | |
| slotProcessingDone(DCOPClientTransaction *myTransaction, const QString &result) | |
| { | |
| QCString replyType = "QString"; | |
| QByteArray replyData; | |
| QDataStream reply(replyData, IO_WriteOnly); | |
| reply << result; | |
| kapp->dcopClient()->endTransaction( myTransaction, replyType, replyData ); | |
| } | |
| DCOP Signals | |
| ------------ | |
| Sometimes a component wants to send notifications via DCOP to other | |
| components but does not know which components will be interested in these | |
| notifications. One could use a broadcast in such a case but this is a very | |
| crude method. For a more sophisticated method DCOP signals have been invented. | |
| DCOP signals are very similair to Qt signals, there are some differences | |
| though. A DCOP signal can be connected to a DCOP function. Whenever the DCOP | |
| signal gets emitted, the DCOP functions to which the signal is connected are | |
| being called. DCOP signals are, just like Qt signals, one way. They do not | |
| provide a return value. | |
| A DCOP signal originates from a DCOP Object/DCOP Client combination (sender). | |
| It can be connected to a function of another DCOP Object/DCOP Client | |
| combination (receiver). | |
| There are two major differences between connections of Qt signals and | |
| connections of DCOP signals. In DCOP, unlike Qt, a signal connections can | |
| have an anonymous sender and, unlike Qt, a DCOP signal connection can be | |
| non-volatile. | |
| With DCOP one can connect a signal without specifying the sending DCOP Object | |
| or DCOP Client. In that case signals from any DCOP Object and/or DCOP Client | |
| will be delivered. This allows the specification of certain events without | |
| tying oneself to a certain object that implementes the events. | |
| Another DCOP feature are so called non-volatile connections. With Qt signal | |
| connections, the connection gets deleted when either sender or receiver of | |
| the signal gets deleted. A volatile DCOP signal connection will behave the | |
| same. However, a non-volatile DCOP signal connection will not get deleted | |
| when the sending object gets deleted. Once a new object gets created with | |
| the same name as the original sending object, the connection will be restored. | |
| There is no difference between the two when the receiving object gets deleted, | |
| in that case the signal connection will always be deleted. | |
| A receiver can create a non-volatile connection while the sender doesn't (yet) | |
| exist. An anonymous DCOP connection should always be non-volatile. | |
| The following example shows how KLauncher emits a signal whenever it notices | |
| that an application that was started via KLauncher terminates. | |
| QByteArray params; | |
| QDataStream stream(params, IO_WriteOnly); | |
| stream << pid; | |
| kapp->dcopClient()->emitDCOPSignal("clientDied(pid_t)", params); | |
| The task manager of the KDE panel connects to this signal. It uses an | |
| anonymous connection (it doesn't require that the signal is being emitted | |
| by KLauncher) that is non-volatile: | |
| connectDCOPSignal(0, 0, "clientDied(pid_t)", "clientDied(pid_t)", false); | |
| It connects the clientDied(pid_t) signal to its own clientDied(pid_t) DCOP | |
| function. In this case the signal and the function to call have the same name. | |
| This isn't needed as long as the arguments of both signal and receiving function | |
| match. The receiving function may ignore one or more of the trailing arguments | |
| of the signal. E.g. it is allowed to connect the clientDied(pid_t) signal to | |
| a clientDied(void) DCOP function. | |
| Using the dcopidl compiler | |
| --------------------- | |
| dcopidl makes setting up a DCOP server easy. Instead of having to implement | |
| the process() method and unmarshalling (retrieving from QByteArray) parameters | |
| manually, you can let dcopidl create the necessary code on your behalf. | |
| This also allows you to describe the interface for your class in a | |
| single, separate header file. | |
| Writing an IDL file is very similar to writing a normal C++ header. An | |
| exception is the keyword 'ASYNC'. It indicates that a call to this | |
| function shall be processed asynchronously. For the C++ compiler, it | |
| expands to 'void'. | |
| Example: | |
| #ifndef MY_INTERFACE_H | |
| #define MY_INTERFACE_H | |
| #include <dcopobject.h> | |
| class MyInterface : virtual public DCOPObject | |
| { | |
| K_DCOP | |
| k_dcop: | |
| virtual ASYNC myAsynchronousMethod(QString someParameter) = 0; | |
| virtual QRect mySynchronousMethod() = 0; | |
| }; | |
| #endif | |
| As you can see, you're essentially declaring an abstract base class, which | |
| virtually inherits from DCOPObject. | |
| If you're using the standard KDE build scripts, then you can simply | |
| add this file (which you would call MyInterface.h) to your sources | |
| directory. Then you edit your Makefile.am, adding 'MyInterface.skel' | |
| to your SOURCES list and MyInterface.h to include_HEADERS. | |
| The build scripts will use dcopidl to parse MyInterface.h, converting | |
| it to an XML description in MyInterface.kidl. Next, a file called | |
| MyInterface_skel.cpp will automatically be created, compiled and | |
| linked with your binary. | |
| The next thing you have to do is to choose which of your classes will | |
| implement the interface described in MyInterface.h. Alter the inheritance | |
| of this class such that it virtually inherits from MyInterface. Then | |
| add declarations to your class interface similar to those on MyInterface.h, | |
| but virtual, not pure virtual. | |
| Example: | |
| class MyClass: public QObject, virtual public MyInterface | |
| { | |
| Q_OBJECT | |
| public: | |
| MyClass(); | |
| ~MyClass(); | |
| ASYNC myAsynchronousMethod(QString someParameter); | |
| QRect mySynchronousMethod(); | |
| }; | |
| Note: (Qt issue) Remember that if you are inheriting from QObject, you must | |
| place it first in the list of inherited classes. | |
| In the implementation of your class' ctor, you must explicitly initialize | |
| those classes from which you are inheriting from. This is, of course, good | |
| practise, but it is essential here as you need to tell DCOPObject the name of | |
| the interface which your are implementing. | |
| Example: | |
| MyClass::MyClass() | |
| : QObject(), | |
| DCOPObject("MyInterface") | |
| { | |
| // whatever... | |
| } | |
| Now you can simply implement the methods you have declared in your interface, | |
| exactly the same as you would normally. | |
| Example: | |
| void MyClass::myAsynchronousMethod(QString someParameter) | |
| { | |
| qDebug("myAsyncMethod called with param `" + someParameter + "'"); | |
| } | |
| It is not necessary (though very clean) to define an interface as an | |
| abstract class of its own, like we did in the example above. We could | |
| just as well have defined a k_dcop section directly within MyClass: | |
| class MyClass: public QObject, virtual public DCOPObject | |
| { | |
| Q_OBJECT | |
| K_DCOP | |
| public: | |
| MyClass(); | |
| ~MyClass(); | |
| k_dcop: | |
| ASYNC myAsynchronousMethod(QString someParameter); | |
| QRect mySynchronousMethod(); | |
| }; | |
| In addition to skeletons, dcopidl2cpp also generate stubs. Those make | |
| it easy to call a DCOP interface without doing the marshalling | |
| manually. To use a stub, add MyInterface.stub to the SOURCES list of | |
| your Makefile.am. The stub class will then be called MyInterface_stub. | |
| Conclusion: | |
| ----------- | |
| Hopefully this document will get you well on your way into the world | |
| of inter-process communication with KDE! Please direct all comments | |
| and/or suggestions to Preston Brown <[email protected]> and Matthias | |
| Ettrich <[email protected]>. | |
| Inter-user communication | |
| ------------------------ | |
| Sometimes it might be interesting to use DCOP between processes | |
| belonging to different users, e.g. a frontend process running | |
| with the user's id, and a backend process running as root. | |
| To do this, two steps have to be taken: | |
| a) both processes need to talk to the same DCOP server | |
| b) the authentication must be ensured | |
| For the first step, you simply pass the server address (as | |
| found in .DCOPserver) to the second process. For the authentication, | |
| you can use the ICEAUTHORITY environment variable to tell the | |
| second process where to find the authentication information. | |
| (Note that this implies that the second process is able to | |
| read the authentication file, so it will probably only work | |
| if the second process runs as root. If it should run as another | |
| user, a similar approach to what kdesu does with xauth must | |
| be taken. In fact, it would be a very good idea to add DCOP | |
| support to kdesu!) | |
| For example | |
| ICEAUTHORITY=~user/.ICEauthority kdesu root -c kcmroot -dcopserver `cat ~user/.DCOPserver` | |
| will, after kdesu got the root password, execute kcmroot as root, talking | |
| to the user's dcop server. | |
| NOTE: DCOP communication is not encrypted, so please do not | |
| pass important information around this way. | |
| Performance Tests: | |
| ------------------ | |
| A few back-of-the-napkin tests folks: | |
| Code: | |
| #include <kapplication.h> | |
| int main(int argc, char **argv) | |
| { | |
| KApplication *app; | |
| app = new KApplication(argc, argv, "testit"); | |
| return app->exec(); | |
| } | |
| Compiled with: | |
| g++ -O2 -o testit testit.cpp -I$QTDIR/include -L$QTDIR/lib -lkdecore | |
| on Linux yields the following memory use statistics: | |
| VmSize: 8076 kB | |
| VmLck: 0 kB | |
| VmRSS: 4532 kB | |
| VmData: 208 kB | |
| VmStk: 20 kB | |
| VmExe: 4 kB | |
| VmLib: 6588 kB | |
| If I create the KApplication's DCOPClient, and call attach() and | |
| registerAs(), it changes to this: | |
| VmSize: 8080 kB | |
| VmLck: 0 kB | |
| VmRSS: 4624 kB | |
| VmData: 208 kB | |
| VmStk: 20 kB | |
| VmExe: 4 kB | |
| VmLib: 6588 kB | |
| Basically it appears that using DCOP causes 100k more memory to be | |
| resident, but no more data or stack. So this will be shared between all | |
| processes, right? 100k to enable DCOP in all apps doesn't seem bad at | |
| all. :) | |
| OK now for some timings. Just creating a KApplication and then exiting | |
| (i.e. removing the call to KApplication::exec) takes this much time: | |
| 0.28user 0.02system 0:00.32elapsed 92%CPU (0avgtext+0avgdata 0maxresident)k | |
| 0inputs+0outputs (1084major+62minor)pagefaults 0swaps | |
| I.e. about 1/3 of a second on my PII-233. Now, if we create our DCOP | |
| object and attach to the server, it takes this long: | |
| 0.27user 0.03system 0:00.34elapsed 87%CPU (0avgtext+0avgdata 0maxresident)k | |
| 0inputs+0outputs (1107major+65minor)pagefaults 0swaps | |
| I.e. about 1/3 of a second. Basically DCOPClient creation and attaching | |
| gets lost in the statistical variation ("noise"). I was getting times | |
| between .32 and .48 over several runs for both of the example programs, so | |
| obviously system load is more relevant than the extra two calls to | |
| DCOPClient::attach and DCOPClient::registerAs, as well as the actual | |
| DCOPClient constructor time. | |