[Из песочницы] Удобный лог не роскошь, а средство отладки, или как подключить dll при помощи h файла

image

ПроЛог


Не один программист, приступая к разработке приложения, не проходит мимо вопроса о логах. Вроде бы простой вопрос, но перебирая уже существующие варианты, понимаешь, что в каждом что-то неудобно: нет run-time отключения лога (только при компиляции), иногда нужно перенаправить лог в файл, иногда в communication port или еще куда-нибудь и т.д. и т.п. Писать полноценный вариант не хватает времени, а создавать наспех еще одну реализацию — рука не поднимается. И получается, как говорится, сапожник без сапог, даже еще хуже, ведь логи это инструмент разработки… А что если подойти к этому вопросу не спеша? Как разработчику мне бы хотелось видеть инструмент отладки таким:

  1. Легким и простым в использовании — чтобы можно было по умолчанию включить один h файл в проект и все заработало будь то старое или новое приложение.
  2. Расширяемым — чтобы добавив один h файл в проект, можно было нарастить функциональность настолько, насколько вам необходимо, не затрагивая при этом самого приложения (ведь часто приложение уже работает у клиента и трогать его не желательно).
  3. Конфигурируемым в полном объеме — разработчик в отличии от пользователя должен контролировать инструмент разработки в полной мере.


Расширяемость


Один из основных принципов расширяемости, это обеспечение максимальных возможностей изменения, при сведении к минимуму воздействия на существующие функции системы. Во первых, это означает, что если мы хотим сделать лог расширяемым, мы должны сделать из него систему, т.е. отделить его от приложения. Таким механизмом в windows являются dynamic link library: приложению все равно с какой библиотекой оно работает, если библиотека предоставляет необходимый интерфейс. Т.е. все требования к библиотеке, сводятся к требованиям к интерфейсу. Расширяемости интерфейса можно добиться, используя механизм интерфейсов с++ (на этом построена Component Object Model). Для этого в dll нужно определить всего две функции:

  int GetLogInterfaceVersion();
  ILog* CreateLogObject();


Где ILog является требуемым интерфейсом и определяется как:

  interface ILog 
  {
    virtual void Log( unsigned int messageId, char *fmt, ... ) = 0; 
  };


