programing

Cdecl 호출이 "표준"P / Invoke 규칙에서 종종 일치하지 않는 이유는 무엇입니까?

nicescript 2021. 1. 17. 10:34
반응형

Cdecl 호출이 "표준"P / Invoke 규칙에서 종종 일치하지 않는 이유는 무엇입니까?


저는 C ++ 기능이 C #에서 P / Invoked되는 다소 큰 코드베이스에서 작업하고 있습니다.

우리 코드베이스에는 다음과 같은 많은 호출이 있습니다.

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.CdeclC # 모두에 있습니다. 왜 그렇지 CallingConvention.Stdcall않습니까? 아니면 왜 __stdcallC ++에 있습니까?

미리 감사드립니다!


이것은 SO 질문에서 반복적으로 발생하므로 이것을 (긴) 참조 답변으로 바꾸려고 노력할 것입니다. 32 비트 코드는 호환되지 않는 호출 규칙의 오랜 역사를 가지고 있습니다. 오래 전에는 의미가 있었지만 오늘날 대부분 후부에서 큰 고통을주는 함수 호출을 만드는 방법에 대한 선택. 64 비트 코드에는 하나의 호출 규칙 만 있으며, 다른 하나를 추가하려는 사람은 남 대서양의 작은 섬으로 보내질 것입니다.

Wikipedia 기사 에있는 내용을 넘어서 그 역사와 관련성에 주석을 달려고합니다 . 시작점은 함수 호출 방법에서 선택해야 할 사항이 인수를 전달할 순서, 인수를 저장할 위치 및 호출 후 정리하는 방법입니다.

  • __stdcall16 비트 Windows 및 OS / 2에서 사용되는 이전의 16 비트 파스칼 호출 규칙을 통해 Windows 프로그래밍에 적용되었습니다. 모든 Windows API 함수와 COM에서 사용하는 규칙입니다. 대부분의 pinvoke는 OS 호출을위한 것이기 때문에 [DllImport] 속성에 명시 적으로 지정하지 않으면 Stdcall이 기본값입니다. 존재의 유일한 이유는 수신자가 정리하도록 지정하기 때문입니다. 640 킬로바이트의 RAM에서 GUI 운영 체제를 짜 내야했던 시절에 매우 중요한 코드를 더 간결하게 생성합니다. 가장 큰 단점은 위험하다는 것 입니다. 호출자가 함수에 대한 인수라고 가정하는 것과 호출 수신자가 구현 한 것 사이의 불일치로 인해 스택이 불균형하게됩니다. 차례로 충돌을 진단하기가 매우 어려울 수 있습니다.

  • __cdeclC 언어로 작성된 코드에 대한 표준 호출 규칙입니다. 존재의 주된 이유는 가변 개수의 인수로 함수 호출을 지원하기 때문입니다. printf () 및 scanf ()와 같은 함수를 사용하는 C 코드에서 일반적입니다. 실제로 몇 개의 인수가 전달되었는지 아는 것은 호출자이기 때문에 정리하는 것은 호출 자라는 부작용이 있습니다. CallingConvention = [DllImport] 선언에서 CallingConvention.Cdecl을 잊어 버리는 것은 매우 일반적인 버그입니다.

  • __fastcall상호 호환되지 않는 선택과 함께 상당히 잘못 정의 된 호출 규칙입니다. 한때 컴파일러 기술에 매우 영향력이 있던 회사였던 볼랜드 컴파일러에서는 해체 될 때까지 일반적이었습니다. 또한 C # 명성의 Anders Hejlsberg를 포함하여 많은 Microsoft 직원의 전 고용주입니다. 스택 대신 CPU 레지스터를 통해 일부전달하여 인수 전달을 더 저렴하게 만들기 위해 발명되었습니다 . 표준화 불량으로 인해 관리 코드에서는 지원되지 않습니다.

  • __thiscallC ++ 코드를 위해 고안된 호출 규칙입니다. __cdecl과 매우 유사하지만 클래스 객체에 대한 숨겨진 this 포인터가 클래스의 인스턴스 메서드에 전달되는 방법도 지정합니다 . C를 넘어서는 C ++의 추가 세부 사항. 구현하기는 간단 해 보이지만 .NET pinvoke marshaller는 이를 지원 하지 않습니다 . C ++ 코드를 고정 할 수없는 주된 이유입니다. 복잡함은 호출 규칙이 아니라 this 포인터 의 적절한 값입니다 . C ++의 다중 상속 지원으로 인해 매우 복잡해질 수 있습니다. 오직 C ++ 컴파일러 만이 정확히 무엇이 전달되어야하는지 알아낼 수 있습니다. 그리고 C ++ 클래스에 대한 코드를 생성 한 똑같은 C ++ 컴파일러 만이 MI를 구현하는 방법과 최적화하는 방법에 대해 다른 선택을했습니다.

  • __clrcall관리 코드에 대한 호출 규칙입니다. 포인터는 __thiscall처럼 전달되는 포인터, __fastcall과 같은 최적화 된 인수 전달, __cdecl과 같은 인수 순서 및 __stdcall과 같은 호출자 정리 의 혼합입니다 . 관리 코드의 가장 큰 장점은 지터에 내장 된 검증 도구 입니다. 이는 발신자와 수신자 사이에 비 호환성이 절대 없을 수 있도록합니다. 따라서 디자이너는 이러한 모든 규칙의 이점을 문제없이 사용할 수 있습니다. 코드를 안전하게 만드는 오버 헤드에도 불구하고 관리 코드가 네이티브 코드와 경쟁력을 유지할 수있는 방법의 예입니다.

