D-BUS學習筆記

我第一個看到的文章是ali's Blog [1], 其中提到二支程式server.c client.c,我試著去compile這二支程式, 沒什麼太大的問題就可以成功。因為這是所謂的dbus c api, 也就是low lever api,在ubuntu下只要安裝套件libdbus-1-dev, 就會安裝這個api:
$ sudo apt-get install libdbus-1-dev
然後就如該文所提到的,在compile時,必須指定library:
$ gcc server.c -o server -l dbus-1
此時再去compile程式,可能會發現找不到dbus.h的問題。其實檔案是存在的,只是gcc不知道去那裡找而已。我們只要加入-I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include 告訴gcc就可以了。
$ gcc -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -o server server.c -l dbus-1
如此,可以compile成功。但每次要打這麼多字,實在記不得,也容易打錯,所以使用Makefile吧!
建一個檔案,其名為Makefile,內容如下:
All: server client

server: server.c
  gcc -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -o server server.c -l dbus-1

client: client.c
  gcc -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -o client client.c -l dbus-1
我們把server.c client.c Makefile三個檔案,都放在同一個目錄,叫做dbus-test好了。這樣以後只要:
$ cd dbus-test
$ make
就會正確的編出server client二支可執行的程式。我們也不用去記這些很長的路徑名稱了。用過Makefile的朋友應該看得出來,其實Makefile可以再簡短一些。改成如下:
All: server client

%: %.c
   gcc -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -o $@ $< -l dbus-1
其中gcc這一行其實可以再改得更好一點,如果有用pkg-config套件的話,這一行可以改為:
$(CC) $(shell pkg-config --cflags dbus-1) -o $@ $< $(shell pkg-config --libs dbus-1)
當然,要記得在檔案前定義CC,例如“CC=gcc"。(其實也可以省略,因為是預設定義)

也許時間已久,文章中所提這二支程式的出處,我找了許久,看來連結已經不見!所以對程式的說明也沒有了。還好程式很簡短,大致上可以看出來,server.c的部份其實只是在bus上附加一個監聽signal的接收功能,並篩選想要的signal,收到時印出其帶來的參數。而client.c 則只是送出一個符合其條件的signal而已。

所以來看看第二篇文章http://www.matthew.ath.cx/misc/dbus[2],對於dbus c api有較多的範例程式。我在試他的範例(dbus-example.c)時,碰到了一個以前沒注意的問題,就是System Bus的使用,必須有授權,在網路上也看到別人在試這篇文章的程式有相同的問題。別人的回答是不要用System Bus,改用Session Bus就可以了。果然也是如此,但我想試試如何授權讓程式可以使用System Bus。所以就花點時間研究一下,發現在/etc/dbus-1目錄下有兩個設定檔:
  • system.conf
  • session.conf
另外有兩個目錄:
  • system.d
  • session.d
想想也就知道分別是各自用來設定system bus及session bus用的。檢查一下system.d目錄, 可以看到許多*.conf的檔案, 基本上每個程式有一個自己的設定檔。我先借用別人的設定檔來改:
$ cd /etc/dbus-1/system.d
$ sudo cp avahi-dbus.conf dbus-example.conf
$ sudo vi dbus-example.conf
將內容改成:
Fig. 1
這樣一來, 原本會出現的錯誤, 就消失了:
$ ./dbus-example listen
Listening for method calls
Name Error (Connection ":1.141" is not allowed to own the service "test.method.server" due to security policies in the configuration file)
Not Primary Owner (-1)
變成
$ ./dbus-example listen
Listening for method calls
所以有兩個方法可以來設定dbus系統滙流排授權: 1.使用/etc/dbus-1/目錄下的system.conf或者複製改名為system-local.conf再修改此檔。 2.另外可以在/etc/dbus-1/system.d/目錄下自己新增任意檔名.conf的設定檔來做,如我上面所提的方式。 兩者的差別在方法一必須重啟dbus服務,而方法二並不需要重啟服務。
這支範例程式的玩法有兩個:收送signal,及回應與叫用方法。分別在兩個terminal中執行:
$ ./dbus-example listen
$ ./dbus-example query hello
可以看到叫用方法並帶一個字串的參數,遠端程序可以收到這個字串參數,並印出來。而叫用端可以看到方法叫用後收到的回應(回傳值),而回應本身可以帶多個參數(此範例為兩個參數)。
同樣分別在兩個terminal中執行:
$ ./dbus-example receive
$ ./dbus-example send hello
可以看到送signal時帶一個參數,遠端程序可以收到這個字串參數,同樣可以正確的印出來。
我在試時,發現方法叫用必須明確的用allow send_destination, allow receive_sender在設定檔中授權,否則不會成功。但是signal只要在設定檔中做完allow owner的部份就可以正常收送了。

