php dbus 0.1.0 撰寫 DBus service 的使用經驗
php dbus 0.1.0 (Derick's php-dbus) 仍然在發展階段,其中還有許多待解決的問題。故目前使用時,可能會碰到許多意外事件。本文整理了我的使用經驗,可為 PHP 程序員以 php-dbus 撰寫 DBus 服務之參考。
基本概念
php-dbus 0.1.0 目前的實現內容,只能將一個 PHP 類別視為一個 DBus 個體(object)的介面(interface)內容。
PHP 類別內的靜態方法將自動被視為 DBus 個體介面的方法。
所以 PHP 實作一個 DBus 個體介面時,要撰寫一個類別,在其中設計靜態方法。
最後調用 Dbus::registerObject($object_path, $interface_name, $php_class_name);
將此類別註冊為指定的 DBus 個體介面。
你註冊的 DBus 介面名稱與其對應的類別名稱無關,而且也沒有綁定關係。 若你需要,你可以重複將這個類別註冊為不同的 DBus 個體之介面。
例如下面的範例,將提供一個名為 blog.rock 的 DBus 服務。 此服務將註冊三個 DBus 個體,並且各自定義了一個 DBus 介面。 而這三個 DBus 介面都是由同一個 PHP 類別所註冊。
class DbusEchoInterface {
static function Echo() {
return "hello";
}
}
$bus->requestName('blog.rock');
// DBus 個體 '/blog/rock/obj1' 定義了介面 'blog.rock.Echo'.
$bus->registerObject('/blog/rock/obj1', 'blog.rock.Echo', 'DbusEchoInterface');
// DBus 個體 '/blog/rock/obj2' 定義了介面 'blog.rock.Echo'.
$bus->registerObject('/blog/rock/obj2', 'blog.rock.Echo', 'DbusEchoInterface');
// DBus 個體 '/blog/rock/abc' 定義了介面 'blog.abc.Hello'.
$bus->registerObject('/blog/rock/abc', 'blog.abc.Hello', 'DbusEchoInterface');
設計介面方法時,基本上和設計一般的 PHP 類別方法相同,在參數列指示傳入的參數名稱,透過 return 回傳值。 傳入的參數可以是任何 PHP 內建的資料型別,例如整數、字串、陣列。回傳的值也可以是任何 PHP 內建的資料型別。 但受限於 DBus Type 本身可處理的內容,回傳值有下列注意要項。
- DbusArray 不允許容納不同資料型別的元素。
- 因為 PHP 語法並不支援一個函數回傳多個值,所以回傳多個值的情形另行處理,而且其中有 bug 。
DbusArray
PHP 的陣列有兩種型式,一種是純數字索引的傳統陣列,另一種是以鍵值索引的關聯式陣列。前者對應於 DbusArray ,後者對應於 DbusDict 。而且 DbusArray 內的元素,必須是同一種資料型別。
當你回傳的 PHP 陣列含有不同資料型別的元素時,php-dbus 會嘗試以 DbusDict 型態處理,但可能會失敗。既使成功轉換為 DbusDict 了,此結果也可能不符合呼叫者的預期。若呼叫者屬於 C/C#/Java 靜態型別語言,它們將擲出型別例外。
多個回傳值與 DbusSet
如果你想直接回傳多個值,而不透過陣列,那麼你必須使用一個 DbusSet 個體包裹它們。此外,DbusSet 目前有 bug 。它不會複製要回傳值的內容,故被呼叫的方法結束後,放置在 DbusSet 內的回傳值,其內容所在的記憶體空間也被釋放了。以至於呼叫者要取得回傳值時,將取不到正確值,甚或是發生違法存取記憶體的錯誤。故而,你要放在 DbusSet 內的回傳值,必須是一個全域變數或是一個類別的靜態變數。如下所示:
static $result_b;
static function EchoTwo($a) {
echo "Two";
//BUG: (php-dbus 0.1.0) It does not copy value into set.
// If you put local variables in set, it would not return correct values.
//return new DbusSet($a, 2); //incorrect
self::$result_b = 2; //put in a static variable.
return new DBusSet($a, self::$result_b);
}
DBusSignal
DBus Signal 被 php-dbus 實作為一個獨立的類別 DbusSignal。要在某個 DBus 介面上實現一個 Signal 的方式是在你新增 DbusSignal 個體時,指定這個訊號要附加的介面名稱。另一方面,為了方便 DBus 個體可以主動發出訊號,最好是將你配置的 DbusSignal 個體指派給 DBus 類別的靜態成員。如此一來,你就可以透過該靜態成員,呼叫 DbusSignal 個體的 send() 方法送出訊號。如下所示。
// add signal 'Signal1' to interface 'blog.rock.Echo'
// of object '/blog/rock/obj1'
class DbusEchoInterface {
static $signal1;
static function emit() {
// 在 DBus 個體內部透過成員主動發出訊號
self::$signal1->send();
}
}
// 新增一個 DbusSignal ,附加於 /blog/rock/obj1 個體之 blog.rock.Echo 介面上。
// 並指派給DbusEchoInterface::$signal1
DbusEchoInterface::$signal1 = new DbusSignal(
$bus,
'/blog/rock/obj1',
'blog.rock.Echo',
'Signal1'
);
//送出訊號。
DbusEchoInterface::$signal1->send();
Introspect
大多數程式語言對於 DBus 的支援,一般皆透過 dbus-glib-binding 實現,同時運用本身的反射機制,自動為你實作的 DBus 個體產生 org.freedesktop.DBus.Introspectable 介面。但是 php-dbus 採用 DBus low-level API 實作,並受限於其能力,故不會自動產生 org.freedesktop.DBus.Introspectable 和 org.freedesktop.DBus.Properties 這兩個泛用介面;後者較為少用,一般也不會實作它。若你需要提供此介面,則你必須自行定義 org.freedesktop.DBus.Introspectable 和 org.freedesktop.DBus.Properties 兩介面的內容。當然不定義也可以,只是其他人就不能透過 org.freedesktop.DBus.Introspectable 查詢你的 DBus 服務的介面。
定義 org.freedesktop.DBus.Introspectable 的方式非常簡單。它只有一個名為 Introspect 的方法,回傳一個內容為 XML 文件的字串。該 XML 文件透過固定的方式描述你的 DBus 服務提供的 DBus 個體與其介面。你可以向 org.freedesktop.DBus 服務查詢其內容,了解其 XML 文件的格式。細節則請參考 D-Bus 規格文件。
在本文中,我使用一個固定的 XML 文件描述本文實作的範例服務。
<?php
// interface and methods, see:
//http://svn.php.net/viewvc/pecl/dbus/trunk/examples/request-name.php?view=markup
class DbusIntrospectableInterface {
static $introspect_xml =<<<INTROSPECT_XML
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" direction="out" type="s"/>
</method>
</interface>
<interface name="blog.rock">
<method name="EchoZero">
<arg direction="out" type="i"/>
</method>
<method name="EchoOne">
<arg name="name" direction="in" type="s"/>
<arg direction="out" type="s"/>
</method>
<method name="EchoTwo">
<arg name="name" direction="in" type="s"/>
<arg direction="out" type="s"/>
<arg direction="out" type="i"/>
</method>
<method name="EchoArray">
<arg direction="out" type="as"/>
</method>
<signal name="Singal1">
</signal>
<signal name="Singal2">
<arg type="s"/>
</signal>
</interface>
</node>
INTROSPECT_XML;
static function Introspect() {
return DbusIntrospectableInterface::$introspect_xml;
}
}
//echo DbusIntrospectableInterface::Introspect();
?>
撰寫 DBus client 與傾聽訊號
我在 《Write a PHP DBus client》一文中,便已示範過如何撰寫 DBus 客戶端以及傾聽服務的訊號。請自行前往閱讀。在此僅提供一個傾聽本文示範服務之訊號的客戶端程式。
<?php
//http://svn.php.net/viewvc/pecl/dbus/trunk/examples/signal-watch.php?view=markup
$bus = new Dbus( Dbus::BUS_SESSION );
$bus->addWatch( 'blog.rock' );
while (true) {
$s = $bus->waitLoop(1000);
if (!$s)
continue;
if ($s->matches('blog.rock', 'Signal1')) {
echo "get signal 1\n";
}
if ($s->matches('blog.rock', 'Signal2')) {
echo "get signal 2\n";
var_dump($s->getData());
}
}
?>
Service sample
綜合以上所述,實作一個完整可用的 DBus 服務。
<?php
require_once 'my_introspectable.php';
// interface and methods, see:
//http://svn.php.net/viewvc/pecl/dbus/trunk/examples/request-name.php?view=markup
class DbusEchoInterface {
static function EchoZero() {
echo "Zero";
return 0;
}
static function EchoOne($name) {
echo "One";
return "Hello " . $name;
}
static $result_b;
static function EchoTwo($a) {
echo "Two";
//BUG: (php-dbus 0.1.0) It does not copy value into set.
// If you put local variables in set, it would not return correct values.
//return new DbusSet($a, 2);
self::$result_b = 2;
return new DBusSet($a, self::$result_b);
}
static function EchoArray() {
echo "Array";
//notice: dbus array cound not contain elements of multiple type.
return array("array", "echo");
}
// signals, see:
//http://svn.php.net/viewvc/pecl/dbus/trunk/examples/signal-send.php?view=markup
static function TestSignal() {
echo "emit signal";
self::$Signal1->send();
self::$Signal2->send("signal2");
}
static $Signal1;
static $Signal2;
}
// connect to SessionBus
$bus = new Dbus( Dbus::BUS_SESSION, true);
define('SERVICE_NAME', 'blog.rock');
define('OBJECT_PATH', '/blog/rock');
define('MAIN_INTERFACE', 'blog.rock');
define('INTROSPECTABLE_INTERFACE', 'org.freedesktop.DBus.Introspectable');
// request service name.
$bus->requestName(SERVICE_NAME);
// add signal
DbusEchoInterface::$Signal1 = new DbusSignal(
$bus,
OBJECT_PATH,
MAIN_INTERFACE,
'Signal1'
);
DbusEchoInterface::$Signal2 = new DbusSignal(
$bus,
OBJECT_PATH,
MAIN_INTERFACE,
'Signal2'
);
// register object path and interface.
$bus->registerObject(OBJECT_PATH, MAIN_INTERFACE, 'DbusEchoInterface');
$bus->registerObject(OBJECT_PATH, INTROSPECTABLE_INTERFACE, 'DbusIntrospectableInterface');
// register another interface...
//$bus->registerObject('/nl/test', 'nl.another.test', 'AnotherService');
while (true) {
try {
$bus->waitLoop(1000);
echo '.';
}
catch (Exception $e) {
echo $e->getMessage();
}
}
?>
各位可以先用 dbus-send 觀察此服務的是否正常運作。
開啟第一個終端機,執行 sample-service.php,放置play.
# 終端機1
$ php sample-service.php
....Zero.....One..........Two...............Array...
開啟第二個終端機,執行 wait-signal.php, 放置play.
# 終端機2
$ php wait-signal.php
get signal 1
get signal 2
string(7) "signal2"
開啟第三個終端機,用 dbus-send 向 sample-service 要求服務,並觀察前兩個終端機輸出的訊息。
# 終端機3
$ dbus-send --session --print-reply --dest=blog.rock \
/blog/rock org.freedesktop.DBus.Introspectable.Introspect
$ dbus-send --session --print-reply --dest=blog.rock \
/blog/rock blog.rock.EchoZero
method return sender=:1.79 -> dest=:1.82 reply_serial=2
int32 0
$ dbus-send --session --print-reply --dest=blog.rock \
/blog/rock blog.rock.EchoOne string:"world"
method return sender=:1.79 -> dest=:1.83 reply_serial=2
string "Hello world"
$ dbus-send --session --print-reply --dest=blog.rock \
/blog/rock blog.rock.EchoTwo string:"two"
method return sender=:1.79 -> dest=:1.84 reply_serial=2
string "two"
int32 2
$ dbus-send --session --print-reply --dest=blog.rock \
/blog/rock blog.rock.EchoArray
method return sender=:1.79 -> dest=:1.85 reply_serial=2
array [
string "array"
string "echo"
]
$ dbus-send --session --print-reply --dest=blog.rock \
/blog/rock blog.rock.TestSignal