Розробка DLL в середовищі Borland Delphi, Різне, Програмування, статті

Якщо ваш комп’ютер працює під управлінням операційної системи windows, То ви не можете не знати про існування динамічних під’єднуваних бібліотек (dynamic link libraries – dll). Досить поглянути на список файлів, розташованих в системному каталозі windows – часом кількість використовуваних операційною системою динамічних бібліотек досягає декількох сотень. dll є невід’ємною частиною функціонування операційних систем сімейства microsoft windows. Однак для вас може бути неочевидна необхідність використання динамічних бібліотек при розробці додатків. В рамках цієї статті ми поговоримо про принципи функціонування dll і їх використання в процесі створення ваших власних програм.

Для початку давайте з’ясуємо, що собою представляє динамічна підключається бібліотека. Отже, dll – це один або кілька логічно закінчених фрагментів коду, які зберігаються у файлі з расшіреніем.dll. Цей код може бути запущений на виконання в процесі функціонування будь-якої іншої програми (такі програми називаються викликають по відношенню до бібліотеки), але сама dll не є запускаються файлом.

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

Отже, процедури та функції, що містяться у динамічній бібліотеці, можна розділити на два типи: ті, які можуть бути викликані з інших додатків. Розглянемо наступний приклад:

screen.cursors[mycursor] := loadcursor(hinstance, mycursor);

loadcursor – функція windows api, яка викликається додатком з динамічної бібліотеки user 32.dll. До речі, прикладом збережених у динамічній бібліотеці ресурсів можуть бути такі стандартні діалоги windows, як діалог відкриття файлу, діалог друку або настройки принтера. Ці діалоги знаходяться у файлі comctl32.dll. Однак багато прикладні розробники використовують функції виклику форм цих діалогів, абсолютно не замислюючись, де зберігається їх опис.

Другий тип процедур – це ті, які використовуються тільки всередині самого файлу бібліотеки.

Аргументи на користь використання dll

Отже, перш ніж перейти до обговорення структури динамічних бібліотек, необхідно поговорити про ті переваги, які надає їх використання розробнику. По-перше, це повторне використання коду. Думаю, немає необхідності пояснювати зручність використання один раз розроблених процедур і функцій при створенні декількох додатків? Крім того, в подальшому ви зможете продати деякі зі своїх бібліотек, не розкриваючи вихідних кодів. А чому тоді це краще компонентів, запитаєте ви? А тим, що функції, які зберігаються в бібліотеці, можуть бути викликані на виконання з додатків, розроблених не на object pascal, а, наприклад, з використанням c + + builder, visual basic, visual c + + і т.д. Такий підхід накладає деякі обмеження на принцип розробки бібліотеки, але це можливо. Звучить заманливо? Мені здається, навіть дуже. Але це ще не все.

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

По-третє, слід поговорити ось про що. Всього кілька років тому при розробці програмного забезпечення ви могли абсолютно не хвилюватися щодо розповсюдження ваших продуктів де-небудь, крім вашої країни. Я хочу сказати, що проблема перекладу на інші мови тексту на елементах управління (пункти меню, кнопки, що випадають списки, підказки), повідомлень про помилки і т.д. не стояла так гостро, як зараз. Однак, з повсюдним впровадженням інтернету у вас з’явилася можливість швидкої передачі готових програмних продуктів практично в будь-яку точку світу. І що будуть робити з вашою програмою де-небудь в Об’єднаних Арабських Еміратах, якщо крім як по-російськи, вона з користувачем спілкуватися не вміє? Ви самі можете оцінити цей ефект, якщо хоч раз на екрані вашого комп’ютера замість з дитинства знайомого російської мови з’являється “арабська в’язь” (наприклад, через “збою” шрифтів). Отже, вже зараз ви повинні планувати можливість розповсюдження ваших програм в інших країнах (якщо, звичайно, у вас є бажання отримати якомога більше прибутку). Відповідно, постає питання швидкого перекладу інтерфейсу вашої програми на інші мови. Одним із шляхів може бути створення ресурсів інтерфейсів усередині dll. Наприклад, можна створити один додаток, що у залежності від версії динамічної бібліотеки буде виводити повідомлення на різних мовах.

