вторник, 8 октября 2013 г.

Создание dll с сигнал-слотами внутри. Для NoQt приложений.

Приветствую всех заглянувших.

В этой статье я опишу свой взгляд и приведу пример создания DLL с системой сигнал-слотов Qt.

Внимание!!! Данная статья написана под Windows, IDE Visual Studio 2008 и микрософтовский же компилятор. Для использование данного метода под другими системами, вам придётся переписать платформозависимый код. Это создание потока в Dll и программа для загрузки Dll.

Дальше будет маленькое лирическое вступление. 

Иногда возникает необходимость написать программу без Qt. Требование ли это заказчика или архитектурная необходимость, но писать надо. И через некоторое время появляется проблема, которую можно легко и просто решить на Qt, но тяжело в NoQt проекте. 
Через некоторое время приходит мысль написать DLL с Си интерфейсов, из которой не видно и не слышно Qt. И вот тут начинается самое интересное.

Сигнал-слотовая система Qt основана на цикле событий. Если в приложении, использующем нашу DLL не используется Qt, то будут работать только сигналы-слоты с типом соединения "Qt::DirectConnection", так называемый прямой вызов методов. Но асинхронные интерфейсы при том работать не будут.

Начнём...

  • Создаём проект типа Qt Library. Назовём его "QLibrary". По умолчанию создастся проект динамической библиотеки. 

  • Следуем дальше указаниям менеджера и получаем три файла в проекте - QLibrary.cpp, QLibrary.h, QLibrary_global.h.
  • Первым делом убираем макрос "QLIBRARY_EXPORT" в объявлении класса. Экспортировать мы будем только функции интерфейса. 
  • Добавляем инклуды в проект. Нам нужны QObject, QFile, QCoreApplication.
  • По умолчанию класс не унаследован ни от кого. Для использования сигнал-слотов, нам необходимо добавить инклуды QObject, отнаследоваться от него же и добавить макрос Q_OBJECT в тело класса. Теперь мы можем использовать сигнал-слоты.



  • Добавим пару методов:

  1. void createFile (QString filename)
  2. void createFileSlot (QString filename)

      createFile у нас будет обычным методом класса, создающим файл в директории "C:/test".

      createFileSlot будет публичным слотом("public slots:") и будет добавлять к имени файла строку "signal". 
  • Добавляем сигнал void signalFileCreate(QString). 


На этом заканчиваем с .h файлом. Открываем .cpp.
  • Создаём глобальную переменную - указатель на наш класс "QLibrary * classPoint;". Через неё наш интерфейс будет взаимодействовать с классом.
  • В конструкторе прописываем соединение нашего сигнала с слотом. Указываем тип соединения - Qt::QueuedConnection. В ином случае соединение будет Qt::DirectConnect, что нам не надо. 
"connect( this, SIGNAL( signalFileCreate(QString) ), this, SLOT( createFileSlot(QString) ), Qt::QueuedConnection);"
  • Так же в конструкторе инициализируем указатель, тем самым обеспечивая возможность обращения к нашему классу.
"classPoint = this;"
  •  Пишем реализации методов класса.
  •  void QLibrary::createFile(QString filename)
    {
     QFile file(QString("c:/Test/%1").arg(filename));
     file.open(QIODevice::WriteOnly);
     file.write("test method!");
     emit signalFileCreate(filename);
    }
    void QLibrary::createFileSlot(QString filename)
    {
     QFile file(QString("c:/Test/%1 signal").arg(filename));
     file.open(QIODevice::WriteOnly);
     file.write("test public slot!");
    }
    


  • Метод createFile будет создавать файл "filename" и испускать сигнал. Слот createFileSlot будет вызываться сигнал-слотовой системой и создавать файл "filename signal". Содержимое тоже будет различаться, как видите.

  • Далее предстоит разобраться с функциями интерфейса и запуска цикла событий. Всего нам потребуются три функции:

  1. void createFile(char *) - будет вызывать метод createFile(Qstring) нашего класса.
  2. DWORD  MyThreadFunction( LPVOID lpParam )  - функция запуска отдельного потока для цикла событий (и для нашего класса)
  3. void instance() - будет создавать объект нашего класса и запускать поток цикла событий
    DWORD  MyThreadFunction( LPVOID lpParam )
    {
     QCoreApplication * app = NULL;
     int argc = 0;
     app = new QCoreApplication(argc, NULL);  
     QLibrary * dllClass =  new QLibrary();
       app->exec();
     return DWORD();
    }
    
    extern "C" QLIBRARY_EXPORT  void  instance()
    {
     LPDWORD lpThreadId = NULL;
     CreateThread(
      NULL,                   // default security attributes
      0,                      // use default stack size
      (LPTHREAD_START_ROUTINE)MyThreadFunction,       // thread function name
      NULL,          // argument to thread function
      0,                      // use default creation flags
      lpThreadId);
    }
    extern "C" QLIBRARY_EXPORT  void  createFile(char * inChar)
    {
     classPoint->createFile(QString::fromStdString(inChar));
    }

  • Экспортироваться будут только две функции - instance и createFile(char *).

  • Разберём что мы делаем. 
  1. Функция instance создаёт поток, в котором исполняется функция MyThreadFinction. В неё же мы создаём QCoreApplication, экземпляр своего класса и запускаем цикл обработки событий.
  2. Функция createFile будучи вызванной дёрнет метод createFile(QString). 

  • Компилируем - наслаждаемся отсутствием ошибок.


  • Имена экспортируемых методов могут меняться. Для пресечения этого необходимо добавить в проект .def файл со следующим содержимым

LIBRARY "QLibrary"
EXPORTS
    ; Explicit exports can go here
instance
createFile

  • Вот мы и создали Dll с циклом событий внутри.


  • Настало время проверить нашу dll. Для этого мы создадим маленькую C++ программу - DllLoader, код которой приведён ниже.
    #include "stdafx.h"
    #include "windows.h"
    #include <conio.h>
    #include <ctype.h>
    int _tmain(int argc, _TCHAR* argv[])
    {
     printf("The program demonstrates the use dll written on Qt. To work correctly, you need to create the directory \"C:/Test/\"\n");
     printf("Press any key to boot dll\n");
      _getch();
     HMODULE library = LoadLibrary(L"QLibrary.dll");
      if (!library)
     {
      printf("Not exists QLibrary.dll.");
      return 0;
     }
     printf("Dll loaded.\n");
     void (*dllInstance) (void);
     dllInstance = (void (*)(void))GetProcAddress(library, "instance");
      if (!dllInstance)
     {
      printf("Not export \"instance\" from DLL.");
      return 0;
     }
     dllInstance();
     printf("Dll initialized. Press any key to continue.\n");  _getch();
     void (*dllCreateFile) (char*);
     char* filename = "otherName";
     dllCreateFile = (void (*)(char*))GetProcAddress(library, "createFile");
     if (!dllCreateFile)
     {
      printf("Not export \"createFile\" from DLL.");
      return 0;
     }
     dllCreateFile(filename);
     printf("Work comlete. Check \"C:/Test/\" Press any key to exit. \n");  _getch();
     return 0;
    }
    



  • Программа загружает библиотеку, инициализирует наш класс вызовом instance и вызывает функцию createFile( "OtherName" ).



  • Для проверки нам необходимо создать директорию "C:/Test/" и скопировать нашу библиотеку в каталог DllLoader'a. Запускаем DllLoader, жмём любую клавишу два раза с небольшим промежутком и смотрим результат. В каталоге должны появиться файлы "otherName" и "otherName signal". 


Профит!!!

 Наша dll загружена, сигнал-слотовая система работает, приложение не знает ничего о Qt.

Архив с исходниками:
https://docs.google.com/file/d/0B_mrvcleB88yclZjSHhQVTQ5eXM/edit?usp=sharing
Зеркало:
https://dl.dropboxusercontent.com/u/62712483/Blog/QtDll_NoQtProg.ZIP

В архивах проекты для VS2008 и pro файл.

Совет:
При написании dll во всех connect'ах указывайте тип соединения. Из-за автоматического определения типа соединения могут возникать непонятные и опасные для программы ситуации. Так же возможно выпадение волос разработчика в процессе отладки и ранняя седина.

Предупреждение:
Загрузка в одно приложение нескольких экземпляров данной библиотеки может вызвать крах. Данная статья описывает лишь создание одного экземпляра Dll.
Если dll будет использоваться в Qt приложениях, необходимо добавить проверку на наличие уже существующего цикла событий.
Пожелания, предложения, замечания - в этот блог(вроде сообщения тут есть) или же на мыло work.bepec.sb@gmail.com.

Копирайтить копирайте, но ссылочку на источник прошу добавлять ^.^

2 комментария:

  1. Загрузка в одно приложение нескольких экземпляров данной библиотеки может вызвать крах. Данная статья описывает лишь создание одного экземпляра Dll.
    -----------
    И почему же?
    Наверное, что-то не так в реализации DWORD MyThreadFunction( LPVOID lpParam )
    ))

    ОтветитьУдалить