Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Система плагинов как упражнение на C 11

Anna | 24.06.2014 | нет комментариев

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

Имеет ли толк что-то сходственное писать либо взять готовое решение. Свое видение по этому вопросу я описывал в статье «О желании изобретать один и тот же велосипед вновь и вновь». Так что в данной статье не будет философии на тему «А для чего оно необходимо».

Ранее я теснее публиковал статью «Своя компонентная модель на C », в которой была разработана компонентная / плагинная модель, живущая в рамках процесса. Для меня решение сходственной задачи увлекательно. В gcc 4.7.2 теснее возникло все, что мне было увлекательно на момент начала этой статьи, а это предисловие этого (2013) года. И здесь я дорвался до C 11… На работе в одном направлении, дома в ином. Дабы поиграться с C 11 я и решил переписать материал из ветхой статьи с новыми вероятностями языка. Сделать в некотором смысле упражнение на C . Но в силу некоторых причин мне не получалось довести материал статьи до конца больше полугода, и статья провалялась в черновиках нетронутой. Достал, стряхнул нафталин. Что из этого получилось можете прочесть дальше.

О решении применять C 11

Мы ожидали, ожидали и наконец-то дождались выхода обновлений C . Вышел новейший эталон языка — C 11. Данная редакция языка программирования принесла много увлекательных и пригодных вероятностей, а стоит ли его применять это пока спорный вопрос, но не все компиляторы его поддерживают либо поддерживают в неполном объеме.

Вступление и немножко философии

Здесь будет немножко рассказано о том, на каких тезисах построена данная реализация плагинной системы либо компонентной модели и отчего выбран тот, а не другой путь. Будет вылито некоторое число воды: мои размышления. Если вас не волнует сходственное философствование, то отважно переходите к реализации.

Интерфейсы и идентификаторы

В основу описываемой разработки будет положено взаимодействие компонент системы посредством интерфейсов. Интерфейс в данном контексте стоит понимать как C конструкцию, содержащую только чисто виртуальные способы. Интерфейс будет некоторой логической единицей, вокруг которой все постороено.

Одним из немаловажных вопросов может быть — что применять в качестве идентификатора интерфейса, реализации, модуля и прочих сущностей. В предыдущей статье в качестве идентификатора применялась C-строка, так как дозволено обеспечить огромную уникальность если, скажем, в качестве идентификатора применять uuid, сгенерированный каким-нибудь инструментом и переведенным в строку. Дозволено в качестве идентификатора применять и числовой идентификатор. У такого решения уникальность будет больше слабой, но есть превосходства — это, как минимум, огромная эффективность, что видимо, сопоставлять строки больше трудоемко нежели числа. В качестве значения числового идентификатора дозволено применять скажем CRC32 от строки. Представим есть интерфейс Ibase и находится он в пространстве имен Common. В качестве идентификатора может стать CRC32 от строки «Common.IBase». Да, если внезапно совпадут где-то идентификаторы интерфейсов, так как это не uuid, то вы получите длинные часы «радостной» отладки и хорошо прокачаетесь в освоении мощной стороны русского языка. Но если у вас нет амбиций, что вашу модель будут применять во каждому мире в глобальных системах, то вероятность исхода с длинной отладкой минимальна. В паре контор имел дело со своими «поделками» в жанре MS-COM’а, в которых в качестве идентификатора применялись числовые значения и не натыкался на загвоздку описанную выше, да и слухов, что у кого-то она появлялась тоже не было. Следственно, в данной реализации будет использован числовой идентификатор. Помимо продуктивности от такого решения есть еще один позитивный момент: с числовым идентификатором в момент компиляции дозволено сделать много увлекательного, так как манипулировать строками как параметрами образцов не получится, а числом легко. И здесь как раз 1-й плюс C 11 будет использован — этоconstexpr, с применением которого дозволено вычислять значения хэшей в момент компиляции.

Кроссплатформенность и помощь со стороны языка

Описываемая модель будет кроссплатформенной. Кросс чего-то там — это один из увлекательных моментов в разработке. Для C разработчика одна из особенно внятных задач — это помощь кроссплатформенности, но реже встречаются и задачи, связанные с кросскомпиляцией, так как то, что легко поддерживается одним компилятором, на другом может не поддерживаться. Один из таких примеров — это до возникновения реализации decltype попытки реализации приобретения типа выражения в момент компиляции. Отличный пример — BOOST_TYPEOF. Если заглянуть «под капот» BOOST_TYPEOF, вы найдете большой комплект палок и костылей, так как сходственные вещи не могли быть реализованы с применением C 03, а решались в основном на каких-то расширенных вероятностях определенного компилятора. Так же в C 11 расширилась стандартная библиотека, которая дала вероятность отказаться от написания собственных оберток над потоками, объектами синхронизации и т.д. За библиотечные функции по поддержке типов разработчикам эталона дозволено сказать отдельное спасибо, так как избавили от необходимости написания своего кода во многих случаях, и, самое основное, дали реализацию таких способов как std::is_pod и прочих, реализовать которые стандартными средствами языка C 03 без применения растяжений компиляторов было немыслимо.

Применять ли сторонние библиотеки

Было желание минимизировать применение сторонних библиотек и по вероятности свести их применение к нулю. Это будет разработка на чистом C . При реализации финальных компонент может быть использовано что желательно в силу задачи, всякие библиотеки, но сама модель, приводимая тут, будет в смысле применения сторонних библиотек чистой.

