__cdecl или __stdcall в Windows?

В настоящее время я разрабатываю библиотеку С++ для Windows, которая будет распространяться как DLL. Моя цель - максимизировать двоичную совместимость; более точно, функции в моей DLL должны использоваться из кода, скомпилированного с несколькими версиями MSVС++ и MinGW, без необходимости перекомпилировать DLL. Тем не менее, я смущен тем, какая конвенция лучше всего подходит, cdecl или stdcall.

Иногда я слышу такие утверждения, как "соглашение о вызове C - это единственное, гарантированное быть одним и тем же компрометирующим", что контрастирует с такими выражениями, как "Есть некоторые вариации в интерпретации cdecl, особенно в том, как возвращать значения ". Это, похоже, не останавливает некоторых разработчиков библиотек (например, libsndfile), чтобы использовать соглашение C-вызова в DLL, которые они распространяют, без видимых проблемы.

С другой стороны, соглашение о вызове stdcall кажется хорошо определенным. Из того, что мне сказали, все компиляторы Windows в основном обязаны следовать за ним, поскольку это соглашение используется для Win32 и COM. Это основано на предположении, что компилятор Windows без поддержки Win32/COM не будет очень полезен. Множество фрагментов кода, размещенных на форумах, объявляет функции как stdcall, но я не могу найти одно сообщение, которое четко объясняет, почему.

Там слишком много противоречивой информации, и каждый поиск, который я запускаю, дает мне разные ответы, которые на самом деле не помогают мне решить между ними. Я ищу четкое, подробное, аргументированное объяснение, почему я должен выбирать один над другим (или почему они эквивалентны).

Обратите внимание, что этот вопрос относится не только к "классическим" функциям, но также к вызовам функций виртуальных членов, поскольку большинство клиентских кодов будут взаимодействовать с моей DLL через "интерфейсы", чистые виртуальные классы (следующие шаблоны, например, здесь и там).

Ответ 1

Я просто провел некоторое тестирование в реальном мире (компиляция DLL и приложений с MSVС++ и MinGW, а затем их микширование). Как оказалось, у меня были лучшие результаты с помощью соглашения о вызове cdecl.

В частности: проблема с stdcall заключается в том, что MSVС++ управляет именами в таблице экспорта DLL, даже при использовании extern "C". Например, foo становится [email protected]. Это происходит только при использовании __declspec(dllexport), а не при использовании файла DEF; однако, по моему мнению, файлы DEF - это проблема обслуживания, и я не хочу их использовать.

Управление именами MSVС++ создает две проблемы:

  • Использование GetProcAddress в DLL становится несколько более сложным;
  • MinGW по умолчанию не присваивает украшаемые имена (например, MinGW будет использовать [email protected] вместо [email protected]), что усложняет привязку. Кроме того, он представляет опасность увидеть "не-подчеркивающие версии" DLL и приложений, которые появляются в дикой природе, которые несовместимы с "символами подчеркивания".

Я пробовал соглашение cdecl: взаимодействие между MSVС++ и MinGW отлично работает, готово, а имена остаются неизмененными в таблице экспорта DLL. Он даже работает для виртуальных методов.

По этим причинам cdecl явный победитель для меня.

Ответ 2

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

Кроме того, я считаю, что управляемые языки используют стандартное соглашение stdcall по умолчанию, поэтому любой, кто использует P/Invoke на нем, должен будет явно указать соглашение о вызове, если вы перейдете с cdecl.

Итак, если все ваши сигнатуры функций будут статически определены, я, вероятно, склоняюсь к stdcall, если не cdecl.

Ответ 3

В терминах безопасности соглашение __cdecl является "более безопасным", потому что это вызывающий объект, который должен освободить стек. Что может произойти в библиотеке __stdcall, так это то, что разработчик мог забыть освободить стеки должным образом, или злоумышленник может ввести некоторый код, повредив стек DLL (например, с помощью API-соединения), который затем не проверяется вызывающим. У меня нет каких-либо примеров безопасности CVS, которые показывают, что моя интуиция верна.