XPCOM часть вторая: Компоненты. Основы.

image

Вольный перевод второй части статьи про технологию XPCOM от Rick Parrish.

Идея XPCOM как технологии заключается в предоставлении модульного фреймворка независящего ни от операционной системы ни от языка.
Компоненты, написанные на C++, практически ничем не отличаются от написанных на JavaScript или другом скриптовом языке (с точки зрения поведения). Это достигается за счет механизма XPCOM называемого библиотекой типов (type library).

Библиотека типов (type library)

Библиотека типов предоставляет общий формат данных (механизм обмена) для описания методов, атрибутов, параметров и интерфейсов компонента. Создавая общий формат, одни и те же интерфейсы могут быть описаны в разных операционных системах и языках программирования. Это полезно для поддержки общего механизма маршаллинга или прокси механизма. Используя информацию о типе, программа может определить каждый параметр любого метода или атрибута некоторого интерфейса. Благодаря этому программа может осуществлять обмен данными между интерфейсом и окружающей средой. Окружающей средой может выступать скриптовый сценарий, прокси механизм пересекающегося процесса, процесс или сетевое окружение. В случае сценария компонент описывается в скриптовом окружении, а значит, скрипт может вызывать методы интерфейса.

XPConnect это дополнительный слой, построенный поверх XPCOM, который способен осуществлять обмен данными между JavaScript движком и XPCOM интерфейсом, прочитав библиотеку типа XPCOM. XPConnect так же позволяет XPCOM компонентам быть написанным полностью на JavaScript. Таким образом, можно обращаться к JavaScript компоненту из C++ или использовать JavaScript для загрузки и управления скомпилированным C++ компонентом. В дополнение к JavaScript, язык программирования Python может быть использован как альтернативный скриптовый язык (он использует механизм схожий с XPConnect).

Описание интерфейса

Независимый от языка способ описания интерфейса состоит в использование так называемого IDL (языка описания интерфейсов). Компилятор IDL — утилита позволяющая создать файл библиотеки типов по описанию интерфейса. Диалект IDL используемый XPCOM несколько отличается от диалекта OMG CORBA или Microsoft IDL, поэтому применяется свой IDL компилятор xpidl –Â. Интересной особенностью компилятора xpidl является способность генерировать C++ заглушки по описанию интерфейса, что позволяет генерировать практически полностью всю декларативную часть кода уже при старте нового проекта. Это очень помогает, когда вы только начинаете. Компиляторы CORBA и Microsoft IDL обладают похожей функциональностью. В листинге 1 приведен синопсис xpidl.

Листинг 1. xpidl командная строка

Usage: xpidl [-m mode] [-w] [-v] [-I path] [-o basename] filename.idl
  -w turn on warnings (recommended)
  -v verbose mode (NYI)
  -I add entry to start of include path for ``#include "nsIThing.idl"''
  -o use basename (e.g. ``/tmp/nsIThing'') for output
  -m specify output mode:
     header        Generate C++ header            (.h)
     typelib       Generate XPConnect typelib     (.xpt)
     doc           Generate HTML documentation    (.html)

Генерация C++ кода лишь бонус, главная задача компилятора IDL — генерировать библиотеку типов для каждого модуля. Генерированные заголовочные C++ файлы можно изменить, описав интерфейсы как виртуальные методы C++ класса. Описание интерфейсов (в форме заголовочных файлов C++) используется во время компиляции (это раннее связывание). Файл библиотеки типов представляет те же возможности  (что и интерфейс) коду который не знает о компоненте, но хочет его использовать (нет необходимости в заголовочных файлах, это позднее связывание).

XPIDL синтаксис описания интерфейса: ключевое слово interface за ним следует имя интерфейса, двоеточие, базовый интерфейс (обычно это nsISupports), открывающаяся фигурная скобка, список атрибутов и методов, каждый из которых заканчивается точкой с запятой. Атрибуты описываются с помощью ключевого слова keyword. Параметры методов могут быть описаны как входящие (input) и исходящие (output) с помощью специальных префиксов in и out. В листинге 2 приведен пример описание интерфейса «экран компьютера».

Листинг 2. Пример интерфейса

#include "nsISupports.idl"
[scriptable, uuid(f728830e-1dd1-11b2-9598-fb9f414f2465)]

interface nsIScreen  : nsISupports
{
  void GetRect(out long left, out long top, out long width, out long height);
  void GetAvailRect(out long left, out long top, out long width, out long height);
  readonly attribute long pixelDepth;
  readonly attribute long colorDepth;
};

