- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF99:D-Bus
Материал из Linuxformat.
Содержание |
D-Bus: шины для вашего Linux
- Уже успели подумать о зимней резине или переломах и вывихах? Возвращайтесь обратно в виртуальный мир – Андрей Боровский имеет в виду шину для обмена данными между настольными приложениями!
Что такое D-Bus? Самый простой ответ – еще одна система межпроцессного взаимодействия (Interprocess Communication или IPC). Ключевые слова здесь «еще одна». Высокоуровневых систем IPC для Unix/Linux существует много. Помимо систем высокого уровня Unix обладает развитыми средствами IPC низкого уровня (сокеты, каналы), каковые успешно используются многими приложениями напрямую. Зачем тогда нам может понадобиться D-Bus? Эта система замышлялась группой FreeDesktop.org как средство IPC, не зависящее от типа рабочего стола, призванное заменить как DCOP в KDE, так и CORBA/Bonobo в GNOME. Вытеснить родные средства IPC KDE и GNOME новой системе пока не удалось [правда, в KDE 4 D-Bus все-таки будет использоваться вместо DCOP, – прим. ред.], но в процессе разработки D-Bus обрела несколько уникальных и полезных особенностей. Важными отличительными чертами D-Bus являются система сигналов и асинхронных вызовов методов, а также система управления выполнением приложений. Таким образом, ответ на вопрос, зачем вам может понадобиться программирование D-Bus, состоит из двух частей. Во-первых, многие важные приложения и системные компоненты (например, Linux HAL и NetworkManager) используют D-Bus как средство общения с внешним миром. Во вторых, D-Bus – это платформенно-независимая система IPC, которая присутствует практически в каждом дистрибутиве Linux и устанавливается по умолчанию во многих из них. Поэтому, если вы пишете приложение, которое должно предоставлять сервисы IPC, не являясь частью какого-либо рабочего стола, вам, безусловно, имеет смысл обратить внимание на D-Bus. При этом следует учитывать и минусы D-Bus. В системе все еще не реализована связь между разными машинами, хотя работа в этом направлении ведется. D-Bus легко может быть перенесена на другие Unix-платформы, но ее версия для Windows по-прежнему далека от завершения.
Среди конкурирующих технологий (в том смысле, что их зачастую можно использовать вместо D-Bus), следует отметить CORBA, SOAP, XML-RPC, DCOM, DCOP, Bonobo. Чем D-Bus отличается от них? CORBA, как и D-Bus, использует быстрый бинарный протокол. В отличие от D-Bus, CORBA предназначается для решения чрезвычайно широкого круга задач и может использоваться как в локальной, так и в распределенной системе. В CORBA отсутствуют такие элементы D-Bus, как система управления выполнением приложений и система сигналов. SOAP и XML-RPC представляют собой протоколы, в которых на низком уровне активно используется XML. Эти технологии межпроцессного взаимодействия хорошо подходят для Интернета, однако при обмене данными между приложениями, работающими на одной машине, использование механизмов XML приводит к пустой трате ресурсов (при этом надо отметить, конечно, что приложения, использующие эти протоколы, чрезвычайно легко масштабируются). Технологии DCOM, DCOP и Bonobo имеют один сходный недостаток – каждая из них предназначена для конкретной платформы (Windows, KDE и GNOME соответственно), и организовать взаимодействие между приложениями разных платформ с их помощью будет весьма непросто.
Вы уже заметили, что в качестве примера приложения, предоставляющего сервисы D-Bus, мы упоминаем клиент Skype. Интерфейс, экспортируемый клиентом Skype очень прост, и в то же время демонстрирует все основные возможности D-Bus. Объект /com/Skype поддерживает один-единственный метод – Invoke, позволяющий внешнему приложению передавать команды клиенту Skype. Единственным аргументом метода Invoke является строка команды, а возвращаемым значением – строка, в которой содержится ответ программы на переданную команду. Однако клиент Skype может не только выполнять команды сторонних приложений, но и передавать им различную информацию, например, о подключении нового пользователя. Чтобы получать сообщения от клиента Skype, приложение должно зарегистрировать класс /com/Skype/Client. Когда клиент Skype хочет проинформировать приложение о чем-либо, он вызывает метод Notify класса /com/Skype/Client, передавая в единственном параметре этого метода строку-сообщение. Метод Notify не возвращает значений.
Немного об архитектуре
В основе структуры D-Bus лежит понятие шины (bus). Шина представляет собой механизм, с помощью которого процессы обмениваются данными. Хотя, в принципе, любые два процесса могут организовать «частную» шину средствами D-Bus и обмениваться данными между собой, интерес представляют общедоступные шины, которые поддерживает демон D-Bus. Исполняемый файл демона имеет имя dbus-daemon. Обычно, если демон D-Bus приходится запускать вручную, используется команда dbus-launch.
Демон D-Bus предоставляет нам две шины: системную шину (system bus) и пользовательскую шину (session-bus). Системная шина может использоваться для передачи данных в масштабах системы, в то время как пользовательская шина позволяет передавать данные между процессами, принадлежащими одному пользователю. Следует учесть, что D-Bus следит за правами пользователей в системе и не позволит вам нарушить политику безопасности Linux с помощью системной шины.
Все процессы, использующие D-Bus для обмена данными, выступают в роли клиентов, которые подключаются к демону D-Bus и, таким образом, получают доступ к одной из шин. Об этом необходимо помнить, между прочим, и для того, чтобы не запутаться в терминологии. Подключаясь к шине, каждый процесс создает соединение (с демоном D-Bus). У каждого соединения есть имя (которое в оригинальной литературе обозначается терминами connection name и bus name). Имена соединений похожи на имена интернет-узлов, вывернутые наизнанку. Например, менеджер HAL создает соединение с именем org.freedesktop.Hal, а клиент Skype – с именем com.Skype.API. Поскольку все приложения, использующие системную или пользовательскую шины D-Bus, соединяются с демоном D-Bus, а не друг с другом, существует возможность использовать одно соединение D-Bus для обмена данными между разными приложениями.
Непосредственно процесс обмена реализован в форме отправки сообщений D-Bus, которые, в свою очередь, делятся на три категории: запросы (requests), ответы на запросы и сигналы. Сообщение-запрос несет адресату некоторую информацию и предполагает получение ответного сообщения. Сообщение-сигнал не предполагает ответа. Системы сообщений «запрос-ответ» формируют двусторонние соединения, тогда как сигналы допускают широковещательную рассылку (в этом случае соответствующее сообщение получает каждый процесс, зарегистрировавшийся как получатель сигнала).
Каждое приложение, которое предоставляет другим приложениям некие сервисы D-Bus, формирует объекты D-Bus, которые являются адресатами сообщений-запросов (а, значит, источниками сообщений-ответов) и сигналов. Каждое соединение может предоставлять несколько объектов. Вполне естественно, что и у объектов тоже есть имена (их иногда называют «путями объектов»). В отличие от имен соединений, они используют символы косой черты (/), откуда и происходит их второе название. Например, клиент Skype создает объект с именем /com/Skype.
Если мы поднимемся выше на один уровень абстракции, то увидим, в чем заключается сходство между объектами D-Bus и объектами ООП. Обмен сообщениями в модели «запрос-ответ» можно рассматривать как вызов метода объекта, в котором сообщение-запрос передает параметры метода, а сообщение-ответ – возвращаемые значения. Именно семантика вызова методов используется при формировании сообщений-запросов и получении ответов D-Bus.
Поскольку в основе вызовов методов объектов D-Bus лежит обмен сообщениями, существует возможность асинхронного вызова. Вызвав метод объекта D-Bus, программа может выполнить какие-то операции, не дожидаясь получения ответа. Можно даже вызвать еще один метод объекта до того, как был получен результат предыдущего вызова. Сообщения-сигналы проще всего сравнить с сигналами Qt.
Совокупность методов и сигналов, предоставляемых объектом D-Bus, называется интерфейсом объекта D-Bus. Декларируя поддержку определенного интерфейса, объект гарантирует определенную реакцию на сообщения, поступающие извне. Каждый объект может поддерживать несколько интерфейсов, которые, естественно, различаются по именам. Некоторую путаницу создает тот факт, что имена интерфейсов имеют ту же структуру, что и имена соединений (и часто совпадают с ними). Например, объект /com/Skype экспортирует интерфейс com.Skype.API (такое же имя имеет соединение, которое создает клиент Skype). Итак, для передачи сообщения другой программе с помощью D-Bus, необходимо знать имя соединения этой программы с демоном D-Bus, имя объекта и имя интерфейса. Для вызова метода, декларируемого интерфейсом, нужно, конечно, знать еще и имя метода. Вы можете придти к выводу, что для того, чтобы обмениваться сообщениями D-Bus с другой программой, вам всегда необходимо создавать объект D-Bus и декларировать поддержку некоторого интерфейса. На самом деле, это не обязательно. Если ваша программа использует D-Bus только для вызова методов объектов других приложений, никаких специальных действий по регистрации объектов выполнять не требуется. Более того, вам не придется придумывать для вашей программы и имя соединения – в процессе подключения к шине демон D-Bus создаст его автоматически. Сгенерированное имя будет иметь вид :x.y, где x и y – некоторые числа. На самом деле, подобное имя создается для каждого соединения D-Bus в рамках данной сессии, и именно оно используется демоном для передачи сообщений. В случае с программами, зарегистрировавшими сервисы D-Bus, имя, начинающееся с двоеточия, является синонимом постоянного имени (такого как com.Skype.API) в рамках текущей сессии. Если рассматривать программу, предоставляющую сервис D-Bus, как аналог web-сервера, то имя соединения, начинающееся с двоеточия, можно сравнить с IP-адресом сервера, а постоянное имя – с доменным именем. Иерархия различных элементов D-BUS представлена на Рис. 1.
Хотя вы и не можете работать с объектами D-Bus напрямую, система предоставляет программистам объектоподобный интерфейс, который реализуется с помощью так называемых прокси-объектов (proxy objects). Прокси можно считать представителем объекта D-Bus в вашей программе. Насколько прокси-объект похож на «настоящий» – зависит от реализации. В языках Java и Python работа с прокси осуществляется практически так же, как с «настоящими» объектами языка. При использовании интерфейсов библиотеки GLib для работы с прокси применяется специальный набор функций.
Обращаясь к объектам D-Bus какого-либо приложения, вы предполагаете, что хотя бы один экземпляр этого приложения запущен в системе. А что, если это не так? Выше отмечалось, что система D-Bus способна управлять выполнением приложения. Демон D-Bus может запустить приложение по вашему требованию (для этого, конечно, данное приложение должно быть особым образом зарегистрировано в системе). Этот механизм известен под именем D-Bus Activation.
Подключайтесь!
Основу низкоуровневого D-Bus API составляют два объекта – DBusConnection и DBusMessage. Первый объект инкапсулирует все, что связано с управлением шиной D-Bus, второй позволяет управлять сообщениями. Еще раз напомню, что когда мы говорим об объектах D-Bus API, речь идет не об объектах в смысле ООП, а об объектах в стиле GTK+ API (интерфейс программирования D-Bus вообще очень похож на интерфейс программирования GTK+).
Следующий код представляет собой минимальную программу, использующую возможности D-BUS.
#include <stdio.h> #include <dbus/dbus.h> int main (int argc, char **argv) { DBusConnection * connection; DBusError error; DBusMessage *call; DBusMessage *reply; const char * arg = "PROTOCOL 6\n"; char * response = NULL; dbus_error_init(&error); connection = dbus_bus_get(DBUS_BUS_SESSION, &error); if (!connection) { printf("Ошибка соединения с D-BUS: %s\n", error.message); dbus_error_free(&error); return 1; } call = dbus_message_new_method_call("com.Skype.API", "/com/Skype", "com.Skype.API", "Invoke"); dbus_message_append_args (call, DBUS_TYPE_STRING, &arg, DBUS_TYPE_INVALID); reply = dbus_connection_send_with_reply_and_block (connection, call, 100000, &error); if (!reply) { printf("Ошибка вызова метода: %s\n", error.message); dbus_error_free(&error); return 1; } dbus_message_get_args (reply, &error, DBUS_TYPE_STRING, &response, DBUS_TYPE_INVALID); if (response != NULL) printf("Ответ: %s\n", response); dbus_message_unref(call); dbus_message_unref(reply); dbus_connection_unref(connection); return 0; }
Объявления типов и функций, связанные с D-Bus API, становятся доступны программе при включении в ее текст заголовочного файла dbus/dbus.h (где найти этот файл, знает команда pkg-config --cflags dbus-1). Соединение с сервером D-Bus устанавливается с помощью функции dbus_bus_get(). Ее первый параметр указывает, к какой шине, системной или пользовательской, мы хотим подключиться (в данном случае выбирается пользовательская шина – DBUS_BUS_SESSION). Вторым параметром должен быть указатель на переменную типа DBusError. Если в процессе подключения к шине произойдет сбой, функция dbus_bus_get() вернет значение NULL, а в переменной error будет записана информация об ошибке.
После того как соединение с шиной установлено, мы вызываем метод объекта другого приложения. Вызов метода состоит из четырех этапов: создания сообщения-запроса, создания списка аргументов для вызываемого метода, передачи сообщения и обработки результата вызова.
Сообщение-запрос на вызов метода создается функцией dbus_message_new_method_call(). Ее четырьмя аргументами являются имена соединения удаленного приложения, объекта, интерфейса и вызываемого метода соответственно. Функция возвращает указатель на созданный ею объект DBusMessage, который содержит информацию о новом вызове. Поскольку создаваемое сообщение предназначено для вызова метода, мы должны сформировать список его аргументов. Это делается с помощью функции dbus_message_append_args(). Первый аргумент этой функции – указатель на объект DBusMessage. Далее следует переменное число параметров, которые передают аргументы вызываемого метода. Каждому аргументу соответствуют два параметра функции dbus_message_append_args(). В первом параметре передается константа, указывающая тип аргумента, во втором – указатель на область памяти, в которой хранится его значение. Завершается список аргументов константой DBUS_TYPE_INVALID. Поскольку у вызываемого нами метода Invoke один параметр, мы передаем dbus_message_append_args() список из трех аргументов. Аргумент DBUS_TYPE_STRING указывает тип параметра Invoke, затем следует указатель на значение (в нашем случае – указатель на переменную типа char*), далее – маркер конца списка DBUS_TYPE_INVALID. Отметим, что функция dbus_message_append_args() – не единственное средство создания списка аргументов. Низкоуровневый интерфейс D-Bus предоставляет в наше распоряжение и другие функции, способные формировать списки аргументов динамически, во время выполнения программы.
Теперь наше сообщение-запрос готово к отправке. Как уже отмечалось, существует два варианта вызова методов объектов D-Bus. При асинхронном вызове наша программа продолжит выполнение, не дожидаясь ответа (он будет обработан, когда поступит). При блокирующем вызове работа программы будет приостановлена до тех пор, пока мы не получим ответ (или пока не истечет заданное нами время ожидания). Ради простоты мы воспользуемся блокирующим вызовом. Функция dbus_connection_send_with_reply_and_block() делает именно то, что предполагает ее название – посылает сообщение-запрос и блокирует выполнение программы в ожидании ответа. Первый аргумент функции – указатель на объект DBusConnection. Далее следует указа тель на объект-сообщение. Затем мы указываем максимальный интервал ожидания ответа (в миллисекундах). Последний аргумент dbus_connection_send_with_reply_and_block() – адрес переменной DBusError. В случае успешного завершения функция возвращает указатель на объект DBusMessage, который содержит сообщение-ответ. В нашем примере это сообщение передает нам значение, возвращенное методом Invoke. Тут следует внести небольшое уточнение. В процессе вызова dbus_connection_send_with_reply_and_block() может возникнуть две разновидности ошибок. Ошибки первой разновидности связаны с возможными сбоями при передаче сообщения-запроса или ответа на него. При возникновении такой ошибки функция dbus_connection_send_with_reply_and_block() возвращает значение NULL, а описание ошибки следует извлекать из переменной error (то же самое происходит, если превышен интервал ожидания ответа). Но возможно возникновение ошибки и другого рода, при которой функция dbus_connection_send_with_reply_and_block() вернет указатель на объект DBusMessage, как и в случае нормального завершения вызова, только соответствующее сообщение будет не ответом на вызов метода, а сообщением об ошибке. Наша программа не обрабатывает возможные ошибки при вызове методов (да и вызвать ошибку в методе Invoke не так-то просто).
Получив ответное сообщение и сохранив указатель на него в переменной reply, мы извлекаем значение, возвращенное методом Invoke. Делается это с помощью функции dbus_message_get_args(). Первый аргумент функции – указатель на объект-сообщение. Далее следует указать на переменную типа DBusError, в которой будет сохранена информация об ошибке, если таковая возникнет. Затем – тот же список аргументов переменной длины, что и у dbus_message_append_args(): тип параметра, переменная для сохранения адреса его значения и маркер конца списка. Важное отличие dbus_message_get_args() от dbus_message_append_args() заключается, конечно, в том, что теперь мы не передаем, а получаем значения параметров. В переменную response в нашем примере записывается адрес строки, возвращенной методом Invoke. Сама строка хранится в недрах сообщения reply, и мы не должны пытаться высвободить (или модифицировать) занимаемую ею память.
На этом работа нашей программы окончена. С помощью функций dbus_message_unref() и dbus_connection_unref() мы сообщаем системе, что созданные нами объекты интерфейса D-Bus нам больше не нужны, и выделенную под них память можно освободить.
А дальше?
Сценарий, в котором программа вызывает метод объекта, экспортируемого другим приложением, выглядит довольно просто. Однако все становится гораздо сложнее, если мы хотим предоставить удаленному приложению возможность вызывать методы объекта D-Bus, экспортируемого нашим приложением. Если вы думаете, что при работе с D-Bus вы сможете избежать создания собственных объектов, то вы ошибаетесь. Возьмем для примера тот же Skype API. После того как приложение установит связь с клиентом Skype, клиент время от времени вызывает метод Notify объекта /com/Skype/Client нашего приложения. Точнее говоря, пытается вызвать. Если объекта не существует (как в приведенном выше примере), ничего катастрофического не случится. Мы просто не получим сообщения от клиента Skype. Сама регистрация объекта – дело несложное (для этого используется функция dbus_connection_register_object_path()). Сложность заключается в том, что, зарегистрировав собственный объект, программа должна быть готова обрабатывать сообщения D-Bus, посылаемые ей системой. В простом примере, приведенном выше, все сообщения генерировались по нашей инициативе. Для обработки сообщений, поступающих асинхронно, нам потребуется создать цикл обработки сообщений D-Bus. Это можно сделать многими способами (на моем сайте, по адресу http://symmetrica.net/d-bus-demo-1, вы найдете пример создания цикла обработки сообщений низкоуровневыми средствами).
Думаю, вы уже поняли, что работать с D-Bus с помощью низкоуровневого API не очень удобно. Неудивительно, что программисты создали многочисленные привязки D-Bus API к различным языкам программирования и библиотекам. В настоящее время D-Bus поддерживается в GTK+/GLib (следует отметить, что это – наиболее проработанные привязки), Qt 3/Qt 4, Python, Java, Perl. Сам я работаю над привязками D-Bus для wxWidgets.
Привязки D-Bus решают три задачи. Во-первых, выполняется интеграция цикла обработки сообщений D-Bus и целевой платформы. Во-вторых, объектная модель D-Bus API отображается в объектную модель, принятую на целевой платформе. В третьих, создаются методы для работы с D-Bus прокси, как с «родными» объектами. Но все это уже совсем другая история... LXF