Природно, вище наведені лише деякі з аргументів на користь використання динамічно підключаються бібліотек при розробці додатків. Отже, сподіваюся, ви все ще зацікавлені в тому, щоб дізнатися, як, власне, dll створюються. Якщо так, то вперед.

Основи розробки dll

Розробка динамічних бібліотек не є якимсь надскладний процес, доступний лише обраним. Якщо ви досить добре знайомі з розробкою додатків на object pascal, то вам не складе особливих зусиль навчитися працювати з механізмом dll. Отже, розглянемо ті особливості створення dll, які вам необхідно знати, а в завершенні статті розробимо свою власну бібліотеку.

Як і будь-який інший модуль, модуль динамічної бібліотеки має фіксований формат. Погляньте на лістинг, представлений нижче.

library myfirstdll;
uses
sysutils,
classes,
forms,
windows;
procedure helloworld(aform : tform);
begin
messagebox(aform.handle, hello world!,
dll message box, mb_ok or mb_iconexclamation);
end;
exports
helloworld;
begin
end.

Перше, на що слід звернути увагу, це ключове слово library, що знаходиться вгорі сторінки. library визначає цей модуль як модуль бібліотеки dll. Далі йде назва бібліотеки. В нашому прикладі ми маємо справу з динамічною бібліотекою, яка містить єдину процедуру: helloworld. Причому зверніть увагу, що дана процедура за структурою нічим не відрізняється від тих, які ви ставите в модулі своїх додатків. Ключове слово exports сигналізує компілятору про те, що перераховані нижче функції та / або процедури повинні бути доступні з викликають додатків (тобто вони як би “експортуються” з бібліотеки). Детальніше про механізм експорту ми поговоримо трохи пізніше.

І, нарешті, в кінці модуля можна побачити ключові слова begin і end. Всередині цього блоку ви можете помістити код, який повинен виконуватися в процесі завантаження бібліотеки. Досить часто цей блок залишається порожнім.

Як вже говорилося вище, всі процедури і функції, що поміщаються в dll, можуть бути розділені на дві групи: що експортуються (викликаються з інших додатків) і локальні. Природно, всередині бібліотеки також можуть бути описані класи, які в свою чергу містять методи, але в рамках даної статті я не буду на цьому зупинятися.

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

На додаток до процедур і функцій, dll може містити глобальні дані, доступ до яких дозволений для всіх процедур і функцій в бібліотеці. Для 16-бітних додатків ці дані існували в єдиному примірнику незалежно від кількості завантажених в оперативну пам’ять програм, які використовують поточну бібліотеку. Іншими словами, якщо одна програма змінює значення глобальної змінної a на 100, то для всіх інших додатків a буде значення 100. Для 32-бітних додатків це не так. Тепер для кожного додатка створюється окрема копія глобальної області даних.

Експорт функцій з dll

Як вже говорилося вище, для експорту процедур і функцій з dll, необхідно використовувати ключове слово export. Ще раз зверніть увагу на представлений вище лістинг бібліотеки mifirstdll. Оскільки процедура helloworld визначена як експортована, то вона може бути викликана на виконання з інших бібліотек або додатків. Існують наступні способи експорту процедур і функцій: експорт по імені і експорт за порядковим номером.

Найбільш поширений спосіб експорту – по імені. Погляньмо на наведений нижче текст:

exports
sayhello,
dosomething,
dosomethingreallycool;

Слід звернути увагу на те, що delphi автоматично призначає порядковий номер кожної експортованої функції (процедури) незалежно від того, визначаєте ви його явно чи ні. Явна визначення індексу дозволяє вам особисто керувати порядковим номером експортується функції або процедури.