Изучая приведенное описание интерфейса, мы видим — интерфейс называется nsIScreen, у него есть два метода (GetRect и GetAvailRect) и два атрибута (pixelDepth и colorDepth). Перед ключевым словом interface присутствует выражение в квадратных скобках, это не обязательная часть описания интерфейса, хранящая полезную метаинформацию. Ключевое слово scriptable означает что данный интерфейс, возможно, будет взаимодействовать с JavaScript или другим скриптовым языком. Ключевое слово uuid определяет UUID или ID интерфейса. Базовым интерфейсом для nsIScreen является nsISupports (после двоеточия), это означает что любые методы и атрибуты, описанные в интерфейсе nsISupports, будут так же присутствовать и в nsIScreen (остановимся на этом позже). Атрибуты определяются по ключевому слову attribute. В данном случае атрибуты используются только для чтения об этом свидетельствует ключевое слово readonly (более подробное описание xpidl синтаксиса можно посмотреть в Приложении).

Раскрытие интерфейса

XPCOM использует подход основанный на интерфейсах для взаимодействия с компонентами. Клиентский код вынужден взаимодействовать с компонентом только через интерфейс предоставляемый компонентом. Для компонентов поддерживающих более одного интерфейса, используется механизм разделения интерфейсов (метод QueryInterface) который позволяет:

  • Определить какие интерфейсы поддерживает компонент
  • Переключение между интерфейсами

Эти две возможности вмести называются раскрытием интерфейса. Основное требование к XPCOM компоненту состоит в реализации стандартного интерфейса для поддержки функций раскрытия интерфейса. Стандартный интерфейс должен быть базовым для любого другого расширенного XPCOM интерфейса (с дополнительными методами и функциональностью). Этот стандартный интерфейс называется nsISupports его упрощенный IDL приведен в листинге 3.

Листинг 3. Стандартный интерфейс, nslSupports

interface nsISupports
{
    void QueryInterface(in nsIIDRef uuid, out nsQIResult result);
    nsrefcnt AddRef();
    nsrefcnt Release();
};

Первый метод QueryInterface в действительности осуществляет раскрытие интерфейса. Два других метода AddRef и Release используются для управления жизненным циклом компонента (как долго компонент должен существовать) через подсчет ссылок. Первый параметр QueryInterface ссылка на UUID — универсальный уникальный числовой идентификатор (128 бит или 16 байт). Например, идентификатор интерфейса nsISupports: 00000000-0000-0000-c000-000000000046.

UUID обычно записывается в виде шестнадцатеричной последовательности цифр записанных через дефис. Идентификатор (UUID) определяющий интерфейс может предоставляться или не предоставляться компонентом. Компонент может вернуть код ошибки, либо установить второй параметр в адрес запрашиваемого интерфейса, затем вернуть код успеха. Разработчик, использующий XPCOM, берет на себя ответственность за генерацию уникальных идентификаторов для новых интерфейсов. В листинге 4 приведен пример (JavaScript) использующий QueryInterface для переключения между интерфейсами одного компонента.

Листинг 4. Переключение между интерфейсами

// first, we create an instance of something...
var file = components.classes["@mozilla.org/file/local;1"].createInstance();
// second, we specify which interface we actually want to use.
file = file.QueryInterface(Components.interfaces.nsIFile);
// do something generic with the nsIFile interface here.
file.create(NORMAL_FILE_TYPE, 0377);
var size = file.fileSize;
// later on, we check to see if an extended interface is supported.
var local = file.QueryInterface(Components.interfaces.nsILocalFile);
if (local)
{
   // do something specific to the nsILocalFile interface...
   local.initWithPath('/usr/tmp/scratch.txt');

   // suppose we're now in some scope where the file variable is no longer
   // visible to use but we want to call some function that absolutely
   // insists on only accepting an nsIFile and not an nsILocalFile.
   // no problem, just QI over to the other interface like so ...

   var insists = local.QueryInterface(Components.interfaces.nsIFile);
   if (insists)
   {
      // at this point we can call our hypothetical function
      // to do some generic file processing...
      hypothetical(insists);
   }
}

Создание компонентов