我還注意到兩個情況:
  • 和使用session bus的程式相比,system bus的訊息傳送速度明顯會慢一點
  • 使用dbus-monitor --system來監看,可以看到signal的傳送,但看不到方法的傳送
目前我還不清楚問題出在那裡,基本上,不應該如此吧。有待研究了!後記:後來看到一篇文章基本的 DBus 偵錯技巧[3]有談到dbus-monitor看不到system bus的訊息,若要能看到必須手動設定權限:
cat > /etc/dbus-1/system-local.conf
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>

  <policy context="default">
    <!-- All messages may be received by default -->
    <allow receive_requested_reply="false" receive_type="method_call" eavesdrop="true"/>
    <allow receive_requested_reply="false" receive_type="method_return" eavesdrop="true"/>
    <allow receive_requested_reply="false" receive_type="error" eavesdrop="true"/>
    <allow receive_requested_reply="false" receive_type="signal" eavesdrop="true"/>
    <allow eavesdrop="true"/>
  </policy>
  <policy user="root">
      <allow send_destination="*" eavesdrop="true"/>
      <allow receive_sender="*" eavesdrop="true"/>
  </policy>
</busconfig>
另外,程式在ubuntu 10.04可以正常執行,在ubuntu 12.10就有狀況: reply_to_method_call()函數在叫用dbus_message_iter_append_basic()時會出問題。
$ ./dbus-example listen
Listening for method calls
Method called with hello
process 6215: arguments to dbus_message_iter_append_basic() were incorrect, assertion "*bool_p == 0 || *bool_p == 1" failed in file ../../dbus/dbus-message.c line 2613.

This is normally a bug in some application using the D-Bus library. 
看了一下dbus-message.c的程式,看來也只是檢查傳入參數是否正確而已。試著把stat變數由bool型別改成dbus_bool_t,並將其值設為TRUE,再重新編譯,就可以了。另外,也許dbus有更新的關係,範例程式在send, query時都會列出程式有bug的訊息,説不可以關閉連線。所以這兩個動作的函數最後,我都將關閉連線的程式碼註解掉。//dbus_connection_close(conn);

不管是以上那一篇文章的範例程式,都是很瑣碎,在撰寫時思考的是如何去組成訊息及其參數。或者是接收到訊息後,要如何解開,並取得其帶來的參數。建議先了解D-BUS訊息運作的基本原理(可看get on the dbus[4])後,再來看範例。
使用這個dbus c api是很低階的沒錯,每一個小操作,例如送一個signal,就要寫好多程式碼。所以官方在文件中也都建議使用binding,即較高階的語言或開發工具提供的較高階的程式庫來使用。這樣可以讓開發者專注心力在服務的內容開發,而不用分心去思考低階的訊息傳送接收問題。也因為這樣,我們會注意到,低階的寫法,思考不太相同!用d-feet來查看範例,都看不到提供了什麼方法:
Fig. 2
當然,範例還是可以正常的執行啦。不過,顯然有一些工作沒有做(可能是Introspect方法沒實作),而這些工作要做也不少,但又無關於功能。所以使用binding,它就會幫你做了。
後記:為了證實我的憶測是否正確,我稍加修改dbus-example.c這支由Matthew[2]撰寫的範例程式,除了原有的test.method.Type.Method外,加入DBus規格書[12]中所規定的標準介面 org.freedesktop.DBus.Introspectable.Introspect。並依規格傳回XML格式的字串(如Fig.10所示),在Terminal下./dbus-example listen命令後,果然d-feet工具就可以看到範例程式所提供的方法了(如Fig.11所示)。而且,在d-feet中直接按Method兩下,也可以正確的執行並得到預期的回傳值(如Fig.12)。修改後的程式碼可在下載。
Fig.10
Fig.11
Fig.12
接著dbus-tutorial[5] 是眾人推薦要讀的,其中第一個範例程式介紹了在C語言中使用GLib程式庫來存取DBUS的程式寫法。同樣的又是不知道如何編譯,網路上有人問了,也有人答了how-to-compile-a-basic-d-bus-glib-example[6] 所以請自行參看,主要是有些套件要裝外,編譯的連結參數-ldbus-glib-1也要下對。這支範例程式主要使用glib的三個函數:

dbus_g_bus_get() 取得至dbus的連線
dbus_g_proxy_new_for_name() 對應遠端物件介面至proxy物件
dbus_g_proxy_call() 利用proxy物件叫用遠端物件的方法

