1 DCOP: Desktop COmmunications Protocol 2 3 Preston Brown <pbrown@kde.org> 4 October 14, 1999 5 6 Revised and extended by Matthias Ettrich <ettrich@kde.org> 7 Mar 29, 2000 8 9 Extended with DCOP Signals by Waldo Bastian <bastian@kde.org> 10 Feb 19, 2001 11 12 13Motivation and Background: 14-------------------------- 15 16The motivation behind building a protocol like DCOP is simple. For 17the past year, we have been attempting to enable interprocess 18communication between KDE applications. KDE already has an extremely 19simple IPC mechanism called KWMcom, which is (was!) used for communicating 20between the panel and the window manager for instance. It is about as 21simple as it gets, passing messages via X Atoms. For this reason it 22is limited in the size and complexity of the data that can be passed 23(X atoms must be small to remain efficient) and it also makes it so 24that X is required. CORBA was thought to be a more effective IPC/RPC 25solution. However, after a year of attempting to make heavy use of 26CORBA in KDE, we have realized that it is a bit slow and memory 27intensive for simple use. It also has no authentication available. 28 29What we really needed was an extremely simple protocol with basic 30authorization, along the lines of MIT-MAGIC-COOKIE, as used by X. It 31would not be able to do NEARLY what CORBA was able to do, but for the 32simple tasks required it would be sufficient. Some examples of such 33tasks might be an application sending a message to the panel saying, 34"I have started, stop displaying the 'application starting' wait 35state," or having a new application that starts query to see if any 36other applications of the same name are running. If they are, simply 37call a function on the remote application to create a new window, 38rather than starting a new process. 39 40Implementation: 41--------------- 42 43DCOP is a simple IPC/RPC mechanism built to operate over sockets. 44Either unix domain sockets or tcp/ip sockets are supported. DCOP is 45built on top of the Inter Client Exchange (ICE) protocol, which comes 46standard as a part of X11R6 and later. It also depends on Qt, but 47beyond that it does not require any other libraries. Because of this, 48it is extremely lightweight, enabling it to be linked into all KDE 49applications with low overhead. 50 51Model: 52------ 53 54The model is simple. Each application using DCOP is a client. They 55communicate to each other through a DCOP server, which functions like 56a traffic director, dispatching messages/calls to the proper 57destinations. All clients are peers of each other. 58 59Two types of actions are possible with DCOP: "send and forget" 60messages, which do not block, and "calls," which block waiting for 61some data to be returned. 62 63Any data that will be sent is serialized (marshalled, for you CORBA 64types) using the built-in QDataStream operators available in all of 65the Qt classes. This is fast and easy. In fact it's so little work 66that you can easily write the marshalling code by hand. In addition, 67there's a simple IDL-like compiler available (dcopidl and dcopidl2cpp) 68that generates stubs and skeletons for you. Using the dcopidl compiler 69has the additional benefit of type safety. 70 71This HOWTO describes the manual method first and covers the dcopidl 72compiler later. 73 74Establishing the Connection: 75---------------------------- 76 77KApplication has gained a method called "KApplication::dcopClient()" 78which returns a pointer to a DCOPClient instance. The first time this 79method is called, the client class will be created. DCOPClients have 80unique identifiers attached to them which are based on what 81KApplication::name() returns. In fact, if there is only a single 82instance of the program running, the appId will be equal to 83KApplication::name(). 84 85To actually enable DCOP communication to begin, you must use 86DCOPClient::attach(). This will attempt to attach to the DCOP server. 87If no server is found or there is any other type of error, attach() 88will return false. KApplication will catch a dcop signal and display an 89appropriate error message box in that case. 90 91After connecting with the server via DCOPClient::attach(), you need to 92register this appId with the server so it knows about you. Otherwise, 93you are communicating anonymously. Use the 94DCOPClient::registerAs(const QCString &name) to do so. In the simple 95case: 96 97/* 98 * returns the appId that is actually registered, which _may_ be 99 * different from what you passed 100 */ 101appId = client->registerAs(kApp->name()); 102 103If you never retrieve the DCOPClient pointer from KApplication, the 104object will not be created and thus there will be no memory overhead. 105 106You may also detach from the server by calling DCOPClient::detach(). 107If you wish to attach again you will need to re-register as well. If 108you only wish to change the ID under which you are registered, simply 109call DCOPClient::registerAs() with the new name. 110 111KUniqueApplication automatically registers itself to DCOP. If you 112are using KUniqueApplication you should not attach or register 113yourself, this is already done. The appId is by definition 114equal to kapp->name(). You can retrieve the registered DCOP client 115by calling kapp->dcopClient(). 116 117Sending Data to a Remote Application: 118------------------------------------- 119 120To actually communicate, you have one of two choices. You may either 121call the "send" or the "call" method. Both methods require three 122identification parameters: an application identifier, a remote object, 123a remote function. Sending is asynchronous (i.e. it returns immediately) 124and may or may not result in your own application being sent a message at 125some point in the future. Then "send" requires one and "call" requires 126two data parameters. 127 128The remote object must be specified as an object hierarchy. That is, 129if the toplevel object is called "fooObject" and has the child 130"barObject", you would reference this object as "fooObject/barObject". 131Functions must be described by a full function signature. If the 132remote function is called "doIt", and it takes an int, it would be 133described as "doIt(int)". Please note that the return type is not 134specified here, as it is not part of the function signature (or at 135least the C++ understanding of a function signature). You will get 136the return type of a function back as an extra parameter to 137DCOPClient::call(). See the section on call() for more details. 138 139In order to actually get the data to the remote client, it must be 140"serialized" via a QDataStream operating on a QByteArray. This is how 141the data parameter is "built". A few examples will make clear how this 142works. 143 144Say you want to call "doIt" as described above, and not block (or wait 145for a response). You will not receive the return value of the remotely 146called function, but you will not hang while the RPC is processed either. 147The return value of send() indicates whether DCOP communication succeeded 148or not. 149 150QByteArray data; 151QDataStream arg(data, IO_WriteOnly); 152arg << 5; 153if (!client->send("someAppId", "fooObject/barObject", "doIt(int)", 154 data)) 155 qDebug("there was some error using DCOP."); 156 157OK, now let's say we wanted to get the data back from the remotely 158called function. You have to execute a call() instead of a send(). 159The returned value will then be available in the data parameter "reply". 160The actual return value of call() is still whether or not DCOP 161communication was successful. 162 163QByteArray data, replyData; 164QCString replyType; 165QDataStream arg(data, IO_WriteOnly); 166arg << 5; 167if (!client->call("someAppId", "fooObject/barObject", "doIt(int)", 168 data, replyType, replyData)) 169 qDebug("there was some error using DCOP."); 170else { 171 QDataStream reply(replyData, IO_ReadOnly); 172 if (replyType == "QString") { 173 QString result; 174 reply >> result; 175 print("the result is: %s",result.latin1()); 176 } else 177 qDebug("doIt returned an unexpected type of reply!"); 178} 179 180N.B.: You cannot call() a method belonging to an application which has 181registered with an unique numeric id appended to its textual name (see 182dcopclient.h for more info). In this case, DCOP would not know which 183application it should connect with to call the method. This is not an issue 184with send(), as you can broadcast to all applications that have registered 185with appname-<numeric_id> by using a wildcard (e.g. 'konsole-*'), which 186will send your signal to all applications called 'konsole'. 187 188Receiving Data via DCOP: 189------------------------ 190 191Currently the only real way to receive data from DCOP is to multiply 192inherit from the normal class that you are inheriting (usually some 193sort of QWidget subclass or QObject) as well as the DCOPObject class. 194DCOPObject provides one very important method: DCOPObject::process(). 195This is a pure virtual method that you must implement in order to 196process DCOP messages that you receive. It takes a function 197signature, QByteArray of parameters, and a reference to a QByteArray 198for the reply data that you must fill in. 199 200Think of DCOPObject::process() as a sort of dispatch agent. In the 201future, there will probably be a precompiler for your sources to write 202this method for you. However, until that point you need to examine 203the incoming function signature and take action accordingly. Here is 204an example implementation. 205 206bool BarObject::process(const QCString &fun, const QByteArray &data, 207 QCString &replyType, QByteArray &replyData) 208{ 209 if (fun == "doIt(int)") { 210 QDataStream arg(data, IO_ReadOnly); 211 int i; // parameter 212 arg >> i; 213 QString result = self->doIt (i); 214 QDataStream reply(replyData, IO_WriteOnly); 215 reply << result; 216 replyType = "QString"; 217 return true; 218 } else { 219 qDebug("unknown function call to BarObject::process()"); 220 return false; 221 } 222} 223 224Receiving Calls and processing them: 225------------------------------------ 226 227If your applications is able to process incoming function calls 228right away the above code is all you need. When your application 229needs to do more complex tasks you might want to do the processing 230out of 'process' function call and send the result back later when 231it becomes available. 232 233For this you can ask your DCOPClient for a transactionId. You can 234then return from the 'process' function and when the result is 235available finish the transaction. In the mean time your application 236can receive incoming DCOP function calls from other clients. 237 238Such code could like this: 239 240bool BarObject::process(const QCString &fun, const QByteArray &data, 241 QCString &, QByteArray &) 242{ 243 if (fun == "doIt(int)") { 244 QDataStream arg(data, IO_ReadOnly); 245 int i; // parameter 246 arg >> i; 247 QString result = self->doIt(i); 248 249 DCOPClientTransaction *myTransaction; 250 myTransaction = kapp->dcopClient()->beginTransaction(); 251 252 // start processing... 253 // Calls slotProcessingDone when finished. 254 startProcessing( myTransaction, i); 255 256 return true; 257 } else { 258 qDebug("unknown function call to BarObject::process()"); 259 return false; 260 } 261} 262 263slotProcessingDone(DCOPClientTransaction *myTransaction, const QString &result) 264{ 265 QCString replyType = "QString"; 266 QByteArray replyData; 267 QDataStream reply(replyData, IO_WriteOnly); 268 reply << result; 269 kapp->dcopClient()->endTransaction( myTransaction, replyType, replyData ); 270} 271 272DCOP Signals 273------------ 274 275Sometimes a component wants to send notifications via DCOP to other 276components but does not know which components will be interested in these 277notifications. One could use a broadcast in such a case but this is a very 278crude method. For a more sophisticated method DCOP signals have been invented. 279 280DCOP signals are very similair to Qt signals, there are some differences 281though. A DCOP signal can be connected to a DCOP function. Whenever the DCOP 282signal gets emitted, the DCOP functions to which the signal is connected are 283being called. DCOP signals are, just like Qt signals, one way. They do not 284provide a return value. 285 286A DCOP signal originates from a DCOP Object/DCOP Client combination (sender). 287It can be connected to a function of another DCOP Object/DCOP Client 288combination (receiver). 289 290There are two major differences between connections of Qt signals and 291connections of DCOP signals. In DCOP, unlike Qt, a signal connections can 292have an anonymous sender and, unlike Qt, a DCOP signal connection can be 293non-volatile. 294 295With DCOP one can connect a signal without specifying the sending DCOP Object 296or DCOP Client. In that case signals from any DCOP Object and/or DCOP Client 297will be delivered. This allows the specification of certain events without 298tying oneself to a certain object that implementes the events. 299 300Another DCOP feature are so called non-volatile connections. With Qt signal 301connections, the connection gets deleted when either sender or receiver of 302the signal gets deleted. A volatile DCOP signal connection will behave the 303same. However, a non-volatile DCOP signal connection will not get deleted 304when the sending object gets deleted. Once a new object gets created with 305the same name as the original sending object, the connection will be restored. 306There is no difference between the two when the receiving object gets deleted, 307in that case the signal connection will always be deleted. 308 309A receiver can create a non-volatile connection while the sender doesn't (yet) 310exist. An anonymous DCOP connection should always be non-volatile. 311 312The following example shows how KLauncher emits a signal whenever it notices 313that an application that was started via KLauncher terminates. 314 315 QByteArray params; 316 QDataStream stream(params, IO_WriteOnly); 317 stream << pid; 318 kapp->dcopClient()->emitDCOPSignal("clientDied(pid_t)", params); 319 320The task manager of the KDE panel connects to this signal. It uses an 321anonymous connection (it doesn't require that the signal is being emitted 322by KLauncher) that is non-volatile: 323 324 connectDCOPSignal(0, 0, "clientDied(pid_t)", "clientDied(pid_t)", false); 325 326It connects the clientDied(pid_t) signal to its own clientDied(pid_t) DCOP 327function. In this case the signal and the function to call have the same name. 328This isn't needed as long as the arguments of both signal and receiving function 329match. The receiving function may ignore one or more of the trailing arguments 330of the signal. E.g. it is allowed to connect the clientDied(pid_t) signal to 331a clientDied(void) DCOP function. 332 333Using the dcopidl compiler 334--------------------- 335 336dcopidl makes setting up a DCOP server easy. Instead of having to implement 337the process() method and unmarshalling (retrieving from QByteArray) parameters 338manually, you can let dcopidl create the necessary code on your behalf. 339 340This also allows you to describe the interface for your class in a 341single, separate header file. 342 343Writing an IDL file is very similar to writing a normal C++ header. An 344exception is the keyword 'ASYNC'. It indicates that a call to this 345function shall be processed asynchronously. For the C++ compiler, it 346expands to 'void'. 347 348Example: 349 350#ifndef MY_INTERFACE_H 351#define MY_INTERFACE_H 352 353#include <dcopobject.h> 354 355class MyInterface : virtual public DCOPObject 356{ 357 K_DCOP 358 359 k_dcop: 360 361 virtual ASYNC myAsynchronousMethod(QString someParameter) = 0; 362 virtual QRect mySynchronousMethod() = 0; 363}; 364 365#endif 366 367As you can see, you're essentially declaring an abstract base class, which 368virtually inherits from DCOPObject. 369 370If you're using the standard KDE build scripts, then you can simply 371add this file (which you would call MyInterface.h) to your sources 372directory. Then you edit your Makefile.am, adding 'MyInterface.skel' 373to your SOURCES list and MyInterface.h to include_HEADERS. 374 375The build scripts will use dcopidl to parse MyInterface.h, converting 376it to an XML description in MyInterface.kidl. Next, a file called 377MyInterface_skel.cpp will automatically be created, compiled and 378linked with your binary. 379 380The next thing you have to do is to choose which of your classes will 381implement the interface described in MyInterface.h. Alter the inheritance 382of this class such that it virtually inherits from MyInterface. Then 383add declarations to your class interface similar to those on MyInterface.h, 384but virtual, not pure virtual. 385 386Example: 387 388class MyClass: public QObject, virtual public MyInterface 389{ 390 Q_OBJECT 391 392 public: 393 MyClass(); 394 ~MyClass(); 395 396 ASYNC myAsynchronousMethod(QString someParameter); 397 QRect mySynchronousMethod(); 398}; 399 400Note: (Qt issue) Remember that if you are inheriting from QObject, you must 401place it first in the list of inherited classes. 402 403In the implementation of your class' ctor, you must explicitly initialize 404those classes from which you are inheriting from. This is, of course, good 405practise, but it is essential here as you need to tell DCOPObject the name of 406the interface which your are implementing. 407 408Example: 409 410MyClass::MyClass() 411 : QObject(), 412 DCOPObject("MyInterface") 413{ 414 // whatever... 415} 416 417Now you can simply implement the methods you have declared in your interface, 418exactly the same as you would normally. 419 420Example: 421 422void MyClass::myAsynchronousMethod(QString someParameter) 423{ 424 qDebug("myAsyncMethod called with param `" + someParameter + "'"); 425} 426 427 428It is not necessary (though very clean) to define an interface as an 429abstract class of its own, like we did in the example above. We could 430just as well have defined a k_dcop section directly within MyClass: 431 432class MyClass: public QObject, virtual public DCOPObject 433{ 434 Q_OBJECT 435 K_DCOP 436 437 public: 438 MyClass(); 439 ~MyClass(); 440 441 k_dcop: 442 ASYNC myAsynchronousMethod(QString someParameter); 443 QRect mySynchronousMethod(); 444}; 445 446In addition to skeletons, dcopidl2cpp also generate stubs. Those make 447it easy to call a DCOP interface without doing the marshalling 448manually. To use a stub, add MyInterface.stub to the SOURCES list of 449your Makefile.am. The stub class will then be called MyInterface_stub. 450 451Conclusion: 452----------- 453 454Hopefully this document will get you well on your way into the world 455of inter-process communication with KDE! Please direct all comments 456and/or suggestions to Preston Brown <pbrown@kde.org> and Matthias 457Ettrich <ettrich@kde.org>. 458 459 460Inter-user communication 461------------------------ 462 463Sometimes it might be interesting to use DCOP between processes 464belonging to different users, e.g. a frontend process running 465with the user's id, and a backend process running as root. 466 467To do this, two steps have to be taken: 468 469a) both processes need to talk to the same DCOP server 470b) the authentication must be ensured 471 472For the first step, you simply pass the server address (as 473found in .DCOPserver) to the second process. For the authentication, 474you can use the ICEAUTHORITY environment variable to tell the 475second process where to find the authentication information. 476(Note that this implies that the second process is able to 477read the authentication file, so it will probably only work 478if the second process runs as root. If it should run as another 479user, a similar approach to what kdesu does with xauth must 480be taken. In fact, it would be a very good idea to add DCOP 481support to kdesu!) 482 483For example 484 485ICEAUTHORITY=~user/.ICEauthority kdesu root -c kcmroot -dcopserver `cat ~user/.DCOPserver` 486 487will, after kdesu got the root password, execute kcmroot as root, talking 488to the user's dcop server. 489 490 491NOTE: DCOP communication is not encrypted, so please do not 492pass important information around this way. 493 494 495Performance Tests: 496------------------ 497A few back-of-the-napkin tests folks: 498 499Code: 500 501#include <kapplication.h> 502 503int main(int argc, char **argv) 504{ 505 KApplication *app; 506 507 app = new KApplication(argc, argv, "testit"); 508 return app->exec(); 509} 510 511Compiled with: 512 513g++ -O2 -o testit testit.cpp -I$QTDIR/include -L$QTDIR/lib -lkdecore 514 515on Linux yields the following memory use statistics: 516 517VmSize: 8076 kB 518VmLck: 0 kB 519VmRSS: 4532 kB 520VmData: 208 kB 521VmStk: 20 kB 522VmExe: 4 kB 523VmLib: 6588 kB 524 525If I create the KApplication's DCOPClient, and call attach() and 526registerAs(), it changes to this: 527 528VmSize: 8080 kB 529VmLck: 0 kB 530VmRSS: 4624 kB 531VmData: 208 kB 532VmStk: 20 kB 533VmExe: 4 kB 534VmLib: 6588 kB 535 536Basically it appears that using DCOP causes 100k more memory to be 537resident, but no more data or stack. So this will be shared between all 538processes, right? 100k to enable DCOP in all apps doesn't seem bad at 539all. :) 540 541OK now for some timings. Just creating a KApplication and then exiting 542(i.e. removing the call to KApplication::exec) takes this much time: 543 5440.28user 0.02system 0:00.32elapsed 92%CPU (0avgtext+0avgdata 0maxresident)k 5450inputs+0outputs (1084major+62minor)pagefaults 0swaps 546 547I.e. about 1/3 of a second on my PII-233. Now, if we create our DCOP 548object and attach to the server, it takes this long: 549 5500.27user 0.03system 0:00.34elapsed 87%CPU (0avgtext+0avgdata 0maxresident)k 5510inputs+0outputs (1107major+65minor)pagefaults 0swaps 552 553I.e. about 1/3 of a second. Basically DCOPClient creation and attaching 554gets lost in the statistical variation ("noise"). I was getting times 555between .32 and .48 over several runs for both of the example programs, so 556obviously system load is more relevant than the extra two calls to 557DCOPClient::attach and DCOPClient::registerAs, as well as the actual 558DCOPClient constructor time. 559 560