К применению сторонних библиотек у меня сложилось определенное отношение: не применять библиотеку в плане если ее функционал не применяется клиентским кодом максимально допустимым образом. Не стоит тащить в план Qt только потому что некоторым нравится применять QStrung и QList. Да, встречал планы, в которых были притянуты за уши некоторые библиотеки и фреймворки только ради того, чтоб применять какую-то малую и неважную ее часть легко из-за повадки некоторых разработчиков. В целом же, невозможно опровергать применение таких библиотек как boost, Qt, Poco и прочих, но они обязаны использоваться к месту, включаться в план только когда в них есть огромная надобность. Не стоит разводить зоопарк, так, завести в плане пару-тройку экзотических зверушек и не больше :) чтоб не получить план в котором есть штук 5-7, а то и больше типов строк, 2-3 из которых — это личные велосипеды, а остальные пришедшие из других библиотек и написанная куча конвертеров из одних реализаций в другие. Как итог разработанная программа взамен пригодной работы тратит абсолютно может быть приметное время на конвертацию между различными реализациями одних и тех же сущностей.

Boss…

Как-то привык я раскладывать код по пространствам имен. В качестве наименования пространства имен и каждой модели будет выбрано Boss (base objects for service solutions). Об истоках происхождения имени дозволено прочесть в предыдущей статье на эту тему. В комментариях к статье было подмечено, что «Boss» может смущать в коде, в силу напоминании о руководстве и стереотипах с этим связанных. Первоначально не было цели сделать ударение в наименовании на некоторого «насяльника» (© Наша Раша). Но если у кого-то вызывает отрицательные ассоциации, то отчего бы не посмотреть под иным углом на это? Есть восхитительная книга Кена Бланшара «Первенство к вершинам триумфа», в которой описываются высокоэффективные организации и начальники-слуги, цель которых сделать максимум, Дабы работнику дать все для его работы с максимальной эффективностью, а не легко стоять с палкой за спиной. Т.е. начальник — помощник в организации высокоэффективной работы. Boss желанно воспринимать как начальника в эффективной организации, тот, что помогает работникам достичь максимальной продуктивности, обеспечивая их каждому нужным для этого. В рамках компонентной модели — это именно поддержка в организации тонкой прослойки для больше простого взаимодействия сущностей в системе, а не монструозный фреймворк с которым нужно бороться и огромная часть работы направлена только на работу с ним, а не на бизнес-логику.

Минимализм в интерфейсе

Один из критериев, тот, что для меня играет немаловажную роль при рассмотрении следующий библиотеки — это скорость начала работы с библиотекой и предоставление ею все болрока. Как-то строки больше славно смотрятся в коде нежели сухие числа, да и нужно иметь определенный комплект данных, от которого генерировать числовой идентификатор. В качестве идентификатора выбран crc32 от строки. И вот она сила нового эталона: дозволено считать crc32 и прочие вещи от строк в момент компиляции! Такой трюк, безусловно, не выйдет со строками динамически создаваемыми в программе, да и не сгодится для решения данной задачи.

Для реализации расчета crc32 понадобится некоторая таблица с данными, которую дозволено легко обнаружить в интернете. С ее поддержкой считать crc32 дозволено приблизительно таким образом:

namespace Boss
{
  namespace Private
  {

    template <typename T>
    struct Crc32TableWrap
    {      
      static constexpr uint32_t const Table[256] =
      {
        0x00000000L, 0x77073096L, 0xee0e612cL, 0x990951baL, 0x076dc419L,
        0x706af48fL, 0xe963a535L, 0x9e6495a3L, 0x0edb8832L, 0x79dcb8a4L,
        ... etc
      };
    };

    typedef Crc32TableWrap<EmptyType> Crc32Table;

    template<int const I>
    inline constexpr std::uint32_t Crc32Impl(char const *str)
    {
        return (Crc32Impl < I - 1>(str) >> 8) ^
          Crc32Table::Table[(Crc32Impl< I - 1>(str) ^ str[I]) & 0x000000FF];
    }

    template<>
    inline constexpr std::uint32_t Crc32Impl<-1>(char const *)
    {
        return 0xFFFFFFFF;
    }

  }

  template <std::size_t N>
  inline constexpr unsigned Crc32(char const (&str)[N])
  {
    return (Private::Crc32Impl<sizeof(str) - 2>(str) ^ 0xFFFFFFFF);
  }
}

Отчего таблица завернута в конструкцию, да еще и в образец? Дабы избавиться от cpp-файла с определением данных, т.е. все только в подключаемом файле и без прелестей static-данных в подключаемых файлах.

Crc32 рассчитан, идентификатор сформирован. Сейчас к рассмотрению того, что кроется под вторым макросом:

BOSS_DECLARE_IBASE_METHODS

#define BOSS_DECLARE_IFACEID(ifaceid_) 
#define BOSS_DECLARE_IBASE_METHODS() 
  virtual Boss::UInt BOSS_CALL AddRef() = 0; 
  virtual Boss::UInt BOSS_CALL Release() = 0; 
  virtual Boss::RetCode BOSS_CALL QueryInterface(Boss::InterfaceId ifaceId, Boss::Ptr *iface) = 0;

Впрочем! Неужели невозможно было легко взять и разместить три способа в конструкцию? Для чего макрос? И добить вопросом о наличии родственников в Индии… Но так как от множественного наследования нет отказа и, больше того, оно дюже приветствуется в данной модели, то для того Дабы утехамирить тревоги компилятора о том что ему не ясно из какой ветки наследования брать какой-нибудь из способов, описанных под макросом, данный макрос будет использован еще в нескольких местах.

Управление временем жизни объектов реализовано через подсчет ссылок. В функции интерфейса IBase входят способы для работы со счетчиком ссылок и способ для запроса интерфейсов у объекта.

Пример определения пользовательского интерфейса:

struct IFace
  : Boss::Inherit<Boss::IBase>
{
  BOSS_DECLARE_IFACEID("IFace")
  virtual void BOSS_CALL Mtd() = 0;
};

Примерно все ясно: интерфейс, объявление его способов, определение идентификатора. Но отчего легко не сделать наследование от Ibase?