В предыдущем примере строчка «@mozilla.org/file/local;1» — идентификатор (ID) контракта. Явное создание компонента требует одну из двух форм идентификации, позволяющую менеджеру компонентов определить какой компонент создавать. Одна форма — идентификатор компонента класса, 128 битное число. Другая форма — идентификатор контракта, просто строка. Этого достаточно для запроса компонента у менеджера компонентов. Цель идентификатора контракта определить поведение и возможности связанных с ним интерфейсов для клиентов желающих использовать этот компонент. Рекомендуемый формат идентификатора контракта — строка вида:

@<internetdomain>/module[/submodule[...]];<version>[?<name>=<value>[&<name>=<value>[...]]]

Квадратные скобки [вроде этих]  подразумевают нечто не обязательное. Вот несколько примеров:

  • @mozilla.org/file/directory_service;1
  • @mozilla.org/file/local;1
  • @mozilla.org/file;1
  • @mozilla.org/filelocator;1
  • @mozilla.org/filepicker;1
  • @mozilla.org/filespec;1

Каждый пример декларирует первую версию.  Последующий контракт второй версии не обязательно должен предоставлять обратную совместимость. Контракт с другим номером версии может включать поведение и интерфейсы других контрактов как часть своего поведения и интерфейсов.

Управление жизненным циклом

Компоненты должны хранить количество пользователей интерфейсов. Серьезной ошибкой будет уничтожение компонента, пока другие участки кода продолжают использовать один из его интерфейсов. Когда объект распределяет еще одну копию интерфейса он увеличивает внутреннее количество ссылок. Когда интерфейс объекта высвобождается — количество ссылок уменьшается. Когда количество ссылок падает до нуля, объект уничтожает себя. Так работает подсчет ссылок.

Обычно метод QueryInterface выполняет неявный вызов AddRef запрашиваемого компонента перед возвратом указателя на интерфейс. Когда клиентский код прекращает использование интерфейса, он вызывает метод Release, сообщая компоненту что работа с ним закончена. Это ответственность всего программного обеспечения использующего XPCOM: для каждого вызова метода QueryInterface или AddRef должен присутствовать вызов метода Release. Большое количество ошибок при применении XPCOM связано с отсутствующим или лишним вызовом метода Release.

Макросы и умные указатели

Для борьбы с этим типом ошибок XPCOM включает несколько шаблонов C++ позволяющие использовать умные указатели на интерфейс. Шаблоны предоставляют вам указатель «присвоил и забыл». Присвоив указатель на интерфейс, шаблон запомнит и освободит его для вас. Присвоив указатель на другой интерфейс, шаблон освободит предыдущий. Звучит достаточно просто. Умный указатель можно использовать как обычный указатель на интерфейс, объявив его в некотором блоке кода, и присвоив ему интерфейс. Как только программа выйдет за область определения умного указателя, вызовется встроенный деструктор, который вызовет метод Release.

Если посмотреть на любой код Mozilla, который использует nsCOMPtr или nsIPtr, то вы увидите что-то вроде кода из листинга 5.

Листинг 5. Mozilla код использующий nsCOMPtr или nslPtr

nsresult nsExample::DoSomething(void)
{
  nsresult rv;
  nsCOMPtr<nsIManager> pManager;
  *aResult = nsnull;
  pManager = do_GetService("Some contract ID goes here");
  if (pManager == nsnull)
    return NS_ERROR_NOT_AVAILABLE;
  rv = pManager->ManageSomething(); // do some more work here ...
  return rv;
}

В листинге 5 объявлен умный указатель на интерфейс nsIManager с именем pManager. Указателю присваивается некоторый сервис. После проверки, что в действительности был возвращен существующий указатель, код разыменовывает указатель для вызова метода ManageSomething. Когда объявленная выше функция завершается указатель pManager разрушается, но перед этим вызывается метод Release для интерфейса указатель на который содержится внутри pManager.

XPCOM избавляет от большого объема работы  благодаря использованию макросов C. Большинство интерфейсов возвращают nsresult. В большинстве случаев nsresult сравнивается со значением NS_OK (Полный список значений nsresult смотрите в файле nsError.h)

XPCOM включает файлы nsCom.h, nsDebug.h, nsError.h, nsIServiceManager.h и nsISupportsUtils.h, предоставляет несколько дополнительных макросов для тестирования, отладки и реализации. Если вы посмотрите заголовочные файлы множества C++ XPCOM компонентов, то увидите NS_DECL_ISUPPORTS как часть описания. Макрос предоставляет описание для интерфейса nsISupports.

Листинг 6. Определение nsISupports

