c # — Почему вызовы Cdecl часто не совпадают в «стандартном» предложении? P / Invoke Convention?

Я работаю над довольно большой кодовой базой, в которой функциональность 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 в С ++?

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

49

Решение

Это часто встречается в 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 ++. Усложнение — это не соглашение о вызовах, а правильная ценность этот указатель. Который может стать очень запутанным из-за поддержки множественного наследования в 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.

139

Другие решения

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

7