2013年10月8日 星期二

QtDBus marshall and demarshall

最近在寫Qt存取DBus服務的程式,才發現要對應DBus的資料型態到Qt的資料型態,必須花點功夫,所以在此筆記一下,以免日後忘記。

首先,若要叫用DBus服務上的一個方法,基本上最簡單的方式:
    // 建立一個要在D-Bus上傳遞的Message
    QDBusMessage m = QDBusMessage::createMethodCall("org.bluez", path, interface, method);

    //send message
    QDBusConnection::systemBus().send(m);
一開始,如果很幸運地,要叫用的方法沒有輸入參數也沒有回傳值,那只要使用以上方法就可以很容易地完成。但是如果需要輸入參數的話呢?在KDE TechBase有一篇文章[1]提到兩個寫法:
《寫法一》
    // 建立一個要在D-Bus上傳遞的Message
    QDBusMessage m = QDBusMessage::createMethodCall("org.bluez", path, interface, method);

    QList args;
    args.append("kde.org");
    m.setArguments(args);

    //send message
    QDBusConnection::systemBus().send(m);
 《寫法二》
    // 建立一個要在D-Bus上傳遞的Message
    QDBusMessage m = QDBusMessage::createMethodCall("org.bluez", path, interface, method);

    m << "kde.org";
    //send message
    QDBusConnection::systemBus().send(m);
Ok, 輸入參數的資料型態是字串的時候,這様也就够了。但是如果不是字串的話呢?那就很可能會讓人找半天也不知道如何寫。例如,輸入參數的資料型態為Object Path的話,就不是以上兩個寫法可以搞定的。Sage[2]在Qt Centre上問了這個問題,anders很好心的給了答案。就是使用QVariant::fromValue(object_path)將資料填入參數列中。如下程式片斷範例所示:
    // 建立一個要在D-Bus上傳遞的Message
    QDBusMessage m = QDBusMessage::createMethodCall("org.bluez", path, interface, method);

    m << QVariant::fromValue(object_path);
    //send message
    QDBusConnection::systemBus().send(m);
以上是常用的marshall部份。
下面來討讑demarshall部份,就是叫用DBus服務的方法後,如果有回傳值,要如何轉成Qt的資料型態。

先從簡單的來,如果回傳值是objectpath,但是我要取出其字串部份:
其中一種寫法如下程式片斷範例,注意因為有回傳值,所以在叫用方法時,不是用之前的send(),而是改用call()。這様程式跑到這裡時,會等待method_reply訊息傳回來才往下執行。所以是同步或blocking的寫法,當然也可以使用callWithCallback()的寫法,那是非同步或nonblocking的作法。此處不討讑,因為現在我們的重點是參數部份。其實DBus可以回傳多個參數,通常回傳值是第一個,如果有輸出參數,則依序為第二個,第三個等。使用QDBusMessage類别的arguments()可以得到這個參數列,再使用at(0)或者[0]或者first()可以取出其第一個參數,也就是大部份情況下的回傳值。
 重點來了,雖然已經取得回傳值了,但是型態不對,前文提到我們已知此方法的回傳值型態是objectpath,但是Qt在此處其實給我們回了一個QDBusArgument的類别,所以我們要自己再轉型一次,也就是使用value<QDBusObjectPath>()所做的事情。最後QDBusObjectPath類别的成員函數path()則會取出其字串部份。
    // 建立一個要在D-Bus上傳遞的Message
    QDBusMessage msg = QDBusMessage::createMethodCall("org.bluez", path, interface, method);
    QDBusMessage reply = QDBusConnection::systemBus().call(msg);
    return reply.arguments().at(0).value<QDBusObjectPath>().path();

再來一個較複雜,但也很常見的情況:回傳值是Array of [Object Path]
這也很常用到的,例如問系統有幾台裝置之類,大都回傳值會是這種型態。rhiakath[3]也碰到這個問題,最後他自己解了。如下程式範例。至於為什縻要這様寫呢?rhiakath沒有詳細的説明,但其實在QDBusArgument Class Reference[4]裡,則説的很詳細了。簡單的説,大部份marshall和demarshall在Qt裡都是由QDBusArgument這個類别來做。回傳回來的參數都會是這個類别的型態。問題在於其實參數的型態可能是簡單的字串或數值,也可能是複雜的型態,例如矩陣或字典。當然簡單的資料型態轉換Qt都已經幫我們做好了。我們可以很簡單地用value<>()或者qdbus_cast<>(),以及>>運算子來完成。就像前一個範例使用value<>()的寫法一様。但是複雜的型態就要自己做點工作了,也可以説Qt沒有做得很完整,他留了一些工作給程式設計師做。就如同這個範例,矩陣是複雜型態,程式設計師要使用QDBusArgument類别的beginArray()和atEnd()及asVariant()函數配合迴圈才能取出矩陣的每個元素。
    // 建立一個要在D-Bus上傳遞的Message
    QDBusMessage msg = QDBusMessage::createMethodCall("org.bluez", path, interface, method);
    QDBusMessage reply = QDBusConnection::systemBus().call(msg);
    QDBusArgument arg = reply.arguments().at(0);
    arg.beginArray();
    while (!arg.atEnd())
    {
        QVariant variant = arg.asVariant();
        QDBusObjectPath op = variant.value<QDBusObjectPath>();
        // do the job, ex: insert op to list...
    }