당신은 언급 extern "C"아니라 상호 운용성 생존을 위해로서 그 중요성이 중요 이해. 언어 컴파일러는 종종 추가 문자로 내 보낸 함수의 이름을 장식 합니다. "이름 맹 글링"이라고도합니다. 그것은 문제를 일으키는 것을 멈추지 않는 꽤 형편없는 속임수입니다. 그리고 [DllImport] 특성의 CharSet, EntryPoint 및 ExactSpelling 속성의 적절한 값을 결정하려면이를 이해해야합니다. 많은 규칙이 있습니다.

  • Windows API 장식. Windows는 원래 문자열에 8 비트 인코딩을 사용하는 비 유니 코드 운영 체제였습니다. Windows NT는 핵심에서 유니 코드가 된 최초의 제품입니다. 그것은 다소 큰 호환성 문제를 일으켰고, 이전 코드는 utf-16 인코딩 된 유니 코드 문자열을 예상하는 winapi 함수에 8 비트 인코딩 된 문자열을 전달하기 때문에 새로운 운영 체제에서 실행할 수 없었을 것입니다. 그들은 두 개 를 써서 이것을 해결했습니다.모든 winapi 함수의 버전. 하나는 8 비트 문자열을 사용하고 다른 하나는 유니 코드 문자열을 사용합니다. 그리고 레거시 버전의 이름 끝에 문자 A (A = Ansi)를 붙이고 새 버전의 끝에 W (W = 와이드)를 붙여 두 가지를 구분합니다. 함수가 문자열을받지 않으면 아무것도 추가되지 않습니다. 핀 보크 마샬 러는 사용자의 도움없이 자동으로이를 처리하며, 가능한 3 가지 버전을 모두 찾으려고합니다. 그러나 항상 CharSet.Auto (또는 유니 코드)를 지정해야합니다. Ansi에서 유니 코드로 문자열을 변환하는 레거시 함수의 오버 헤드는 불필요하고 손실이 많습니다.

  • __stdcall 함수의 표준 장식은 _foo @ 4입니다. 인수의 결합 된 크기를 나타내는 선행 밑줄 및 @n 접미사. 이 접미사는 호출자와 호출 수신자가 인수 수에 동의하지 않는 경우 불쾌한 스택 불균형 문제를 해결하는 데 도움이되도록 설계되었습니다. 잘 작동하지만 오류 메시지는 좋지 않지만 pinvoke marshaller는 진입 점을 찾을 수 없다고 알려줍니다. 주목할 점은 Windows가 __stdcall을 사용하는 동안 이 장식을 사용 하지 않는다는 것입니다. 이는 의도적으로 프로그래머에게 GetProcAddress () 인수를 올바르게 가져올 수있는 기회를 제공했습니다. pinvoke marshaller는 또한 이것을 자동으로 처리합니다. 먼저 @n 접미사를 사용하여 진입 점을 찾고 다음에는없는 항목을 시도합니다.

  • __cdecl 함수의 표준 장식은 _foo입니다. 단일 선행 밑줄. pinvoke marshaller는 이것을 자동으로 분류합니다. 슬프게도 __stdcall에 대한 선택적 @n 접미사는 CallingConvention 속성이 잘못되었음을 알리는 것을 허용하지 않습니다.

  • C ++ 컴파일러는 이름 맹 글링을 사용하여 "operator new"의 내 보낸 이름 인 "?? 2 @ YAPAXI @ Z"와 같이 정말 기이하게 보이는 이름을 생성합니다. 이것은 함수 과부하에 대한 지원으로 인해 필요한 악이었습니다. 그리고 원래는 레거시 C 언어 도구를 사용하여 프로그램을 빌드하는 전처리기로 설계되었습니다. 따라서 a void foo(char)void foo(int)과부하를 서로 다른 이름 으로 구분해야했습니다 . 곳이다 extern "C"구문은 놀이에 와서는로 C ++ 컴파일러를 알려줍니다, 하지함수 이름에 이름 맹 글링을 적용합니다. interop 코드를 작성하는 대부분의 프로그래머는 다른 언어로 선언을 더 쉽게 작성하기 위해 의도적으로이를 사용합니다. 실제로 실수입니다. 장식은 불일치를 잡는 데 매우 유용합니다. 링커의 .map 파일 또는 Dumpbin.exe / exports 유틸리티를 사용하여 장식 된 이름을 확인합니다. undname.exe SDK 유틸리티는 잘린 이름을 원래 C ++ 선언으로 다시 변환하는 데 매우 편리합니다.

따라서 이것은 속성을 정리해야합니다. EntryPoint를 사용하여 내 보낸 함수의 정확한 이름을 제공합니다.이 이름은 특히 C ++ 이름이 변경된 경우 자신의 코드에서 호출하려는 것과 일치하지 않을 수 있습니다. 그리고 ExactSpelling을 사용하여 pinvoke marshaller에게 이미 올바른 이름을 제공했기 때문에 대체 이름을 찾으려고하지 않도록 지시합니다.

I'll nurse my writing cramp for a while now. The answer to your question title should be clear, Stdcall is the default but is a mismatch for code written in C or C++. And your [DllImport] declaration is not compatible. This should produce a warning in the debugger from the PInvokeStackImbalance Managed Debugger Assistant, a debugger extension that was designed to detect bad declarations. And can rather randomly crash your code, particularly in the Release build. Make sure you didn't turn the MDA off.


cdecl and stdcall are both valid and usable between C++ and .NET, but they should consistent between the two unmanaged and managed worlds. So your C# declaration for InvokedFunction is invalid. Should be stdcall. The MSDN sample just gives two different examples, one with stdcall (MessageBeep), and one with cdecl (printf). They are unrelated.

ReferenceURL : https://stackoverflow.com/questions/15660722/why-are-cdecl-calls-often-mismatched-in-the-standard-p-invoke-convention

반응형