2-й пример пользовательского интерфейса, чтоб последующее трактование было внятнее:

struct IFace1
  : Boss::Inherit<Boss::IBase>
{
  BOSS_DECLARE_IFACEID("IFace1")
  virtual void BOSS_CALL Mtd1() = 0;
};

struct IFace2
  : Boss::Inherit<Boss::IBase>
{
  BOSS_DECLARE_IFACEID("IFace2")
  virtual void BOSS_CALL Mtd2() = 0;
};

struct IFace3
  : Boss::Inherit<IFace1, IFace2>
{
  BOSS_DECLARE_IFACEID("IFace3")
  virtual void BOSS_CALL Mtd3() = 0;
};

Сейчас все раскрылось? Нет? Все легко: при наличии множественного наследования даже только интерфейсов необходимо как-то иметь вероятность их «обходить» в поисках надобного при реализации QueryInterface. Случай немножко эзотерический, но изредка я натыкался на сходственное. Представим у вас есть указатель на IFace3, ясно, что все способы его базовых классов вы можете здесь на месте и вызвать. А если передать его в другую функцию, больше обобщенную, которая от некоторого интерфейса, необязательно с такой конструкцией наследования, неизменно запрашивает IFace1 либо IFace2, то она теснее не на C механизмы опирается, а на реализованный QueryInterface, реализации которого нужно как-то эту иерархию обойти. Вот здесь-то и пригождается некоторая примесь: Boss::Inherit, которая имеет следующую реализацию:

namespace Boss
{
  template <typename ... T>
  struct Inherit
    : public T ...
  {
    virtual ~Inherit() {}
    typedef std::tuple<T ... > BaseInterfaces;
    BOSS_DECLARE_IBASE_METHODS()
  };
}

Данная примесь легко наследуется от переданного списка базовых интерфейсов, «успокаивает» компилятор от неразборчивости выбора надобного способа (применение BOSS_DECLARE_IBASE_METHODS) и «прикапывает» список унаследованных интерфейсов. Здесь новейший эталон дает такое превосходство как образцы с переменным числом параметров. Ура, дождались! Ранее это решалось через массивные списки типов в жанре Александреску. Ну также здесь новые «плюсы» еще дали бонус в виде кортежа, избавив от написания своего аналогичного велосипеда.

Как, от чего и отчего определить пользовательские интерфесы рассмотрено, но их где-то и как-то нужно реализовывать. Для начала небольшой пример реализации интерфейсов:

struct IFace1
  : Boss::Inherit<Boss::IBase>
{
  BOSS_DECLARE_IFACEID("IFace1")
  virtual void Mtd1() = 0;
};
class Face_1
  : public Boss::CoClass<Boss::Crc32("Face_1"), IFace1>
{
public:
  virtual void Mtd1()
  {
    // TODO:
  }
};

И огромный пример

struct IFace1
  : Boss::Inherit<Boss::IBase>
{
  BOSS_DECLARE_IFACEID("IFace1")
  virtual void Mtd1() = 0;
};

struct IFace2
  : Boss::Inherit<Boss::IBase>
{
  BOSS_DECLARE_IFACEID("IFace2")
  virtual void Mtd2() = 0;
};

struct IFace3
  : Boss::Inherit<Boss::IBase>
{
  BOSS_DECLARE_IFACEID("IFace3")
  virtual void Mtd3() = 0;
};

class Face1
  : public Boss::CoClass<Boss::Crc32("Face1"), IFace1>
{
public:
  virtual void Mtd1()
  {
    // TODO:
  }
};

class Face2
  : public Boss::CoClass<Boss::Crc32("Face2"), IFace2>
{
public:
  virtual void Mtd2()
  {
    // TODO:
  }
};

class Face123
  : public Boss::CoClass<Boss::Crc32("Face123"), Face1, Face2, IFace3>
{
public:
  virtual void Mtd3()
  {
    // TODO:
  }
};

struct IFace4
  : Boss::Inherit<Boss::IBase>
{
  BOSS_DECLARE_IFACEID("IFace4")
  virtual void Mtd4() = 0;
};

struct IFace5
  : Boss::Inherit<Boss::IBase>
{
  BOSS_DECLARE_IFACEID("IFace5")
  virtual void Mtd5() = 0;
};

struct IFace6
  : Boss::Inherit<IFace4, IFace5>
{
  BOSS_DECLARE_IFACEID("IFace6")
  virtual void Mtd6() = 0;
};

class Face123456
  : public Boss::CoClass<Boss::Crc32("Face123456"), Face123, IFace6>
{
public:
  virtual void Mtd4()
  {
    // TODO:
  }
  virtual void Mtd5()
  {
    // TODO:
  }
  virtual void Mtd6()
  {
    // TODO:
  }
};

со всеми «подлостями» для реализации, которые она должна будет разобрать. ?сно, что дозволено наворотить больший хаос. Приведенный пример описывает вероятности реализации в построении «из кубиков».

Сложно не подметить, что всякая реализация наследуется от CoClass. CoClass имеет крайне примитивную реализацию:

namespace Boss
{
  template <ClassId ClsId, typename ... T>
  class CoClass
    : public virtual Private::CoClassAdditive
    , public T ...
  {
  public:
    typedef std::tuple<T ... > BaseEntities;
    CoClass()
      : Constructed(false)
    {
    }

    // IBase
    BOSS_DECLARE_IBASE_METHODS()

  private:
    template <typename Y>
    friend void Private::SetConstructedFlag(Y *, bool);
    template <typename Y>
    friend bool Private::GetConstructedFlag(Y *);

    bool Constructed;
  };
}

Данный класс, так же как и конструкция Inherit, наследуется от списка переданных сущностей, «прикапывает» данный список наследования, наследуется от некоторой

