Nesneleri, özellikle STL nesnelerini bir DLL'ye ve DLL'den nasıl güvenle geçirebilirim?


106

Sınıf nesnelerini, özellikle STL nesnelerini bir C ++ DLL'ye / DLL'den nasıl geçirebilirim?

Uygulamamın DLL dosyaları biçiminde üçüncü taraf eklentilerle etkileşime girmesi gerekiyor ve bu eklentilerin hangi derleyiciyle oluşturulduğunu kontrol edemiyorum. STL nesneleri için garantili ABI olmadığının farkındayım ve uygulamamda kararsızlığa neden olmaktan endişe duyuyorum.


4
C ++ Standart Kitaplıktan bahsediyorsanız, muhtemelen öyle demelisiniz. STL, bağlama bağlı olarak farklı şeyler ifade edebilir. (Ayrıca bkz. Stackoverflow.com/questions/5205491/… )
Micha Wiedenmann

Yanıtlar:


157

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, int4 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 packDerleyiciyi 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 _cdeclC ++ için varsayılan olarak _stdcall bildirirseniz ve kötü şeyler kullanarak onu çağırmaya çalışırsanız ortaya çıkacaktır . _cdeclbununla birlikte, C ++ işlevleri için varsayılan arama kuralıdır, bu nedenle bu, _stdcallbir yerde ve _cdeclbaş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/ freebeklediğ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 GetCCDLLkarış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: GetProcAddressYanlış 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 GetCCDLLbiraz 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 packve 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)

