LINUX.ORG.RU

Как сделать уникальные имена классов в библиотеке?

 


0

5

Всем доброго утра!

Решил разбить проект на библиотеки, ранее с ними не работал и заинтересовался, как правильно создавать уникальные имена классов, чтобы не было конфликтов с внешним кодом.

Встречал две возможности:

1. собственный namespace (a-ля boost::) 2. стандартные префиксы к классам (а-ля cv в OpenCV)

А вот почему используется в одних библиотеках один подход, а в других - другой? Какие в будущем могут встретиться проблемы каждого способа? Есть ли еще варианты решения задачи уникальности имен?


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

Iron_Bug ★★★★★ ()

namespace используй, если библиотека на плюсах. Если для С потом нужно, то делай extern «C» {}.

Допустим в Qt используют префиксы, потому если регистрируешь тип как Qt metaobject, то нужно все namespace'ы указывать, что не всегда удобно.

UVV ★★★★★ ()
Ответ на: комментарий от Iron_Bug

Да и перейти к пространству имен проще всего. Рефакторить названия всех классов, чтобы воспользоваться вторым подходом слишком геморно. А вот насчет паранойи: QtCreator позволяет поменять имя определенного нэймспэйса во всем проекте в один клик, так что, на мой взгляд добавлять лишние директивы избыточно

Burns ()
Ответ на: комментарий от UVV

Второй человек за namespace :)

В принципе я их и собирался использовать, но подумал, мало ли. Спасибо за хороший пример с Qt. Действительно, у них же там типы регистрируются для сигналов, работает все через собственный препроцессор и хз во что все это выльется с пространством имен)) В общем будет потом время - надо будет попробовать..

Burns ()
Ответ на: комментарий от Burns

Не второй, остальные просто молча согласны :)

Пространства имён создавались для решения проблем с коллизиями и если нету явных причин их не использовать, то их стоит применять. Префиксы может для C-интерфейса быть удобно и после using namespace, но в обоих случаях это можно сделать отдельно от основного кода с пространствами имён. В итоге практически нету причин выбирать префиксы.

xaizek ★★★★★ ()
Последнее исправление: xaizek (всего исправлений: 1)

Генерировать из uuid.

anonymous ()

Создам еще одну подтему внутри этой темы, под названием: Что за херню творит линковщик?

Изучаю, что будет, если не делать защиту уникальных имен. Пусть есть статическая либа с классами A и B. A - внутренний класс библиотеки и используется в B. В - это интерфейс библиотеки с внешним миром. По h и сpp все распихано правильно, для краткости описания перенесу реализацию в объявление класса. Либа:

 
class A
{
public:
    A()
    {
        std::cout << "class A: created at dynamic library\n";
    }
    void bla()
    {
        std::cout << "bla at dynamic\n";
    }
};

class B
{
public:
    B()
    {
        A a;
        a.bla();
    }
};

Если подключить в проекте созданную библиотеку и создать объект класса B, то на экран выведутся строки, говорящие что класс A создан из библиотеки и метод класса A вызван из библиотеки. Далее занимаемся мазохизмом: берем и в нашем проекте создаем свой класс A:

 
class A
{
public:
    A()
    {
        std::cout << "class A: created at code\n";
    }
    void blablabla()
    {
        std::cout << "blablabla at code\n";
    }
};

void main()
{
    B b;
}

Компилируем и запускаем :) Как думаете, что будет при создании объекта класса B? Можно сказать типичный вопрос для собеседования. В общем в результате конструктор класса A вызывается из кода, а метод «bla» из библиотеки. Если в конструктор в коде добавить параметр, то при создании класса B вызовется конструктор и метод класса A из библиотеки.

В общем почему линковщик не ругается на повторяющиеся имена? Что это еще такое, бага, или какая-то фича, или просто момент, который лучше обходить стороной?

Burns ()

При кажущейся привлекательности пространств имен, стоит знать об альтернативах. Это импорт классов/модулей как например в python или java. Довольно удобно.

При детальном рассмотрении импорта, напрашивается вывод что такая замена ничем не хуже.

Можно еще задействовать под уникальное пространство домен. Только есть неудобство com.

anonymous ()
Ответ на: комментарий от Burns

Это нарушение ODR. Вообще-то оно в некоторых случаях допустимо, при этом линкер считает, что реализация везде одинакова, а потому не ругается и выбирает любую.

А в примере вопрос был бы нормален для собеседования (именно в формулировке «что кокретно произойдёт»), если такое поведение стандартизовано, в чём я сильно сомневаюсь.

