Почему вызовы Cdecl часто не совпадают в" стандартном " соглашении P/Invoke?

Я работаю над довольно большой кодовой базой, в которой функциональность c++ p / вызывается из 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);

из 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 в C++?

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

2 ответов


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

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

  • __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 compilers, компании, когда-то очень влиятельной в технологии компиляторов, пока они не распались. Также бывший работодатель многих Сотрудники Microsoft, включая Андерса Хейлсберга из C# fame. Он был изобретен, чтобы сделать аргумент, проходящий дешевле, проходя некоторые из них через регистры CPU вместо стека. Он не поддерживается в управляемом коде из-за плохой стандартизации.

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

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

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

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

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

  • стандартное украшение для функции _ _ cdecl-_foo. Одного подчеркивания. Маршаллер pinvoke сортирует это автоматически. К сожалению, необязательный @ N postfix для __stdcall не позволяет ему сказать вам, что ваше свойство CallingConvention неверно, большая потеря.

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

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

Я буду нянчиться со своей писательской судорогой некоторое время. Ответ на ваш вопрос должен быть ясным, Stdcall по умолчанию, но является несоответствием для кода, написанного на C или c++. И ваша декларация [DllImport] -не совместимость. Это должно привести к появлению предупреждения в отладчике из помощника отладчика PInvokeStackImbalance Managed, расширения отладчика, предназначенного для обнаружения неисправностей декларативные заявления. И может довольно случайно разбить ваш код, особенно в сборке выпуска. Убедитесь, что вы не выключили MDA.


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