podBöylece sınıf, her temel veri türü için uzmanlaşmıştır intotomatik ş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 ( booluzmanlık, a'ya dönüştürüldüğü için biraz fazladan mantığa sahiptir int8_tve daha sonra int8_tgeri 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. podBir podkendisine 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 ShowMessageiş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 podnormale kopyalar ve wstringbir 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.

Sonucu daha sonra gösteren resim.


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.


1
Hmm, fena değil! Bir Windows DLL ile etkileşim kurmak için standart c ++ türlerini kullanmaya karşı oldukça iyi bir argüman koleksiyonu çektiniz ve buna göre etiketlendiniz. Bu belirli ABI kısıtlamaları, MSVC dışındaki diğer alet zincirleri için geçerli olmayacaktır. Bundan bile bahsedilmeli ...
πάντα ῥεῖ

12
@DavidHeffernan Sağ. Ama bu benim için birkaç haftalık bir araştırmanın sonucudur, bu yüzden başkalarının aynı araştırmayı yapmasına gerek kalmaması için öğrendiklerimi belgelemenin faydalı olacağını düşündüm ve aynı çalışan bir çözümü birlikte hackleme girişimleri. Dahası, buralarda yarı yaygın bir soru gibi göründüğü için.
cf Monica ile

@ πάνταῥεῖ Bu belirli ABI kısıtlamaları, MSVC dışındaki diğer araç zincirleri için geçerli olmayacaktır . Bundan bile bahsedilmeli ... Bunu doğru anladığımdan emin değilim. Bu ABI sorunlarının MSVC'ye özel olduğunu ve örneğin clang ile oluşturulmuş bir DLL'nin GCC ile oluşturulmuş bir EXE ile başarılı bir şekilde çalışacağını mı belirtiyorsunuz? Biraz kafam karıştı, çünkü bu tüm araştırmalarımla çelişiyor ...
cf Monica ile

@computerfreaker Hayır PE ve ELF'in farklı ABI formatları kullandığını söylüyorum ...
πάντα ῥεῖ

3
@computerfreaker Çoğu büyük C ++ derleyicisi (GCC, Clang, ICC, EDG, vb.) Itanium C ++ ABI'yi takip eder. MSVC yapmaz. Yani evet, bu ABI sorunları büyük ölçüde MSVC'ye özgüdür, ancak yalnızca değil - Unix platformlarındaki C derleyicileri (ve hatta aynı derleyicinin farklı sürümleri!) Mükemmelden daha az birlikte çalışabilirlikten muzdariptir. Onlar hiç olmaz bunu bulmak için sürpriz olsa da, yakın yeterince genellikle konum olabilir başarıyla bir Clang inşa DLL bağlantı bir GCC inşa yürütülebilir.
Stuart Olsen

17

@computerfreaker, tür tanımları kullanıcı denetimi altında olduğunda ve her iki programda da aynı belirteç dizisi kullanıldığında bile, genel durumda ABI eksikliğinin C ++ nesnelerinin DLL sınırlarının ötesine geçmesini neden engellediğine dair harika bir açıklama yazdı. (İşe yarayan iki durum vardır: standart düzen sınıfları ve saf arayüzler)

C ++ Standardında tanımlanan nesne türleri için (Standart Şablon Kitaplığından uyarlananlar dahil) durum çok çok daha kötüdür. C ++ Standardı tam bir tür tanımı sağlamadığından, yalnızca minimum gereksinimleri sağladığından, bu türleri tanımlayan simgeler birden çok derleyicide aynı DEĞİLDİR. Ayrıca, bu tür tanımlarında görünen tanımlayıcıların ad araması da aynı şeyi çözmez. C ++ ABI olan sistemlerde bile, modül sınırları boyunca bu tür türleri paylaşmaya çalışmak, Tek Tanımlama Kuralı ihlalleri nedeniyle çok büyük tanımlanmamış davranışlara neden olur.

Bu, Linux programcılarının uğraşmaya alışkın olmadığı bir şeydi, çünkü g ++ 'nın libstdc ++ fiili bir standarttı ve neredeyse tüm programlar bunu kullanıyordu, dolayısıyla ODR'yi tatmin ediyordu. clang'ın libc ++ bu varsayımı bozdu ve ardından C ++ 11 neredeyse tüm Standart kitaplık türlerinde zorunlu değişikliklerle birlikte geldi.

Standart kitaplık türlerini modüller arasında paylaşmayın. Tanımlanmamış bir davranış.


16

Buradaki cevaplardan bazıları, C ++ sınıflarını geçmek kulağa korkutucu geliyor, ancak alternatif bir bakış açısını paylaşmak istiyorum. Diğer yanıtların bazılarında bahsedilen saf sanal C ++ yöntemi aslında düşündüğünüzden daha temiz çıkıyor. Konsept etrafında bütün bir eklenti sistemi kurdum ve yıllardır çok iyi çalışıyor. LoadLib () ve GetProcAddress () (ve Linux eşdeğerlerini kullanarak onu çapraz platform yapmak için çalıştırılabilir) kullanarak dinamik olarak dll'leri belirli bir dizinden yükleyen bir "PluginManager" sınıfım var.

İster inanın ister inanmayın, saf sanal arayüzünüzün sonuna yeni bir fonksiyon eklemek ve bu yeni fonksiyon olmadan arayüze karşı derlenmiş dll'leri yüklemeye çalışmak gibi tuhaf şeyler yapsanız bile bu yöntem affedicidir - çok iyi yüklenirler. Elbette ... yürütülebilir dosyanızın yalnızca işlevi uygulayan daha yeni dll'ler için yeni işlevi çağırdığından emin olmak için bir sürüm numarasını kontrol etmeniz gerekir. Ama iyi haber şu: işe yarıyor! Yani bir bakıma, arayüzünüzü zaman içinde geliştirmek için basit bir yönteminiz var.

Saf sanal arabirimlerle ilgili bir başka harika şey - istediğiniz kadar çok arabirim devralabilirsiniz ve asla elmas sorunuyla karşılaşmayacaksınız!

Bu yaklaşımın en büyük dezavantajının, hangi türleri parametre olarak geçirdiğiniz konusunda çok dikkatli olmanız gerektiğini söyleyebilirim. Önce saf sanal arabirimlerle sarmalamadan hiçbir sınıf veya STL nesnesi olmaz. Yapı yok (pragma paketi büyüsünden geçmeden). Sadece ilkel tipler ve diğer arayüzlere işaretçiler. Ayrıca, bir rahatsızlık olan, ancak bir gösteri durdurucu olmayan işlevleri aşırı yükleyemezsiniz.

İyi haber şu ki, bir avuç kod satırı ile STL dizelerini, vektörleri ve diğer kapsayıcı sınıflarını sarmak için yeniden kullanılabilir genel sınıflar ve arayüzler oluşturabilirsiniz. Alternatif olarak, insanların listelerde dolaşmasına izin vermek için arayüzünüze GetCount () ve GetVal (n) gibi işlevler ekleyebilirsiniz.

Bizim için eklenti oluşturan insanlar bunu oldukça kolay buluyor. ABI sınırı veya herhangi bir konuda uzman olmaları gerekmez - sadece ilgilendikleri arayüzleri miras alırlar, destekledikleri işlevleri kodlar ve istemedikleri için yanlış döndürürler.

Bütün bunları çalıştıran teknoloji bildiğim kadarıyla herhangi bir standarda dayanmıyor. Anladığım kadarıyla, Microsoft, COM oluşturabilmek için sanal tablolarını bu şekilde yapmaya karar verdi ve diğer derleyici yazarları da aynı şeyi yapmaya karar verdi. Bu, GCC, Intel, Borland ve diğer birçok büyük C ++ derleyicisini içerir. Belirsiz bir gömülü derleyici kullanmayı planlıyorsanız, bu yaklaşım muhtemelen sizin için işe yaramayacaktır. Teorik olarak, herhangi bir derleyici şirketi sanal tablolarını herhangi bir zamanda değiştirebilir ve bir şeyleri bozabilir, ancak bu teknolojiye bağlı olarak yıllar boyunca yazılan büyük miktarda kod göz önüne alındığında, büyük oyunculardan herhangi biri rütbeyi bozmaya karar verirse çok şaşırırdım.

Öyleyse hikayenin ahlaki ... Birkaç aşırı durum haricinde, ABI sınırının ilkel tiplerle temiz kalmasını sağlayacak ve aşırı yüklemeyi önleyebilecek arayüzlerden sorumlu bir kişiye ihtiyacınız var. Bu koşulda sorun yoksa, derleyiciler arasında DLL'ler / SO'lerdeki sınıflara arayüzleri paylaşmaktan korkmam. Sınıfları doğrudan paylaşmak == sorun, ancak saf sanal arayüzleri paylaşmak o kadar da kötü değil.


Bu iyi bir nokta ... "Arayüzleri sınıflarla paylaşmaktan korkmayın" demeliydim. Cevabımı düzenleyeceğim.
Ph0t0n

2
Hey bu harika bir cevap, teşekkürler! Bence onu daha da iyi yapan şey, bahsettiğiniz şeylerin (veya hatta bazı kodların) bazı örneklerini gösteren daha ileri okumalara bağlantılar olabilir - örneğin STL sınıflarını paketlemek için vb. Aksi takdirde okuyorum bu cevap ama sonra bu şeylerin gerçekte nasıl görüneceği ve onları nasıl arayacağım konusunda biraz kayboldum.
Ela782

8

Tüm modüller (.exe ve .DLL'ler) aynı C ++ derleyici sürümüyle ve CRT'nin aynı ayarları ve tatları ile oluşturulmadığı sürece, STL nesnelerini DLL sınırları boyunca güvenli bir şekilde geçiremezsiniz, ki bu oldukça kısıtlayıcıdır ve açıkça sizin durumunuz değildir.

DLL'nizden nesne yönelimli bir arabirimi açığa çıkarmak istiyorsanız, saf C ++ arabirimlerini açığa çıkarmalısınız (COM'un yaptığı gibi). CodeProject ile ilgili bu ilginç makaleyi okuyun:

Nasıl Yapılır: C ++ sınıflarını bir DLL'den dışa aktarma

Ayrıca, DLL sınırında saf bir C arabirimi göstermeyi ve ardından arayan sitede bir C ++ sarmalayıcı oluşturmayı da düşünebilirsiniz.
Bu, Win32'de olana benzer: Win32 uygulama kodu neredeyse C ++ 'dır, ancak birçok Win32 API'si saf bir C arabirimi sunar (ayrıca COM arabirimlerini açığa çıkaran API'ler de vardır). Daha sonra ATL / WTL ve MFC, bu saf C arayüzlerini C ++ sınıfları ve nesneleriyle sarar.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.