devsdc ★★ ()
Ответ на: комментарий от Burns

С --whole-archive должны быть ошибки. Это побочный эффект того как обрабатываются статические библиотеки. Из них берутся только те символы, которые нужны и в данный момент не известны (тут основной код предоставляет часть символов и «скрывает» одноимённые из библиотек), а остальные игнорируются. Эффект известный и используется для подмены функций, например, malloc, free с контролем за выделением памяти.

xaizek ★★★★★ ()
Ответ на: комментарий от xaizek

очередная интересная команда линковки, спасибо)) Но она работает только для статических библиотек, из названия понятно. Для динамических есть какое-нибудь решение? Что там вообще происходит?

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

Запускаем.. дошли до создания в библиотеке класса, а далее в коде ассемблера происходит call _someLabudaOfClassA (callq 4f40 <_ZN1AC1Ev@plt>). Эта «лабуда» и называется символами библиотеки, верно? А у библиотеки есть список необходимых символов для каждой ее точки входа, линковщик строит список всех используемых точек входа (символов) во всей программе и если нужные находятся в основном коде, то он записывает туда адреса и пофиг, где используется эта точка входа - в библиотеке или в основном коде, а при запуске записываются только пустые адреса методов. Я правильно понял, что там происходит?

совсем непонятно написал, после работы тогда более структурированно перепишу

Burns ()
Ответ на: комментарий от Burns

Для динамических есть какое-нибудь решение? Что там вообще происходит?

В принципе процесс аналогичный, только выполняется уже динамическим линкером.

Эта «лабуда» и называется символами библиотеки, верно?

Вроде того, только идёт через один уровень косвенности (эту PLT). Если собирать без lazy-loading, то в исполняемом файле будут не заглушки, подгружающие библиотеки по запросу, а что-то типо *UND*, которые к моменту старта должны быть полностью сопоставлены с функциями из библиотек.

А у библиотеки есть список необходимых символов для каждой ее точки входа

Такого вроде нет, просто некоторые символы помечены как доступные извне.

если нужные находятся в основном коде, то он записывает туда адреса

Да. Но тут важно понимать, что вполне можно один и тот же символ на две разных реализации отобразить, что-то вроде:

gcc code1.o -lfakeSymVersion1 code2.o -lfakeSymVersion2
может привести к тому, что code1 и code2 будут использовать разные реализации. Есть хорошая статья по теме: Руководство новичка по эксплуатации компоновщика.

и пофиг где используется эта точка входа - в библиотеке или в основном коде

Библиотека скорее всего останется при своём символе, так как ей нет необходимости обращаться к нему через jump-table (PLT).

совсем непонятно написал, после работы тогда более структурированно перепишу

Более-менее, просто надо ближе познакомиться с тем как работает линкер. Рекомендую статью вверху, она объясняет многие моменты.

P.S. Я не специалист по линковке во всех возможных ситуациях, так что тут могут быть неточности, но грубых ошибок быть не должно.

xaizek ★★★★★ ()
Ответ на: комментарий от Burns

с дефайнами не надо ничего менять. определил макрос в Makefile'е - и всё. да я проект может собираться на машине, где нет гуя и всяких там креаторов.

Iron_Bug ★★★★★ ()
Ответ на: комментарий от xaizek

Прошу прощения за столь долгое ожидание, вчера совсем не было сил разложить все вышесказанное по полкам.

Для динамических есть какое-нибудь решение? Что там вообще происходит?

В принципе процесс аналогичный, только выполняется уже динамическим линкером.

Я тестовый проект собирал с помощью cmake. Указал эту опцию вот так

 
set(LIBRARY_LIST -Wl,-whole-archive
                 ${CMAKE_CURRENT_LIST_DIR}/../lib/libstaticlib.a
                 ${CMAKE_CURRENT_LIST_DIR}/../lib/libdynamiclib.so
                 -Wl,-no-whole-archive)
target_link_libraries(${TARGET_NAME} ${LIBRARY_LIST})
но подействовала она только на статическую библиотеку. А каким образом и где этот параметр задавать динамическому линкеру?

Эта «лабуда» и называется символами библиотеки, верно?

Вроде того, только идёт через один уровень косвенности (эту PLT). Если собирать без lazy-loading, то в исполняемом файле будут не заглушки, подгружающие библиотеки по запросу, а что-то типо *UND*, которые к моменту старта должны быть полностью сопоставлены с функциями из библиотек.