另一個也很常見的用法,就是回傳值的型態為字典,而特别常見的字典為Dict of {String, Variant}。
這裡我們分兩個情況來討讑:
  • 簡單型:Variant其實為String
  • 複雜型:Variant其實為Array
先看簡單型,回傳的字典中Variant部份,也就是值的部份是字串之類的簡單型態。Macieira[5]在一篇mailing list中提到使用qdbus_cast<>()來做轉型的寫法,如下程式範例所示。其實使用>>運算子也可以,此做法可以參看[4]的文章最後一段的説明。大家不覺得很奇怪嗎?明明字典是複雜的資料型態,不是説要程式設計師自己做轉型的工作嗎?其實是因為這個Dict of {String, Variant}在DBus算是太常見的資料型態了,所以Qt已經幫我們實做了資料轉型的工作。當然在取得字典之後,我們就可以使用Dict["Key"]的語法來取得對應的值部份。沒錯,值部份的資料型態為Variant,但其實為字串,故要再轉型一次,使用value<>()就可以了。
    // 建立一個要在D-Bus上傳遞的Message
    QDBusMessage msg = QDBusMessage::createMethodCall("org.bluez", path, interface, method);
    QDBusMessage reply = QDBusConnection::systemBus().call(msg);

    QVariantMap properties = qdbus_cast<QVariantMap>(reply.arguments().first());

    name = properties["Name"].value<QString>();
    address = properties["Address"].value<QString>();

複雜型,就有點討讑的空間了。基本上要看情況而定,我們討讑一個也很常見的狀況:字典中的某一個值為Object Path的矩陣。原理也不難,Qt在這種情況下,會把這個矩陣包在QDbusArgument類别中,所以我們先用qdbus_cast<>()取得字典後,再以字典的Dict["Key"]語法取得其相對應的值部份,此部份其資料型態要先轉為QDbusArgument。然後就可以套用之前討讑取得回傳值為Array of [Object Path]的作法了。
    // 建立一個要在D-Bus上傳遞的Message
    QDBusMessage msg = QDBusMessage::createMethodCall("org.bluez", path, interface, method);
    QDBusMessage reply = QDBusConnection::systemBus().call(msg);

    QVariantMap properties = qdbus_cast<QVariantMap>(reply.arguments().first());
    QDBusArgument arg = properties["Devices"].value<QDBusaAgument>();
    arg.beginArray();
    while (!arg.atEnd())
    {
        QVariant variant = arg.asVariant();
        QDBusObjectPath op = variant.value<QDBusObjectPath>();
        // do the job, ex: insert op.path() to list...
    };

就如前文討讑的,簡單資料型態大致上直接轉型就好,複雜的資料型態有一些比較不好處理,特别是如字典中有字典,或者是矩陣中有矩陣,或是字典中有矩陣等。此時就要依情況來分析,用Qt的qDebug() << reply;可以在Qt Creator的Application Output視窗中看到變數的内容,用來找問題還蠻有幫助的。下面的輸出就是前一範例的情況,可以看得出來回傳值是一個字典(signature="a{sv}"),而其Key部份為字串,值部份為Variant。Variant可為任何資料型態,在此範例中可以看到有QString, uint, bool, QStringList, Argument: ao等情況。有了這個資料為依據,來修改程式就容易多了。
QDBusMessage(type=MethodReturn, service=":1.3", signature="a{sv}", contents=([Argument: a{sv} {"Address" = [Variant(QString): "00:1A:7D:xx:xx:xx"], "Name" = [Variant(QString): "ubuntu-x"], "Class" = [Variant(uint): 0], "Powered" = [Variant(bool): true], "Discoverable" = [Variant(bool): false], "Pairable" = [Variant(bool): true], "DiscoverableTimeout" = [Variant(uint): 0], "PairableTimeout" = [Variant(uint): 0], "Discovering" = [Variant(bool): false], "Devices" = [Variant: [Argument: ao {[ObjectPath: /org/bluez/827/hci0/dev_00_19_86_xx_xx_xx], [ObjectPath: /org/bluez/827/hci0/dev_58_12_43_xx_xx_xx], [ObjectPath: /org/bluez/827/hci0/dev_84_00_D2_xx_xx_xx], [ObjectPath: /org/bluez/827/hci0/dev_94_CE_2C_xx_xx_xx], [ObjectPath: /org/bluez/827/hci0/dev_00_22_F3_xx_xx_xx], [ObjectPath: /org/bluez/827/hci0/dev_08_FC_88_xx_xx_xx], [ObjectPath: /org/bluez/827/hci0/dev_98_0D_2E_xx_xx_xx]}]], "UUIDs" = [Variant(QStringList): {"00001000-0000-1000-8000-00805f9bxxxx", "00001001-0000-1000-8000-00805f9bxxxx", "0000112d-0000-1000-8000-00805f9bxxxx", "00001112-0000-1000-8000-00805f9bxxxx"}]}]) ) 


參考來源:
[1] Accessing D-Bus Interfaces, KDE TechBase, 2012/7/13
[2] Sage, How to send objectpath as an argument in dbus method?, Qt Centre, 2009/8/6
[3] rhiakath, [SOLVED] Extracting several QObjectPath from reply, QtForum.org, 2012/7/24
[4] QDBusArgument Class Reference, Qt Reference Documentation
[5] Thiago Macieira, [Interest] QDBus Dict, 2012/7/7 

張貼留言