Как передать объекты класса, особенно объекты STL, в C ++ DLL и обратно?
Мое приложение должно взаимодействовать со сторонними плагинами в виде файлов DLL, и я не могу контролировать, на каком компиляторе созданы эти плагины. Я знаю, что не существует гарантированного ABI для объектов STL, и меня беспокоит нестабильность моего приложения.
Ответы:
Краткий ответ на этот вопрос - нет . Поскольку нет стандартного C ++ ABI (двоичный интерфейс приложения, стандарт для соглашений о вызовах, упаковки / выравнивания данных, размера типа и т. Д.), Вам придется перепрыгнуть через множество обручей, чтобы попытаться применить стандартный способ работы с классом. объекты в вашей программе. Нет даже гарантии, что он будет работать после того, как вы прыгнете через все эти обручи, и нет гарантии, что решение, работающее в одной версии компилятора, будет работать в следующей.
Просто создайте простой C - интерфейс с использованием
extern "C"
, так как C ABI является четко определенной и стабильной.Если вы действительно, действительно хотите передать объекты C ++ через границу DLL, это технически возможно. Вот некоторые из факторов, которые вам необходимо учитывать:
Упаковка / выравнивание данных
Внутри данного класса отдельные члены данных обычно специально помещаются в память, чтобы их адреса соответствовали кратному размеру типа. Например,
int
можно выровнять по 4-байтовой границе.Если ваша DLL скомпилирована с помощью другого компилятора, чем ваш EXE, версия DLL данного класса может иметь другую упаковку, чем версия EXE, поэтому, когда EXE передает объект класса в DLL, DLL может быть не в состоянии должным образом получить доступ к данный член данных в этом классе. DLL будет пытаться читать с адреса, указанного в ее собственном определении класса, а не в определении EXE, и, поскольку требуемый элемент данных на самом деле там не хранится, это приведет к мусорным значениям.
Вы можете обойти это с помощью
#pragma pack
директивы препроцессора, которая заставит компилятор применить определенную упаковку. Компилятор по-прежнему будет применять упаковку по умолчанию, если вы выберете значение пакета больше, чем то, которое выбрал бы компилятор , поэтому, если вы выберете большое значение упаковки, класс по-прежнему может иметь различную упаковку между компиляторами. Решением для этого является использование#pragma pack(1)
, которое заставит компилятор выровнять элементы данных по однобайтовой границе (по сути, упаковка не будет применяться). Это не лучшая идея, так как это может вызвать проблемы с производительностью или даже сбои в некоторых системах. Тем не менее, она будет обеспечивать согласованность в том , как члены данных вашего класса выравниваются в памяти.Изменение порядка участников
Если ваш класс не является стандартным , компилятор может переупорядочить его элементы данных в памяти . Не существует стандарта того, как это делается, поэтому любое изменение порядка данных может вызвать несовместимость между компиляторами. Следовательно, для передачи данных в DLL и обратно потребуются классы стандартного макета.
Соглашение о вызове
У данной функции может быть несколько соглашений о вызовах . Эти соглашения о вызовах определяют, как данные должны передаваться в функции: параметры хранятся в регистрах или в стеке? В каком порядке аргументы помещаются в стек? Кто очищает все аргументы, оставшиеся в стеке после завершения функции?
Важно поддерживать стандартное соглашение о вызовах; если вы объявите функцию как
_cdecl
значение по умолчанию для C ++ и попытаетесь вызвать ее с использованием_stdcall
, произойдут неприятности ._cdecl
однако это соглашение о вызовах по умолчанию для функций C ++, поэтому это одна вещь, которая не сломается, если вы намеренно не нарушите ее, указав_stdcall
в одном месте, а_cdecl
в другом.Размер типа данных
Согласно этой документации , в Windows большинство основных типов данных имеют одинаковые размеры независимо от того, является ли ваше приложение 32-битным или 64-битным. Однако, поскольку размер данного типа данных определяется компилятором, а не каким-либо стандартом (все стандартные гарантии заключаются в том
1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), рекомендуется использовать типы данных фиксированного размера, чтобы гарантировать совместимость размеров данных там, где это возможно.Проблемы с кучей
Если ваша DLL связана с другой версией среды выполнения C, чем ваш EXE, два модуля будут использовать разные кучи . Это особенно вероятная проблема, учитывая, что модули компилируются разными компиляторами.
Чтобы смягчить это, вся память должна быть выделена в общую кучу и освобождена из той же кучи. К счастью, Windows предоставляет API, чтобы помочь с этим: GetProcessHeap позволит вам получить доступ к куче EXE хоста, а HeapAlloc / HeapFree позволит вам выделить и освободить память в этой куче. Важно, чтобы вы не использовали normal
malloc
/,free
поскольку нет гарантии, что они будут работать так, как вы ожидаете.Проблемы с STL
Стандартная библиотека C ++ имеет собственный набор проблем с ABI. Нет гарантии, что данный тип STL размещен в памяти одинаковым образом, и нет гарантии, что данный класс STL имеет одинаковый размер от одной реализации к другой (в частности, отладочные сборки могут помещать дополнительную отладочную информацию в данный тип STL). Следовательно, любой контейнер STL должен быть распакован на основные типы перед передачей через границу DLL и переупаковкой на другой стороне.
Изменение имени
Ваша DLL предположительно будет экспортировать функции, которые ваш EXE захочет вызывать. Однако компиляторы C ++ не имеют стандартного способа изменять имена функций . Это означает,
GetCCDLL
что указанная функция может быть искажена_Z8GetCCDLLv
в GCC и?GetCCDLL@@YAPAUCCDLL_v1@@XZ
MSVC.Вы уже не сможете гарантировать статическое связывание с вашей DLL, поскольку DLL, созданная с помощью GCC, не будет создавать файл .lib, а для статического связывания DLL в MSVC он требуется. Динамическое связывание кажется намного более чистым вариантом, но искажение имени мешает вам: если вы попытаетесь
GetProcAddress
указать неправильное искаженное имя, вызов завершится неудачно, и вы не сможете использовать свою DLL. Это требует небольшого взлома, и это довольно важная причина, по которой передача классов C ++ через границу DLL - плохая идея.Вам нужно будет создать свою DLL, а затем изучить созданный файл .def (если он создан; это будет зависеть от параметров вашего проекта) или использовать такой инструмент, как Dependency Walker, чтобы найти искаженное имя. Затем вам нужно будет написать свой собственный файл .def, определяющий несвязанный псевдоним для искаженной функции. В качестве примера давайте воспользуемся
GetCCDLL
функцией, о которой я упоминал чуть позже. В моей системе следующие файлы .def работают для GCC и MSVC соответственно:GCC:
MSVC:
Перестройте свою DLL, а затем еще раз проверьте экспортируемые функции. Среди них должно быть имя функции без запутывания. Обратите внимание , что вы не можете использовать перегруженные функции этого пути : имя unmangled функции является псевдонимом для одной конкретной перегрузки функции , как это определенно искаженным именем. Также обратите внимание, что вам нужно будет создавать новый файл .def для вашей DLL каждый раз, когда вы меняете объявления функций, поскольку измененные имена будут меняться. Что наиболее важно, обходя искажение имен, вы отменяете любые меры защиты, которые компоновщик пытается предложить вам в отношении проблем несовместимости.
Весь этот процесс будет проще, если вы создадите интерфейс для своей DLL, так как у вас будет только одна функция, для которой нужно определить псевдоним, вместо того, чтобы создавать псевдоним для каждой функции в вашей DLL. Однако все же действуют те же предостережения.
Передача объектов класса в функцию
Это, вероятно, самая тонкая и самая опасная проблема, мешающая передаче данных кросс-компилятора. Даже если вы обрабатываете все остальное, не существует стандарта того, как аргументы передаются функции . Это может вызвать незначительные сбои без видимой причины и непростого способа их отладки . Вам нужно будет передать все аргументы через указатели, включая буферы для любых возвращаемых значений. Это неуклюже и неудобно, и это еще один хакерский обходной путь, который может работать, а может и не работать.
Собрав воедино все эти обходные пути и опираясь на творческую работу с шаблонами и операторами , мы можем попытаться безопасно передавать объекты через границу DLL. Обратите внимание, что поддержка C ++ 11 является обязательной, как и поддержка
#pragma pack
и его варианты; MSVC 2013 предлагает эту поддержку, как и последние версии GCC и clang.pod
Класс специализирован для всех основных типов данных, так чтоint
автоматически завернуть кint32_t
,uint
будет обернуто вuint32_t
и т.д. Это все происходит за кулисами, благодаря перегруженным=
и()
операторам. Я пропустил остальные специализации базовых типов, поскольку они почти полностью идентичны, за исключением базовых типов данных (bool
специализация имеет немного дополнительной логики, поскольку она преобразуется в a,int8_t
а затемint8_t
сравнивается с 0 для преобразования обратно вbool
, но это довольно тривиально).Мы также можем обернуть типы STL таким образом, хотя это требует небольшой дополнительной работы:
Теперь мы можем создать DLL, которая использует эти типы модулей. Во-первых, нам нужен интерфейс, поэтому у нас будет только один метод, для которого нужно разобраться.
Это просто создает базовый интерфейс, который может использовать как DLL, так и любой вызывающий объект. Обратите внимание, что мы передаем указатель на объект
pod
, а не наpod
сам объект . Теперь нам нужно реализовать это на стороне DLL:А теперь реализуем
ShowMessage
функцию:Ничто не слишком фантазии: это только копии переданная
pod
в нормальнуюwstring
и показывает , что в MessageBox. В конце концов, это просто POC , а не полная служебная библиотека.Теперь мы можем создать DLL. Не забудьте о специальных файлах .def, чтобы обойти искажение имени компоновщика. (Примечание: структура CCDLL, которую я фактически построил и запустил, имеет больше функций, чем та, которую я представляю здесь. Файлы .def могут работать не так, как ожидалось.)
Теперь EXE для вызова DLL:
И вот результаты. Наша DLL работает. Мы успешно справились с прошлыми проблемами STL ABI, прошлыми проблемами C ++ ABI, прошлыми проблемами искажения, и наша DLL MSVC работает с GCC EXE.
В заключение, если вам абсолютно необходимо передавать объекты C ++ через границы DLL, вы это делаете именно так. Однако ничто из этого не гарантирует работы ни с вашей, ни с чьей-либо системой. Любое из этого может сломаться в любое время и, вероятно, сломается за день до того, как запланирован основной выпуск вашего программного обеспечения. Этот путь полон взломов, рисков и общего идиотизма, за который, наверное, стоит пристрелить. Если вы все же пойдете по этому пути, пожалуйста, проверяйте с особой осторожностью. И действительно ... просто не делай этого вообще.
источник
@computerfreaker написал отличное объяснение того, почему отсутствие ABI предотвращает передачу объектов C ++ через границы DLL в общем случае, даже когда определения типов находятся под контролем пользователя и в обеих программах используется одна и та же последовательность токенов. (Есть два случая, которые действительно работают: классы стандартного макета и чистые интерфейсы)
Для типов объектов, определенных в стандарте C ++ (включая адаптированные из стандартной библиотеки шаблонов), ситуация намного хуже. Токены, определяющие эти типы, НЕ являются одинаковыми для разных компиляторов, поскольку стандарт C ++ не предоставляет полного определения типа, а только минимальные требования. Кроме того, поиск имен идентификаторов, которые появляются в этих определениях типов, не разрешает то же самое. Даже в системах, где есть C ++ ABI, попытка совместного использования таких типов через границы модуля приводит к массовому неопределенному поведению из-за нарушений правила одного определения.
Это то, с чем программисты Linux не привыкли иметь дело, потому что libstdc ++ g ++ был стандартом де-факто, и практически все программы его использовали, таким образом удовлетворяя требованиям ODR. libc ++ clang сломал это предположение, а затем пришел C ++ 11 с обязательными изменениями почти для всех типов стандартных библиотек.
Просто не делитесь типами стандартных библиотек между модулями. Это неопределенное поведение.
источник
Некоторые ответы здесь делают прохождение классов C ++ действительно пугающим, но я хотел бы поделиться альтернативной точкой зрения. Чистый виртуальный метод C ++, упомянутый в некоторых других ответах, на самом деле оказывается чище, чем вы думаете. Я построил целую систему плагинов вокруг этой концепции, и она работает очень хорошо в течение многих лет. У меня есть класс «PluginManager», который динамически загружает библиотеки DLL из указанного каталога с помощью LoadLib () и GetProcAddress () (и эквивалентов Linux, так что исполняемый файл делает его кроссплатформенным).
Вы не поверите, но этот метод простителен, даже если вы делаете какие-то дурацкие вещи, например, добавляете новую функцию в конец своего чистого виртуального интерфейса и пытаетесь загрузить библиотеки DLL, скомпилированные для интерфейса без этой новой функции - они загрузятся нормально. Конечно ... вам нужно будет проверить номер версии, чтобы убедиться, что ваш исполняемый файл вызывает новую функцию только для новых dll, которые реализуют эту функцию. Но есть и хорошие новости: это работает! Таким образом, у вас есть грубый метод развития интерфейса с течением времени.
Еще одна интересная особенность чистых виртуальных интерфейсов - вы можете унаследовать столько интерфейсов, сколько захотите, и вы никогда не столкнетесь с проблемой ромба!
Я бы сказал, что самым большим недостатком этого подхода является то, что вы должны быть очень осторожны с тем, какие типы вы передаете в качестве параметров. Никаких классов или объектов STL без предварительного обертывания их чистыми виртуальными интерфейсами. Никаких структур (без прохождения прагмы пак вуду). Просто примитивные типы и указатели на другие интерфейсы. Кроме того, нельзя перегружать функции, что является неудобством, но не препятствием.
Хорошая новость заключается в том, что с помощью нескольких строк кода вы можете создавать универсальные классы и интерфейсы многократного использования для обертывания строк, векторов и других контейнерных классов STL. Кроме того, вы можете добавить в свой интерфейс функции, такие как GetCount () и GetVal (n), чтобы люди могли просматривать списки.
Люди, создающие плагины для нас, находят это довольно легко. Им не обязательно быть экспертами в области ABI или чего-то еще - они просто наследуют интерфейсы, которые им интересны, кодируют функции, которые они поддерживают, и возвращают false для тех, которые им не нужны.
Насколько мне известно, технология, которая выполняет всю эту работу, не основана ни на каких стандартах. Насколько я понимаю, Microsoft решила сделать свои виртуальные таблицы таким образом, чтобы они могли создавать COM, и другие разработчики компиляторов решили последовать их примеру. Сюда входят GCC, Intel, Borland и большинство других основных компиляторов C ++. Если вы планируете использовать малоизвестный встроенный компилятор, этот подход, вероятно, вам не подойдет. Теоретически любая компания-производитель компиляторов может изменить свои виртуальные таблицы в любое время и сломать что-то, но, учитывая огромное количество кода, написанного за эти годы и зависящего от этой технологии, я был бы очень удивлен, если бы кто-то из крупных игроков решил снизить рейтинг.
Итак, мораль этой истории такова ... За исключением нескольких крайних обстоятельств, вам нужен один человек, отвечающий за интерфейсы, который может следить за тем, чтобы граница ABI оставалась чистой с примитивными типами и избегала перегрузки. Если вы согласны с этим условием, то я не побоюсь делиться интерфейсами с классами в библиотеках DLL / SO между компиляторами. Совместное использование классов напрямую == проблема, но совместное использование чистых виртуальных интерфейсов не так уж и плохо.
источник
Вы не можете безопасно передавать объекты STL через границы DLL, если все модули (.EXE и .DLL) не построены с одной и той же версией компилятора C ++ и теми же настройками и разновидностями CRT, что сильно ограничивает и явно не ваш случай.
Если вы хотите предоставить объектно-ориентированный интерфейс из своей библиотеки DLL, вы должны предоставить чистые интерфейсы C ++ (что аналогично тому, что делает COM). Прочтите эту интересную статью о CodeProject:
Вы также можете рассмотреть возможность предоставления чистого интерфейса C на границе DLL, а затем создания оболочки C ++ на сайте вызывающей стороны.
Это похоже на то, что происходит в Win32: код реализации Win32 - это почти C ++, но многие API Win32 предоставляют чистый интерфейс C (есть также API, которые предоставляют интерфейсы COM). Затем ATL / WTL и MFC оборачивают эти чистые интерфейсы C классами и объектами C ++.
источник