Извиняюсь за нарушение терминологии. Под заглушками и подразумевались *UND*. А насчет PLT надо будет почитать, за него тоже спасибо, я думал это часть крокозябр имени символа))

А у библиотеки есть список необходимых символов для каждой ее точки входа

Такого вроде нет, просто некоторые символы помечены как доступные извне.

И далее Вы статью привели. Да, я ранее ее читал, забыл про полезную штуку nm:

$ nm --demangle ./debug/main_binary 
...
000000000040a746 T A::blablalba()
000000000040a6e6 T A::A()
000000000040a6e6 T A::A()
...

$ nm --demangle ./lib/libdynamiclib.so 
...
000000000000d04e T A::bla(int)
000000000000d020 T A::A()
000000000000d020 T A::A()
...

Как видно отсюда, если не запрещать доступ извне (судя по статье оператором static), то присутствуют все-таки все символы. Кстати, оператор static в статье описан для С компилятора. А как запрещать доступ для С++ классов?? Ведь если метод какого-нибудь класса объявить статическим, то он использует только статические данные и пошло поехало, нельзя же всю библиотеку делать статической. Как тогда?

и пофиг где используется эта точка входа - в библиотеке или в основном коде

Библиотека скорее всего останется при своём символе, так как ей нет необходимости обращаться к нему через jump-table (PLT).

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

совсем непонятно написал, после работы тогда более структурированно перепишу

Более-менее...

Спасибо, хоть кто-то меня понимает :)

Burns ()
Ответ на: комментарий от Burns

но подействовала она только на статическую библиотеку. А каким образом и где этот параметр задавать динамическому линкеру?

Оно обычно задаётся при сборке .so, чтобы ничего не потерялось, поэтому нет необходимости что-то дополнительно делать. Тут есть разница в том, что динамический линкер работает с тем, что осталось после статического и не может откатить его решения. В целом символы должны разрешаться также, только вместо порядка аргументов командной строки, тут порядок загрузки библиотек, я полагаю (на самом деле можно код посмотреть).

А как запрещать доступ для С++ классов?

Объявить их внутри анонимного пространства имён. Другой вариант привёл аноним. Также для библиотек иногда практикуют что-то вроде «белого списка», делается с помощью version script (как-то так, самому не доводилось).

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

Я не так понял изначально. Тогда, наверное, через глобальную таблицу обращается. Проще посмотреть уже в ассемблере, мне казалось, что при формировании .so должно происходить разрешение доступных символов. Интересно при использовании LTO во время создания .so так же будет или нет.

xaizek ★★★★★ ()
Ответ на: комментарий от anonymous

Супер, все достаточно просто и работает. Все символы класса А после объявления стали локальными и теперь при существовании дублера класса А в коде вызывается конструктор A из библиотеки.

Эмм, а если есть такая защита, то зачем тогда вообще создавать собственный namespace (или префиксы к классам)? Вcе что мы хотели скрыть в библиотеке - мы скрыли, ничего ни с чем не конфликтует.

Burns ()
Ответ на: комментарий от Burns

Так namespace (или префиксы) делается не только для скрытия. В стороннем коде хочется использовать библиотеки (boost, Qt, stl) и не думать о том, что какие-то из названий могут сколлизиться.

devsdc ★★ ()
Ответ на: комментарий от xaizek

и приведенные Вами способы надо обязательно сравнить в удобстве использования :)

Только что-то в лоб не удалось поместить A в анонимный namespace. Вот мы A.h окунули в это пространство, а что делать с A.cpp? По логике вещей с ним ничего не надо делать, однако тут со мной поздоровался

