Почему вызовы Cdecl часто не совпадают в "стандартной" конвенции P/Invoke?
Я работаю над довольно большой кодовой базой, в которой функциональность C++ вызывается из C#.
В нашей кодовой базе много вызовов, таких как...
C++:
extern "C" int __stdcall InvokedFunction(int);
С соответствующим C#:
[DllImport("CPlusPlus.dll", ExactSpelling = true, SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern int InvokedFunction(IntPtr intArg);
Я обыскивал сеть (насколько я в состоянии) для рассуждений о том, почему существует это очевидное несоответствие. Например, почему в C# есть Cdecl, а в C++ __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"
в коде C++, и CallingConvention.Cdecl
в C#. Почему нет CallingConvention.Stdcall
? Или, кроме того, почему там __stdcall
в С ++?
Заранее спасибо!
2 ответа
Это повторяется в SO вопросах, я постараюсь превратить это в (длинный) справочный ответ. 32-битный код обременен длинной историей несовместимых соглашений о вызовах. Выбор того, как сделать вызов функции, который имел смысл давным-давно, но сегодня в основном является огромной болью в тыловой части. У 64-битного кода есть только одно соглашение о вызовах, кто бы ни добавил другое, его отправят на маленький остров в Южной Атлантике.
Я постараюсь аннотировать эту историю и их актуальность помимо того, что в статье в Википедии. Отправной точкой является то, что при выборе вызова функции следует выбирать порядок передачи аргументов, места хранения аргументов и способы очистки после вызова.
__stdcall
нашла свое отражение в программировании Windows благодаря старому соглашению о 16-битных вызовах Паскаля, используемому в 16-битных Windows и OS/2. Это соглашение используется всеми функциями API Windows, а также COM. Поскольку большинство pinvoke предназначались для вызовов ОС, Stdcall используется по умолчанию, если вы не укажете это явно в атрибуте [DllImport]. Его единственная причина существования заключается в том, что он указывает, что вызываемый абонент убирает. Который производит более компактный код, что очень важно еще во времена, когда им приходилось втискивать операционную систему с графическим интерфейсом в 640 килобайт оперативной памяти. Его самый большой недостаток в том, что этоопасно. Несоответствие между тем, что предполагает вызывающая сторона, является аргументами для функции, и тем, что реализованный вызываемый объект вызывает дисбаланс в стеке. Что, в свою очередь, может привести к чрезвычайно сложным диагностикам сбоев.__cdecl
стандартное соглашение о вызовах для кода, написанного на языке C. Его основная причина существования заключается в том, что он поддерживает выполнение вызовов функций с переменным числом аргументов. Обычный в C-коде с такими функциями, как printf() и scanf(). С побочным эффектом, так как именно вызывающая сторона знает, сколько аргументов было фактически передано, именно вызывающая сторона очищает. Забывание CallingConvention = CallingConvention.Cdecl в объявлении [DllImport] являетсяочень распространенной ошибкой.__fastcall
довольно плохо определенное соглашение о вызовах с несовместимыми вариантами. Это было обычным явлением в компиляторах Borland, когда-то очень влиятельных в технологии компиляторов, пока они не распались. Также бывший работодатель многих сотрудников Microsoft, в том числе Андерс Хейлсберг из C# Fame. Это было изобретено, чтобы сделать передачу аргументов более дешевой, передаваянекоторые из них через регистры процессора вместо стека. Это не поддерживается в управляемом коде из-за плохой стандартизации.__thiscall
это соглашение о вызовах, разработанное для кода C++. Очень похоже на __cdecl, но также указывает, как скрытый указатель для объекта класса передается в методы экземпляра класса. Дополнительные подробности в C++, помимо C. Хотя это выглядит простым в реализации, маршаллер.NET pinvoke не поддерживает его. Основная причина, по которой вы не можете закрепить код C++. Сложность не в соглашении о вызовах, а в правильном значении указателя this. Который может стать очень запутанным из-за поддержки множественного наследования в C++. Только компилятор C++ может понять, что именно нужно передать. И только один и тот же компилятор C++, сгенерировавший код для класса C++, разные компиляторы сделали разные выборы относительно того, как реализовать MI и как его оптимизировать.__clrcall
это соглашение о вызовах для управляемого кода. Это смесь других, этот указатель передается как __thiscall, оптимизированный аргумент передается как __fastcall, порядок аргументов как __cdecl и очистка вызывающей стороны как __stdcall. Большим преимуществом управляемого кода является встроенный в джиттер верификатор. Что гарантирует, что между вызывающим абонентом и вызываемым абонентом не может быть несовместимости. Таким образом, позволяя дизайнерам воспользоваться преимуществами всех этих конвенций, но без лишних хлопот. Пример того, как управляемый код может оставаться конкурентоспособным по отношению к нативному коду, несмотря на накладные расходы по обеспечению безопасности кода.
Вы упоминаете extern "C"
Понимание значения этого также важно для выживания при взаимодействии. Компиляторы языка часто украшают имена экспортируемых функций дополнительными символами. Также называется "искажение имени". Это довольно дурацкий трюк, который никогда не перестает доставлять неприятности. И вам нужно понять это, чтобы определить правильные значения свойств 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 неправильное, большая потеря.
Компиляторы C++ используют искажение имен, производя действительно причудливые имена, такие как "??2@YAPAXI@Z", экспортированное имя для "operator new". Это было неизбежное зло из-за поддержки перегрузки функций. И изначально он был разработан в качестве препроцессора, который использовал инструменты языка C для создания программы. Что сделало необходимым провести различие между, скажем,
void foo(char)
иvoid foo(int)
перегружать, давая им разные имена. Это гдеextern "C"
вступает в игру синтаксис, он говорит компилятору C++ не применять искажение имени к имени функции. Большинство программистов, которые пишут код взаимодействия, намеренно используют его для облегчения написания декларации на другом языке. Что на самом деле является ошибкой, украшение очень полезно для выявления несоответствий. Вы бы использовали.map файл компоновщика или утилиту Dumpbin.exe /exports, чтобы увидеть оформленные имена. Утилита undname.exe SDK очень удобна для преобразования искаженного имени обратно в исходное объявление C++.
Так что это должно прояснить свойства. Вы используете EntryPoint, чтобы дать точное имя экспортируемой функции, которая может не подходить для того, что вы хотите назвать в своем собственном коде, особенно для искаженных имен C++. И вы используете ExactSpelling, чтобы сказать маршаллеру pinvoke не пытаться найти альтернативные имена, потому что вы уже дали правильное имя.
Я буду ухаживать за своей пиской судорогой на некоторое время. Ответ на заголовок вашего вопроса должен быть ясным, Stdcall используется по умолчанию, но не соответствует коду, написанному на C или C++. И ваша декларация [DllImport] не совместима. Это должно привести к появлению предупреждения в отладчике от PInvokeStackImbalance Managed Debugger Assistant, расширения отладчика, которое было разработано для обнаружения некорректных объявлений. И может довольно случайно вывести из строя ваш код, особенно в сборке Release. Убедитесь, что вы не выключили MDA.
cdecl
а также stdcall
оба являются допустимыми и применимыми между C++ и.NET, но они должны согласовываться между двумя неуправляемыми и управляемыми мирами. Поэтому ваше объявление C# для InvokedFunction недопустимо. Должно быть stdcall. В примере MSDN приведены два разных примера: один с stdcall (MessageBeep), а другой с cdecl (printf). Они не связаны.