примеси (Private::CoClassAdditive) / марки

namespace Boss
{
  namespace Private
  {
    struct CoClassAdditive
    {
      virtual ~CoClassAdditive() {}
    };
  }
}

(по которой будет производится систематизация сущностей: интерфейс либо реализация), так же освобождает от неразборчивости компилятор (подпихнув ему способы через BOSS_DECLARE_IBASE_METHODS) и содержит знак сконструированности объекта (Constructed).

Есть интерфейсы, есть их реализации, но пока не было реализации IBase. Реализация этого интерфейса, вероятно будет одной из трудных.

Для создания объекта из приведенного выше большого примера будет выглядеть приблизительно так:

auto Obj = Boss::Base<Face123456>::Create();

Boss::Base — это класс-реализация Boss::IBase. В реализации для выполнения тех либо иных операций придется обходить иерархию класса. Так для примера, приведенного выше, упрощенная иерархия будет выглядеть так:

Обход иерархии классов в поисках надобного ненадолго отложу. Стремительно пройдусь по больше простым способам.

Подсчет ссылок осуществляется вызовом способов AddRef (увеличивает счетчик ссылок) и Release (сокращает счетчик ссылок и при достижении нуля удаляет объект, делая delete this). Так как предполагается вероятность применения объектов в многопоточной среде, то работа со счетчиком осуществляется через std::atomic, что разрешает атомарно увеличивать и сокращать счетчик в многопоточной среде. Да, наконец-то C признало существование потоков и возникла помощь работы с потоками и примитивы синхронизации.

Способ Create имеет такую реализацию:

template <typename ... Args>
static RefObjPtr<T> Create(Args const & ... args)
{
  Private::ModuleCounter::ScopedLock Lock;
  RefObjPtr<T> NewInst(new Base<T>(args ...));
  Private::FinalizeConstruct<T>::Construct(NewInst.Get());
  return std::move(NewInst);
}

Присутствие образцов с переменным числом параметров разрешает сделать способ для конструирования объектов и в конструктор передавать надобные параметры. Ранее такого сделать невозможно было и если объект нуждался в каких-то первоначальных настройках, то доводилось создавать объект, у которого был определенный способ (свой специфичный) типа Init, в тот, что нужное и передавалось.

ModuleCounter

namespace Boss
{

  namespace Private
  {
    struct ModuleRefCounterTypeStub
    {
    };

    template <typename T>
    class ModuleRefCounter
    {
    public:
      static void AddRef()
      {
        Counter.fetch_add(1, std::memory_order_relaxed);
      }
      static void Release()
      {
        Counter.fetch_sub(1, std::memory_order_relaxed);
      }
      static UInt GetCounter()
      {
        return Counter;
      }

    private:
      static std::atomic<UInt> Counter;

    public:
      class ScopedLock
      {
      public:
        ScopedLock(ScopedLock const &) = delete;
        ScopedLock(ScopedLock &&) = delete;
        ScopedLock operator = (ScopedLock const &) = delete;
        ScopedLock operator = (ScopedLock &&) = delete;

        ScopedLock()
        {
          ModuleRefCounter<T>::AddRef();
        }
        ~ScopedLock()
        {
          ModuleRefCounter<T>::Release();
        }
      };
    };

    template <typename T>
    std::atomic<UInt> ModuleRefCounter<T>::Counter(0);

    typedef ModuleRefCounter<ModuleRefCounterTypeStub> ModuleCounter;
  }
}

Управляет счетчиком ссылок модуля. Имеется два счетчика ссылок — это счетчик ссылок непринужденно у объекта и счетчик всех ссылок модуля. Счетчик ссылок модуля необходим для того, Дабы дозволено было осознать, когда в модуле есть «живые» объекты, а когда нет ни одного и модуль дозволено выгрузить.

Дабы отказаться от статических библиотек и реализовать паттерн «одиночка» (для всякого из модулей) необходимо для сущности ModuleRefCounter реализовать его только во включаемом файле, то здесь абсолютно сгодится трюк с образцами и статическими объектами. Больше детально об этом дозволено прочесть в предыдущей статье. Коротко описать это дозволено так: если сделать образец типа со статическим полем и инстанцировать его любым типом, то экземпляр этого объекта будет исключительный на каждый модуль. Получается маленькая хитрость, используемая для написания одиночек во включаемых файлах без реализации где-то в cpp-файле (одиночки в include’ах).
И в этом прекрасном решении есть грабли, детские грабли: черенок в два раза короче, бьет вернее и больнее… Это решение восхитительно работает в .dll, но в .so поймал задачу: образец со статическими полями, инстанцированный одним и тем же типом стал одним на все .so с компонентами данной модели в рамках процесса! Отчего я немножко позже понял, но пришлось от прекрасного решения отказаться в пользу больше простого, основанного на безымянных пространствах имен и включаемом файле, тот, что в всякий модуль включается не больше одного раза (кому увлекательно — boss/include/plugin/module.h).

Язык C многие считают языком, дозволяющим легко «выстрелить себе в ногу». И, как правило, Зачастую гонения на него идут именно из-за учета парности операций по выделению/освобождению источников, а в частности памяти. Но если применять мудрые указатели, то одной головной болью становится поменьше. RefObjPtr как раз и яии все через тот же SFINAE способа BeforeRelease и при наличии вызова его. Реализация логики по работе с BeforeRelease аналогична логике FinalizeConstruct, но только в обратном порядке обход.