Linking CXX shared library ../../../lib/libdynamiclib.so
/usr/bin/ld: CMakeFiles/dynamiclib.dir/dynamic_lib.cpp.o: relocation R_X86_64_PC32 against undefined symbol `_ZN12_GLOBAL__N_11AC1Ev' can not be used when making a shared object; recompile with -fPIC

Данный флаг в сmake я пытался скормить target_compile_options и target_link_libraries и все бестолку, выдается эта ошибка.

Кстати, первая ссылка с такой же проблемой - принадлежит этому сайту, в которой Pavval приводит внушительных объемов статью о том, как правильно писать статические библиотеки.. в общем действительно думаю, будет полезно почитать.

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

Кстати, если несложно, можете на пальцах объяснить, что такое позиционно независимый код, а именно что значит «Модуль с такой адресацией можно грузить с любого адреса без всякой перенастройки». Что это за проблемы загрузки по адресам?

Burns ()
Ответ на: комментарий от devsdc

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

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

Burns ()
Ответ на: комментарий от Burns

А зачем код хедера засовывать в анонимный namespace? Анонимные неймспейсы нужны для сокрытия реализации, а хедера - это _публичный_ интерфейс твоей библиотеки.

Код грузится в память по какому-то адресу. Внутри объектника могут использоваться ссылки на статические данные, на функции. Какими они будут? Либо это будут фиксированные адреса, по которым нужные данные окажутся в рантайме (при этом надо зафиксировать адрес, по которому будет загружен бинарник), либо можно использовать относительную адресацию (относительно текущего места в коде, например) и не зависеть от того, куда попадёт бинарник при загрузке.

Относительная адресация немного геморройнее, но гибче. Например, несколько разных библиотек компилировались независимо друг от друга с использованием абсолютной адресации (и фиксацией адреса, по которому нужно грузить библиотеку). Как ты сможешь гарантировать, что адреса, по которым их надо грузить, не сколлизятся? Никак. Потому и нужна относительная адресация.

devsdc ★★ ()
Ответ на: комментарий от Burns

Смотря что ты понимаешь под остальным кодом. Код, реализующий хедера, должен быть в тех же неймспейсах, что и объявленный в хедерах, очевидно. Всё остальное желательно скрывать (если, конечно, ты не хочешь иметь доступ к символам извне).

devsdc ★★ ()

<flame>А в Go о таком пишут даже в офф доках.</flame>

iu0v1 ()
Ответ на: комментарий от Burns

devsdc ответил уже, поэтому просто подтвержу в основном.

Только что-то в лоб не удалось поместить A в анонимный namespace.

Это аналог static для типов и точно также должен применяться для приватных частей, в публичном интерфейсе так не получится. Так мне надо было писать изначально.

приводит внушительных объемов статью о том, как правильно писать статические библиотеки..

Только там про динамические. Это известная статья Дреппера, которую я сам не читал, так как в основном пользуюсь динамическими библиотеками. Хотя да, надо.

Кстати, если несложно, можете на пальцах объяснить, что такое позиционно независимый код, а именно что значит «Модуль с такой адресацией можно грузить с любого адреса без всякой перенастройки». Что это за проблемы загрузки по адресам?

Если библиотека не PIC, то у неё должен быть какой-то базовый адрес, так же у неё есть какой-то размер. Теперь, если есть две библиотеки, с параметрами вроде

lib1_base = 0x10000000;
lib1_size = 0x10000;
lib2_base = 0x10008000;
lib2_size = 0x8000;
То они пересекутся, поэтому все ссылки на сущности должны быть относительными, тогда образ библиотеки в памяти можно свободно перемещать в адресном пространстве процесса и он будет везде работать. При этом одна и таже библиотека может без изменений быть загружена в разные процессы по разным виртуальным адресам (при работающем ASLR это должно быть частое явление), а физическая копия её кода будет только одна.

xaizek ★★★★★ ()
Ответ на: комментарий от xaizek

Только там про динамические

вот блин, снова оговорился, я и хотел сказать динамические

Если библиотека не PIC, то у неё должен быть какой-то базовый адрес, так же у неё есть какой-то размер

А как на готовой библиотеке посмотреть, PIC она или нет?

Пожалуй на этом все, всем большое спасибо, тут достаточно пищи для размышлений, чтения и переваривания)) Я как-нибудь могу повлиять на увеличение количества Ваших звездочек на сайте? :)

Burns ()
Ответ на: комментарий от Burns

А как на готовой библиотеке посмотреть, PIC она или нет?

Какого-то флага или чего-то подобного нету. Но динамические в линуксе должны быть PIC, иначе линкер ругается (причём на конкретную инструкцию, так как больше ему нечего проверять).

Вообще это сильно платформо-зависимо. Для x86 можно глянуть readelf --relocs, если хотя бы один relocation есть, то это PIC, на x86_64 уже только на глаз, видимо, так как там RIP-относительная адресация.

Я как-нибудь могу повлиять на увеличение количества Ваших звездочек на сайте?

Они начисляются за день активности в части из разделов форума и означают только количество таких дней и ничего больше.

xaizek ★★★★★ ()
Ответ на: комментарий от Burns

Да да, почитай Дреппера. Очень полезная статья.

Pavval ★★★★★ ()
Ответ на: комментарий от Burns

QtCreator позволяет поменять имя определенного нэймспэйса во всем проекте в один клик

И что - будешь в чужих проектах (либах) менять? Впрочем, пересечение маловероятно.

DarkEld3r ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.