Bu sorunun kısa cevabı yapma . Standart bir C ++ ABI (uygulama ikili arabirimi, arama kuralları, veri paketleme / hizalama, tür boyutu vb.) Olmadığı için, sınıfla başa çıkmanın standart bir yolunu denemek ve uygulamak için çok sayıda çemberden geçmeniz gerekecektir. programınızdaki nesneler. Tüm bu çemberleri atladıktan sonra çalışacağının bir garantisi bile yok, bir derleyici sürümünde çalışan bir çözümün bir sonrakinde çalışacağının garantisi bile yok.
Sadece kullanılarak ara düz C oluşturmak extern "C"
Cı ABI yana olan iyi tanımlanmış ve istikrarlı.
C ++ nesnelerini bir DLL sınırından gerçekten, gerçekten geçirmek istiyorsanız, bu teknik olarak mümkündür. İşte hesaba katmanız gereken faktörlerden bazıları:
Veri paketleme / hizalama
Belirli bir sınıf içinde, bireysel veri üyeleri genellikle belleğe özel olarak yerleştirilir, böylece adresleri tür boyutunun bir katına karşılık gelir. Örneğin, int
4 baytlık bir sınıra hizalanabilir.
DLL dosyanız EXE'nizden farklı bir derleyici ile derlenmişse, belirli bir sınıfın DLL sürümü EXE sürümünden farklı olabilir, bu nedenle EXE sınıf nesnesini DLL'ye ilettiğinde DLL düzgün bir şekilde erişemeyebilir. o sınıf içinde verilen veri üyesi. DLL, EXE tanımıyla değil, kendi sınıf tanımıyla belirtilen adresten okumaya çalışır ve istenen veri üyesi gerçekte orada depolanmadığından, gereksiz değerler ortaya çıkar.
#pragma pack
Derleyiciyi belirli paketler uygulamaya zorlayacak önişlemci yönergesini kullanarak bu sorunu çözebilirsiniz . Derleyicinin seçeceğinden daha büyük bir paket değeri seçerseniz, derleyici yine de varsayılan paketlemeyi uygulayacaktır , bu nedenle büyük bir paketleme değeri seçerseniz, bir sınıfın derleyiciler arasında yine de farklı paketleme olabilir. Bunun çözümü #pragma pack(1)
, derleyiciyi veri üyelerini bir baytlık sınırda hizalamaya zorlayacak şekilde kullanmaktır (esasen paketleme uygulanmaz). Bu, bazı sistemlerde performans sorunlarına ve hatta çökmelere neden olabileceği için harika bir fikir değildir. Ancak, sınıfınızın veri üyelerinin bellekte hizalanma biçiminde tutarlılık sağlayacaktır .
Üye yeniden sıralama
Sınıfınız standart düzen değilse , derleyici veri üyelerini bellekte yeniden düzenleyebilir . Bunun nasıl yapıldığına dair bir standart yoktur, bu nedenle herhangi bir veri yeniden düzenleme derleyiciler arasında uyumsuzluklara neden olabilir. Bu nedenle, verileri bir DLL'ye ileri geri iletmek için standart düzen sınıfları gerekir.
Çağrı kuralı
Belirli bir işlevin sahip olabileceği birden fazla arama kuralı vardır. Bu çağrı kuralları, verilerin fonksiyonlara nasıl aktarılacağını belirler: parametreler kayıtlarda mı yoksa yığın üzerinde mi saklanır? Bağımsız değişkenler yığına hangi sırayla gönderilir? İşlev tamamlandıktan sonra yığında kalan argümanları kim temizler?
Standart bir arama kuralına sahip olmanız önemlidir; bir işlevi _cdecl
C ++ için varsayılan olarak _stdcall
bildirirseniz ve kötü şeyler kullanarak onu çağırmaya çalışırsanız ortaya çıkacaktır . _cdecl
bununla birlikte, C ++ işlevleri için varsayılan arama kuralıdır, bu nedenle bu, _stdcall
bir yerde ve _cdecl
başka bir yerde belirterek kasıtlı olarak kırmadığınız sürece bozulmayacak bir şeydir .
Veri türü boyutu
Bu belgelere göre , Windows'ta, uygulamanızın 32 bit veya 64 bit olmasına bakılmaksızın çoğu temel veri türü aynı boyutlara sahiptir. Bununla birlikte, belirli bir veri türünün boyutu herhangi bir standart tarafından değil, derleyici tarafından uygulandığından (tüm standart garantiler budur 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), mümkün olduğunda veri türü boyutu uyumluluğunu sağlamak için sabit boyutlu veri türlerini kullanmak iyi bir fikirdir .
Yığın sorunları
DLL'niz C çalışma zamanının EXE'nizden farklı bir sürümüne bağlanırsa, iki modül farklı yığınlar kullanır . Modüllerin farklı derleyicilerle derlendiği düşünüldüğünde, bu özellikle olası bir sorundur.
Bunu hafifletmek için, tüm belleğin paylaşılan bir yığına tahsis edilmesi ve aynı öbekten ayrılması gerekecektir. Neyse ki, Windows bu konuda yardımcı olacak API'ler sağlar: GetProcessHeap , ana EXE'sinin yığınına erişmenize izin verir ve HeapAlloc / HeapFree , bu yığın içinde bellek ayırmanıza ve boşaltmanıza izin verir. Normal kullanmamanız malloc
/ free
beklediğiniz şekilde çalışacaklarının garantisi olmadığı için önemlidir .
STL sorunları
C ++ standart kitaplığının kendi ABI sorunları kümesi vardır. Orada hiçbir garantisi verilen bir STL tipi bellekte aynı şekilde ortaya koydu ki, ne var özellikle (belirli bir STL sınıfı başka bir uygulamadan aynı boyuta sahip bir garantisidir, ayıklama a içine ekstra hata ayıklama bilgilerini koyabilir kurar STL tipi verilir). Bu nedenle, herhangi bir STL konteynerinin DLL sınırını geçmeden ve diğer tarafta yeniden paketlenmeden önce temel türlere açılması gerekir.
İsim değiştirme
DLL dosyanız, muhtemelen EXE'nizin çağırmak isteyeceği işlevleri dışa aktaracaktır. Ancak, C ++ derleyicilerinin işlev adlarını karıştırmanın standart bir yolu yoktur . Bu, adlı bir işlevin GCC'de ve MSVC'de GetCCDLL
karıştırılabileceği anlamına gelir ._Z8GetCCDLLv
?GetCCDLL@@YAPAUCCDLL_v1@@XZ
GCC ile üretilen bir DLL bir .lib dosyası oluşturmayacağından ve MSVC'de bir DLL'yi statik olarak bağlamak için bir DLL dosyası gerektiğinden, zaten DLL'nize statik bağlanmayı garanti edemezsiniz. Dinamik olarak bağlanma çok daha temiz bir seçenek gibi görünse de, adın karıştırılması önünüze çıkar: GetProcAddress
Yanlış karıştırılmış adı denerseniz , çağrı başarısız olur ve DLL'nizi kullanamazsınız. Bu, etrafta dolaşmak için biraz bilgisayar korsanlığı gerektirir ve C ++ sınıflarını bir DLL sınırından geçirmenin kötü bir fikir olmasının oldukça önemli bir nedenidir.
DLL'nizi oluşturmanız, ardından üretilen .def dosyasını incelemeniz (üretilirse; bu, proje seçeneklerinize göre değişiklik gösterir) veya karıştırılmış adı bulmak için Dependency Walker gibi bir araç kullanmanız gerekir. Ardından, karıştırılmış işleve yönlendirilmemiş bir takma ad tanımlayarak kendi .def dosyanızı yazmanız gerekir . Örnek olarak GetCCDLL
biraz daha bahsettiğim fonksiyonu kullanalım . Sistemimde aşağıdaki .def dosyaları sırasıyla GCC ve MSVC için çalışır:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
DLL dosyanızı yeniden oluşturun, ardından dışa aktardığı işlevleri yeniden inceleyin. Bunların arasında bir unmangled işlev adı olmalıdır. Aşırı yüklenmiş işlevleri bu şekilde kullanamayacağınızı unutmayın : unmangled işlev adı, karıştırılmış adla tanımlanan belirli bir işlev aşırı yüklemesi için bir takma addır. Ayrıca, karıştırılan adlar değişeceğinden, işlev bildirimlerini her değiştirdiğinizde DLL'niz için yeni bir .def dosyası oluşturmanız gerekeceğini unutmayın. En önemlisi, adı bozmayı atlayarak, bağlayıcının uyumsuzluk sorunlarıyla ilgili olarak size sunmaya çalıştığı tüm korumaları geçersiz kılıyorsunuz.
DLL'nizin izlemesi için bir arabirim oluşturursanız , bu işlemin tamamı daha basittir , çünkü DLL'nizdeki her işlev için bir takma ad oluşturmak yerine bir takma ad tanımlamak için yalnızca bir işleviniz olur. Ancak aynı uyarılar hala geçerlidir.
Sınıf nesnelerini bir işleve geçirme
Bu, derleyiciler arası veri geçişini engelleyen sorunların muhtemelen en ince ve en tehlikeli olanıdır. Her şeyi hallediyor olsanız bile, argümanların bir işleve nasıl aktarıldığına dair bir standart yoktur . Bu, görünürde bir neden olmaksızın ve bunlarda hata ayıklamanın kolay bir yolu olmayan ince çökmelere neden olabilir . Herhangi bir dönüş değeri için tamponlar dahil olmak üzere tüm argümanları işaretçiler aracılığıyla geçirmeniz gerekir . Bu beceriksiz ve zahmetli ve işe yarayabilecek veya çalışmayabilecek başka bir geçici çözümdür.
Tüm bu geçici çözümleri bir araya getirerek ve şablonlar ve işleçlerle bazı yaratıcı çalışmalara dayanarak , nesneleri bir DLL sınırından güvenli bir şekilde geçirmeyi deneyebiliriz. C ++ 11 desteğinin, destek #pragma pack
ve türevlerinin olduğu gibi zorunlu olduğunu unutmayın ; MSVC 2013, GCC ve clang'ın son sürümlerinde olduğu gibi bu desteği sunar.
//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
Böylece sınıf, her temel veri türü için uzmanlaşmıştır int
otomatik şekilde sarılmış olacaktır int32_t
, uint
şekilde sarılmış olacaktır uint32_t
, Bu, tüm perde arkasında meydana vb aşırı sayesinde =
ve ()
operatörler. Temel tür uzmanlıklarının geri kalanını atladım, çünkü temel veri türleri dışında neredeyse tamamen aynı olduklarından ( bool
uzmanlık, a'ya dönüştürüldüğü için biraz fazladan mantığa sahiptir int8_t
ve daha sonra int8_t
geri dönüştürmek için 0 ile karşılaştırılır. bool
, ancak bu oldukça önemsizdir).
STL türlerini de bu şekilde paketleyebiliriz, ancak biraz daha fazla çalışma gerektiriyor:
#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)
Şimdi bu pod türlerini kullanan bir DLL oluşturabiliriz. Öncelikle bir arayüze ihtiyacımız var, bu yüzden karıştırmayı çözmek için sadece bir yöntemimiz olacak.
//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();
Bu, hem DLL hem de arayanların kullanabileceği temel bir arabirim oluşturur. pod
Bir pod
kendisine değil a'ya bir gösterici geçirdiğimize dikkat edin. Şimdi bunu DLL tarafında uygulamamız gerekiyor:
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;
}
Ve şimdi ShowMessage
işlevi uygulayalım :
#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);
}
Çok süslü bir şey yok: bu sadece iletileni pod
normale kopyalar ve wstring
bir mesaj kutusunda gösterir. Sonuçta, bu sadece bir POC , tam bir yardımcı program kitaplığı değil.
Şimdi DLL'yi oluşturabiliriz. Bağlayıcının adının bozulmasına çözüm bulmak için özel .def dosyalarını unutmayın. (Not: Aslında oluşturduğum ve çalıştırdığım CCDLL yapısının burada sunduğumdan daha fazla işlevi vardı. .Def dosyaları beklendiği gibi çalışmayabilir.)
Şimdi bir EXE'nin DLL'yi çağırması için:
//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;
}
Ve işte sonuçlar. DLL'miz çalışıyor. Geçmişteki STL ABI sorunlarına, geçmiş C ++ ABI sorunlarına, geçmiş karıştırma sorunlarına başarıyla ulaştık ve MSVC DLL'miz bir GCC EXE ile çalışıyor.
Sonuç olarak, C ++ nesnelerini DLL sınırlarından kesinlikle geçirmeniz gerekiyorsa , bunu böyle yaparsınız. Ancak, bunların hiçbirinin sizin kurulumunuzla veya başkasınınkiyle çalışacağı garanti edilmez. Bunlardan herhangi biri herhangi bir zamanda bozulabilir ve muhtemelen yazılımınızın büyük bir sürüme sahip olması planlanmadan önceki gün kesintiye uğrayacaktır. Bu yol, muhtemelen vurulmam gereken hackler, riskler ve genel aptallıkla dolu. Bu rotaya giderseniz, lütfen çok dikkatli test edin. Ve gerçekten ... sadece bunu yapma.