Для того, щоб визначити чи виконується ваш кодек в dll або в зухвалій додатку, можна скористатися глобальної змінної islibrary. Вона приймає значення true в тому випадку, якщо код викликається з бібліотеки і false у разі виконання процедури або функції з викликає програми.

Крім цього, в поставку delphi входить дуже корисна утиліта tdump, яка надає дані про те, яка інформація експортується з вказаної dll.

Використання dllproc

Вище я вже говорив про те, що код ініціалізації динамічної бібліотеки може бути поміщений в блок beginend. Однак крім цього найчастіше необхідно передбачити деякі дії, що виконуються в процесі вивантаження dll з оперативної пам’яті. На відміну від інших типів модулів, модуль dll не має ні секції initialization, ні секції finalization. Наприклад, ви можете динамічно виділити пам’ять в головному блоці, однак не зрозуміло, де ця пам’ять повинна бути звільнена. Для вирішення цієї проблеми існує dllproc – спеціальна процедура, що викликається в певні моменти функціонування dll.

Для початку слід сказати про саму причини існування dllproc. Динамічна бібліотека отримує повідомлення від windows в моменти свого завантаження і вивантаження з оперативної пам’яті, а також у тих випадках, коли-небудь черговий процес, що використовує функції та / або ресурси, що зберігаються в бібліотеці, завантажується в пам’ять. Така ситуація можливо в тому випадку, коли бібліотека необхідна для функціонування декількох додатків. А для того, щоб ви мали можливість вказувати, що саме має відбуватися в такі моменти, необхідно описати спеціальну процедуру, яка і буде відповідальна за такі дії. Наприклад, вона може виглядати наступним чином:

procedure myfirstdllproc(reason: integer);
begin
if reason = dll_process_detach then
{dll is unloading. cleanup code here.}
end;

Однак системі зовсім не очевидно, що саме процедура myfirstdllproc відповідальна за обробку розглянутих вище ситуацій. Тому ви повинні поставити у відповідність адресу нашої процедури глобальної змінної dllproc. Це необхідно зробити в блоці beginend приблизно так:

begin
dllproc := @mydllproc; {Небудь ще, що повинно виконуватися в процесі ініціалізації бібліотеки}
end.

Нижче представлений код, що демонструє один з можливих варіантів застосування dllproc.

library myfirstdll;
uses
sysutils,
classes,
forms,
windows;
var
somebuffer : pointer;
procedure myfirstdllproc(reason: integer);
begin
if reason = dll_process_detach then {Dll is вивантажується з пам’яті. Звільняємо пам’ять, виділену під буфер.}
freemem(somebuffer);
end;
procedure helloworld(aform : tform);
begin
messagebox(aform.handle, hello world!,
dll message box, mb_ok or mb_iconexclamation);
end;
{Небудь код, в якому використовується somebuffer.}
exports
helloworld;
begin {Ставимо у відповідність змінної dllproc адресу нашої процедури.}
dllproc := @myfirstdllproc;
somebuffer := allocmem(1024);
end.

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

dll_process_detach – бібліотека вивантажується з пам’яті; використовується один раз;

dll_thread_attach – в оперативну пам’ять завантажується новий процес, що використовує ресурси та / або код з даної бібліотеки;

dll_thread_detach – один з процесів, які використовують бібліотеку, “вивантажується” з пам’яті.

Завантаження dll

Перш ніж почати використання будь-якої процедури або функції, що знаходиться у динамічній бібліотеці, вам необхідно завантажити dll в оперативну пам’ять. Завантаження бібліотеки може бути здійснена одним з двох способів: статична завантаження і динамічне завантаження. Обидва методи мають як переваги, так і недоліки.