public:
    NS_IMETHOD QueryInterface(REFNSIID aIID void** aInstancePtr);
    NS_IMETHOD_(nsrefcnt) AddRef(void);
    NS_IMETHOD_(nsrefcnt) Release(void);
    nsrefcnt mRefCnt;

Когда вы посмотрите соответствующий файл реализации, то увидете другой макрос NS_IMPL_ISUPPORTS1 (или похожий). Макрос предоставляет действительную реализацию интерфейса nsISupports. Цифра «1» на конце макроса обозначает количество интерфейсов (кроме nsISupports) которые компонент реализует. Если класс реализует 2 интерфейса, то макрос должен быть NS_IMPL_ISUPPORTS2.

В листинге 7 приведено как определяется макрос NS_IMPL_ISUPPORTS1 в nsISupportsUtils.h:

Листинг 7. NS_IMPL_ISUPPORTS1

#define NS_IMPL_ISUPPORTS1(_class, _interface) 
  NS_IMPL_ADDREF(_class)                       
  NS_IMPL_RELEASE(_class)                      
  NS_IMPL_QUERY_INTERFACE1(_class, _interface)

Как видите, он определяется тремя другими макросами. Начнем с NS_IMPL_ADDREF:

Листинг 8. NS_IMPL_ADDREF

