DLL로 변환해야하는 C++로 작성된 라이브러리가 있습니다. 이 라이브러리는 다른 컴파일러로 수정하고 다시 컴파일 할 수 있어야하며 여전히 작동해야합니다.함수 포인터로 가득 찬 구조체는 C++ 바이너리 호환성을위한 좋은 해결책입니까?
__declspec (dllexport)를 사용하여 모든 클래스를 직접 내보내는 경우 컴파일러/버전간에 완전한 바이너리 호환성을 얻지 못할 가능성이 있음을 읽었습니다.
필자는 순수 가상 인터페이스를 DLL에서 가져 와서 단순히 함수 포인터로 가득 찬 테이블을 전달하여 이름 맹 글링 문제를 제거 할 수 있음을 읽었습니다. 그러나 일부 컴파일러는 연속적인 릴리스간에 vtable의 함수 순서를 변경할 수도 있기 때문에이 경우에도 실패 할 수 있음을 읽었습니다.
그래서 결국, 난 그냥 내 자신의 vtable을 구현할 수있는 생각, 나는에서 나는 곳이다 :
Test.h
#pragma once
#include <iostream>
using namespace std;
class TestItf;
extern "C" __declspec(dllexport) TestItf* __cdecl CreateTest();
class TestItf {
public:
static TestItf* Create() {
return CreateTest();
}
void Destroy() {
(this->*vptr->Destroy)();
}
void Print(const char *something) {
(this->*vptr->Print)(something);
}
~TestItf() {
cout << "TestItf dtor" << endl;
}
typedef void(TestItf::*pfnDestroy)();
typedef void(TestItf::*pfnPrint)(const char *something);
struct vtable {
pfnDestroy Destroy;
pfnPrint Print;
};
protected:
const vtable *const vptr;
TestItf(vtable *vptr) : vptr(vptr){}
};
extern "C"__declspec(dllexport) void __cdecl GetTestVTable(TestItf::vtable *vtable);
Test.cpp에
#include "Test.h"
class TestImp : public TestItf {
public:
static TestItf::vtable TestImp_vptr;
TestImp() : TestItf(&TestImp_vptr) {
}
~TestImp() {
cout << "TestImp dtor" << endl;
}
void Destroy() {
delete this;
}
void Print(const char *something) {
cout << something << endl;
}
};
TestItf::vtable TestImp::TestImp_vptr = {
(TestItf::pfnDestroy)&TestImp::Destroy,
(TestItf::pfnPrint)&TestImp::Print,
};
extern "C" {
__declspec(dllexport) void __cdecl GetTestVTable(TestItf::vtable *vtable) {
memcpy(vtable, &TestImp::TestImp_vptr, sizeof(TestItf::vtable));
}
__declspec(dllexport) TestItf* __cdecl CreateTest() {
return new TestImp;
}
}
main.cpp
int main(int argc, char *argv[])
{
TestItf *itf = TestItf::Create();
itf->Print("Hello World!");
itf->Destroy();
return 0;
}
첫 번째 두 가지 방법과 올바른 호환성을 얻을 수 없다는 위의 가정이 맞습니까?
제 3의 솔루션은 휴대하고 안전합니까?
- 특히 TestItem의 기본 유형 TestItf에서 캐스팅 된 함수 포인터를 사용하는 효과에 대해 걱정이됩니다. 이 간단한 테스트 케이스에서 작동하는 것처럼 보이지만 정렬이나 오브젝트 레이아웃 변경과 같은 작업으로 인해이 경우가 안전하지 않을 수 있다고 생각합니다.
이 방법은 C#에서도 사용할 수 있습니다. 위의 코드는 약간 수정되었습니다.
Test.cs
struct TestItf {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct VTable {
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
public delegate void pfnDestroy(IntPtr itf);
[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
public delegate void pfnPrint(IntPtr itf, string something);
[MarshalAs(UnmanagedType.FunctionPtr)]
public pfnDestroy Destroy;
[MarshalAs(UnmanagedType.FunctionPtr)]
public pfnPrint Print;
}
[DllImport("cppInteropTest", CallingConvention = CallingConvention.Cdecl)]
private static extern void GetTestVTable(out VTable vtable);
[DllImport("cppInteropTest", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateTest();
private static VTable vptr;
static TestItf() {
vptr = new VTable();
GetTestVTable(out vptr);
}
private IntPtr itf;
private TestItf(IntPtr itf) {
this.itf = itf;
}
public static TestItf Create() {
return new TestItf(CreateTest());
}
public void Destroy() {
vptr.Destroy(itf);
itf = IntPtr.Zero;
}
public void Print(string something) {
vptr.Print(itf, something);
}
}
Program.cs 모든
static class Program
{
[STAThread]
static void Main()
{
TestItf test = TestItf.Create();
test.Print("Hello World!");
test.Destroy();
}
}
만약 당신이 그걸로 갈 예정이라면, 정의에 의해 거의 바이너리 호환이 될 C 인터페이스를 제공하십시오 ... –
나는 이걸 잠재적으로 극도로 고통 스럽다는 이유로 당신 말을 듣고 싶습니다. " "...하지만 99 %가 이것이 효과가있을 것이라고 확신합니다. 안드로이드의 OpenSLES API는 구조체에서 fptrs를 호출 할 때 __thiscall 호출 규칙을 사용하여 기본 ptr을 호출 할 때 첫 번째 매개 변수로 "self"를 전달한다는 점을 제외하고이 메소드를 사용합니다. 내가 아는 한, __thiscall은 __stdcall과 동일하지만 "this"가 ecx로 이동되었습니다. 그래서만큼 (& TestItf == & TestImp) 어떻게 실패 할 수 있는지 모르겠다. 의견은 코드 스타일을 기반으로합니까? 또는 기술적 인 문제입니까? – bitwise
또 다른 대안은 해당 함수 테이블을 COM 클래스로 노출하는 것입니다. 꼭 COM _layout_에서 표준화하는 모든 COM 규칙을 따라야한다는 것을 의미하지는 않습니다. Windows로 제한되어 있지만 물론 "DLL"이라고했습니다. – MSalters