barbitoff programmer`s blog

Здесь я публикую заметки из программерской жизни: грабли, на которые мне случилось наступить, проблемы, для которых было найдено элегантное (или не очень) решение, а также все, с чем мне пришлось столкнуться и чем хотелось бы поделиться =)
PS Если хотите меня поблагодарить - на странице есть 3 места, чтобы это сделать =)

вторник, 25 октября 2011 г.

Создание расширения Firefox на C++ c использованием XPCOM

Внимание: использование XPCOM уже не рекомендуется разработчиками Mozilla (как минимум из-за того, что придется перекомпилировать бинарные компоненты с выходом новой версии FF, которые теперь будут выходить часто). В версиях FF 4+ можно использовать гораздо более простой способ - js-ctypes (https://developer.mozilla.org/en/js-ctypes/Using_js-ctypes). js-ctypes, с небольшими изменениями, работает даже в FF 3.6.20 (например, там нет ctypes.char, но есть ctypes.ustring).

Уф, один рабочий день и наконец-то получилось понять, как же все-таки писать расширения для FF с использованием XPCOM. MDN оказался куда хуже чем я ожидал - туториалы все древние и не работают с новыми версиями xulrunner-sdk, а то, что удалось накопать по новому движку Gecko2 - толком не описано, поэтому пришлось искать решение практически методом тыка.

Задача для начала простая - написать расширение, к которому можно было обратиться из JavaScript`а на веб-странице  вызвать метод echo, принимающий на входе строку и возвращающий её же.

Для начала качаем Gecko SDK, он же xulrunner-sdk: http://releases.mozilla.org/pub/mozilla.org/xulrunner/releases/7.0.1/sdk/xulrunner-7.0.1.en-US.win32.sdk.zip. Затем - проект-пример: https://developer.mozilla.org/samples/xpcom/xpcom-test.zip. Проект создан для старой версии xulrunner, поэтому компилироваться не будет, но всё же удобнее использовать его чем начинать с нуля. 
Пусть xulrunner-sdk распакован в "O:\xulrunner-sdk", а проект-пример - в O:\xpcom-test.

Во-первых, нужно описать интерфейс будущего расширения на языке idl (это не майкросовтовский IDL, а мозилловский собственный). Создаем файл (пусть он будет называться echo.idl, его нужно положить в O:\xpcom-test) со следующим содержимым (uuid можно сгенерировать любым генератором UUID):
#include "nsISupports.idl"
[scriptable, uuid(27110427-209a-4f4b-b829-8ca5ebe356a6)]
interface ISpecialThing : nsISupports
{
 AString echo(in AString srcStr);
};
В проект нужно внести следующие изменения:
  • Открываем xpcom-test/xpidl-build.bat и меняем его содержимое на:
    ..\xulrunner-sdk\bin\xpidl.exe -m header -I..\xulrunner-sdk\idl echo.idl
    ..\xulrunner-sdk\bin\xpidl.exe -m typelib -I..\xulrunner-sdk\idl echo.idl
    Запускаем батник, находясь в O:\xpcom-test (создадутся файлы echo.xpt и echo.h).
  • Открываем проект xpcom-test (я использовал MS VS 2005: во-первых, другого под рукой не нашлось, да и проект подойдет для него без конвертации). 
    • Заходим в свойства проектаC/C++ > General > Additional Include Directories, меняем на "..\xulrunner-sdk\include".
    • Затем - в Linker > General > additional Library Directories, ставим "..\xulrunner-sdk\lib"
    • Linker > Input > Additional Dependencies, "nspr4.lib xpcom.lib xpcomglue_s.lib mozalloc.lib"
  • Исключаем из проекта все файлы, кроме созданного нами idl. В свойствах idl-файла отменяем его компиляцию ("Exclude from build" = Yes). Включаем в проект созданный в пункте 1 h-файл с заголовком интерфейса. В комментариях этот файл содержит заготовку для класса, реализующего этот интерфейс.
  • Создаем класс, реализующий описанный в h-файле интерфейс. Пусть он будет называться echoImpl. Ниже приведен код заголовочного файла этого класса:
#include "echo.h"
#include "nsStringAPI.h"
#define ECHO_CONTRACTID "@some.domain.com/echosample;1"
#define ECHO_CLASSNAME "echosample"
#define ECHO_CID { 0xe99152f7, 0x73f7, 0x45d3, { 0x8e, 0x94, 0x26, 0x3e, 0x21, 0x10, 0x63, 0x57 } }
static const nsCID kEchoCID = IACSIGN_CID;
class echoImpl: public ISpecialThing
{
public:
  NS_DECL_ISUPPORTS
  NS_DECL_ISPECIALTHING
  echoImpl();
  ~echoImpl();
};
Define`ами в данном файле задает 3 важных параметра будущего компонента:  
    • ECHO_CONTRACTID - contract id компонента, имеющий вид домен/модуль/компонент;версия и используемый для идентификации компонента в массиве Components.classes при обращении из JavaScript.
    • ECHO_CLASSNAME - имя класса компонента (не нашел пока, где оно используется при эксплуатации компонента)
    •  ECHO_CID - уникальный GUI класса компонент. Сгенерировать его можно любым средством генерации GUID (например, http://www.guidgenerator.com/), после чего представить в указанном виде.
Теперь - само тело класса:
#include "echoImpl.h"
#include "nsStringAPI.h"
#include "nsIClassInfoImpl.h"
#include "nsMemory.h"
NS_IMPL_CLASSINFO(echoImpl, NULL, 0, ECHO_CID)
NS_IMPL_ISUPPORTS1_CI(echoImpl, ISpecialThing) 
echoImpl::echoImpl(){}
echoImpl::~echoImpl(){} 
NS_IMETHODIMP echoImpl::echo(const nsAString & srcStr, nsAString & _retval NS_OUTPARAM)
{
return NS_StringCopy(_retval,srcStr);
}
Тут все предельно просто - пара макросов и собственно метод echo.
  • Теперь необходимо описать модуль - код, который будет сообщать FF о написанном нами модуле. Создаем файл echoModule.cpp со следующим наполнением (код взят из примера http://mxr.mozilla.org/mozilla-central/source/xpcom/sample/, который, правда, не является 100% рабочим):
#include "echoImpl.h"
#include "mozilla/ModuleUtils.h"
#include "nsIClassInfoImpl.h"
////////////////////////////////////////////////////////////////////////
// With the below sample, you can define an implementation glue
// that talks with xpcom for creation of component nsSampleImpl
// that implement the interface nsISample. This can be extended for
// any number of components.
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
// Define the contructor function for the object nsSampleImpl
//
// What this does is defines a function nsSampleImplConstructor which we
// will specific in the nsModuleComponentInfo table. This function will
// be used by the generic factory to create an instance of nsSampleImpl.
//
// NOTE: This creates an instance of nsSampleImpl by using the default
// constructor nsSampleImpl::nsSampleImpl()
//
NS_GENERIC_FACTORY_CONSTRUCTOR(echoImpl);
// The following line defines a kNS_SAMPLE_CID CID variable.
NS_DEFINE_NAMED_CID(ECHO_CID);
// Build a table of ClassIDs (CIDs) which are implemented by this module. CIDs
// should be completely unique UUIDs.
// each entry has the form { CID, service, factoryproc, constructorproc }
// where factoryproc is usually NULL.
nsresult myF (nsISupports* aOuter,
                 const nsIID& aIID,
                 void** aResult) {
*aResult = new echoImpl();
return NS_OK;
}
static const mozilla::Module::CIDEntry kSampleCIDs[] = {
{ &kEchoCID , false, NULL, myF },
    { NULL }
};
// Build a table which maps contract IDs to CIDs.
// A contract is a string which identifies a particular set of functionality. In some
// cases an extension component may override the contract ID of a builtin gecko component
// to modify or extend functionality.
static const mozilla::Module::ContractIDEntry kSampleContracts[] = {
    { ECHO_CONTRACTID , &kEchoCID },
    { NULL }
};
// Category entries are category/key/value triples which can be used
// to register contract ID as content handlers or to observe certain
// notifications. Most modules do not need to register any category
// entries: this is just a sample of how you'd do it.
// @see nsICategoryManager for information on retrieving category data.
static const mozilla::Module::CategoryEntry kSampleCategories[] = {
    //{ "my-category", "my-key", ECHO_CONTRACTID },
    { NULL }
};
static const mozilla::Module kSampleModule = {
    mozilla::Module::kVersion,
    kSampleCIDs,
    kSampleContracts,
    kSampleCategories
};
// The following line implements the one-and-only "NSModule" symbol exported from this
// shared library.
NSMODULE_DEFN(nsSampleModule) = &kSampleModule;
// The following line implements the one-and-only "NSGetModule" symbol
// for compatibility with mozilla 1.9.2. You should only use this
// if you need a binary which is backwards-compatible and if you use
// interfaces carefully across multiple versions.
// NS_IMPL_MOZILLA192_NSGETMODULE(&kSampleModule)

Здесь стоит обратить внимание на функцию myF - она возвращает объект созданного ранее класса echoImpl. В примере с MDN её не было (точнее имя-то функции было, а вот тела - нет).

Всё, проект можно компилировать. Если всё пройдет ок, то на выходе получим dll-файл, и представляющий собой компонент (к совокупности с полученным ранее xpt-файлом, описывающим его интерфейс).

Теперь дополнение необходимо добавить в FF. Для этого понадобиться xpi-файл со следующей структурой:
  • components/ - директория, куда нужно положить колченные xpt и dll файлы
  • chrome.manifest - файл реестра FF, описывающий компонент, со следующим содержимым:
binary-component components/echo.dll
interfaces components/echo.xpt
component {99152f77-3f74-5d38-e942-63e21106357} components/echo.dll
Здесь в фигурных скобках указан GUID, установленный в коде компонента с помощью константы ECHO_CID.

  •  install.rdf - файл, описывающий компонент и необходимый для его установки. Минимальное содержимое файла следующее:
<?xml version="1.0" encoding="UTF-8"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
  <Description about="urn:mozilla:install-manifest">
    <em:id>echo@demo.com</em:id>
    <em:type>2</em:type>
    <em:name>Echo echo</em:name>
    <em:version>0.2.1</em:version>
    <em:creator>barbitoff</em:creator>
    <em:contributor/>
    <em:description>Blablabla echo</em:description>
    <em:aboutURL/>
<em:unpack>true</em:unpack>
    <em:targetApplication>
      <Description>
        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--FF-->
        <em:minVersion>3.0</em:minVersion>
        <em:maxVersion>7.*</em:maxVersion>
      </Description>
    </em:targetApplication>
  </Description>
</RDF>
Такое содержимое  install.rdf позволит установить xpi на версии FF от 3.0 до 7.* (т.е. любой 7ой версии).
Всё, теперь пакуем созданную структуру в zip-файл, меняем расширение на xpi и перетаскиваем его на окно FF. Компонент должен успешно установиться, перезапускаем FF. Вызвать наш echo-метод из JS можно:
var a = Components.classes["@some.domain.com/echosample;1"].createInstance();
alert(a.echo("HELLO WORLD!"));
Примечание - на эту операцию требуется разрешение. Читал, что оно устанавливается вызовом netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect") (естественно, использовать его можно только в тестовых целях, а не на боевых веб-страницах), но у меня почему-то и с ним не работает. Приведенный выше вызов работает из js-консоли Firebug, и должен также работать при вызове из js-кода расширения.

Комментариев нет:

Отправить комментарий