вівторок, 31 травня 2016 р.

Створення та використання C++ бібліотек

Розробляючи програми на C++ ми маємо можливість повторно використовувати уже написаний код організований у бібліотеки. Бібліотека — це уже готовий, скомпільовай, об’єктний код, який можна підключати до своєї програми. Бібліотеки бувають статичними та динамічними.

Зміст

1. Статичні бібліотеки

Статична бібліотека, на етапі компіляції вбудовується у виконуваний файл. Схематично це можна зобразити таким малюнком:

При такому підході, кожна наша програма, яка використовує статичну бібліотеку, має свою, особисту її копію.

Догори

2. Динамічні бібліотеки

При динамічній лінковці у виконуваний файл вбудовується не вся бібліотека, а лише таблиця посилань цієї бібліотеки. Схематично це зображається наступним малюнком:

Більш детально про бібліотеки можна прочитати тут чи тут. Ми ж, сконцентруємося більше на практиці і послідовно розглянемо сам процес створення та використання бібліотек.

Догори

3. Проект calc

Давайте, створимо невеликий проект на прикладі якого будемо описувати процес. Це буде простий консольний калькулятор, який буде вміти додавати, віднімати, множити та ділити два числа. Числа і операції будемо передавати параметрами командної стрічки. При цьому, реалізацію самих операцій, у вигляді методів, ми винесемо у бібліотеку, а в основній програмі будемо використовувати ці операції. Отже, почнемо з написання коду бібліотеки.

Догори

4. Створення бібліотеки

Створимо два файла calc_library.h:

і lib.cpp:

Догори

4.1. Створення статичної бібліотеки

Тепер, маючи код нашої бібліотеки, ми можемо скомпілювати його і почнемо ми зі статичної бібліотеки.

Для того, щоб отримати статичну бібліотеку послідовно виконаємо наступні команди:

$ g++ --std=c++11 -Wall -c calc_library.cpp -o calc_library_static.o
$ ar rcs libcalc_library.a calc_library_static.o
g++ --std=c++11 -Wall -c calc_library.cpp -o calc_library_static.o
ar rcs libcalc_library.a calc_library_static.o

Перша команда створює об’єктний файл, а друга — запаковує цей файл у статичну бібліотеку. По своїй природі статична бібліотека є звичайним архівом, тому ми використовуємо команду архівації ar. Власне, саме тому розширення статичної бібліотеки .a походить від слова archive (архів).

В результаті, ми отримаємо готову до використання статичну бібліотеку libcalc_library.a.

Зверніть увагу на префікс lib у назві бібліотеки libcalc_library.a, який є обов’язковим.

Догори

4.2. Створення динамічної бібліотеки

Тепер створимо динамічну бібліотеку. Для цього послідовно виконаємо команди:

$ g++ --std=c++11 -Wall -fpic -c calc_library.cpp -o calc_library_dynamic.o
$ g++ -shared calc_library_dynamic.o -o libcalc_library.so
g++ --std=c++11 -Wall -c calc_library.cpp -o calc_library_dynamic.o
g++ -shared calc_library_dynamic.o -o libcalc_library.dll

Як бачите, уся відмінність у командах це розширення результуючого файла. Для Windows динамічна бібліотека має розширення .dll (dynamic linked library — динамічно з’єднувана бібліотека), а для *NIX подібних систем динамічна бібліотека має розширення .so (shared object — розділюваний об’єкт).

Зверніть увагу на префікс lib у назві бібліотеки libcalc_library.so. Для *NIX подібних систем він є обов’язковим.

Догори

5. Використання бібліотеки

Для того щоб скористатися щойно створеними бібліотеками, ми напишемо програму калькулятор у файлі main.cpp. Код програми наступний:

Зверніть увагу, що тут ми нігде не оголошуємо і не описуємо методи add(), minus(), div() і mult(). Тому, якщо ми попробуємо скомпілювати цю програму уже відомою нам командою:

$ g++ --std=c++11 -Wall min.cpp -o calc

то компілятор видасть помилку, оскільки він не знайде цих методів. Ми повинні явно вказати компілятору бібліотеку в якій знаходиться реалізація цих методів.

Догори

5.1. Використання статичної бібліотеки

Для того щоб скомпілювати нашу програму, потрібно використати додаткові ключі команди компіляції, а саме:

$ g++ --std=c++11 -Wall -L . -static main.cpp -l calc_library -o calc_static
g++ --std=c++11 -Wall -L . -static main.cpp -l calc_library -o calc_static.exe
  • Ключом -L ми задаємо шлях де компілятор повинен шукати нашу бібліотеку. Оскільки, ми вказали відносний шлях ., то компілятор буде шукати бібліотеку у поточному каталозі;
  • Ключ -l (ел маленьке) вказує компілятору, яку бібліотеку потрібно підключити. Зверніть увагу, що ми опустили префікс lib перед назвою бібліотеки;
  • Ключ -static вказує, що компіляція буде статичною.

Слід зауважити, що і після ключа -L, і після ключа -l (ел маленьке) можна опустити пропуск, це ніяк не вплине на компіляцію. Тобто, ми з успіхом можемо запустити таку команду:

g++ --std=c++11 -Wall -L. -static main.cpp -lcalc_library -o calc_static

Також, варто сказати, що, якщо нам потрібно зазначити декілька шляхів чи бібліотек, то на кожен шлях чи бібліотеку потрібен свій ключ -L, чи -l (ел маленьке), відповідно.

Нарешті, ми можемо запустити програму і подивитися на її результати:

$ ./calc_static 10 + 20
$ ./calc_static 10 - 20
$ ./calc_static 10 \* 20
$ ./calc_static 10 / 20
calc_static 10 + 20
calc_static 10 - 20
calc_static 10 * 20 — не працює
calc_static 10 / 20

Зверніть увагу на операцію множення. Оскільки, знак * є спеціальним символом командної стрічки, то, для того щоб використати цей знак як параметр нашої програми, ми повинні екранувати цей символ. Для екранування в *NIX подібних системах використовується символ \. А от для Windows екранувати * не получилося.

Догори

5.1. Використання динамічної бібліотеки

Для того, щоб скомпілювати нашу програму з використанням динамічної бібліотеки, потрібно запустити таку ж команду як і у випадку статичної бібліотеки тільки без ключа -static, тобто:

$ g++ --std=c++11 -Wall -L./ main.cpp -lcalc_library -o calc_dynamic
g++ --std=c++11 -Wall -L./ main.cpp -lcalc_library -o calc_dynamic.exe

В результаті, ми отримаємо виконуваний файл. Запустимо його на виконання:

$ ./calc_dynamic 10 + 20
calc_dynamic 10 + 20

У *NIX подібній системі програма не відпрацює, а видасть помилку. Це тому, що наша програма не знає де шукати бібліотеку libcalc_library.so. Кожна динамічно скомпільована програма шукає всі бібліотеки від якої вона залежить за шляхами прописаними у змінній оточення LD_LIBRARY_PATH. Таким чином, нам потрібно додати до цієї змінної оточення шлях до каталога в якому знаходиться наша бібліотека. Робиться це за допомогою наступної команди:

export LD_LIBRARY_PATH=”$LD_LIBRARY_PATH:./”

Тільки тепер програма запуститься без помилки.

Тепер, коли ми маємо дві програми, статичної і динамічної лінковки, зверніть увагу на їх розмір. Програма скомпільована як статична набагато більша у розмірі за динамічно скомпільовану програму. Це тому, що при статичній лінковці бібліотека вбудовується у виконуваний файл, а при динамічній лінковці — цього не відбувається.

Догори

6. Автоматизація процесу компіляції

Щоб автоматизувати процес компіляції, ми напишемо Makefile. В цьому Makefile’і ми опишемо 6 цілей. Коротко розглянемо кожну з них:

  1. Ціль static_library — має залежність від файла calc_library.cpp і створює статичну бібліотеку libcalc_library.a;
  2. Ціль dynamic_library — має залежність від файла calc_library.cpp і створює динамічну бібліотеку libcalc_library.so чи calc_library.dll, залежно від системи;
  3. Ціль static — залежить від статичної бібліотеки libcalc_library.a і створює статично злінкований виконуваний файл;
  4. Ціль dynamic — залежить від динамічної бібліотеки libcalc_library.so і створює динамічно злінкований виконуваний файл;
  5. Ціль all — створює і статичний, і динамічний виконувані файли.
  6. Ціль clean — видаляє усі скомпільовані файли.

І, власне, сам Makefile:

Зверніть увагу, що в цьому Makefile’і ми задали параметр PARAMS. Параметри спрощують написання та підтримку Makefile’ів.

Тепер, ми можемо компілювати нашу програму одною командою:

$ make
mingw32-make

Вище ми описали, що, в *NIX подібних системах, для запуску динамічно злінкованої програми calc_dynamic, нам необхідно встановити змінну оточення LD_LIBRARY_PATH. Описаний спосіб встановлення цієї змінної командою export розповсюджується лише на активний сеанс терміналу. Це означає, що після того, як ми закриємо поточний термінал ця змінна оточення пропаде. Таким чином, кожен раз, коли ми запускаємо нову консоль нам необхідно виконувати export. Ми можемо в певній мірі спростити і цей процес, створивши файл setup_env.txt такого вмісту:

Тепер, для того щоб встановити змінну оточення ми виконуємо просту команду:

source setup_env.txt

Зрозуміло, що для однієї змінної оточення, як у нашому випадку, від такого спрощення мало користі. Але, якщо налаштування середовища проекту більш складніші, то такий підхід дуже зручний.

Догори

7. Рефакторинг проекту

Ми завжди стараємося огранізовувати файлову структуру проекту таким чином, щоб усі файли не були в одному каталозі. Наприклад, файли заголовків, ми розміщуємо у каталозі inc (inclide), файли з кодом — у каталозі src (source), тимчасові, проміжні, об’єктні файли — у папці obj (object), скомпільовані файли бібліотек — у папці lib, а результуючі, виконувальні файли — у папці bin (binary). Отже, займемося цим.

Створимо ці папки у каталозі проекту і перенесемо відповідні файли у призначені для них місця. Файлова структура проекту набуде такого вигляду:

Гарно, акуратно.

При таких змінах наш Makefile став неробочим і потребує внесення деяких змін. Зміни, в основному, пов’язані із шляхами до файлів. Також, в деяких командах потрібно явно вказати компілятору шлях де містяться файли заголовків. Робиться це за допомогою ключа -I. Ми не робили цього дотепер, оскільки наш файл заголовку calc_library.h розміщувався в одній директорії із файлом реалізації calc_library.cpp. Відредагуємо Makefile наступним чином:

Зверніть увагу, що в параметрі PROJECT_PATH обов’язково потрібно вказувати абсолютний шлях, а не відносний.

Успіхів!

Догори

Немає коментарів:

Дописати коментар