Статична завантаження означає, що динамічна бібліотека завантажується автоматично при запуску на виконання використовує її застосування. Для того щоб використовувати такий спосіб завантаження, вам необхідно скористатися ключовим словом external при описі експортованої з динамічної бібліотеки функції або процедури. dll автоматично завантажується при старті програми, і Ви зможете використовувати будь-які експортуються з неї підпрограми точно так само, як якби вони були описані всередині модулів програми. Це найбільш легкий спосіб використання коду, вміщеного в dll. Недолік методу полягає в тому, що якщо файл бібліотеки, на який є посилання в додатку, відсутній, програма відмовиться завантажуватися.

Сенс динамічного методу полягає в тому, що ви завантажуєте бібліотеку не при старті програми, а в той момент, коли вам це дійсно необхідно. Самі посудіть, адже якщо функція, описана в динамічної бібліотеці, використовується тільки при 10% запусків програми, то абсолютно немає сенсу використовувати статичний метод завантаження. Вивантаження бібліотеки з пам’яті в даному випадку також здійснюється під вашим контролем. Ще одне переваги такого способу завантаження dll – це зменшення (зі зрозумілих причин) часу старту вашого застосування. А які ж у цього способу є недоліки? Основний, як мені здається, – це те, що використання даного методу є більш важким, ніж розглянута вище статичне завантаження. Спочатку вам необхідно скористатися функцією windows api loadlibrary. Для отримання вказівника на експортованої процедури або функції повинна використовуватися функція getprocaddress. Після завершення використання бібліотеки dll повинна бути вивантажено із застосуванням freelibrary.

Виклик процедур і функцій, завантажених з dll


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

Виклик функцій і процедур з статично завантажених dll досить простий. Спочатку в додатку має міститися опис експортується функції (процедури). Після цього ви можете їх використовувати точно так само, як якби вони були описані в одному з модулів вашого застосування. Для імпорту функції або процедури, що міститься в dll, необхідно використовувати модифікатор external в їх оголошенні. Приміром, для розглянутої нами вище процедури helloworld в зухвалій додатку повинна бути поміщена наступний рядок:

procedure sayhello(aform : tform); external myfirstdll.dll;

Ключове слово external повідомляє компілятору, що дана процедура може бути знайдена у динамічній бібліотеці (в нашому випадку – myfirstdll.dll). Далі виклик цієї процедури виглядає наступним чином:


helloworld(self);

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

Імпорт з dll може проводитися на ім’я процедури (функції), порядковому номеру або з присвоєнням іншого імені.

У першому випадку ви просто повідомляєте ім’я процедури і бібліотеку, з якої її імпортуєте (ми це розглянули трохи вище). Імпорт за порядковим номером вимагає від вас вказівку цього самого номера:

procedure helloworld(aform : tform);
external myfirstdll.dll index 15;

В цьому випадку ім’я, яке ви даєте процедурою при імпорті не обов’язково має збігатися з тим, яке було визначено для неї в самій dll. Тобто наведена вище запис означає, що ви імпортуєте з динамічної бібліотеки myfirstdll.dll процедуру, яка в ній експортувалася п’ятнадцятої, і при цьому в рамках вашого застосування цієї процедури дається ім’я sayhello.

Якщо ви з якихось причин не застосовуєте описаний вище спосіб імпорту, але тим не менш хочете змінити ім’я імпортованої функції (процедури), то можна скористатися третім методом:

procedure coolprocedure;
external myfirstdll.dll name dosomethingreallycool;

Тут імпортованої процедурі coolprocedure дається ім’я dosomethingreallycool. Виклик процедур і функцій, що імпортуються з динамічно завантажуваних бібліотек трохи більш складний, ніж розглянутий нами вище спосіб. В даному випадку потрібно оголосити покажчик на функцію або процедуру, яку ви збираєтеся використовувати. Пам’ятайте процедуру helloworld? Давайте подивимося, що необхідно зробити для того, щоб викликати її на виконання у разі динамічної завантаження dll. По-перше, вам необхідно оголосити тип, який описував би цю процедуру:

type
thelloworld = procedure(aform : tform);

Тепер ви повинні завантажити динамічну бібліотеку, за допомогою getprocaddress отримати покажчик на процедуру, викликати цю процедуру на виконання, і, нарешті, вивантажити dll з пам’яті. Нижче наведено код, демонструє, як це можна зробити:

var
dllinstance : thandle;
helloworld : thelloworld;
begin {Завантажуємо dll}
dllinstance := loadlibrary(myfirstdll.dll); {Отримуємо покажчик}
@helloworld := getprocaddress(dllinstance, helloworld); {Викликаємо процедуру на виконання}
helloworld(self); {Вивантажуємо dll з оперативної пам’яті}
freelibrary(dllinstance);
end;

Як вже говорилося вище, одним з недоліків статичної завантаження dll є неможливість продовження роботи програми при відсутності однієї чи кількох бібліотек. У випадку з динамічної завантаженням у вас з’являється можливість програмно обробляти такі ситуації і не допускати, щоб програма “вивалювалася” самостійно. За повертається функціями loadlibrary і getprocaddress значенням можна визначити, чи успішно пройшла завантаження бібліотеки та знайдено в ній необхідна додатком процедура. Наведений нижче код демонструє це.

procedure tform1.dynamicloadbtnclick(sender: tobject);
type
thelloworld = procedure(aform : tform);
var
dllinstance : thandle;
helloworld : thelloworld;
begin
dllinstance := loadlibrary(myfirstdll.dll);
if dllinstance = 0 then begin messagedlg (Неможливо завантажити dll, mterror, [mbok], 0);
exit;
end;
@helloworld := getprocaddress(dllinstance, helloworld);
if @helloworld <> nil then
helloworld (self)
else messagedlg (Не знайдена шукана процедура!., mterror, [mbok], 0);
freelibrary(dllinstance);
end;

У dll можна зберігати не тільки код, але й форми. Причому створення і приміщення форм у динамічну бібліотеку не дуже сильно відрізняється від роботи з формами в звичайному проекті. Спочатку ми розглянемо, яким чином можна написати бібліотеку, яка містить форми, а потім ми поговоримо про використання технології mdi в dll.

Розробку dll, що містить форму, я продемонструю на прикладі.

Отже, по-перше, створимо новий проект динамічної бібліотеки. Для цього виберемо пункт меню file / new, а потім двічі клацнемо на іконку dll. Після цього ви побачите приблизно наступний код:

library project2; {Тут були коментарі}
uses
sysutils,
classes;
{$r *.res}
begin
end.

Збережіть отриманий проект. Назвемо його dllforms.dpr.

Тепер слід створити нову форму. Це можна зробити по-різному. Наприклад, вибравши пункт меню file / new form. Додайте на форму якісь компоненти. Назвемо форму dllform і збережемо вийшов модуль під ім’ям dllformunit.pas.

Повернемося до головного модулю проекту і помістимо в нього функцію showform, У завдання якої входитиме створення форми та її виведення на екран. Використовуйте для цього наведений нижче код.

function showform : integer; stdcall;
var
form : tdllform;
begin
form := tdllform.create(application);
result := form.showmodal;
form.free;
end;

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

Експортуємо нашу функцію з використанням ключового слова exports:

exports
showform;

Компілюємо проект і отримуємо файл dllforms.dll. Ці прості кроки – все, що необхідно зробити для створення динамічної бібліотеки, яка містить форму. Зверніть увагу, що функція showform оголошена з використанням ключового слова stdcall. Воно сигналізує компілятору використовувати при експорті функції угоду за стандартним викликом (standard call calling convention). Експорт функції таким чином створює можливість використання розробленої dll не тільки в додатках, створених в delphi.

Угода за викликом (calling conventions) визначає, яким чином передаються аргументи при виконанні функції. Існує п’ять основних угод: stdcall, cdecl, pascal, register і safecall. Детальніше про це можна дізнатися, подивившись розділ “calling conventions” в файлі допомоги delphi.

Також зверніть увагу, що значення, що повертається функцією showform, Відповідає значенню showmodal. Таким чином ви можете передавати деяку інформацію про стан форми зухвалому додатком.

