C++ plugin infrastructure and type safety
Jan. 7th, 2012 04:43 pmПро що можуть думати люди напередодні Нового Року? Про випивки, про закуски, про подарунки і ялинки. Про людей в кінці кінців. Я ж, як відомо, схибнутий на строгій типізації, думав про плагіни в C++ і безпеку їх використання з точки зору типів. І надумав страшну річ: C, на відміну від C++, повністю втрачає інформацію про тип функції на етапі компіляції. Але про все по порядку.
Про те як робляться плагіни у C я говорити не буду, бо це мова програмування для людей міцних духом (не плутати з міцним перегаром). Але у реалізації C-шних і C++-них плагінів є спільна риса - використання функцій dlopen/dlsym/etc. Ці функції надають можливість керувати динамічним завантажувачем, завантажувати динамічні бібліотеки в runtime і імпортувати із них функції. Так, саме функції і тільки функції. Так як libdl це C-шна бібліотека, то ні про які класи і об’єкти вона нічого не знає. Але із цієї ситуації є кілька виходів.
Перший, найпростіший вихід, зустрічається доволі часто і використовується у Stargazer. Колись давно його показав мені Борис і я прийняв його як даність, навіть не намагаючись шукати альтернатив. Суть методу полягає у наданні плагіном функції що виконує функції (вибачте за тавтологію) фабрики об’єктів. Функція має створювати об’єкт і повертати вказівник на нього, який уже в головній програмі використовується як вказівник на екземпляр плагіну. Говорити про це можна багато, але слова не варті нічого. Тож перейдемо до конкретного прикладу.
plugin.h
У цьому файлі ми визначаємо узагальнений інтерфейс плагіну, через який основна програма буде із ним спілкуватись. У нашому прикладі він примітивний і має лише один метод hello що приймає у якості аргументу рядок. За задумкою, наші плагіни будуть вітати когось або щось різними способами. Я не став робити цей метод константним щоб не обмежувати фантазію уявних розробників плагінів. Також визначаємо віртуальний деструктор щоб коректно знищувати створені нащадки цього вельми абстрактного класу.
Перейдемо до реалізації самого плагіну.
myplugin.cpp
Створюємо нащадка від абстрактного Plugin і реалізовуємо метод hello таким чином щоб виводити привітання у stdout. Поки що нічого незвичного, плюсаті програмісти роблять такі штуки із самого свого народження. Технічно, більш правильним було б винести визначення класу MyPlugin у окремий заголовочний файл myplugin.h, але не будемо "роздувати" наш примітивний код.
Що ж, з класом покінчили. Але це ще не весь код. Треба реалізувати ту саму функцію-фабрику про яку я писав вище. Зверніть увагу, функція getPlugin задекларована як extern "C". Ця магічна фраза говорить компілятору не займатися name mangling, бо тоді нічого не буде працювати. Саме в цьому заключається трюк і саме це є слабким місцем підходу. Але про це пізніше.
Ця функція така ж примітивна як і весь попередній код - вона просто створює екземпляр класу (або об’єкт, це одне і те ж) і повертає вказівник на нього. Навіщо це потрібно? Справа у тому, що основна програма нічого не знає про конкретний тип що використовується у плагіні і не може створити його екземпляр. Компілятор просто не знайде коду необхідного для цього. З іншого боку, плагін містить весь необхідний код для створення об’єкту і проблем з цим немає. Іронія полягає у тому що завдяки використанню віртуального деструктора головна програма може без проблем знищувати об’єкт плагіну, хоча і нічого не знає про його код. Справа тут у тому що правильний деструктор, реалізований в середині плагіну, буде викликаний підстановкою адреси методу із таблиці віртуальних методів що заповнюється самим плагіном. Така от асиметрія: створити не можемо, зате знищуємо без проблем. Тут можна на хвилинку відволіктись і подумати про домінування ентропії і відому народну мудрість "ломать - не строить".
Дехто може задатись питанням: "Як же так, функція задекларована як C, а реалізована на C++ і містить код який ніякого відношення до C не має...". Нічого неймовірного тут немає, extern "C" тільки регулює назву символу функції у об’єктному файлі. Я ще повернусь до цього пізніше. А поки перейдемо до реалізації основної програми.
main.cpp
Перше що кидається в очі - наявність незвичного заголовочного файлу dlfcn.h. У цьому файлі визначені функції dlopen, dlsym, dlclose і набір корисних констант RTLD_* які регулюють поведінку динамічного завантажувача. Я навіть більше скажу, на платформі Linux треба вказувати ключ -ldl компоновщику щоб він підключив бібліотеку libdl.so у якій знаходиться код динамічного завантажувача. У FreeBSD його код міститься у libc і додаткових танців з бубном виконувати не треба. Підозрюю що те саме можна сказати і про Mac OS X. Пояснювати як працювати з функціями dl* не буду, кому цікаво - може звернутись до відповідного man.
Подивимось тепер як усе це працює:
Що ж, виглядає непогано. А тепер подивимось уважно на код. Почнемо з коду основної програми. Перше що має викликати блювотні рефлекси у таких схибнутих на type safety програмістів як я - використання reinterpret_cast. Це самий адський cast із усіх cast'ів C++. Гірше нього тільки C-style-casts, але не набагато. Хоча dlsym повертає void *, static_cast тут безсилий, оскільки він не може приводити типи вказівник-на-фукнцію. Так що тут залишається тільки два варіанти: поганий і ще гірший.
Добре, чим же це може нам загрожувати? Уявімо собі що дехто написав плагін у якому функція getPlugin повертає, скажімо, вказівник на int. Що буде у цьому випадку? За найкращих обставин буде щось таке: "Ба-бах! Хрясь!!! Тр-р-р! Гр-р-р! Segmentation fault". А як же, скажете, getPlugin може повернути вказівник на int? У нього ж чітко прописано return type!
І тут ми переходимо до коду плагіну. Я уже звертав вашу увагу на те що getPlugin задекларовано як extern "C". Так от, завдяки цим магічним словам тип цієї функції може бути будь-яким! Я серйозно! Можна написати "extern "C" void getPlugin() {}", можна написати "extern "C" int getPlugin() { return 13917; }" або навіть так: "extern "C" MyType * getPlugin(const MyAnotherType & args) { return new MyType(args); }". Компілятор все це з’їсть і навіть нічого вам не скаже. Не вірите? Дивіться:
Чули страшний гуркіт? Оце воно і було.
А справа у тому що компілятор C втрачає інформацію про типи функцій під час компіляції. Вдумайтесь у це. Втрачає. Інформацію. Про типи. Ця страшна думка прийшла мені в голову двадцять дев’ятого грудня перед сном. І муляла до тих пір поки тридцятого грудня вранці я не підійшов до ноутбука і не перевірив її.
main.c
foo.c
Як же це виходить? В процесі компіляції компілятор перевіряє типи фукнції спираючись на їх декларацію. У файлі main.c функція foo задекларована як така що приймає на вхід один аргумент типу int і нічого не повертає. У точці виклику цій функції передано в точності один такий аргумент, все добре. Реалізації цієї функції у модулі main.c немає, але це нічого не значить. Реалізація знаходиться в у модулі foo.c. І тут тип функції визначений як такий що приймає на вхід структуру з двома полями типу char. Реалізація функції бере із наданої структури значення полів і виводить їх. Тут теж усе добре і компілятор успішно компілює цей файл. Потім приходить черга компоновщика. Він має підставити замість назв символів функцій що викликаються їх реальні адреси використовуючи таблицю символів. Так от, компілятор C в якості назви символу функції використовує просто її ім’я.
Літерою "T" позначено символ що містить код, літерою "U" позначено невизначений символ. Так і є, код функції main наявний у модулі, на відміну від коду фукнції foo. Символ foo наявний у таблиці бо він використовується, компоновщик для кожного такого символу має знайти у іншому модулі його код. Якщо код не буде знайдено то виникне помилка компоновки: undefined symbol.
У модулі foo.o міститься код функції foo, але немає коду функції printf. Функція printf визначена у стандартній бібліотеці libc яка використовується компоновщиком автоматично.
Компоновщик нічого не знає про типи, він оперує лише іменами символів, тому з його точки зору теж усе правильно:
Із цього можна зробити висновок: наведена вище система плагінів не надає ніяких гарантій з точки зору type safety. Помилки у типах на рівні інтерфейсу плагіну не будуть виявлені і спливуть уже у runtime, лякаючи юзерів страшними наслідками. А помилку такого роду допустити легко: основна програма може змінити API. І плагіни що використовують нове API, і плагіни що використовують старе API (якщо, звісно, не зміниться назва функції-фабрики) будуть успішно завантажені. А потім можна чекати всього що завгодно. Навіть того що старі плагіни продовжуватимуть успішно працювати. Або замість друку чеку на касовому апараті проведуть пуск міжконтинентальних ракет із ядерною БЧ. У нашому світі все можливо.
На відміну від компілатора C, компілятор C++ частково зберігає інформацію про типи після компіляції. Це було зроблено для підтримки ad-hoc поліморфізму, який у світі C++ називається function overloading. C++ допускає (і активно використовує!) існування функцій з однаковими іменами але різними параметрами. Для цього у назві символа кодується не тільки ім’я функції, але і (частково) її тип. Це називається name mangling. Подивимось як це виглядає:
Замість очікуваного символа foo маємо _Z3fooi. Тут треба зауважити що різні компілятори використовують різні схеми name mangling. З іншого боку, більшість компіляторів для Linux намагаються бути сумісними із gcc щоб не доводилось перекомпілювати сотні тисяч бібліотек, тому вони використовують ту-ж схему name mangling що і gcc. Зверніть увагу - для функції main не виконувався name mangling. Це дуже специфічна функція. Спеціально щоб читати такі імена символів є утиліта c++filt:
Що ж, подивимось що буде з функцією foo у модулі foo.c:
Як бачимо, ім’я символу не співпадає із тим що було у main.o. Більш того, c++filt показує нам що у ім’я символу було включено тип аргументу. Щоб було цікавіше, подивиомсь у що перетворюються більш складні конструкції:
Як бачите, адський ад. Тепер перевіримо як буде реагувати компоновщик на такі назви символів.
Як і очікувалось, він не знайшов необхідний символ. Я використовував компоновщик gcc, тому що різниця між ним і тим що використовує g++ для нас несуттєва. Щоб розкрити тему name mangling до кінця я покажу як можна обхитрити і її. Змінимо код у модулі foo.c для більшої відповідності прототипу функціїї із main.c:
Якщо ви не забули правила function overloading то трюк уже має бути зрозумілий.
Справа в тому що у назву символа не потрапляє тип результату функції. Як пам’ятаємо, робити function overloading дозволяється тільки по її аргументам, але не по результату (для overloading по результату можна використати шаблони).
Можна піти й іншим шляхом. Якщо ви звернули увагу, у оригінальному варіанті функції foo тип аргументу був позначений просто за його іменем. Візьмемо оригінальний код модуля foo.c, але змінимо модуль main.c:
Компілятор без проблем створить об’єктний файл з цього модуля, а компоновщик успішно об’єднає його з foo.o утворивши робочу програму:
Щоб побороти проблему типізації, у C-style plugin system часто використовують версіонування API і перевірку версії на етапі завантаження плагіну. Хороша ідея, але такий підхід теж мало що гарантує бо спирається на совість розробників. А, як відомо, совісті у програмістів немає.
На цьому покищо зупинюсь, бо букаф і так вийшло забагато. Вихід із цієї скрутної ситуації опишу у наступному пості.
Продовження
Про те як робляться плагіни у C я говорити не буду, бо це мова програмування для людей міцних духом (не плутати з міцним перегаром). Але у реалізації C-шних і C++-них плагінів є спільна риса - використання функцій dlopen/dlsym/etc. Ці функції надають можливість керувати динамічним завантажувачем, завантажувати динамічні бібліотеки в runtime і імпортувати із них функції. Так, саме функції і тільки функції. Так як libdl це C-шна бібліотека, то ні про які класи і об’єкти вона нічого не знає. Але із цієї ситуації є кілька виходів.
Перший, найпростіший вихід, зустрічається доволі часто і використовується у Stargazer. Колись давно його показав мені Борис і я прийняв його як даність, навіть не намагаючись шукати альтернатив. Суть методу полягає у наданні плагіном функції що виконує функції (вибачте за тавтологію) фабрики об’єктів. Функція має створювати об’єкт і повертати вказівник на нього, який уже в головній програмі використовується як вказівник на екземпляр плагіну. Говорити про це можна багато, але слова не варті нічого. Тож перейдемо до конкретного прикладу.
plugin.h
#ifndef __PLUGIN_H__ #define __PLUGIN_H__ #include <string> class Plugin { public: virtual ~Plugin() {} virtual void hello(const std::string & name) = 0; }; #endif |
| _Winnie C++ Colorizer |
У цьому файлі ми визначаємо узагальнений інтерфейс плагіну, через який основна програма буде із ним спілкуватись. У нашому прикладі він примітивний і має лише один метод hello що приймає у якості аргументу рядок. За задумкою, наші плагіни будуть вітати когось або щось різними способами. Я не став робити цей метод константним щоб не обмежувати фантазію уявних розробників плагінів. Також визначаємо віртуальний деструктор щоб коректно знищувати створені нащадки цього вельми абстрактного класу.
Перейдемо до реалізації самого плагіну.
myplugin.cpp
#include <iostream> #include <string> #include "plugin.h" class MyPlugin : public Plugin { public: void hello(const std::string & name); }; void MyPlugin::hello(const std::string & name) { std::cout << "Hello, " << name << "!" << std::endl; } extern "C" Plugin * getPlugin() { return new MyPlugin; } |
| _Winnie C++ Colorizer |
Створюємо нащадка від абстрактного Plugin і реалізовуємо метод hello таким чином щоб виводити привітання у stdout. Поки що нічого незвичного, плюсаті програмісти роблять такі штуки із самого свого народження. Технічно, більш правильним було б винести визначення класу MyPlugin у окремий заголовочний файл myplugin.h, але не будемо "роздувати" наш примітивний код.
Що ж, з класом покінчили. Але це ще не весь код. Треба реалізувати ту саму функцію-фабрику про яку я писав вище. Зверніть увагу, функція getPlugin задекларована як extern "C". Ця магічна фраза говорить компілятору не займатися name mangling, бо тоді нічого не буде працювати. Саме в цьому заключається трюк і саме це є слабким місцем підходу. Але про це пізніше.
Ця функція така ж примітивна як і весь попередній код - вона просто створює екземпляр класу (або об’єкт, це одне і те ж) і повертає вказівник на нього. Навіщо це потрібно? Справа у тому, що основна програма нічого не знає про конкретний тип що використовується у плагіні і не може створити його екземпляр. Компілятор просто не знайде коду необхідного для цього. З іншого боку, плагін містить весь необхідний код для створення об’єкту і проблем з цим немає. Іронія полягає у тому що завдяки використанню віртуального деструктора головна програма може без проблем знищувати об’єкт плагіну, хоча і нічого не знає про його код. Справа тут у тому що правильний деструктор, реалізований в середині плагіну, буде викликаний підстановкою адреси методу із таблиці віртуальних методів що заповнюється самим плагіном. Така от асиметрія: створити не можемо, зате знищуємо без проблем. Тут можна на хвилинку відволіктись і подумати про домінування ентропії і відому народну мудрість "ломать - не строить".
Дехто може задатись питанням: "Як же так, функція задекларована як C, а реалізована на C++ і містить код який ніякого відношення до C не має...". Нічого неймовірного тут немає, extern "C" тільки регулює назву символу функції у об’єктному файлі. Я ще повернусь до цього пізніше. А поки перейдемо до реалізації основної програми.
main.cpp
#include <dlfcn.h> // dl*, RTLD_* #include <cstdlib> // EXIT_* #include <iostream> #include "plugin.h" typedef Plugin * (* GetPlugin)(); int main(int, char **) { void * handle = dlopen("./myplugin.so", RTLD_NOW); if (!handle) { std::cerr << "Error loading myplugin.so: " << dlerror() << std::endl; return EXIT_FAILURE; } GetPlugin getPlugin = reinterpret_cast<GetPlugin>(dlsym(handle, "getPlugin")); if (!getPlugin) { std::cerr << "Error getting GetPlugin from myplugin.so: " << dlerror() << std::endl; return EXIT_FAILURE; } Plugin * myPlugin = getPlugin(); myPlugin->hello("World"); return EXIT_SUCCESS; } |
| _Winnie C++ Colorizer |
Перше що кидається в очі - наявність незвичного заголовочного файлу dlfcn.h. У цьому файлі визначені функції dlopen, dlsym, dlclose і набір корисних констант RTLD_* які регулюють поведінку динамічного завантажувача. Я навіть більше скажу, на платформі Linux треба вказувати ключ -ldl компоновщику щоб він підключив бібліотеку libdl.so у якій знаходиться код динамічного завантажувача. У FreeBSD його код міститься у libc і додаткових танців з бубном виконувати не треба. Підозрюю що те саме можна сказати і про Mac OS X. Пояснювати як працювати з функціями dl* не буду, кому цікаво - може звернутись до відповідного man.
Подивимось тепер як усе це працює:
$ g++ -W -Wall -Wextra -fPIC -shared myplugin.cpp -o myplugin.so $ g++ -W -Wall -Wextra main.cpp -ldl -o test $ ./test Hello, World!
Що ж, виглядає непогано. А тепер подивимось уважно на код. Почнемо з коду основної програми. Перше що має викликати блювотні рефлекси у таких схибнутих на type safety програмістів як я - використання reinterpret_cast. Це самий адський cast із усіх cast'ів C++. Гірше нього тільки C-style-casts, але не набагато. Хоча dlsym повертає void *, static_cast тут безсилий, оскільки він не може приводити типи вказівник-на-фукнцію. Так що тут залишається тільки два варіанти: поганий і ще гірший.
Добре, чим же це може нам загрожувати? Уявімо собі що дехто написав плагін у якому функція getPlugin повертає, скажімо, вказівник на int. Що буде у цьому випадку? За найкращих обставин буде щось таке: "Ба-бах! Хрясь!!! Тр-р-р! Гр-р-р! Segmentation fault". А як же, скажете, getPlugin може повернути вказівник на int? У нього ж чітко прописано return type!
І тут ми переходимо до коду плагіну. Я уже звертав вашу увагу на те що getPlugin задекларовано як extern "C". Так от, завдяки цим магічним словам тип цієї функції може бути будь-яким! Я серйозно! Можна написати "extern "C" void getPlugin() {}", можна написати "extern "C" int getPlugin() { return 13917; }" або навіть так: "extern "C" MyType * getPlugin(const MyAnotherType & args) { return new MyType(args); }". Компілятор все це з’їсть і навіть нічого вам не скаже. Не вірите? Дивіться:
$ g++ -W -Wall -Wextra -fPIC -shared myplugin.cpp -o myplugin.so $ ./test Segmentation fault
Чули страшний гуркіт? Оце воно і було.
А справа у тому що компілятор C втрачає інформацію про типи функцій під час компіляції. Вдумайтесь у це. Втрачає. Інформацію. Про типи. Ця страшна думка прийшла мені в голову двадцять дев’ятого грудня перед сном. І муляла до тих пір поки тридцятого грудня вранці я не підійшов до ноутбука і не перевірив її.
main.c
void foo(int v); int main(void) { foo(65535); return 0; } |
| _Winnie C++ Colorizer |
foo.c
#include <stdio.h> typedef struct MyStruct { char a; char b; } MyStruct; void foo(MyStruct s) { printf("a: %d, b: %d\n", s.a, s.b); } |
| _Winnie C++ Colorizer |
$ gcc -W -Wall -Wextra main.c foo.c -o test $ ./test a: -1, b: -1
Як же це виходить? В процесі компіляції компілятор перевіряє типи фукнції спираючись на їх декларацію. У файлі main.c функція foo задекларована як така що приймає на вхід один аргумент типу int і нічого не повертає. У точці виклику цій функції передано в точності один такий аргумент, все добре. Реалізації цієї функції у модулі main.c немає, але це нічого не значить. Реалізація знаходиться в у модулі foo.c. І тут тип функції визначений як такий що приймає на вхід структуру з двома полями типу char. Реалізація функції бере із наданої структури значення полів і виводить їх. Тут теж усе добре і компілятор успішно компілює цей файл. Потім приходить черга компоновщика. Він має підставити замість назв символів функцій що викликаються їх реальні адреси використовуючи таблицю символів. Так от, компілятор C в якості назви символу функції використовує просто її ім’я.
$ gcc -W -Wall -Wextra -c main.c -o main.o
$ nm main.o
U foo
0000000000000000 T main
Літерою "T" позначено символ що містить код, літерою "U" позначено невизначений символ. Так і є, код функції main наявний у модулі, на відміну від коду фукнції foo. Символ foo наявний у таблиці бо він використовується, компоновщик для кожного такого символу має знайти у іншому модулі його код. Якщо код не буде знайдено то виникне помилка компоновки: undefined symbol.
$ gcc -W -Wall -Wextra -c foo.c -o foo.o
$ nm foo.o
0000000000000000 T foo
U printf
У модулі foo.o міститься код функції foo, але немає коду функції printf. Функція printf визначена у стандартній бібліотеці libc яка використовується компоновщиком автоматично.
Компоновщик нічого не знає про типи, він оперує лише іменами символів, тому з його точки зору теж усе правильно:
$ gcc main.o foo.o -o test $ ./test a: -1, b: -1
Із цього можна зробити висновок: наведена вище система плагінів не надає ніяких гарантій з точки зору type safety. Помилки у типах на рівні інтерфейсу плагіну не будуть виявлені і спливуть уже у runtime, лякаючи юзерів страшними наслідками. А помилку такого роду допустити легко: основна програма може змінити API. І плагіни що використовують нове API, і плагіни що використовують старе API (якщо, звісно, не зміниться назва функції-фабрики) будуть успішно завантажені. А потім можна чекати всього що завгодно. Навіть того що старі плагіни продовжуватимуть успішно працювати. Або замість друку чеку на касовому апараті проведуть пуск міжконтинентальних ракет із ядерною БЧ. У нашому світі все можливо.
На відміну від компілатора C, компілятор C++ частково зберігає інформацію про типи після компіляції. Це було зроблено для підтримки ad-hoc поліморфізму, який у світі C++ називається function overloading. C++ допускає (і активно використовує!) існування функцій з однаковими іменами але різними параметрами. Для цього у назві символа кодується не тільки ім’я функції, але і (частково) її тип. Це називається name mangling. Подивимось як це виглядає:
$ g++ -W -Wall -Wextra -c main.c -o main.o
$ nm main.o
U _Z3fooi
0000000000000000 T main
Замість очікуваного символа foo маємо _Z3fooi. Тут треба зауважити що різні компілятори використовують різні схеми name mangling. З іншого боку, більшість компіляторів для Linux намагаються бути сумісними із gcc щоб не доводилось перекомпілювати сотні тисяч бібліотек, тому вони використовують ту-ж схему name mangling що і gcc. Зверніть увагу - для функції main не виконувався name mangling. Це дуже специфічна функція. Спеціально щоб читати такі імена символів є утиліта c++filt:
$ c++filt _Z3fooi foo(int)
Що ж, подивимось що буде з функцією foo у модулі foo.c:
$ g++ -W -Wall -Wextra -c foo.c -o foo.o
$ nm foo.o
0000000000000000 T _Z3foo8MyStruct
U printf
$ c++filt _Z3foo8MyStruct
foo(MyStruct)
Як бачимо, ім’я символу не співпадає із тим що було у main.o. Більш того, c++filt показує нам що у ім’я символу було включено тип аргументу. Щоб було цікавіше, подивиомсь у що перетворюються більш складні конструкції:
$ c++filt _ZZN5boost9function1ISt6vectorINS_15program_options12basic_optionIcEESaIS4_EERS1_ISsSaISsEEE9assign_toINS_3_bi6bind_tIS6_NS_4_mfi3mf1IS6_NS2_6detail7cmdlineES9_EENSC_5list2INSC_5valueIPSH_EENS_3argILi1EEEEEEEEEvT_E13stored_vtable void boost::function1<std::vector<boost::program_options::basic_option<char>, std::allocator<boost::program_options::basic_option<char> > >, std::vector<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&>::assign_to<boost::_bi::bind_t<std::vector<boost::program_options::basic_option<char>, std::allocator<boost::program_options::basic_option<char> > >, boost::_mfi::mf1<std::vector<boost::program_options::basic_option<char>, std::allocator<boost::program_options::basic_option<char> > >, boost::program_options::detail::cmdline, std::vector<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&>, boost::_bi::list2<boost::_bi::value<boost::program_options::detail::cmdline*>, boost::arg<1> > > >(boost::_bi::bind_t<std::vector<boost::program_options::basic_option<char>, std::allocator<boost::program_options::basic_option<char> > >, boost::_mfi::mf1<std::vector<boost::program_options::basic_option<char>, std::allocator<boost::program_options::basic_option<char> > >, boost::program_options::detail::cmdline, std::vector<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&>, boost::_bi::list2<boost::_bi::value<boost::program_options::detail::cmdline*>, boost::arg<1> > >)::stored_vtable $ c++filt _ZN8Achernar6Logger12SysLogWriter5writeERKSsNS0_5Level5LevelE Achernar::Logger::SysLogWriter::write(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Achernar::Logger::Level::Level)
Як бачите, адський ад. Тепер перевіримо як буде реагувати компоновщик на такі назви символів.
$ gcc main.o foo.o -o test main.o: In function `main': main.c:(.text+0xa): undefined reference to `foo(int)' collect2: ld returned 1 exit status
Як і очікувалось, він не знайшов необхідний символ. Я використовував компоновщик gcc, тому що різниця між ним і тим що використовує g++ для нас несуттєва. Щоб розкрити тему name mangling до кінця я покажу як можна обхитрити і її. Змінимо код у модулі foo.c для більшої відповідності прототипу функціїї із main.c:
#include <stdio.h> int foo(int a) { printf("a: %d\n", a); return 65535; } |
| _Winnie C++ Colorizer |
Якщо ви не забули правила function overloading то трюк уже має бути зрозумілий.
$ gcc main.o foo.o -o test
$ ./test
a: 65535
$ nm foo.o
0000000000000000 T _Z3fooi
U printf
Справа в тому що у назву символа не потрапляє тип результату функції. Як пам’ятаємо, робити function overloading дозволяється тільки по її аргументам, але не по результату (для overloading по результату можна використати шаблони).
Можна піти й іншим шляхом. Якщо ви звернули увагу, у оригінальному варіанті функції foo тип аргументу був позначений просто за його іменем. Візьмемо оригінальний код модуля foo.c, але змінимо модуль main.c:
typedef struct MyStruct { unsigned int a; double b; } MyStruct; void foo(MyStruct s); int main(void) { MyStruct s = {65535, 3.14}; foo(s); return 0; } |
| _Winnie C++ Colorizer |
Компілятор без проблем створить об’єктний файл з цього модуля, а компоновщик успішно об’єднає його з foo.o утворивши робочу програму:
$ g++ -W -Wall -Wextra -c main.c -o main.o
$ gcc main.o foo.o -o test
$ ./test
a: -1, b: -1
$ nm main.o
U _Z3foo8MyStruct
0000000000000000 T main
$ nm foo.o
0000000000000000 T _Z3foo8MyStruct
U printf
Щоб побороти проблему типізації, у C-style plugin system часто використовують версіонування API і перевірку версії на етапі завантаження плагіну. Хороша ідея, але такий підхід теж мало що гарантує бо спирається на совість розробників. А, як відомо, совісті у програмістів немає.
На цьому покищо зупинюсь, бо букаф і так вийшло забагато. Вихід із цієї скрутної ситуації опишу у наступному пості.
Продовження