看起來,是比使用原生的dbus c api要好了一些。不過,我在試這支程式時,發現在d-feet裡一直找不到它所使用的物件,這可是奇了,是d-feet不太行,還是這是個特别的服務呢?後記:之後在另一台ubuntu 13.04試時,發現在d-feet裡可以看到程式要叫用的方法。如Fig.3, 查看了一下,發現ubuntu 13.04的d-feet為0.3.3-1是比原本試的那台ubuntu 12.10的d-feet版本新。(不過,我只看到在連接org.freedesktop.DBus下有一個物件/,而其實程式中叫用的是物件/org/freedesktop/DBus)
Fig.3

我想在此先談一下名詞和基本概念:除了前面提到的get on the dbus[4]外, tuxpool dbus-tutorial[7], fmddlmyy[8],都是很好的入門文章。fmddlmyy是大陸朋友寫的,個人覺得寫的很好,深入淺出值得推薦,不過簡體字對我們是個問題,特别是專有名詞方面。剛好接下來,我會介紹一個語言vala,也正好有大陸朋友做了一個不錯的文件翻譯vala introduce[9],其中正好有英文與簡體字的對照。大家對照著看,或丢到Word去做簡繁轉換及專有名稱替換,之後再印出來讀,會好一點。下面簡單整理一下重要名詞及其相當用詞。
  • bus
  • connection = bus name(d-feet) = service
  • object = object path = path name
  • interface
  • methods and signals
Fig. 4
我個人對這些名詞和其之間關係的理解,畫成如上Fig.4,不過,詳細的説明和介紹,還是請參看以上提到的文章,當然還有很多寫得不錯的文章,可以google到。

dbus-tutorial[5]雖然介紹了基本的dbus-glib程式寫法(該文有一半在講解如何寫glib程式),但是只有一個簡單的客戶端程式範例和一些零散的程式片段,其實並不容易上手。石頭閒語[10]則提到比較實用的作法是去使用DBusGlibBindings。正如石頭成先生所說,DBusGlibBindings這個源碼,其實是個比較完整的架構,借用其工具,我們只要修改其中program.c和XML檔就可以産生自訂的服務。

Fig.5
但是當我連入這個網頁時,卻看到其警語:「Note: The example code is outdated. It needs to be converted from dbus-glib to GIO's gdbus.」如Fig.5所示,這也是沒錯啦,dbus-glib是已經有點過時了,看來現在是比較推GIO's gdbus。再往下看,我注意到他們推另一個方式,用vala來讓一切更容易一點。我看了這個網頁並且試了一下範例程式,真得簡單很多。vala是一個看來很有意思的語言,基本上語法是大量借用C#,所以玩過微軟的.NET就會覺得很熟悉。建議自己去試一下範例程式,網頁上也教了編譯的方法,很容易就上手。我試了一下,用dbus c api寫的客戶端程式去叫用vala寫的服務也是可以運作的。

以下比較一下客戶端寫法,在叫用同一個服務的方法時,所需撰寫程式的不同:
1. dbus c api
  dbmsg = dbus_message_new_method_call("org.example.Demo", // connection
                                "/org/example/demo",       // object
                                "org.example.Demo",        // interface
                                "Ping");                   // method
  if (!dbmsg) {
      return -1;
  }

  word = "hello world";
  if (!dbus_message_append_args(dbmsg, DBUS_TYPE_STRING, &word, DBUS_TYPE_INVALID)) {
      return -1;
  }

  // send message and wait for reply, -1 means wait forever
  reply = dbus_connection_send_with_reply_and_block(dbconn, dbmsg, -1, NULL);
  if (!reply) {
      return -1;
  }
  
  // read the parameters
  dbus_message_iter_init(reply, &iter);
  dbus_message_iter_get_basic(&iter, &ri);
  
  dbus_connection_flush(dbconn);
  printf("return value: %d\n", ri);

  dbus_message_unref(dbmsg);
  dbus_message_unref(reply);
2. dbus-glib
  proxy = dbus_g_proxy_new_for_name (connection,
                                     "org.example.Demo",
                                     "/org/example/demo",
                                     "org.example.Demo");
  dbus_g_proxy_call (proxy, "Ping", &error, G_TYPE_INVALID,
                          G_TYPE_INT, &ri, G_TYPE_INVALID);
  g_print ("return value: %d\n", ri);
3. vala
  demo = Bus.get_proxy_sync (BusType.SESSION, "org.example.Demo",
                                                 "/org/example/demo");
  int reply = demo.ping ("Hello from Vala");
  stdout.printf ("%d\n", reply);