Сейчас есть вероятность доконструировать объект позже его полного создания и высвободить что-то перед уничтожением объекта. Но в конструкторе дозволено известить о задаче, бросив из него исключение. Такое же поведение реализовано в данной модели: в любом способе FinalizeConstruct в иерархии дозволено бросить исключение и остальная цепочка FinalizeConstruct теснее не будет вызываться, помимо того, для объектов иерархии, для которых FinalizeConstruct теснее прошел удачно будет вызван BeforeRelease. Получается полная параллель конструкторам и деструкторам C . BeforeRelease вызывается из реализации способа Release и при обходе иерархии BeforeRelease будет вызван только для тех объектов, для которых прошел удачный вызов FinalizeConstruct, а успешность вызова определяется флагом Constructed, находящегося в CoClass’е (помните?). Так же стоит подметить, что если нет необходимости в парности этих способов в классе, то может присутствовать только один, если он вообще необходим.

Осталось реализовать логику

QueryInterface

namespace Boss
{
  namespace Private
  {
    template <typename T, bool IsImpl>
    struct QueryInterface;

    template <typename T, std::size_t I>
    struct QueryInterfacesListIter
    {
      template <typename ObjType>
      static RetCode Query(ObjType *obj, InterfaceId ifaceId, Ptr *iface)
      {
        typedef typename std::tuple_element<I, T>::type CurType;
        if (ifaceId == InterfaceTraits<CurType>::Id)
        {
          *iface = static_cast<CurType *>(obj);
          return Status::Ok;
        }
        return QueryInterfacesListIter<T, I - 1>::Query(obj, ifaceId, iface) == Status::Ok ?
          Status::Ok : QueryInterface<CurType, false>::Query(obj, ifaceId, iface);
      }
    };

    template <typename T>
    struct QueryInterfacesListIter<T, -1>
    {
      template <typename ObjType>
      static RetCode Query(ObjType *, InterfaceId, Ptr *)
      {
        return Status::InterfaceNotFound;
      }
    };

    template <typename T>
    struct QueryFromInterfacesList
    {
      template <typename ObjType>
      static RetCode Query(ObjType *obj, InterfaceId ifaceId, Ptr *iface)
      {
        typedef typename T::BaseInterfaces BaseInterfaces;
        enum { BaseInterfaceCount = std::tuple_size<BaseInterfaces>::value - 1 };
        return QueryInterfacesListIter<BaseInterfaces, BaseInterfaceCount>::Query(obj, ifaceId, iface);
      }
    };

    template <>
    struct QueryFromInterfacesList<IBase>
    {
      template <typename ObjType>
      static RetCode Query(ObjType *obj, InterfaceId ifaceId, Ptr *iface)
      {
        if (ifaceId == InterfaceTraits<IBase>::Id)
        {
          *iface = static_cast<IBase *>(obj);
          return Status::Ok;
        }
        return Status::InterfaceNotFound;
      }
    };

    template
    <
      typename T,
      bool IsCoClass = std::is_base_of<CoClassAdditive, T>::value
    >
    struct QueryInterface
    {
      template <typename ObjType>
      static RetCode Query(ObjType *obj, InterfaceId ifaceId, Ptr *iface)
      {
        if (ifaceId == InterfaceTraits<T>::Id)
        {
          *iface = static_cast<T *>(obj);
          return Status::Ok;
        }
        return QueryFromInterfacesList<T>::Query(static_cast<T *>(obj), ifaceId, iface);
      }
    };

    template <typename T, std::size_t I>
    struct QueryInterfaceIter
    {
      template <typename ObjType>
      static RetCode Query(ObjType *obj, InterfaceId ifaceId, Ptr *iface)
      {
        typedef typename std::tuple_element<I, T>::type CurType;
        return QueryInterface<CurType>::Query(static_cast<ObjType *>(obj), ifaceId, iface) == Status::Ok ?
          Status::Ok : QueryInterfaceIter<T, I - 1>::Query(obj, ifaceId, iface);
      }
    };

    template <typename T>
    struct QueryInterfaceIter<T, -1>
    {
      template <typename ObjType>
      static RetCode Query(ObjType *obj, InterfaceId ifaceId, Ptr *iface)
      {
        return Status::InterfaceNotFound;
      }
    };

    template <typename T>
    struct QueryInterface<T, true>
    {
      template <typename ObjType>
      static RetCode Query(ObjType *obj, InterfaceId ifaceId, Ptr *iface)
      {
        typedef typename T::BaseEntities BaseEntities;
        enum { BaseEntityCount = std::tuple_size<BaseEntities>::value - 1 };
        return QueryInterfaceIter<BaseEntities, BaseEntityCount>::Query(static_cast<T *>(obj), ifaceId, iface);
      }
    };
  }
}

которая по огромному счету не особенно отличается от обхода иерархий, описанному выше. При обходе иерархии и при встрече в дереве класса реализации берется его «прикопанный» список базовых сущностей и рекурсивно обходится он в поисках надобного интерфейса. Есть одно дополнение. Так как есть вероятность работы с интерфейсами, множественно наследованными от других интерфейсов, то при встрече в иерархии поиска интерфейса, оark!
Но сам класс реализации реестра сервисов может поставлять несколько интерфейсов. Ради чего все затевалось? Делать наборные компоненты.

Класс-реализация реестра сервисов

namespace Boss
{

  class ServiceRegistry
    : public CoClass
        <
          Service::Id::ServiceRegistry,
          IServiceRegistry,
          IServiceRegistryCtrl,
          ISerializable
        >
  {
  public:
    ServiceRegistry();
    virtual ~ServiceRegistry();

  private:
    // IServiceRegistry
    virtual RetCode BOSS_CALL GetServiceInfo(ClassId clsId, IServiceInfo **info) const;
    // IServiceRegistryCtrl
    virtual RetCode BOSS_CALL AddService(IServiceInfo *service);
    virtual RetCode BOSS_CALL DelService(ServiceId serviceId);
    // ISerializable
    virtual RetCode BOSS_CALL Load(IIStream *stream);
    virtual RetCode BOSS_CALL Save(IOStream *stream);
    // ...
  };
}

