четверг, 3 октября 2013 г.

С++ & dll & lib статическая и динамическая линковка библиотек.

Немного поговорим о том, что вам интересно, но вы стесняетесь спросить. Виды распространения программных компонентов, повторное использование кода и актуальных проблемах.



Этапы сборки проекта


Программа на C++ обычно состоит из объявлений (типов, функций) и определений (реализаций тел функций, методов, выделение памяти под глобальные и статические объекты и т.п.). Объявления, как правило, выносятся в заголовочные файлы (т.н. "хэдеры") с расширением (*.h), а определения оказываются в файлах исходного кода с расширением (*.cpp). Заголовочные файлы играют вспомогательную функцию и в команды не компилируются сами непосредственно (их содержимое копируются в файлы исходного кода с помощью команды #include "header_file_name.h"). Файлы исходного кода содержат алгоритмы и компилируются непосредственно в команды процессора в виде объектных файлов (*.obj), по одному на каждый файл исходного кода. Чтобы собрать объектные файлы в один двоичный файл (непосредственно исполняемый *.exe, или библиотеки *.dll (*.so для Linux), *.lib) необходимо произвести линковку. Этим занимается программа Linker (компоновщик, линкер), ориентируясь по хэдерам.
Программирование ценно модульностью. То есть написав однажды код для работы, например, с аппаратом теории графов, мы можем его использовать во многих других проектах. Для этого мы можем распространять как полностью открытый исходный код, так и бинарные (двоичные) библиотеки, которые уже скомпилированы, но можно прилинковать к своему проекту.

Компоновка с открытым исходным кодом


Плюсы:

  • Достаточно распространять одну версию для многих платформ и аппаратного обеспечения (соблюдая требования переносимости).

  • Код компилируется конкретным компилятором пользователя под конкретную платформу, возможности оптимизации максимальны, программа может выполняться быстрее.

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

Недостатки:



  • Раз алгоритмы открыты, ваши наработки (по крайней мере, идейные, если опасаться ограничений свободного ПО (LGPL, GPL и др.)) можно обнаружить в чужих проектах. Если вы хотели продавать свои модули - забудьте.

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


Бинарные библиотеки lib и dll

Библиотеки могут также распространяться в виде скомпилированных двоичных файлов, которые могут быть прилинкованы к программе клиента. Линковка может быть статическая и динамическая. Статическая представляет собой собранние *.obj-файлов библиотеки в *.lib, который мы можем, указав линкеру, прицепить к нашей программе в момент компиляции. Содержимоей библиотеки, как всегда, описывается в хэдерах, которые распространяются вместе с *.lib . На выходе мы получим одинокий исполняемый файл вашей программы (*.exe).
Динамическая линковка выполняется средствами платформы (операционной системы) в процессе работы программы. Все так же у нас в руках *.lib и *.h файлы, однако, теперь к ним добавляется *.dll (*.so для Linux). *.lib-файл теперь содержит только вызовы к *.dll, где лежат непосредственно алгоритмы и которые вызываются уже на ходу, а не компилируются. Потому теперь у нас *.exe + *.dll . Несколько программ могут использовать один *.dll одновременно, тем самым не занимая оперативную память одинаковыми кусками и сами программы меньше размером. Так, например, работают многие драйверы и графические библиотеки (DirectX и OpenGL). Однако, сейчас это не такая актуальная проблема, тянут недостатки - несовместимости версий, отсутствие нужных библиотек, ад зависимостей для установки приложений (работая в Linux с графическим окружением Gnome (основанной на библиотеке GTK+) если скачать малюсенький текстовый редактор Kate для ГО KDE (основанной на Qt), то придется тянуть этот-самый Qt на десятки мегобайт). Потому, сейчас рекомендуют не увлекаться динамической линковкой и стараться связывать программы статически.


Статическая компоновка

Пусть у нас есть простой проект с одним классом, который мы хотим передать клиенту в двоичном виде:


//------------------
// Header.h
//------------------
class SomeType {
int i;
public:
SomeType();
SomeType& incr(int value = 1);
int getI();
};

И его реализация:
//--------------------------
// Implementation.cpp
//--------------------------
#include "Header.h"

SomeType::SomeType()
: i(0)
{
}
SomeType& SomeType::incr(int value/* = 1*/) {
i += value;
return *this;
}
int SomeType::getI() {
return i;
}

Ничего интересного, тип инкапсулирует переменную, которую мы можем увеличивать и получать ее значение.

Попробуем слинковать статически с другим проектом в Microft Visual Studio 2010:
//-----------------------------
// main.cpp
//-----------------------------
#include <iostream>
#include <cstdlib>
#include "Header.h" // Обращаемся к хэдеру библиотеки
using std::cout;
using std::endl;

int main() {
SomeType st;
st.incr().incr(3).incr();

cout << "Result is " << st.getI() << endl;
std::cin.get();
return EXIT_SUCCESS;
}

Сначала убедимся, что компилируем наш проект-библиотеку как *.lib: зайдем в свойства проекта (Project -> имя_проекта Properties...), там на Configuration Properties -> General. Далее в Project Defaults в пункте Configuration Type должно быть установлено Static library (*.lib). Соберем проект, убедимся, что в папке Debug/Release появился файл *.lib
Затем зайдем в свойства проекта-клиента (Project -> имя_проекта Properties...), там на Configuration Properties -> C/C++ в Additional Include Directories добавим папку с хэдером библиотеки (можно также добавить хэдер в раздел Header Files в Solution Explorer`е к остальным исходникам). Теперь нам нужно сказать линкеру, чтобы он нашел и прилинковал *.lib к нашему проекту. Там же в Configuration Properties -> Linker -> General в Additional Library Directories указать папку с *.lib . И в соседнем пункте дерева настроек линкера Input найти самый верхний пункт Additional Dependencies, куда добавить имя_библиотеки.lib . Все готово! Клиент не знает, что делают ваш Implementation.cpp, ибо у него на руках только имя_библиотеки.lib и Header.h .


Динамическая компоновка

Операции аналогичны статической линковке за исключением того, что теперь придется таскать с собой *.dll . Для компиляции динамической библиотеки необходимо в библиотечном проекте снова изменить тип проекта на Dynamic library (.dll), скомпилировать и положить dll туда, где появится исполняемый файл клиентской программы или в %windir%\System32 (операции же с хэдерами и lib те же, что и при статической линковке). Нужно изменить кое-что в коде. Нам необходимо указать, какие функции/классы будут "торчать наружу" в dll (при ее компиляции) с помощью спецификатора _declspec(dllexport) и какие будут ориентированы на вызов из dll от клиента с помощью похожего _declspec(dllimport). Ставятся перед именем в объявлении функции/класса, т.е. при компиляции dll и при компиляции клиента (exe) меняется только хэдер. Для этого пишется простой макрос, который определяется перед импортом (#import) хэдера в dll. Клиентский код не меняется (только настройки проекта). Новый вид такой:


//------------------
// Header.h
//------------------
#ifdef DLL_EXPORT
# define DLL_API _declspec(dllexport)
#else
# define DLL_API _declspec(dllimport)
#endif

class DLL_API SomeType {

int i;
public:
SomeType();
SomeType& incr(int value = 1);
int getI();
};

И его реализация:
//--------------------------
// Implementation.cpp
//--------------------------
/*
Определяем макрос, чтобы при компиляции dll
спецификатор стал dllexport, на клиенте же
макрос не определяется, потому будет dllimport
*/
#define DLL_EXPORT
#include "Header.h"

SomeType::SomeType()
: i(0)
{
}
SomeType& SomeType::incr(int value/* = 1*/) {
i+=value;
return *this;
}
int SomeType::getI() {
return i;
}


ЗюЫю

Загрузка функции из dll динамически, средствами Win32 API:



//--------------
// header.h
//--------------
#ifdef BUILD_DLL
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif

int DLL_EXPORT Foo(int a, int b);

//-----------------
// implementation.cpp
//-----------------
#include "header.h"

int DLL_EXPORT Foo(int a, int b) {
return a * b;
}

Клиентский код:



//-----------------
// client.cpp
//-----------------
#include "header.h"
#include <iostream>
#include <windows.h>
using std::cout;

int main() {
// Определяем соответствующий указатель на ф-ю
typedef int (*Multiplies)(int x, int y);
Multiplies mm;

// Средствами WinAPI загружаем библиотеку, извлекаем
// адрес функции в указатель
HINSTANCE h = 0;
h = LoadLibrary("libfile.dll");
mm = reinterpret_cast<int(*)(int, int)>(GetProcAddress(h, "Foo"));

// ...
// PROFIT!!1
cout << mm(3,4);
}
/*****************\
|* Output: 12 *|
\*****************/

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

  1. Спасибо за статью. Грамотно все расписано :)

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