Почему вызовы Cdecl часто несовместимы в "стандартной" конвенции P/Invoke?

Я работаю над довольно большой базой кода, в которой функциональность С++ P/Вызывается из С#.

В нашей кодовой базе есть много вызовов, таких как...

С++:

extern "C" int __stdcall InvokedFunction(int);

С соответствующим С#:

[DllImport("CPlusPlus.dll", ExactSpelling = true, SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
    private static extern int InvokedFunction(IntPtr intArg);

Я подсчитал сеть (насколько я могу) рассуждать о том, почему это кажущееся несоответствие существует. Например, почему существует Cdecl внутри С# и __stdcall в С++? По-видимому, это приводит к тому, что стек очищается дважды, но в обоих случаях переменные помещаются в стек в том же обратном порядке, что я не вижу никаких ошибок, хотя вероятность того, что возвращаемая информация будет очищена в случае попытка трассировки во время отладки?

Из MSDN: http://msdn.microsoft.com/en-us/library/2x8kf7zx%28v=vs.100%29.aspx

// explicit DLLImport needed here to use P/Invoke marshalling
[DllImport("msvcrt.dll", EntryPoint = "printf", CallingConvention = CallingConvention::Cdecl,  CharSet = CharSet::Ansi)]

// Implicit DLLImport specifying calling convention
extern "C" int __stdcall MessageBeep(int);

И снова в коде С++ есть как extern "C", так и CallingConvention.Cdecl в С#. Почему это не CallingConvention.Stdcall? Или, более того, почему существует __stdcall в С++?

Спасибо заранее!

Ответ 1

Это повторяется в SO-вопросах, я попытаюсь превратить это в (длинный) справочный ответ. 32-разрядный код обременен длинной историей несовместимых вызовов. Выбор того, как сделать вызов функции, который имеет смысл давным-давно, но в основном представляет собой гигантскую боль в задней части сегодня. У 64-битного кода есть только одно соглашение о вызове, то, кто собирается добавить еще один, он будет отправлен на небольшой остров в Южной Атлантике.

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

  • __stdcall нашел свой путь в программировании Windows через старое 16-битное соглашение о вызове pascal, используемое в 16-битных Windows и OS/2. Это соглашение, используемое всеми функциями api для Windows, а также COM. Поскольку большинство pinvoke предназначено для вызова ОС, Stdcall является значением по умолчанию, если вы не укажете его явно в атрибуте [DllImport]. Его единственная причина существования заключается в том, что она указывает, что вызываемый человек очищается. Который производит более компактный код, очень важный в те времена, когда им приходилось сжимать операционную систему GUI в 640 килобайтах ОЗУ. Его самым большим недостатком является то, что это опасно. Несоответствие между тем, что принимает собеседник, является аргументом для функции, и то, что реализовано вызываемым, приводит к тому, что стек становится несбалансированным. Это, в свою очередь, может привести к чрезвычайно сложной диагностике сбоев.

  • __cdecl - стандартное соглашение о вызове для кода, написанного на языке C. Его основной причиной существования является то, что он поддерживает вызовы функций с переменным числом аргументов. Распространяется в коде C с такими функциями, как printf() и scanf(). С побочным эффектом, что, поскольку он является вызывающим, который знает, сколько аргументов было фактически передано, это вызывающий, который очищает. Забывание CallingConvention = CallingConvention.Cdecl в объявлении [DllImport] является очень распространенной ошибкой.

  • __fastcall - довольно плохо определенное соглашение о вызовах с взаимно несовместимыми выборами. Это было распространено в компиляторах Borland, когда-то очень влиятельных в технологии компилятора, пока они не распались. Также бывший работодатель многих сотрудников Microsoft, в том числе Андерс Хейлсберг из С# славы. Было придумано, чтобы аргумент проходил дешевле, передавая некоторые из них через регистры процессора вместо стека. Он не поддерживается в управляемом коде из-за плохой стандартизации.

  • __thiscall - это конвенция вызова, разработанная для кода С++. Очень похож на __cdecl, но он также указывает, как скрытый этот указатель для объекта класса передается методам экземпляра класса. Дополнительная деталь в С++ за пределами C. Хотя это выглядит просто, чтобы реализовать,.NET pinvoke marshaller не поддерживает его. Основная причина, по которой вы не можете вывести код С++. Усложнение не является вызовом, это правильное значение этого указателя. Который может стать очень запутанным из-за поддержки С++ для множественного наследования. Только компилятор С++ может когда-либо выяснить, что именно нужно передать. И только тот же самый компилятор С++, который сгенерировал код для класса С++, разные компиляторы сделали разные варианты того, как реализовать MI и как оптимизировать его.

  • __clrcall является вызывающим соглашением для управляемого кода. Это смесь других, этот указатель передается как __thiscall, оптимизированный аргумент, проходящий как __fastcall, порядок аргументов, такой как __cdecl и очистка вызывающего абонента, как __stdcall. Большим преимуществом управляемого кода является верификатор, встроенный в дрожание. Который гарантирует, что никогда не может быть несовместимости между вызывающим и вызываемым. Это позволяет дизайнерам воспользоваться преимуществами всех этих конвенций, но без багажа неприятностей. Пример того, как управляемый код может оставаться конкурентоспособным с собственным кодом, несмотря на накладные расходы на обеспечение безопасности кода.

Вы упоминаете extern "C", понимая важность этого, важно также пережить взаимодействие. Компиляторы языка часто украшают имена экспортируемой функции дополнительными символами. Также называется "mangling". Это довольно дерьмовый трюк, который никогда не перестает вызывать проблемы. И вам нужно понять это, чтобы определить правильные значения свойств CharSet, EntryPoint и ExactSpelling атрибута [DllImport]. Существует много соглашений:

  • Декорация Windows api. Windows изначально была операционной системой, отличной от Unicode, с использованием 8-битной кодировки для строк. Windows NT стала первой, которая стала Unicode по своему ядру. Это вызвало довольно серьезную проблему совместимости, старый код не смог бы работать в новых операционных системах, поскольку он передавал 8-битные кодированные строки для функций winapi, ожидающих кодировку Unicode, кодированную utf-16. Они решили это, написав две версии каждой функции winapi. Один, который принимает 8-битные строки, другой, который принимает строки Unicode. И выделили между ними путем склеивания буквы A в конце имени устаревшей версии (A = Ansi) и W в конце новой версии (W = wide). Ничего не добавляется, если функция не принимает строку. Маршрутизатор pinvoke обрабатывает это автоматически без вашей помощи, он просто попытается найти все 3 возможные версии. Однако вы должны всегда указывать CharSet.Auto(или Unicode), накладные расходы унаследованной функции, перевод строки из Ansi в Unicode, не нужны и теряются.

  • Стандартное украшение для функций __stdcall - _foo @4. Ведущее подчеркивание и постфикс @n, который указывает объединенный размер аргументов. Этот постфикс был разработан, чтобы помочь решить неприятную проблему дисбаланса стека, если вызывающий и вызываемый не согласны с количеством аргументов. Хорошо работает, хотя сообщение об ошибке не очень велико, маршаллер pinvoke скажет вам, что он не может найти точку входа. Примечательно, что Windows при использовании __stdcall не использует это оформление. Это было намеренно, давая программистам возможность получить аргумент GetProcAddress(). Маршрутизатор pinvoke также позаботится об этом автоматически, сначала пытаясь найти точку входа с постфиналом @n, затем попробуйте один из них.

  • Стандартным украшением для функции __cdecl является _foo. Единственное подчеркивание. Маршрутизатор pinvoke сортирует это автоматически. К сожалению, необязательный постфикс @n для __stdcall не позволяет ему сообщать вам, что ваше свойство CallingConvention ошибочно, большая потеря.

  • Компиляторы С++ используют манипуляции с именами, создавая действительно причудливые имена, такие как "?? 2 @YAPAXI @Z", экспортированное имя для "operator new". Это было необходимым злом из-за поддержки перегрузки функций. И он изначально был разработан как препроцессор, который использовал устаревшую инструментарию на языке C для создания программы. Из-за чего было необходимо различать, скажем, перегрузку void foo(char) и a void foo(int), задавая им разные имена. Здесь синтаксис extern "C" вступает в игру, он сообщает компилятору С++ не применять привязку имени к имени функции. Большинство программистов, которые пишут код взаимодействия, намеренно используют его, чтобы сделать декларацию на другом языке легче писать. На самом деле это ошибка, украшение очень полезно, чтобы поймать несоответствия. Вы должны использовать файл компоновщика .map или утилиту Dumpbin.exe/exports, чтобы увидеть декорированные имена. Утилита undname.exe SDK очень удобна для преобразования исковеренного имени в исходное объявление С++.

Таким образом, это должно очистить свойства. Вы используете EntryPoint для указания точного имени экспортируемой функции, которая может быть не совсем подходящей для того, что вы хотите назвать ее в своем собственном коде, особенно для испорченных имен С++. И вы используете ExactSpelling, чтобы сообщить маркеру pinvoke не пытаться найти альтернативные имена, потому что вы уже дали правильное имя.

Теперь я буду кормить свою судорогу. Ответ на заголовок вопроса должен быть ясным, Stdcall по умолчанию, но является несоответствием для кода, написанного на C или С++. И ваше объявление [DllImport] несовместимо. Это должно вызвать предупреждение в отладчике из PInvokeStackImbalance Managed Debugger Assistant, расширение отладчика, которое предназначено для обнаружения плохих объявлений. И, скорее всего, случайным образом разбивает ваш код, особенно в сборке Release. Убедитесь, что вы не выключили MDA.

Ответ 2

cdecl и stdcall являются действительными и могут использоваться между С++ и .NET, но они должны согласовываться между двумя неуправляемыми и управляемыми мирами. Таким образом, ваше объявление С# для InvokedFunction недействительно. Должно быть stdcall. В примере MSDN приведены два разных примера: один с stdcall (MessageBeep) и один с cdecl (printf). Они не связаны.