т.е. реализация предоставляет интерфейс для манипулирования реестром (IServiceRegistryCtrl,) и его загрузкой и сохранением (ISerializable).

Реализация фабрики классов

namespace Boss
{
  class ClassFactory
    : public CoClass
        <
          Service::Id::ClassFactory,
          IClassFactory,
          IClassFactoryCtrl
        >
  {
  public:
    // IClassFactory
    virtual RetCode BOSS_CALL CreateObject(ClassId clsId, IBase **inst);
    // IClassFactoryCtrl
    virtual RetCode BOSS_CALL SetRegistry(IServiceRegistry *registry);    
    // ...
  };
}

так же поставляет несколько интерфейсов: один стержневой (IClassFactory), которым будут пользоваться все заказчики для создания объектов и вспомогательный (IClassFactoryCtrl), которым пользуется загрузчик для настройки фабрики на реестр.

Код загрузчика крайне примитивен, но, к сожалению, С 11 немного признал платформу (ОС). Многопоточность они признали, а вот существование таких пророческой как динамические библиотеки пока нет. Так что для загрузки модулей будет применяться код, зависящий от операционной системы. Безусловно же запрятанный велико. Здесь недурно было бы припомнить про pImple, но так как взят курс на отказ от статических библиотек, то будет немножко иное: реализация для всякой ОС в своем заголовочном файле и файл-интерфейс, разбирающий что включить на основе макросов __linux__ и _WIN32.

Небольшой пример применения сервисов в рамках модели плагинов, обитающих в одном процессе:

#include <iostream>
#include "plugin/loader.h"
#include "plugin/module.h"

int main()
{
  try
  {
    Boss::Loader Ldr("Registry.xml", "./libservice_registry.so", "./libclass_factory.so");
    Boss::RefObjQIPtr<Boss::IBase> Inst;
    Inst = Ldr.CreateObject<Boss::IBase>(Boss::Crc32("MyClass"));
  }
  catch (std::exception const &e)
  {
    std::cerr << e.what() << std::endl;
  }
  return 0;
}

Как и было подмечено в начале раздела, все крайне легко, только понадобилось написать некоторое число вспомогательного кода.

Примеры

Наилучший пример — это настоящая задача, а не неестественно выдуманное нагромождение, по максимуму демонстрирующее ту либо другую вероятность.

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

Еще раз приведу класс-реализацию для реестра сервисов.

Реализация реестра сервисов

namespace Boss
{

  class ServiceRegistry
    : public CoClass
        <
          Service::Id::ServiceRegistry,
          IServiceRegistry,
          IServiceRegistryCtrl,
          ISerializable
        >
  {
  public:
    ServiceRegistry();
    virtual ~ServiceRegistry();

  private:
    // IServiceRegistry
    virtual RetCode BOSS_CALL GetServiceInfo(ClassId clsId, IServiceInfo **info) const;
    // IServiceRegistryCtrl
    virtual RetCode BOSS_CALL AddService(IServiceInfo *service);
    virtual RetCode BOSS_CALL DelService(ServiceId serviceId);
    // ISerializable
    virtual RetCode BOSS_CALL Load(IIStream *stream);
    virtual RetCode BOSS_CALL Save(IOStream *stream);
    // ...
  };
}

Сейчас испробую описать, что тут происходит…
Для создания класс-реализации одного либо нескольких интерфейсов необходимо сделать класс производный от класса-образца CoClass. Данный класс в качестве параметров принимает идентификатор класс-реализации (тот, что теснее может быть использован при создании объекта через фабрику классов) и список наследуемых интерфейсов либо теснее готовых реализаций интерфейсов. Если взглянуть на приведенный класс-реализацию реестра обслуживания, то в ней как раз и виден идентификатор (Service::Id::ServiceRegistry) и дальше перечислены интерфейсы, которые реализованы в этом классе (IServiceRegistry — интерфейс реестра сервисов, тот, что будет использован фабрикой классов; ISrviceRegistryCtrl — интерфейс управления реестром; ISerializable — реестр должен быть куда-то сохранен и откуда-то загружен и данный интерфейс разрешает исполнять требуемое). На этом каждая работа по созданию компонента завершена и необходимо каждого лишь реализовать его способы.

Компонент готов. Осталось его как-то опубликовать, т.е. дать вероятность доступа к нему извне модуля, в котором он находится.

Для этого необходимо воспользоваться макросом BOSS_DECLARE_MODULE_ENTRY_POINT

#include "service_registry.h"
#include "plugin/module.h"

namespace
{

  typedef std::tuple
    <
      Boss::ServiceRegistry
    >
    ExportedCoClasses;

}

BOSS_DECLARE_MODULE_ENTRY_POINT("ServiceRegistry", ExportedCoClasses)

В макрос передается строка от которой будет рассчитан CRC32, применяемый в качестве идентификатора модуля и список класс-реализаций, тот, что экспортируется данным модулем. Позже чего компонент и его модуль готов (а в модуле может находиться несколько компонент), им дозволено пользоваться, зарегистрировав в реестре (исключение: реестр сервисов и фабрику классов для обыкновенного применения модели дозволено не регистрировать).

Еще один подобный пример: реализация фабрики классов, тот, что так же теснее был приведен выше.

Фабрика классов

namespace Boss
{
  class ClassFactory
    : public CoClass
        <
          Service::Id::ClassFactory,
          IClassFactory,
          IClassFactoryCtrl
        >
  {
  public:
    // IClassFactory
    virtual RetCode BOSS_CALL CreateObject(ClassId clsId, IBase **inst);
    // IClassFactoryCtrl
    virtual RetCode BOSS_CALL SetRegistry(IServiceRegistry *registry);    
    // ...
  };
}

