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