В случае если, нам необходимо добавить новую функцию в наш интерфейс:

  interface ILog 
  {
    virtual void Log( unsigned int messageId, char *fmt, ... ) = 0;  
    virtual void RedirectLog( void (*log) (char *)) = 0;
  };


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

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

  template < class LogConfigPolicy, class LogPolicy > class TLog : public LogConfigPolicy, public LogPolicy, public ILog
  {
    TLog() : LogConfigPolicy(), LogPolicy( this ) // стратегия конфигурирования имеет фиксированный интерфейс и передается в стратегию вывода в лог
    {
      defaultFilterLevel = LOG_DEBUG;
      if( GetString("common","filterLevel",out,sizeof(out),"debug") ) // GetString – метод стратегии конфигурирования, режим вывод в лог может быть, например, прочитан из файла или из реестра
  …
  };


Таким образом можно легко менять не только то, куда мы выводим лог, но и откуда читается конфигурация из реестра или из файла. Функция CreateLogObject будет выглядеть следующим образом:

  ILog* CreateLogObject()
  {
    try
    {
  #if defined(LOG_REG_CONFIG_POLICY) && defined(LOG_DEBUG_POLICY)
      return new TLog< LogRegConfigPolicy, LogDebugPolicy >();
  #elif defined(LOG_REG_CONFIG_POLICY) && defined(LOG_FILE_POLICY)
      return new TLog< LogFileConfigPolicy, LogFilePolicy >();
  #elif defined(LOG_FILE_CONFIG_POLICY) && defined(LOG_DEBUG_POLICY)
      return new TLog< LogRegConfigPolicy, LogDebugPolicy >();
  #elif defined(LOG_FILE_CONFIG_POLICY) && defined(LOG_FILE_POLICY)
      return new TLog< LogFileConfigPolicy, LogFilePolicy >();
  #else
      #error Log policies weren't defined
  #endif        
    }
    catch(...)
    {
      return NULL;
    }
  }


Теперь, комбинируя определения препроцессора можно получить библиотеку логера log.dll с теми или иными свойствами.

Простота использования


Загрузить dll в приложение не такая уж сложная задача, но если это делать в каждом проекте, это может и надоесть, а главное, в уже существующих приложениях маловероятно, чтобы кто-то при использовании лога, пользовался библиотекой… А можно ли сделать это автоматически? Очевидно, что нужны какие-то средства языка. Паттерн Singleton — первое, что напрашивается для реализации лога (при этом dll загружается в конструкторе Singleton). Необходимость использования данного паттерна диктуется не только удобством, но и необходимостью, ведь, обращаться к логу могут и глобальные объекты, а порядок их создания не определен. Попытка реализовать Singleton в виде шаблона класса со стратегиями, привела меня к интересному факту — инстанционирование статической переменной класса производится в h файле! Получается, что весь логер, на стороне приложения, можно реализовать всего в одном h файле, не добавляя ни cpp ни lib:

  template  class TSingleton: public CreatePolicy
  {
  private:
    static TSingleton *instance;

    TSingleton():CreatePolicy(){}
    TSingleton( const TSingleton& ){}
    TSingleton& operator=( TSingleton& ){}
    virtual ~TSingleton(){}

  public:
    static MainInterface* GetSingletonObject()
    {
      if(!instance)
      {
        NamedMutexObject mutex( CreatePolicy::GetMutexName() );         
        if(!instance) instance = new TSingleton();
      }
      return instance->mainInterface;
    }

    static void ReleaseSingletonObject()
    {
      if(instance)
      {
        NamedMutexObject mutex( CreatePolicy::GetMutexName() );         
        if(instance) delete instance;
      }
    }
  };

  template  
    TSingleton< CreatePolicy, MainInterface, NamedMutexObject > * 
    TSingleton< CreatePolicy, MainInterface, NamedMutexObject >::instance = 0; 


CreatePolicy — стратегия, которая, в нашем случае, загружает библиотеку log.dll и создает объект логера, сохраняя его интерфейс в mainInterface. Интерфейс MainInterface, это ILog, NamedMutexObject — это объект синхронизации, используемый при создании объекта instance. Макрос записи ошибки в лог будет выглядеть следующим образом:

  #define log_err(fmt,...) TSingleton::GetSingletonObject()->Log( LOG_ERROR, fmt, ##__VA_ARGS__)


Таким образом, приложение example.cpp, использующее данный логер, будет иметь вид:

  #include "log.h"

  int main(int argc, char* argv[])
  {
    log_inf("Some info...\n");
    return 0;
  }


Команда сборки будет выглядеть следующим образом:

cl.exe example.cpp

Конфигурирование

Полнота возможностей конфигурирования обеспечивается принципом предоставления разработчику возможности конфигурирования на всех уровнях, будь, то уровень компиляции или run-time.

Настройки компилятора определяют, включен ли, тот или иной кусок кода библиотеки или приложения. Как было показано, для библиотеки логера log.dll (см. реализацию функции CreateLogObject) настройки определяют, какие стратегии используются в шаблонном классе логера. Таким образом, создавая необходимые стратегии для логера и комбинируя их при помощи настрек компилятора, можно реализовать в библиотеке логера необходимую функциональность, не изменяя при этом уже существующий код.

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

  • LOG_DISABLE — запрещает вывод в лог всех типов сообщений;
  • LOG_DISABLE_INFO — запрещает сообщения информационного типа;
  • LOG_DISABLE_WARNING — запрещает сообщения о предупреждениях,
  • LOG_DISABLE_ERROR — запрещает сообщения об ошибках.


На уровне run-time настройки полностью определяются стратегией, а значит тем, что реализует программист. Например, стратегия конфигурирования логера из файла, реализует настройку фильтра сообщений, которая находится в ini файле:

[common]
filterLevel = info

Теперь, когда программисту даны все возможности по отладке, а если их нет то их легко добавить, можно приступать к разработке самого приложения, ведь, добавив всего один h файл к приложению, вы сможете потом легко изменить лог, не меняя самого приложения.

© Habrahabr.ru