Всецело подобный пример. Так же наследование от CoClass, идентификатор и список реализуемых интерфейсов. Фабрика классов помещена в отдельном модуле, соответственно она имеет и свою

точку входа

#include "class_factory.h"
#include "plugin/module.h"

namespace
{

  typedef std::tuple
    <
      Boss::ClassFactory
    >
    ExportedCoClasses;

}

BOSS_DECLARE_MODULE_ENTRY_POINT("ClassFactory", ExportedCoClasses)

аналогичную точке входа реестра сервисов.

Это были примитивные реализации компонент, в которых всякая компонента наследовала только список интерфейсов, реализовывала их способы и все. Не было наследования теснее готовых реализаций. А если Вы посмотрите еще раз на интерфейс реестра сервисов, то в нем Вы увидите работу с IServiceInfo, через тот, что и передается каждая информация. IServiceInfo может передавать только всеобщую информацию о сервисе, но есть и частная. Первоначально хотел сделать плагины, живущие не только в динамических библиотеках, но и разбросанные по процессам, в своих исполняемых модулях. Отсель и различная информация: для плагинов в динамических библиотеках только дополнение о пути к ней, а для плагинов в отдельных исполняемых модулях куча дополнительной информации: информация о Proxy/Stub’ах, транспорте и т.д. (но, к сожалению, я не довел эту часть до конца, а зачатки отрезал, чтоб не загрязнять код недоделками). Сейчас как раз и приведу пример, в котором теснее компоненты наследуются не только от интерфейсов, но и от реализаций.

Реализация информации о сервисах

#ifndef __BOSS_PLUGIN_SERVICE_INFO_H__
#define __BOSS_PLUGIN_SERVICE_INFO_H__

#include "../core/base.h"
#include "../core/error_codes.h"
#include "../core/ref_obj_ptr.h"
#include "../common/enum.h"
#include "../common/entity_id.h"
#include "../common/string.h"
#include "iservice_info.h"

#include <string>

namespace Boss
{

  namespace Private
  {

    template <typename T, bool = !!std::is_base_of<IServiceInfo, T>::value>
    class ServiceInfo;

    template <typename T>
    class ServiceInfo<T, true>
      : public CoClass<Crc32("Boss.ServiceInfo"), T>
    {
    public:
      // …

      void SetServiceId(ServiceId srvId)
      {
          // ...
      }
      void AddCoClassId(ClassId clsId)
      {
        // ...
      }
      void AddCoClassIds(RefObjPtr<IEnum> coClassIds)
      {
        // ...
      }

    private:
      // …

      // IServiceInfo
      virtual RetCode BOSS_CALL GetServiceId(ServiceId *serviceId) const
      {
        // ...
      }
      virtual RetCode BOSS_CALL GetClassIds(IEnum **ids) const
      {
        // ...
      }
    };

  }

  class LocalServiceInfo
    : public CoClass<Crc32("Boss.LocalServiceInfo"), Private::ServiceInfo<ILocalServiceInfo>>
  {
  public:
    void SetModulePath(std::string const &path)
    {
      // ...
    }
    void SetModulePath(RefObjPtr<IString> path)
    {
      // ...
    }

  private:
    // ...

    // ILocalServiceInfo
    virtual RetCode BOSS_CALL GetModulePath(IString **path) const
    {
      // ...
    }
  };

  class RemoteServiceInfo
    : public CoClass<Crc32("Boss.RemoteServiceInfo"), Private::ServiceInfo<IRemoteServiceInfo>>
  {
  public:
    void SetProps(RefObjPtr<IPropertyBag> props)
    {
      // ...
    }

  private:
    // ...

    // IRemoteServiceInfo
    virtual RetCode BOSS_CALL GetProperties(IPropertyBag **props) const
    {
      // ...
    }
  };

}

#endif  // !__BOSS_PLUGIN_SERVICE_INFO_H__

Реализация ServiceInfo может показаться немножко сложноватой. Для чего и здесь образец? Это теснее тонкость реализации конструкции данных, которая мне пришла в голову, а не подать, отдаваемая компонентной модели / плагинной системе. Дабы немножко прояснилась повод такой реализации, приведу интерфейс:

Интерфейс информации о сервисе

#ifndef __BOSS_PLUGIN_ISERVICE_INFO_H__
#define __BOSS_PLUGIN_ISERVICE_INFO_H__

#include "../core/ibase.h"
#include "../common/ienum.h"
#include "../common/istring.h"
#include "../common/iproperty_bag.h"

namespace Boss
{

  struct IServiceInfo
    : public Inherit<IBase>
  {
    BOSS_DECLARE_IFACEID("Boss.IServiceInfo")

    virtual RetCode BOSS_CALL GetServiceId(ServiceId *serviceId) const = 0;
    virtual RetCode BOSS_CALL GetClassIds(IEnum **ids) const = 0;
  };

  struct ILocalServiceInfo
    : public Inherit<IServiceInfo>
  {
    BOSS_DECLARE_IFACEID("Boss.ILocalServiceInfo")

    virtual RetCode BOSS_CALL GetModulePath(IString **path) const = 0;
  };

  struct IRemoteServiceInfo
    : public Inherit<IServiceInfo>
  {
    BOSS_DECLARE_IFACEID("Boss.IRemoteServiceInfo")

    virtual RetCode BOSS_CALL GetProperties(IPropertyBag **props) const = 0;
  };

}

#endif  // !__BOSS_PLUGIN_ISERVICE_INFO_H__

Чуть больше внятная реализация с наследованием интерфейсов и реализаций приведена в изложении ядра с затейливым классом Face123456 без любых образцов :)