#define NS_IMPL_ADDREF(_class)                               
NS_IMETHODIMP_(nsrefcnt) _class::AddRef(void)                
{                                                            
  NS_PRECONDITION(PRInt32(mRefCnt) >= 0, "illegal refcnt");  
  NS_ASSERT_OWNINGTHREAD(_class);                            
  ++mRefCnt;                                                 
  NS_LOG_ADDREF(this, mRefCnt, #_class, sizeof(*this));      
  return mRefCnt;                                            
}

Наконец-то код! 3 строчки из пяти относятся к макросам отладки, мы можем спокойно их игнорировать. Последняя строчка — код возврата. Любой код вызывающий AddRef предположительно игнорирует возвращаемое значение, потому мы можем пропустить и эту строчку.

Осталась одна интересующая нас строчка ++mRefCnt. Все что она делает — увеличивает счетчик вызовов AddRef некоторого интерфейса. Теперь рассмотрим NS_IMPL_RELEASE макрос:

Листинг 9. NS_IMPL_RELEASE

#define NS_IMPL_RELEASE(_class)                              
NS_IMETHODIMP_(nsrefcnt) _class::Release(void)               
{                                                            
  NS_PRECONDITION(0 != mRefCnt, "dup release");              
  NS_ASSERT_OWNINGTHREAD(_class);                            
  --mRefCnt;                                                 
  NS_LOG_RELEASE(this, mRefCnt, #_class);                    
  if (mRefCnt == 0) {                                        
    mRefCnt = 1; /* stabilize */                             
    NS_DELETEXPCOM(this);                                    
    return 0;                                                
  }                                                          
  return mRefCnt;                                            
}

Вновь у нас есть 3 строчки, относящиеся к макросам отладки, а так же два кода возврата которые мы так же проигнорируем. Нас волнуют две строчки кода —mRefCnt, который уменьшает внутренний счетчик и if (mRefCnt == 0), которая проверяет не достиг ли счетчик значения 0. Следующие строчки требуют от объекта удаления себя, если внутренний счетчик стал равен нулю. Таким образом, AddRef увеличивает счетчик, Release уменьшает, когда количество вызовов AddRef сравняется с количеством вызовов Release, количество ссылок станет равным нулю — компонент уничтожит себя. Вся идея подсчета ссылок начинает выглядеть довольно просто. Перейдем к  NS_IMPL_QUERY_INTERFACE1.

Листинг 10. NS_IMPL_QUERY_INTERFACE1

#define NS_IMPL_QUERY_INTERFACE1(_class, _i1)            
  NS_INTERFACE_MAP_BEGIN(_class)                         
    NS_INTERFACE_MAP_ENTRY(_i1)                          
    NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, _i1)   
  NS_INTERFACE_MAP_END

И снова макросы! MFC разработчикам данный вид макросов должен быть знаком. Эти макросы строят функцию выбора (ассоциативную таблицу) интерфейсов компонента, их порядок и положение важны при реализации QueryInterface. Макрос NS_INTERFACE_MAP_BEGIN является синонимом для макроса NS_IMPL_QUERY_HEAD, который приведен в листинге 11.

Листинг 11. NS_INTERFACE_MAP_BEGIN

#define NS_IMPL_QUERY_HEAD(_class)                                       
NS_IMETHODIMP _class::QueryInterface(REFNSIID aIID, void** aInstancePtr) 
{                                                                        
  NS_ASSERTION(aInstancePtr, "QueryInterface requires a non-NULL destination!"); 
  if ( !aInstancePtr )                                                   
    return NS_ERROR_NULL_POINTER;                                        
  nsISupports* foundInterface;

Данный код, на самом деле, не выполняет ни какой работы. Он вводит первоначальную декларацию и поверхностную проверку ошибок (проверку на null указатель). В коде недостает закрывающейся фигурной скобки, логично предположить, что за данным макросом должны следовать другие дополняющие код. Следующий макрос NS_INTERFACE_MAP_ENTRY — синоним NS_IMPL_QUERY_BODY (листинг 12).

Листинг 12. NS_IMPL_QUERY_BODY

#define NS_IMPL_QUERY_BODY(_interface)                  
  if ( aIID.Equals(NS_GET_IID(_interface)) )            
    foundInterface = NS_STATIC_CAST(_interface*, this); 
  else

Это критичный участок вводящий новое соответствие в функцию выбора. Такое применение блока if/else позволяет использовать множество NS_IMPL_QUERY_BODY для создания функции способной обрабатывать любое количество идентификаторов интерфейсов. Следующий макрос NS_INTERFACE_MAP_ENTRY_AMBIGUOUS — синоним NS_IMPL_QUERY_BODY_AMBIGUOUS (листинг 13).

Листинг 13. NS_IMPL_QUERY_BODY_AMBIGUOUS

#define NS_IMPL_QUERY_BODY_AMBIGUOUS(_interface, _implClass)             
  if ( aIID.Equals(NS_GET_IID(_interface)) )                             
    foundInterface = NS_STATIC_CAST(_interface*, NS_STATIC_CAST(_implClass*, this)); 
  else

NS_IMPL_QUERY_BODY_AMBIGUOUS делает ту же самую работу, что и NS_IMPL_QUERY_BODY, только дополнительно этот макрос позволяет избежать компиляционной ошибки, в случае, когда необходимо вернуть указатель на nsISupports, однако существуют два или больше интерфейса унаследованные от nsISupports. Каждый из них является правильным указателем на интерфейс nsISupport, в связи с этим компилятор C++ не может выбрать один из них. Требование, накладываемое на механизм получения указателей на интерфейс XPCOM, состоит в том, что возвращаться должен указатель на интерфейс с соответствующим ID. Данный макрос помогает соблюдать это правило, определяя указатель на наследника nsISupports, который будет использоваться как nsISupports. В листинге 14 приведен макрос NS_INTERFACE_MAP_END — синоним NS_IMPL_QUERY_TAIL_GUTS.

Листинг 14. NS_IMPL_QUERY_TAIL_GUTS

#define NS_IMPL_QUERY_TAIL_GUTS    
    foundInterface = 0;            
  nsresult status;                 
  if ( !foundInterface )           
    status = NS_NOINTERFACE;       
  else                             
    {                              
      NS_ADDREF(foundInterface);   
      status = NS_OK;              
    }                              
  *aInstancePtr = foundInterface;  
  return status;                   
}

Мы добрались до конца реализации QueryInterface. Последний кусочек кода возвращает код ошибки NS_NOINTERFACE, если для запрашиваемого идентификатора интерфейса не найдено соответствие. В противном случае в коде вызывается метод AddRef и возвращается указатель на интерфейс и код успеха NS_OK. Приведенный выше код есть реализация nsISupports для компонента с одним интерфейсом. Реализация поддерживающая несколько интерфейсов похожа. На самом деле компоненты могут использовать это реализацию либо предоставить какую-то свою. В большинстве случаев используются макросы из nsISupportsUtils.h. Теперь вы должны понимать, почему так важно разбираться в макросах C (особенно с вложенными определениями), если вы хотите читать и понимать кодовую базу mozilla/XPCOM.

Заключение

Данный материал должен помочь при исследовании исходных кодов mozilla. Мы узнали, как компоненты распределяют указатели на интерфейсы, каким образом реализован жизненный цикл компонента. Далее мы будем изучать на более высоком уровне возможности для управления компонентами. Наш следующий шаг провести обзор утилит разработчика и других требований для сборки браузера mozilla и весь XPCOM фреймворк лежащий в основе. Главной задачей будет настройка окружения для разработки под XPCOM.

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s