4. python
  bus = dbus.SessionBus() 
  obj = bus.get_object('org.example.Demo', '/org/example/demo')
  iface = dbus.Interface(obj, 'org.example.Demo')
  ri = iface.Ping('Hello')
  print ri
很明顯的,這是進化。由低階到高階的進步,光由程式碼的行數就可以看出來,用vala寫的程式較少,而且其語法較簡潔自然。和用python寫的程式相比,甚至更短了一些。不過兩者都已經很簡潔了,一個是編譯式,一個是直譯式,各有其用途啦。

以下整理各個範例程式,以供有興趣的朋友測試,請注意程式來源皆在前文已提及,在此只是加入Makefile以方便編譯,目前只有在ubuntu Linux 12.10及13.04測試過可用。

DBus C API simple server and client for signal: dbus-test
DBus C API Matthew's sample code: dbus-example
dbus-glib sample code: dbus-glib
GLib/GIO GDbus API vala sample: vala-dbus
python dbus sample client to call vala sample's service: demo-py

前面談到用vala來寫server端或服務會比較容易,後來看到用python來寫服務也很容易(似乎更容易一點)可參考python dbus[11]這一篇:
它介紹到兩支程式,一支短一點,另一支長一點:
第一支程式跑起來後,用d-feet看起來如下圖(Fig.6):
Fig.6
就只是提供兩個method,一個是HelloWorld(),叫用它時你可以提供一個字串參數,它會被印在Terminal,同時它會返回一個字串陣列。如下圖(Fig.7)。當然用d-feet來叫用method會比較輕鬆,只要點選就好了。我們也可以下dbus-send指令:
$ dbus-send --print-reply --dest=com.example.SampleService /SomeObject com.example.SampleInterface.HelloWorld string:hello

method return sender=:1.113 -> dest=:1.117 reply_serial=2
   array [
      string "Hello"
      string " from example-service.py"
      string "with unique name"
      string ":1.113"
   ]

Fig.7
請注意返回值中最後一個字串是服務的唯一名。我這個版本的d-feet似乎有個小bug,就是無法正確顯示每個服務的唯一名,不論你切到那一個服務,它顯示的唯一名都一樣。反而舊版的d-feet可以正確顯示!
另一個method是Exit(),叫用這個method就會結束這支程式(或此服務),但是我直接在d-feet中叫用,好像會造成d-feet有點問題!不過,應該是d-feet的bug,不是DBus的問題。

第二支程式稍為長了一點,但也展示了較多的寫法:首先它在服務端多了一個signal,程式展示如何設置signal,以及如何在需要時觸發這個signal。另外寫了一個類別會去註冊要接收這個signal,當收到時會去叫用一個叫做handler()的method,此方法會列印出一些訊息。它還公開另兩個方法,一個是Say(),一個是Stop()。Stop()和前一支程式的Exit()一樣,就是用來結束自己的服務。Say()有點意思,它其實做了三件事:
  1. 叫用另一個DBus服務的方法,以便在螢幕上快顯一個訊息提示。
  2. 觸發它唯一的signal
  3. 返回一個字串
用d-feet查看如下圖(Fig.8):
Fig.8
Fig.9
叫用Say()的效果如Fig.9,螢幕右上角會出現快顯提示,而左下角可以看到服務本身的signal接收器也收到這個signal的發生,而且做了動作。
是不是感覺用python寫dbus服務真的容易許多。

參考來源:
[1] lazycat, D-Bus, Ali's Blog, 2012/3/5
[2] Matthew Johnson, Using the DBUS C API
[3] Rex Tsai, 基本的DBus偵錯技巧, Rex's blah blah blah, 2011/3/8
[4] Robert Love, Get on the D-Bus, Linux Journal, 2005/1/5
[5] Pennington et al., D-Bus Tutorial
[6] tvuillemin, How to compile a basic D-Bus/glib example, stackoverflow, 2013/1/10
[7] Michael, dbus tutorial - part 1, TuxPool, 2010/4/23
[8] fmddlmyy, dbus實例講解, fmddlmyy的专栏, 2008/12/23
[9] I'm Matrix, 【原创翻译】Vala编程手册, ubuntu forum, 2013/2/8
[10] 石頭成, dbus-glib bindings入門磚, 石頭閒語, 2010/7/12
[11] 石頭成, Python DBus教學精要, 石頭閒語, 2011/4/14
[12] Pennington et al., D-Bus Specification 0.19, 2012/2/20

留言

這個網誌中的熱門文章

關於藍牙裝置找尋(inquiry, scan)兩三事

Cisco Switch學習筆記: EtherChannel