Как компоненты реализовывать все прояснилось. Все легко. А как запрашивать и трудиться с интерфейсами, запрашивать из одного иной — дозволено разглядеть на примере загрузчика, тот, что загружает реестр сервисов, получает из него надобные интерфейсы, настраивает данный самый реестр, загружает фабрику классов и настраивает ее на работу с реестром. Дальше, безусловно, каждая работа заказчика идет теснее с фабрикой классов и заказчик теснее не должен трудиться с модулями, а напротив ради чего каждая эта абстрактность затевалась-то.

Загрузчик

#ifndef __BOSS_PLUGIN_LOADER_H__
#define __BOSS_PLUGIN_LOADER_H__

#include "iservice_registry.h"
#include "iclass_factory.h"
#include "iclass_factory_ctrl.h"
#include "module_holder.h"
#include "service_ids.h"
#include "core/exceptions.h"
#include "common/file_stream.h"
#include "common/iserializable.h"

#include <string>

namespace Boss
{

  BOSS_DECLARE_RUNTIME_EXCEPTION(Loader)

  class Loader final
  {
  public:
    Loader(Loader const &) = delete;
    Loader& operator = (Loader const &) = delete;

    Loader(std::string const &registryFilePath,
           std::string const &srvRegModulePath,
           std::string const &clsFactoryModulePath)
      : SrvRegistry([&] ()
          {
            auto SrvRegModule(ModuleHolder(std::move(DllHolder(srvRegModulePath))));
            auto SrvReg = SrvRegModule.CreateObject<IServiceRegistry>(Service::Id::ServiceRegistry);
            RefObjQIPtr<ISerializable> Serializable(SrvReg);
            if (!Serializable.Get())
              throw LoaderException("Failed to get ISerializable interface from Registry object.");
            if (Serializable->Load(Base<IFileStream>::Create(registryFilePath).Get()) != Status::Ok)
              throw LoaderException("Failed to load Registry.");
            return std::move(std::make_pair(std::move(SrvRegModule), std::move(SrvReg)));
          } ())
      , ClsFactory([&] ()
          {
            auto ClassFactoryModule(ModuleHolder(std::move(DllHolder(clsFactoryModulePath))));
            auto NewClsFactory = ClassFactoryModule.CreateObject<IClassFactory>(Service::Id::ClassFactory);
            RefObjQIPtr<IClassFactoryCtrl> Ctrl(NewClsFactory);
            if (!Ctrl.Get())
              throw LoaderException("Failed to get ICalssFactoryCtrl interface from ClassFactory object.");
            if (Ctrl->SetRegistry(SrvRegistry.second.Get()) != Status::Ok)
              throw LoaderException("Failed to set Registry into ClassFactory.");
            return std::move(std::make_pair(std::move(ClassFactoryModule), std::move(NewClsFactory)));
          } ())
    {
    }
    template <typename T>
    RefObjPtr<T> CreateObject(ClassId clsId)
    {
      RefObjPtr<IBase> NewInst;
      if (ClsFactory.second->CreateObject(clsId, NewInst.GetPPtr()) != Status::Ok)
        throw LoaderException("Failed to create object.");
      RefObjQIPtr<T> Ret(NewInst);
      if (!Ret.Get())
        throw LoaderException("Interface not found.");
      return Ret;
    }
    ~Loader()
    {
      ClsFactory.second.Release();
      SrvRegistry.second.Release();
    }

  private:
    std::pair<ModuleHolder, RefObjPtr<IServiceRegistry>> SrvRegistry;
    std::pair<ModuleHolder, RefObjPtr<IClassFactory>> ClsFactory;
  };

}

#endif  // !__BOSS_PLUGIN_LOADER_H__

Помимо приведенных примеров, актуальны примеры из статьи с реализацией предыдущей версии на C 03. Исключительное различие — работа с идентификаторами. В новой модели не нужно добавлять отдельным макросом в класс-реализацию, про тот, что дозволено позабыть. Если в новой модели позабыть про идентификатор, то компилятор об этом Вам напомнит, так как сейчас это параметр образца.

Завершение

Была некоторая огромная задумка, но реализовалась только на 2/3:

  • Реализовано всецело ядро
  • Реализованы базовые сервисы для существования плагинной системы
  • Плагины, которые обязаны были обитать в других процессах, не доведены до ума, а все зачатки вырезаны, Дабы не загрязнять код

Как-то так сложилось, что особенно увлекателен для меня момент построения каркаса либо скелета системы, а вот наращивание мышц и вливание жира (разработка каждых полезностей / псевдополезностей) это теснее может изредка быть работой, которая выполняется крайне стремительно в силу отличной осведомленности в системе. В силу этого получилось крайне полное (местами может быть избыточно полное) ядро (сферические кони в вакууме меня неизменно привлекали). Есть маленькая часть мышц (основные компоненты плагинной системы: реестр сервисов и фабрика классов), Дабы модель хоть как-то могла существовать. Но данная реализация получилась всецело обезжиренной: в ней нет ничего вспомогательного. Собран скелет системы, наращено немножко мышц и дан пинок под зад, чтоб это хоть как-то двинулось с места — стало материалом и статьей Програ.

План непременно должен быть либо выпущен, либо прерван как дозволено ранее, пока он не съел все источники и успешно не пропал из поля внимания. В силу этого мнения и того, что материал статьи получился великоват и, допустимо, местами сложноват, и той поводы, что мне не удалось огромнее полугода уделить внимания данной статье, часть с плагинами пока отсутствует. Скоро может скажем возникнуть C 14 и тогда материал этой статьи, посвященный C 11 теснее может стать неактуальным. Абсолютно может быть нереализованная часть выйдет отдельным постом… Данный материал будет основываться на материале статьи«Proxy/Stubs своими руками», тот, что я хотел переработать с учетом эталона C 11, добавить маршалинг интерфейсов и подложить под это все транспорт (реализовать один из механизмов IPC).

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

Каждый начальный код доступен для скачивания.

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB