суббота, 29 июня 2013 г.

Создание DLL консольными средствами Visual Studio

MS Visual Studio имеет графический интерфейс для разработки приложений и управления их проектами и решениями. Однако за кулисами всё выполняется консольными утилитами, так же входящими в состав этой IDE. В данной заметке я покажу маленький пример создания dll файла консольными средствами MS Visual Studio, с последующим его использованием в другом приложении. В качестве языка программирования использован C++. Весь приведённый ниже программный код можно набирать в любом текстовом редакторе (я использовал Notepad++).

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

Разработка библиотеки
Для начала, создадим каталог library, в котором будет размещаться проект нашей тестовой библиотеки. Затем в нём создадим подкаталоги:
  • headers - заголовочные файлы
  • obj - объектные файлы
  • output - конечный результат компиляции
  • sources - файлы исходного программного кода
В каталоге headers создаём файл some_library.h, в котором определяем интерфейс взаимодействия с нашей библиотекой. Подобных файлов может быть сколько угодно, в зависимости от функционала, разрабатываемой библиотеки. В данном примере ограничимся одним заголовочным файлом:

/*
some_library.h
© Andrey Bushman, 29/06/2013
Some library
*/
//--------------------------------------------------------------------
#ifndef SOME_LIBRARY_H
#define SOME_LIBRARY_H
#include <iostream>
#include <string>
#include <vector>
// http://msdn.microsoft.com/ru-ru/library/a90k134d.aspx
#define DllExport __declspec( dllexport )
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
namespace Some_namespace{
//--------------------------------------------------------------------
       class DllExport Info
       // Named info: phone, email, site, e.t.c.
       {
       public:
             std::string get_name() const;
             void set_name(const std::string& val);
             std::string get_number() const;
             void set_number(const std::string& val);
             Info(const std::string& number, const std::string& name);
             ~Info();
       private:
             std::string num;
             std::string str;
       };
//--------------------------------------------------------------------
       struct DllExport Employee
       // Employee info
       {
       public:
             enum Sex{ male, female };
             Employee(const std::string& name, const std::string& surname,
                    int age, Sex x);
             static int get_emp_count();
             std::string get_name() const;
             void set_name(const std::string& val);
             std::string get_surname() const;
             void set_surname(const std::string& val);
             int get_age() const;
             void set_age(int val);
             Sex get_sex() const;
             void set_sex(Sex val);
             std::vector<Info> phones;
             std::vector<Info> emails;
             std::vector<Info> sites;
             ~Employee();
       private:           
             std::string nm; // name
             std::string snm; // surname
             int ag; // age
             Sex sx;     
             static int emp_count;            
       };
//--------------------------------------------------------------------   
       DllExport void print(std::ostream& os, const Employee& emp);
//--------------------------------------------------------------------   
}

#endif

Для того, чтобы пометить функции, классы и переменные используется, как правило __declspec (см. ссылку в комментариях кода).

Теперь в каталоге sources создадим файл some_library.cpp:

/*
some_library.cpp
© Andrey Bushman, 29/06/2013
Some library
*/
//--------------------------------------------------------------------
#include "../headers/some_library.h"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
namespace Some_namespace{
//--------------------------------------------------------------------   
       void print(std::ostream& os, const Employee& emp){
             os << emp.get_name() << ' ' << emp.get_surname() << ' '
                    << emp.get_age() << " age, " << ( emp.get_sex() ==
                    Employee::male ? "male" : "female" );
       }
//--------------------------------------------------------------------
       std::string Info::get_name() const { return str; }
//--------------------------------------------------------------------
       void Info::set_name(const std::string& val) { str = val; }
//--------------------------------------------------------------------
       std::string Info::get_number() const { return num; }
//--------------------------------------------------------------------
       void Info::set_number(const std::string& val) { num = val; }
//--------------------------------------------------------------------
       Info::Info(const std::string& number, const std::string& name)
             : num(number), str(name) {
             std::cout << "Info::Info(): " << this << std::endl;
       }
//--------------------------------------------------------------------
       Info::~Info() {
             std::cout << "Info::~Info(): " << this << std::endl;
       }
//--------------------------------------------------------------------
       Employee::Employee(const std::string& name, const std::string&
             surname, int age, Sex x): nm(name), snm(surname), ag(age),
             sx(x){
                    ++Employee::emp_count;
                    std::cout << "Employee::Employee(): " << this
                           << std::endl;
             }
//--------------------------------------------------------------------
       int Employee::emp_count = 0;
//--------------------------------------------------------------------
       int Employee::get_emp_count(){
             return Employee::emp_count;
       }
//--------------------------------------------------------------------
       std::string Employee::get_name() const { return nm; }
//--------------------------------------------------------------------
       void Employee::set_name(const std::string& val) { nm = val; }
//--------------------------------------------------------------------
       std::string Employee::get_surname() const { return snm; }
//--------------------------------------------------------------------
       void Employee::set_surname(const std::string& val) { snm = val; }
//--------------------------------------------------------------------
       int Employee::get_age() const { return ag; }
//--------------------------------------------------------------------
       void Employee::set_age(int val) { ag = val; }
//--------------------------------------------------------------------
       Employee::Sex Employee::get_sex() const { return sx; }
//--------------------------------------------------------------------
       void Employee::set_sex(Sex val) { sx = val; } 
//--------------------------------------------------------------------
       Employee::~Employee(){
             --Employee::emp_count;
             std::cout << "Employee::~Employee(): " << this
                    << std::endl;
       }
//--------------------------------------------------------------------   

}

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

# makefile.mak
# © Andrey Bushman, 29/06/2013
# ЗАДАЧА: Создание тестовой библиотеки some_library.dll
# СБОРКА ПРОЕКТА ОСУЩЕСТВЛЯЕТСЯ КОМАНДОЙ: nmake -f makefile.mak
#---------------------------------------------------------------------
# создание dll файла:
./output/some_library.dll: ./obj/some_library.obj
link /DLL /OUT:./output/some_library.dll ./obj/some_library.obj
#---------------------------------------------------------------------
# создание lib и exp файлов:
./output/some_library.lib: ./obj/some_library.obj
lib /DEF ./obj/some_library.obj
#---------------------------------------------------------------------
# создание obj файла:
./obj/some_library.obj: ./headers/some_library.h ./sources/some_library.cpp
cl /c /EHsc /Fo./obj/ ./sources/some_library.cpp 
#---------------------------------------------------------------------

Теперь из командной строки Visual Studio переходим в каталог library и запускаем команду:

    nmake -f makefile.mak

В результате, в каталоге obj появится файл some_library.obj, а в каталоге output - файлы some_library.exp, some_library.lib и some_library.dll. Наша библиотека готова к использованию.

Имея на руках lib файл,  всегда можно посмотреть перечень сигнатур всех функций, которые экспортируются этой библиотекой. Для этого из командной строки Visual Studio переходим в каталог интересующего нас lib файла и запускаем команду:

dumpbin /EXPORTS some_library.lib /OUT:output.txt

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

??0Employee@Some_namespace@@QEAA@AEBU01@@Z (public: __cdecl Some_namespace::Employee::Employee(struct Some_namespace::Employee const &))
??0Employee@Some_namespace@@QEAA@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@0HW4Sex@01@@Z (public: __cdecl Some_namespace::Employee::Employee(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,int,enum Some_namespace::Employee::Sex))
??0Info@Some_namespace@@QEAA@AEBV01@@Z (public: __cdecl Some_namespace::Info::Info(class Some_namespace::Info const &))
??0Info@Some_namespace@@QEAA@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@0@Z (public: __cdecl Some_namespace::Info::Info(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &))
??1Employee@Some_namespace@@QEAA@XZ (public: __cdecl Some_namespace::Employee::~Employee(void))
??1Info@Some_namespace@@QEAA@XZ (public: __cdecl Some_namespace::Info::~Info(void))
??4Employee@Some_namespace@@QEAAAEAU01@AEBU01@@Z (public: struct Some_namespace::Employee & __cdecl Some_namespace::Employee::operator=(struct Some_namespace::Employee const &))
??4Info@Some_namespace@@QEAAAEAV01@AEBV01@@Z (public: class Some_namespace::Info & __cdecl Some_namespace::Info::operator=(class Some_namespace::Info const &))
?emp_count@Employee@Some_namespace@@0HA (private: static int Some_namespace::Employee::emp_count)
?get_age@Employee@Some_namespace@@QEBAHXZ (public: int __cdecl Some_namespace::Employee::get_age(void)const )
?get_emp_count@Employee@Some_namespace@@SAHXZ (public: static int __cdecl Some_namespace::Employee::get_emp_count(void))
?get_name@Employee@Some_namespace@@QEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ (public: class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl Some_namespace::Employee::get_name(void)const )
?get_name@Info@Some_namespace@@QEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ (public: class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl Some_namespace::Info::get_name(void)const )
?get_number@Info@Some_namespace@@QEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ (public: class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl Some_namespace::Info::get_number(void)const )
?get_sex@Employee@Some_namespace@@QEBA?AW4Sex@12@XZ (public: enum Some_namespace::Employee::Sex __cdecl Some_namespace::Employee::get_sex(void)const )
?get_surname@Employee@Some_namespace@@QEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ (public: class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl Some_namespace::Employee::get_surname(void)const )
?print@Some_namespace@@YAXAEAV?$basic_ostream@DU?$char_traits@D@std@@@std@@AEBUEmployee@1@@Z (void __cdecl Some_namespace::print(class std::basic_ostream<char,struct std::char_traits<char> > &,struct Some_namespace::Employee const &))
?set_age@Employee@Some_namespace@@QEAAXH@Z (public: void __cdecl Some_namespace::Employee::set_age(int))
?set_name@Employee@Some_namespace@@QEAAXAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z (public: void __cdecl Some_namespace::Employee::set_name(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &))
?set_name@Info@Some_namespace@@QEAAXAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z (public: void __cdecl Some_namespace::Info::set_name(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &))
?set_number@Info@Some_namespace@@QEAAXAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z (public: void __cdecl Some_namespace::Info::set_number(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &))
?set_sex@Employee@Some_namespace@@QEAAXW4Sex@12@@Z (public: void __cdecl Some_namespace::Employee::set_sex(enum Some_namespace::Employee::Sex))
?set_surname@Employee@Some_namespace@@QEAAXAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z (public: void __cdecl Some_namespace::Employee::set_surname(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &))

Внимание: __cdecl - это одно из возможных соглашений о вызовах. О нём и о других соглашениях можно почитать здесь.

Разработка приложения
Теперь создадим каталог application, в котором будет размещаться проект приложения, использующего только что созданную нами библиотеку. Затем в каталоге application создадим подкаталоги:
  • headers - заголовочные файлы
  • obj - объектные файлы
  • libs - lib файлы внешних библиотек
  • output - конечный результат компиляции
  • sources - файлы исходного программного кода
В каталоге headers создаём файл import_some_library.h:

/*
import_some_library.h
© Andrey Bushman, 29/06/2013
*/
//--------------------------------------------------------------------
#ifndef IMPORT_SOME_LIBRARY_H
#define IMPORT_SOME_LIBRARY_H
#include <iostream>
#include <string>
// http://msdn.microsoft.com/ru-ru/library/a90k134d.aspx
#define DllImport __declspec( dllimport )
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
namespace Some_namespace{
//--------------------------------------------------------------------
       class DllImport Info
       // Named info: phone, email, site, e.t.c.
       {
       public:
             std::string get_name() const;
             void set_name(const std::string& val);
             std::string get_number() const;
             void set_number(const std::string& val);
             Info(const std::string& number, const std::string& name);
             ~Info();
       private:
             std::string num;
             std::string str;
       };
//--------------------------------------------------------------------
       struct DllImport Employee
       // Employee info
       {
       public:
             enum Sex{ male, female };
             Employee(const std::string& name, const std::string& surname,
                    int age, Sex x);
             static int get_emp_count();
             std::string get_name() const;
             void set_name(const std::string& val);
             std::string get_surname() const;
             void set_surname(const std::string& val);
             int get_age() const;
             void set_age(int val);
             Sex get_sex() const;
             void set_sex(Sex val);
             std::vector<Info> phones;
             std::vector<Info> emails;
             std::vector<Info> sites;
             ~Employee();
       private:           
             std::string nm; // name
             std::string snm; // surname
             int ag; // age
             Sex sx;     
             static int emp_count;
       };
//--------------------------------------------------------------------   
       DllImport void print(std::ostream& os, const Employee& emp);
//--------------------------------------------------------------------   
}

#endif

Обратите внимание на то, что файл import_some_library.h отличается от заголовочного файла some_library.h: вместо строки кода

    #define DllExport __declspec( dllexport )

в этот раз используется

    #define DllImport __declspec( dllimport )

и, кроме того, везде вместо DllExport применялся DllImport.

Теперь создаём заголовочный файл нашего приложения, назвав его bush_main.h:

/*
bush_main.h
© Andrey Bushman, 28/06/2013
ЗАДАЧА: использовать функционал библиотеки some_library.lib
*/
//--------------------------------------------------------------------
#ifndef BUSH_MAIN_H
       #define BUSH_MAIN_H
       #include <iostream>
       #include <exception>
       #include <string>
       #include <vector>
       #include "../headers/import_some_library.h"
       using namespace std;

#endif

В подкаталог libs копируем файлы some_library.exp и  some_library.lib, скомпилированный нами в первом проекте (см. подкаталог library/output). Затем в каталоге sources создаём файл main.cpp:

/*
main.cpp
© Andrey Bushman, 28/06/2013
ЗАДАЧА: использовать функционал библиотеки some_library.dll
*/
//--------------------------------------------------------------------
#include "../headers/bush_main.h"
//====================================================================
int main()
try{
       namespace A = Some_namespace;
       cout << "(c) Andrey Bushman, 28/06/2013" << endl;
      
       A::Employee emp("Jon", "Smit", 30, A::Employee::male);
       cout << endl << "~~~~" << endl;
       emp.phones.push_back(A::Info("123-456-789", "work"));
       A::print(cout, emp);
       cout << endl << "~~~~" << endl;
       A::Employee emp2("Mery", "White", 45, A::Employee::female);
       A::print(cout, emp2);
       cout << endl << "~~~~" << endl;
       int count = A::Employee::get_emp_count();
       cout << "count: " << count << endl;
}
catch(exception& e){
       cerr << e.what() << endl;
       return 1;
}
catch(...){
       cerr << "Unknown exception." << endl;
       return 2;

}

Ну и, в заключении, в каталоге application создадим make файл makefile.mak, подобно тому, как мы это сделали выше для нашей библиотеки:

# makefile.mak
# © Andrey Bushman, 28/06/2013
# ЗАДАЧА: Создание тестового приложения app.exe
# СБОРКА ПРОЕКТА ОСУЩЕСТВЛЯЕТСЯ КОМАНДОЙ: nmake -f makefile.mak
#---------------------------------------------------------------------
# создание exe файла:
./output/main.exe: ./obj/main.obj
cl /Fe./output/app.exe /EHsc ./obj/main.obj ./libs/some_library.lib
#---------------------------------------------------------------------
# создание obj файла:
./obj/main.obj: ./headers/bush_main.h ./headers/import_some_library.h ./sources/main.cpp ./libs/some_library.lib ./libs/some_library.exp
cl /c /EHsc /Fo./obj/ ./sources/main.cpp
#---------------------------------------------------------------------

Теперь из командной строки Visual Studio переходим в каталог application и запускаем команду:

    nmake -f makefile.mak

В результате, в каталоге obj появится файл main.obj, а в каталоге output - файл app.exe.  Однако, для работы нашего файла app.exe необходим файл some_library.dll, скомпилированный нами в первом проекте. Поэтому копируем файл some_library.dll в тот же каталог, где находится app.exe

Всё. Теперь, запустив программу app.exe мы увидим в консоли следующий вывод:

(c) Andrey Bushman, 28/06/2013
Employee::Employee(): 000000000027FBB0

~~~~
Info::Info(): 000000000027FB70
Info::~Info(): 000000000027FB70
Jon Smit 30 age, male
~~~~
Employee::Employee(): 000000000027FC40
Mery White 45 age, female
~~~~
count: 2
Employee::~Employee(): 000000000027FC40
Employee::~Employee(): 000000000027FBB0
Info::~Info(): 000000000036CCC0

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

Архив с исходным кодом можно скачать отсюда: dll_using.zip (7 KB)


Дополнительные полезные ссылки, имеющие отношение к данной теме:

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

Дима_ комментирует...

+ к сказанному - для компиляции dll (да и exe) с "основных" .Net языков, VisualStudio вообще не нужен. Полноценные компиляторы есть в составе "генеральных" версий .Net (2.0, 4.0) - в папке %windir%\microsoft.net\framework в поддирикториях соотв. версий лежат компиляторы: ilasm.exe - IL ассемблер, csc.exe- С#, vbc.exe Visual Basic, jsc.exe - JScript.

Андрей Бушман комментирует...

да, я в курсе. :)