Нижче представлено два лістингу, перший з яких містить повний код файлу проекту dll (модуль з формою тут не наводиться), а другий – модуль викликає додатки, в якому використовується тільки що розроблена нами бібліотека.

library dllforms;
uses
sysutils,
classes,
forms,
dllformunit in dllformunit.pas {dllform};
{$r *.res}
function showform : integer; stdcall;
var
form : tdllform;
begin
form := tdllform.create(application);
result := form.showmodal;
form.free;
end;
begin
end.
unit testappunit;
interface
uses
windows, messages, sysutils, classes, graphics,
controls, forms, dialogs, stdctrls;
type
tform1 = class(tform)
button1: tbutton;
procedure button1click(sender: tobject);
private
{ private declarations }
public
{ public declarations }
end;
var
form1: tform1;
function showform : integer; stdcall;
external dllforms.dll;
implementation
{$r *.dfm}
procedure tform1.button1click(sender: tobject);
begin
showform;
end;
end.

Прошу зауважити, що при експорті функції також було використано ключове слово stdcall.

Слід звернути особливу увагу на роботу з дочірніми формами в dll. Якщо, приміром, в зухвалій додатку головна форма має значення властивості formstyle, рівним mdiform, То при спробі дзвінка зі dll mdichild-форми, на екрані з’явиться повідомлення про помилку, у якому буде говоритися, що немає жодної активної mdi-форми.

В той момент, коли ви намагаєтеся показати ваше дочірнє вікно, vcl перевіряє коректність властивості formstyle головної форми програми. Проте в нашому випадку все начебто правильно. Так у чому ж справа? Проблема в тому, що при проведенні такої перевірки, розглядається об’єкт application, що належить не викликає додатком, а власне динамічної бібліотеці. Ну, і природно, оскільки в dll немає головної форми, перевірка видає помилку. Для того щоб уникнути такої ситуації, треба призначити об’єкту application динамічної бібліотеки об’єкт application викликає додатки. Природно, це запрацює тільки в тому випадку, коли викликає програма – vcl-додаток. Крім того, перед вивантаженням бібліотеки з пам’яті необхідно повернути значення об’єкта application бібліотеки в первинний стан. Це дозволить менеджеру пам’яті очистити оперативну пам’ять, займану бібліотекою. Отже, вам потрібно зберегти покажчик на “рідний” для бібліотеки об’єкт application в глобальної змінної, яка може бути використана при відновленні його значення.

Отже, повернемося трохи назад і перерахуємо кроки, необхідні нам для роботи з поміщеним в dll mdichild-формами.


  1. В динамічної бібліотеці створюємо глобальну змінну типу tapplication.
  2. Зберігаємо покажчик на об’єкт application dll в глобальної змінної.
  3. Об’єкту application динамічної бібліотеки ставимо у відповідність вказівник на application викликає додатки.
  4. Створюємо mdichild-форму і працюємо з нею.
  5. Повертаємо в первісний стан значення об’єкта application динамічної бібліотеки та вивантажуємо dll з пам’яті.

Перший крок простий. Просто поміщаємо наступний код у верхній частині модуля dll:

var
dllapp : tapplication;

Потім створюємо процедуру, яка буде змінювати значення об’єкта application і створювати дочірню форму. Процедура може виглядати приблизно так:

procedure showmdichild(mainapp : tapplication);
var
child : tmdichild;
begin
if not assigned(dllapp) then begin
dllapp := application;
application := mainapp;
end;
child := tmdichild.create(application.mainform);
child.show;
end;

Все, що нам тепер необхідно зробити, – це передбачити повернення значення об’єкта application в початковий стан. Робимо це за допомогою процедури mydllproc:

procedure mydllproc(reason: integer);
begin
if reason = dll_process_detach then {Dll is вивантажується. Відновлюємо значення покажчика application}
if assigned(dllapp) then
application := dllapp;
end;

Замість висновку

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

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*