클래스 개체, 특히 STL 개체를 C ++ DLL과주고받는 방법은 무엇입니까?
내 응용 프로그램은 DLL 파일 형식의 타사 플러그인과 상호 작용해야하며 이러한 플러그인이 빌드되는 컴파일러를 제어 할 수 없습니다. STL 개체에 대해 보장 된 ABI가 없다는 것을 알고 있으며 내 응용 프로그램이 불안정해질 수 있습니다.
클래스 개체, 특히 STL 개체를 C ++ DLL과주고받는 방법은 무엇입니까?
내 응용 프로그램은 DLL 파일 형식의 타사 플러그인과 상호 작용해야하며 이러한 플러그인이 빌드되는 컴파일러를 제어 할 수 없습니다. STL 개체에 대해 보장 된 ABI가 없다는 것을 알고 있으며 내 응용 프로그램이 불안정해질 수 있습니다.
답변:
이 질문에 대한 짧은 대답은 하지 않습니다 . 표준 C ++ ABI (애플리케이션 바이너리 인터페이스, 호출 규칙, 데이터 패킹 / 정렬, 유형 크기 등 의 표준)가 없기 때문에 클래스를 처리하는 표준 방법을 시도하고 시행하려면 많은 수고를 거쳐야합니다. 프로그램의 개체. 이러한 모든 단계를 뛰어 넘은 후에 작동 할 것이라는 보장도 없으며 한 컴파일러 릴리스에서 작동하는 솔루션이 다음 버전에서 작동 할 것이라는 보장도 없습니다.
extern "C"
C ABI 는 잘 정의되고 안정적 이므로을 사용하여 일반 C 인터페이스를 만드십시오 .
당신이 정말로, 경우 실제로 는 DLL 경계를 넘어 C ++ 객체를 전달하려면, 그것은 기술적으로 가능합니다. 고려해야 할 몇 가지 요소는 다음과 같습니다.
데이터 패킹 / 정렬
주어진 클래스 내에서 개별 데이터 멤버는 일반적으로 메모리에 특별히 배치되므로 해당 주소는 유형 크기의 배수에 해당합니다. 예를 들어 int
는 4 바이트 경계로 정렬 될 수 있습니다.
DLL이 EXE가 아닌 다른 컴파일러로 컴파일 된 경우 지정된 클래스의 DLL 버전이 EXE 버전과 다른 패킹을 가질 수 있으므로 EXE가 클래스 개체를 DLL에 전달할 때 DLL이 파일에 제대로 액세스하지 못할 수 있습니다. 해당 클래스 내에서 주어진 데이터 멤버. DLL은 EXE의 정의가 아닌 클래스의 자체 정의에 지정된 주소에서 읽기를 시도하며 원하는 데이터 멤버가 실제로 거기에 저장되지 않기 때문에 가비지 값이 발생합니다.
#pragma pack
컴파일러가 특정 패킹을 적용하도록하는 전 처리기 지시문을 사용하여이 문제를 해결할 수 있습니다 . 컴파일러가 선택한 것보다 큰 팩 값을 선택하면 컴파일러는 여전히 기본 패킹을 적용 하므로 큰 패킹 값을 선택하면 클래스가 컴파일러간에 다른 패킹을 가질 수 있습니다. 이에 대한 해결책은를 사용 #pragma pack(1)
하는 것입니다. 그러면 컴파일러가 1 바이트 경계에서 데이터 멤버를 정렬하도록 강제합니다 (기본적으로 패킹이 적용되지 않음). 이는 성능 문제를 일으키거나 특정 시스템에서 충돌을 일으킬 수 있으므로 좋은 생각이 아닙니다. 그러나, 그것은 것입니다 클래스의 데이터 멤버가 메모리에 정렬되는 방식의 일관성을 보장 할 수 있습니다.
회원 재정렬
클래스가 standard-layout 이 아닌 경우 컴파일러 는 메모리에서 데이터 멤버를 재정렬 할 수 있습니다 . 이것이 수행되는 방법에 대한 표준이 없으므로 데이터 재 배열로 인해 컴파일러간에 비 호환성이 발생할 수 있습니다. 따라서 데이터를 DLL로 앞뒤로 전달하려면 표준 레이아웃 클래스가 필요합니다.
콜링 컨벤션
주어진 함수가 가질 수있는 여러 호출 규칙 이 있습니다. 이러한 호출 규칙은 데이터가 함수에 전달되는 방법을 지정합니다. 매개 변수가 레지스터에 저장됩니까, 스택에 저장됩니까? 인수가 스택에 푸시되는 순서는 무엇입니까? 함수가 완료된 후 스택에 남아있는 인수를 누가 정리합니까?
표준 호출 규칙을 유지하는 것이 중요합니다. 함수 _cdecl
를 C ++의 기본값 인 으로 선언하고 _stdcall
나쁜 일을 사용하여 호출하려고 하면 발생 합니다. _cdecl
그러나 C ++ 함수에 대한 기본 호출 규칙이므로 한곳 _stdcall
에서 를 지정하고 다른 곳 에서을 지정하여 의도적으로 중단하지 않는 한 중단되지 않습니다 _cdecl
.
데이터 유형 크기
이 문서 에 따르면 Windows에서 대부분의 기본 데이터 유형은 앱이 32 비트인지 64 비트인지에 관계없이 동일한 크기를 갖습니다. 그러나 주어진 데이터 유형의 크기는 표준이 아닌 컴파일러에 의해 적용되므로 (모든 표준 보장은 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
) 가능하면 데이터 유형 크기 호환성을 보장하기 위해 고정 크기 데이터 유형 을 사용하는 것이 좋습니다 .
힙 문제
DLL이 EXE와 다른 버전의 C 런타임에 연결되는 경우 두 모듈은 다른 힙을 사용 합니다. 이것은 모듈이 다른 컴파일러로 컴파일되고 있다는 점에서 특히 가능성이 높은 문제입니다.
이를 완화하려면 모든 메모리를 공유 힙에 할당하고 동일한 힙에서 할당 해제해야합니다. 다행히 Windows는이를 지원하는 API를 제공합니다. GetProcessHeap 을 사용하면 호스트 EXE의 힙에 액세스 할 수 있고 HeapAlloc / HeapFree를 사용 하면이 힙 내에서 메모리를 할당하고 해제 할 수 있습니다. 당신이 정상적인 사용하지 않는 것이 중요하다 malloc
/를 free
그들은 당신이 기대하는 방식으로 작동한다는 보장이 없기 때문에.
STL 문제
C ++ 표준 라이브러리에는 자체 ABI 문제가 있습니다. 없다 보장 주어진 STL 유형이 메모리에 같은 방식으로 배치되는이 없으며,이 특히 (주어진 STL 클래스는 다른 하나의 구현에서 같은 크기를 가지고있는 보장, 디버그는에 추가 디버그 정보를 넣을 수 있습니다 구축 주어진 STL 유형). 따라서 모든 STL 컨테이너는 DLL 경계를 통과하여 다른 쪽에서 다시 포장되기 전에 기본 유형으로 압축을 풀어야합니다.
이름 맹 글링
DLL은 아마도 EXE가 호출하려는 함수를 내보낼 것입니다. 그러나 C ++ 컴파일러 에는 함수 이름을 변경하는 표준 방법이 없습니다 . 이것은 명명 된 함수 가 GCC 및 MSVC 에서 GetCCDLL
엉망이 될 수 있음을 의미합니다 ._Z8GetCCDLLv
?GetCCDLL@@YAPAUCCDLL_v1@@XZ
GCC로 생성 된 DLL은 .lib 파일을 생성하지 않고 MSVC에서 DLL을 정적으로 연결하려면 DLL이 필요하기 때문에 이미 DLL에 대한 정적 링크를 보장 할 수 없습니다. 동적 연결은 훨씬 깔끔한 옵션처럼 보이지만 이름 변경이 방해가됩니다 GetProcAddress
. 잘못된 이름 을 입력하려고 하면 호출이 실패하고 DLL을 사용할 수 없습니다. 이 작업을 수행하려면 약간의 해커가 필요하며 DLL 경계를 넘어 C ++ 클래스를 전달하는 것이 나쁜 생각 인 상당히 중요한 이유입니다.
DLL을 빌드 한 다음 생성 된 .def 파일 (생성 된 경우, 프로젝트 옵션에 따라 달라짐)을 검사하거나 Dependency Walker와 같은 도구를 사용하여 잘린 이름을 찾아야합니다. 그런 다음 자신의 .def 파일 을 작성하여 엉킨 함수에 대한 얽 히지 않은 별칭을 정의해야합니다. 예를 들어 GetCCDLL
조금 더 위에서 언급 한 함수를 사용하겠습니다 . 내 시스템에서 다음 .def 파일은 각각 GCC 및 MSVC에서 작동합니다.
GCC :
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC :
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
DLL을 다시 빌드 한 다음 내보내는 함수를 다시 검사하십시오. 얽 히지 않은 함수 이름이 그 사이에 있어야합니다. 이런 식으로 오버로드 된 함수를 사용할 수 없다는 점에 유의 하십시오. 변형 되지 않은 함수 이름은 변형 된 이름으로 정의 된 특정 함수 오버로드에 대한 별칭입니다 . 또한 함수 선언을 변경할 때마다 DLL에 대한 새 .def 파일을 만들어야합니다. 뭉쳐진 이름이 변경되기 때문입니다. 가장 중요한 것은 이름 맹 글링을 우회함으로써 비 호환성 문제와 관련하여 링커가 제공하려는 모든 보호를 무시하는 것입니다.
DLL의 모든 함수에 대한 별칭을 만들 필요없이 별칭을 정의 할 함수가 하나만 있기 때문에 DLL이 따를 인터페이스 를 만들면 이 전체 프로세스가 더 간단합니다 . 그러나 동일한 경고가 여전히 적용됩니다.
함수에 클래스 객체 전달
이것은 아마도 크로스 컴파일러 데이터 전달을 괴롭히는 문제 중 가장 미묘하고 가장 위험한 문제 일 것입니다. 다른 모든 것을 처리하더라도 인수가 함수에 전달되는 방식에 대한 표준은 없습니다 . 이로 인해 명백한 이유없이 쉽게 디버깅 할 수없는 미묘한 충돌 이 발생할 수 있습니다 . 반환 값에 대한 버퍼를 포함하여 모든 인수를 포인터를 통해 전달해야 합니다. 이것은 서투르고 불편하며 작동하거나 작동하지 않을 수있는 또 다른 해키 해결 방법입니다.
이러한 모든 해결 방법을 모으고 템플릿 및 연산자를 사용하여 창의적인 작업 을 수행하면 DLL 경계를 넘어 안전하게 개체를 전달할 수 있습니다. #pragma pack
및 그 변형에 대한 지원 과 마찬가지로 C ++ 11 지원은 필수입니다 . MSVC 2013은 GCC 및 clang의 최신 버전과 마찬가지로이 지원을 제공합니다.
//POD_base.h: defines a template base class that wraps and unwraps data types for safe passing across compiler boundaries
//define malloc/free replacements to make use of Windows heap APIs
namespace pod_helpers
{
void* pod_malloc(size_t size)
{
HANDLE heapHandle = GetProcessHeap();
HANDLE storageHandle = nullptr;
if (heapHandle == nullptr)
{
return nullptr;
}
storageHandle = HeapAlloc(heapHandle, 0, size);
return storageHandle;
}
void pod_free(void* ptr)
{
HANDLE heapHandle = GetProcessHeap();
if (heapHandle == nullptr)
{
return;
}
if (ptr == nullptr)
{
return;
}
HeapFree(heapHandle, 0, ptr);
}
}
//define a template base class. We'll specialize this class for each datatype we want to pass across compiler boundaries.
#pragma pack(push, 1)
// All members are protected, because the class *must* be specialized
// for each type
template<typename T>
class pod
{
protected:
pod();
pod(const T& value);
pod(const pod& copy);
~pod();
pod<T>& operator=(pod<T> value);
operator T() const;
T get() const;
void swap(pod<T>& first, pod<T>& second);
};
#pragma pack(pop)
//POD_basic_types.h: holds pod specializations for basic datatypes.
#pragma pack(push, 1)
template<>
class pod<unsigned int>
{
//these are a couple of convenience typedefs that make the class easier to specialize and understand, since the behind-the-scenes logic is almost entirely the same except for the underlying datatypes in each specialization.
typedef int original_type;
typedef std::int32_t safe_type;
public:
pod() : data(nullptr) {}
pod(const original_type& value)
{
set_from(value);
}
pod(const pod<original_type>& copyVal)
{
original_type copyData = copyVal.get();
set_from(copyData);
}
~pod()
{
release();
}
pod<original_type>& operator=(pod<original_type> value)
{
swap(*this, value);
return *this;
}
operator original_type() const
{
return get();
}
protected:
safe_type* data;
original_type get() const
{
original_type result;
result = static_cast<original_type>(*data);
return result;
}
void set_from(const original_type& value)
{
data = reinterpret_cast<safe_type*>(pod_helpers::pod_malloc(sizeof(safe_type))); //note the pod_malloc call here - we want our memory buffer to go in the process heap, not the possibly-isolated DLL heap.
if (data == nullptr)
{
return;
}
new(data) safe_type (value);
}
void release()
{
if (data)
{
pod_helpers::pod_free(data); //pod_free to go with the pod_malloc.
data = nullptr;
}
}
void swap(pod<original_type>& first, pod<original_type>& second)
{
using std::swap;
swap(first.data, second.data);
}
};
#pragma pack(pop)
이 pod
클래스는 모든 기본 데이터 유형에 특화되어 있으므로 int
자동으로에 래핑되고 int32_t
에 uint
래핑됩니다 uint32_t
.이 모든 작업은 오버로드 =
및 ()
연산자 덕분에이면에서 발생합니다 . 그들은합니다 (기본 데이터 유형을 제외하고 거의 같은 것 때문에 나는 기본 유형 전문의 나머지 부분을 생략 한 bool
그것이로 변환 이후 전문화, 여분의 논리의 약간을 가지고 int8_t
다음이 int8_t
로 다시 변환하는 0과 비교된다 bool
, 그러나 이것은 상당히 사소한 것입니다).
약간의 추가 작업이 필요하지만 이러한 방식으로 STL 유형을 래핑 할 수도 있습니다.
#pragma pack(push, 1)
template<typename charT>
class pod<std::basic_string<charT>> //double template ftw. We're specializing pod for std::basic_string, but we're making this specialization able to be specialized for different types; this way we can support all the basic_string types without needing to create four specializations of pod.
{
//more comfort typedefs
typedef std::basic_string<charT> original_type;
typedef charT safe_type;
public:
pod() : data(nullptr) {}
pod(const original_type& value)
{
set_from(value);
}
pod(const charT* charValue)
{
original_type temp(charValue);
set_from(temp);
}
pod(const pod<original_type>& copyVal)
{
original_type copyData = copyVal.get();
set_from(copyData);
}
~pod()
{
release();
}
pod<original_type>& operator=(pod<original_type> value)
{
swap(*this, value);
return *this;
}
operator original_type() const
{
return get();
}
protected:
//this is almost the same as a basic type specialization, but we have to keep track of the number of elements being stored within the basic_string as well as the elements themselves.
safe_type* data;
typename original_type::size_type dataSize;
original_type get() const
{
original_type result;
result.reserve(dataSize);
std::copy(data, data + dataSize, std::back_inserter(result));
return result;
}
void set_from(const original_type& value)
{
dataSize = value.size();
data = reinterpret_cast<safe_type*>(pod_helpers::pod_malloc(sizeof(safe_type) * dataSize));
if (data == nullptr)
{
return;
}
//figure out where the data to copy starts and stops, then loop through the basic_string and copy each element to our buffer.
safe_type* dataIterPtr = data;
safe_type* dataEndPtr = data + dataSize;
typename original_type::const_iterator iter = value.begin();
for (; dataIterPtr != dataEndPtr;)
{
new(dataIterPtr++) safe_type(*iter++);
}
}
void release()
{
if (data)
{
pod_helpers::pod_free(data);
data = nullptr;
dataSize = 0;
}
}
void swap(pod<original_type>& first, pod<original_type>& second)
{
using std::swap;
swap(first.data, second.data);
swap(first.dataSize, second.dataSize);
}
};
#pragma pack(pop)
이제 이러한 포드 유형을 사용하는 DLL을 만들 수 있습니다. 먼저 인터페이스가 필요하므로 맹 글링을 파악하는 방법은 하나뿐입니다.
//CCDLL.h: defines a DLL interface for a pod-based DLL
struct CCDLL_v1
{
virtual void ShowMessage(const pod<std::wstring>* message) = 0;
};
CCDLL_v1* GetCCDLL();
이것은 DLL과 모든 호출자가 사용할 수있는 기본 인터페이스를 생성합니다. 우리는 자체가 pod
아니라에 포인터를 전달하고 있음을 유의하십시오 pod
. 이제 DLL 측에서 구현해야합니다.
struct CCDLL_v1_implementation: CCDLL_v1
{
virtual void ShowMessage(const pod<std::wstring>* message) override;
};
CCDLL_v1* GetCCDLL()
{
static CCDLL_v1_implementation* CCDLL = nullptr;
if (!CCDLL)
{
CCDLL = new CCDLL_v1_implementation;
}
return CCDLL;
}
이제 ShowMessage
함수를 구현해 보겠습니다 .
#include "CCDLL_implementation.h"
void CCDLL_v1_implementation::ShowMessage(const pod<std::wstring>* message)
{
std::wstring workingMessage = *message;
MessageBox(NULL, workingMessage.c_str(), TEXT("This is a cross-compiler message"), MB_OK);
}
너무 화려하지는 않습니다. 전달 pod
된 내용을 Normal로 복사하여 wstring
메시지 상자에 표시합니다. 결국 이것은 전체 유틸리티 라이브러리가 아닌 POC 일뿐 입니다.
이제 DLL을 빌드 할 수 있습니다. 링커의 이름 맹 글링을 해결하기 위해 특별한 .def 파일을 잊지 마십시오. (참고 : 실제로 빌드하고 실행 한 CCDLL 구조체에는 여기에 제시 한 것보다 더 많은 기능이 있습니다. .def 파일이 예상대로 작동하지 않을 수 있습니다.)
이제 EXE가 DLL을 호출합니다.
//main.cpp
#include "../CCDLL/CCDLL.h"
typedef CCDLL_v1*(__cdecl* fnGetCCDLL)();
static fnGetCCDLL Ptr_GetCCDLL = NULL;
int main()
{
HMODULE ccdll = LoadLibrary(TEXT("D:\\Programming\\C++\\CCDLL\\Debug_VS\\CCDLL.dll")); //I built the DLL with Visual Studio and the EXE with GCC. Your paths may vary.
Ptr_GetCCDLL = (fnGetCCDLL)GetProcAddress(ccdll, (LPCSTR)"GetCCDLL");
CCDLL_v1* CCDLL_lib;
CCDLL_lib = Ptr_GetCCDLL(); //This calls the DLL's GetCCDLL method, which is an alias to the mangled function. By dynamically loading the DLL like this, we're completely bypassing the name mangling, exactly as expected.
pod<std::wstring> message = TEXT("Hello world!");
CCDLL_lib->ShowMessage(&message);
FreeLibrary(ccdll); //unload the library when we're done with it
return 0;
}
그리고 여기에 결과가 있습니다. DLL이 작동합니다. 우리는 과거 STL ABI 문제, 과거 C ++ ABI 문제, 과거 맹 글링 문제에 성공적으로 도달했으며 MSVC DLL은 GCC EXE와 함께 작동합니다.
결론적으로, DLL 경계를 넘어서 C ++ 개체를 반드시 전달 해야하는 경우 에는 이렇게합니다. 그러나 이것 중 어느 것도 귀하의 설정이나 다른 사람의 설정과 함께 작동하지 않을 수도 있습니다. 이 중 어느 것이 든 언제든지 중단 될 수 있으며 소프트웨어가 주요 릴리스가 예정된 전날 중단 될 것입니다. 이 경로는 내가 총을 맞아야 할 해킹, 위험 및 일반적인 어리 석음으로 가득 차 있습니다. 이 경로를 사용하는 경우 극도로주의하여 테스트하십시오. 그리고 정말로 .. 그냥 이러지 마세요.
@computerfreaker는 형식 정의가 사용자 제어하에 있고 두 프로그램에서 똑같은 토큰 시퀀스가 사용되는 경우에도 ABI가 부족하여 일반적인 경우 DLL 경계를 넘어 C ++ 개체를 전달하지 못하는 이유에 대한 훌륭한 설명을 작성했습니다. (작동하는 두 가지 경우가 있습니다 : 표준 레이아웃 클래스와 순수 인터페이스)
C ++ 표준에 정의 된 객체 유형 (표준 템플릿 라이브러리에서 조정 된 유형 포함)의 경우 상황은 훨씬 더 나쁩니다. 이러한 유형을 정의하는 토큰은 C ++ 표준이 완전한 유형 정의를 제공하지 않고 최소 요구 사항 만 제공하므로 여러 컴파일러에서 동일하지 않습니다. 또한 이러한 유형 정의에 나타나는 식별자의 이름 조회는 동일하게 확인되지 않습니다. C ++ ABI가있는 시스템에서도 모듈 경계를 넘어 이러한 유형을 공유하려고하면 단일 정의 규칙 위반으로 인해 정의되지 않은 대규모 동작이 발생합니다.
이것은 g ++의 libstdc ++가 사실상의 표준이고 사실상 모든 프로그램이이를 사용하여 ODR을 만족시키기 때문에 Linux 프로그래머가 처리하는 데 익숙하지 않은 것입니다. clang의 libc ++는 이러한 가정을 깨뜨 렸고 C ++ 11은 거의 모든 표준 라이브러리 유형에 대한 필수 변경 사항과 함께 제공되었습니다.
모듈간에 표준 라이브러리 유형을 공유하지 마십시오. 정의되지 않은 동작입니다.
여기에 대한 답변 중 일부는 C ++ 클래스 통과가 정말 무섭게 들리지만 다른 관점을 공유하고 싶습니다. 일부 다른 응답에서 언급 된 순수 가상 C ++ 방법은 실제로 생각보다 더 깨끗한 것으로 밝혀졌습니다. 나는 개념을 중심으로 전체 플러그인 시스템을 구축했으며 수년 동안 매우 잘 작동했습니다. LoadLib () 및 GetProcAddress ()를 사용하여 지정된 디렉토리에서 동적으로 dll을로드하는 "PluginManager"클래스가 있습니다 (및 Linux 등가물을 사용하여 플랫폼을 교차하도록 실행 가능).
믿거 나 말거나,이 방법은 순수한 가상 인터페이스 끝에 새 함수를 추가하고 새 함수없이 인터페이스에 대해 컴파일 된 dll을로드하려고 시도하는 것과 같은 이상한 작업을 수행하더라도 용서할 수 있습니다. 제대로로드됩니다. 물론 ... 실행 파일이 함수를 구현하는 최신 dll에 대해서만 새 함수를 호출하는지 확인하려면 버전 번호를 확인해야합니다. 그러나 좋은 소식은 작동합니다! 따라서 시간이 지남에 따라 인터페이스를 발전시키는 조잡한 방법이 있습니다.
순수한 가상 인터페이스의 또 다른 멋진 점은 원하는만큼의 인터페이스를 상속 할 수 있으며 다이아몬드 문제에 부딪히지 않을 것입니다!
이 접근 방식의 가장 큰 단점은 매개 변수로 전달하는 유형에 대해 매우주의해야한다는 것입니다. 먼저 순수한 가상 인터페이스로 래핑하지 않고는 클래스 나 STL 개체가 없습니다. 구조체가 없습니다 (pragma 팩 부두를 거치지 않고). 다른 인터페이스에 대한 기본 유형과 포인터입니다. 또한 기능에 과부하가 걸리지 않아 불편할 수 있지만 과시하지 않습니다.
좋은 소식은 몇 줄의 코드로 재사용 가능한 제네릭 클래스 및 인터페이스를 만들어 STL 문자열, 벡터 및 기타 컨테이너 클래스를 래핑 할 수 있다는 것입니다. 또는 인터페이스에 GetCount () 및 GetVal (n)과 같은 함수를 추가하여 사람들이 목록을 반복 할 수 있도록 할 수 있습니다.
우리를 위해 플러그인을 만드는 사람들은 매우 쉽습니다. 그들은 ABI 경계 또는 다른 것에 대한 전문가 일 필요가 없습니다. 그들은 관심있는 인터페이스를 상속하고 지원하는 기능을 코딩 한 다음 그렇지 않은 것에 대해 false를 반환합니다.
이 모든 작업을 수행하는 기술은 내가 아는 한 어떤 표준에도 기반하지 않습니다. 내가 수집 한 내용에서 Microsoft는 COM을 만들 수 있도록 가상 테이블을 그렇게하기로 결정했고 다른 컴파일러 작성자는이를 따르기로 결정했습니다. 여기에는 GCC, Intel, Borland 및 대부분의 기타 주요 C ++ 컴파일러가 포함됩니다. 모호한 임베디드 컴파일러를 사용할 계획이라면이 접근 방식이 효과가 없을 것입니다. 이론적으로 모든 컴파일러 회사는 언제든지 가상 테이블을 변경하여 작업을 중단 할 수 있지만이 기술에 따라 수년에 걸쳐 작성된 엄청난 양의 코드를 고려할 때 주요 플레이어가 순위를 깨기로 결정하면 매우 놀랍습니다.
따라서 이야기의 교훈은 ... 몇 가지 극단적 인 상황을 제외하고는 ABI 경계가 기본 유형으로 깨끗하게 유지되고 과부하를 피할 수 있도록 인터페이스를 담당하는 한 사람이 필요합니다. 이 규정에 동의한다면 컴파일러간에 DLL / SO의 클래스에 대한 인터페이스를 공유하는 것을 두려워하지 않을 것입니다. 클래스를 직접 공유 == 문제가 있지만 순수 가상 인터페이스를 공유하는 것은 그렇게 나쁘지 않습니다.
모든 모듈 (.EXE 및 .DLL)이 동일한 C ++ 컴파일러 버전과 CRT의 동일한 설정 및 특징으로 빌드되지 않는 한 STL 개체를 DLL 경계를 넘어 안전하게 전달할 수 없습니다.
DLL에서 개체 지향 인터페이스를 노출하려면 C ++ 순수 인터페이스를 노출해야합니다 (COM이 수행하는 것과 유사 함). CodeProject에 대한이 흥미로운 기사를 읽어보십시오.
DLL 경계에서 순수 C 인터페이스를 노출 한 다음 호출자 사이트에서 C ++ 래퍼를 빌드하는 것도 고려할 수 있습니다.
이는 Win32에서 발생하는 것과 유사합니다. Win32 구현 코드는 거의 C ++이지만 많은 Win32 API는 순수한 C 인터페이스를 노출합니다 (COM 인터페이스를 노출하는 API도 있음). 그런 다음 ATL / WTL 및 MFC는 이러한 순수 C 인터페이스를 C ++ 클래스 및 개체로 래핑합니다.