2013-09-25 1 views
2

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(); 
    } 
} 
+3

만약 당신이 그걸로 갈 예정이라면, 정의에 의해 거의 바이너리 호환이 될 C 인터페이스를 제공하십시오 ... –

+0

나는 이걸 잠재적으로 극도로 고통 스럽다는 이유로 당신 말을 듣고 싶습니다. " "...하지만 99 %가 이것이 효과가있을 것이라고 확신합니다. 안드로이드의 OpenSLES API는 구조체에서 fptrs를 호출 할 때 __thiscall 호출 규칙을 사용하여 기본 ptr을 호출 할 때 첫 번째 매개 변수로 "self"를 전달한다는 점을 제외하고이 메소드를 사용합니다. 내가 아는 한, __thiscall은 __stdcall과 동일하지만 "this"가 ecx로 이동되었습니다. 그래서만큼 (& TestItf == & TestImp) 어떻게 실패 할 수 있는지 모르겠다. 의견은 코드 스타일을 기반으로합니까? 또는 기술적 인 문제입니까? – bitwise

+0

또 다른 대안은 해당 함수 테이블을 COM 클래스로 노출하는 것입니다. 꼭 COM _layout_에서 표준화하는 모든 COM 규칙을 따라야한다는 것을 의미하지는 않습니다. Windows로 제한되어 있지만 물론 "DLL"이라고했습니다. – MSalters

답변

0

번호

상호 운용성이 아이디어를 탐험 내 원래의 동기 부여의 큰 부분이었다.

원래 질문에 사용 된 C# 예제는 Windows에서 작동하지만 mac osx에서는 실패합니다. C#/Mono와 C++ 사이의 vtable 크기는 멤버 함수 포인터 크기가 다르기 때문에 일치하지 않습니다. Mono는 4 바이트 함수 포인터를 기대하지만 xcode/C++ 컴파일러는 8 바이트가 될 것으로 예상합니다.

분명히 멤버 함수 포인터는 포인터 이상입니다. 경우에 따라 특정 상속 상황을 처리하기 위해 추가 데이터가 포함 된 구조를 가리킬 수도 있습니다.

8 바이트 멤버 함수 포인터를 4 바이트로 잘라서 모노로 보내는 것은 실제로 작동합니다. 이것은 POD 클래스 유형을 사용하고 있기 때문일 수 있습니다. 이처럼 해킹에 의존하고 싶지는 않습니다.

모든 것을 고려하면, 원래의 질문에서 제안 된 interop에 사용 된 방법은 가치가있는 것보다 훨씬 어려울 것이며, 글 머리 기호를 바이트로 선택하고 C 인터페이스로 이동하기로했습니다.

0

첫째 : 당신이 기본 조상으로 후손 형식을 반환 때문에 TestItf 소멸자는 가상이어야한다. 가상이 없으면 일부 컴파일러에서 메모리 누수가 발생합니다.

이제 바이너리 호환성에 따라. 일반적인 함정은 다음과 같습니다.

  1. 전화 규칙. 두 컴파일러 (여러분과 클라이언트 중 하나)가 여러분이 선택한 호출 규칙을 알고 있다면 괜찮습니다. (Win32 API와 같은 일반 클래스없는 stdcall 규칙은 C++뿐만 아니라 수년 동안 여러 언어에 대해 검증 된 솔루션입니다)
  2. 구조체 조정. 게시 된 구조를 1 바이트 정렬로 묶습니다. 대부분의 컴파일러는 pragma 또는 컴파일 키를 통해 적절한 설정을합니다.

두 가지 점을 염두에두면 대부분의 플랫폼에서 안전하게 게임을 즐길 수 있습니다.편리한 객체 지향 방법으로 언어 사이

+0

나는 이것을 검사해야했지만 두 소멸자 모두 호출해야합니다. "delete"는 명시 적 Destroy 함수에 의해 DLL에 다시 위임됩니다.이 함수는 TestImp를 알고 있으며 두 소멸자를 모두 호출합니다. TestItf는 데이터 멤버가없는 순수한 가상 인터페이스를 모방하기 때문에 그리고 TestItf를 가상 클래스로 만들면 잠재적으로 컴파일러가있는 POD가 아닌 클래스가되기 때문에 소멸자가 될 필요는 없으며 가상 일 필요가 없습니다. 특정 레이아웃. – bitwise

+0

정렬 : 이제는 기본 클래스가 실제로 비어있는 것은 아니지만 하나의 포인터가 포함되어 있으므로 파생 클래스가 실제 포인터 크기보다 큰 정렬로 끝날 수 있는지 궁금합니다. – bitwise

+0

"삭제"에 동의 –