ฉันจะส่งผ่านวัตถุอย่างปลอดภัยโดยเฉพาะวัตถุ STL เข้าและออกจาก DLL ได้อย่างไร


107

ฉันจะส่งผ่านคลาสออบเจ็กต์โดยเฉพาะออบเจ็กต์ STL ไปยังและจาก C ++ DLL ได้อย่างไร

แอปพลิเคชันของฉันต้องโต้ตอบกับปลั๊กอินของบุคคลที่สามในรูปแบบของไฟล์ DLL และฉันไม่สามารถควบคุมได้ว่าปลั๊กอินเหล่านี้สร้างขึ้นด้วยคอมไพเลอร์ใด ฉันทราบว่าไม่มี ABI ที่รับประกันสำหรับออบเจ็กต์ STL และฉันกังวลว่าจะทำให้แอปพลิเคชันของฉันไม่เสถียร


4
หากคุณกำลังพูดถึง C ++ Standard Library คุณควรเรียกมันว่า STL อาจหมายถึงสิ่งที่แตกต่างกันขึ้นอยู่กับบริบท (ดูstackoverflow.com/questions/5205491/… )
Micha Wiedenmann

คำตอบ:


158

คำตอบสั้น ๆ คำถามนี้ทำไม่ได้ เนื่องจากไม่มี C ++ ABIมาตรฐาน(อินเทอร์เฟซไบนารีของแอปพลิเคชันมาตรฐานสำหรับการเรียกประชุมการบรรจุ / การจัดตำแหน่งข้อมูลขนาดประเภท ฯลฯ ) คุณจะต้องข้ามห่วงจำนวนมากเพื่อพยายามบังคับใช้วิธีมาตรฐานในการจัดการกับคลาส วัตถุในโปรแกรมของคุณ ไม่มีแม้แต่การรับประกันว่าจะใช้งานได้หลังจากที่คุณข้ามห่วงเหล่านั้นทั้งหมดและไม่มีการรับประกันว่าโซลูชันที่ใช้งานได้ในคอมไพเลอร์รีลีสหนึ่งจะใช้งานได้ในครั้งต่อไป

เพียงสร้างอินเทอร์เฟซ C ธรรมดาโดยใช้extern "C"เนื่องจาก C ABI ได้รับการกำหนดไว้อย่างดีและมีเสถียรภาพ


ถ้าคุณต้องการส่งผ่านวัตถุ C ++ ข้ามขอบเขต DLL จริงๆมันเป็นไปได้ในทางเทคนิค นี่คือปัจจัยบางประการที่คุณจะต้องพิจารณา:

การบรรจุ / การจัดตำแหน่งข้อมูล

ภายในคลาสที่กำหนดสมาชิกข้อมูลแต่ละคนมักจะถูกวางไว้เป็นพิเศษในหน่วยความจำดังนั้นที่อยู่ของพวกเขาจึงสอดคล้องกับขนาดของประเภท ตัวอย่างเช่นintอาจมีการจัดแนวตามขอบเขต 4 ไบต์

หาก DLL ของคุณถูกคอมไพล์ด้วยคอมไพเลอร์อื่นที่ไม่ใช่ EXE ของคุณเวอร์ชันของคลาสที่กำหนดของ DLL อาจมีแพ็กเกจที่แตกต่างจากเวอร์ชันของ EXE ดังนั้นเมื่อ EXE ส่งผ่านคลาสอ็อบเจ็กต์ไปยัง DLL DLL อาจไม่สามารถเข้าถึง a สมาชิกข้อมูลที่กำหนดภายในคลาสนั้น DLL จะพยายามอ่านจากที่อยู่ที่ระบุโดยนิยามของคลาสไม่ใช่นิยามของ EXE และเนื่องจากสมาชิกข้อมูลที่ต้องการไม่ได้ถูกเก็บไว้ที่นั่นจริงค่าขยะจะส่งผลให้

คุณสามารถแก้ไขปัญหานี้ได้โดยใช้#pragma packคำสั่งพรีโปรเซสเซอร์ซึ่งจะบังคับให้คอมไพเลอร์ใช้การบรรจุเฉพาะ คอมไพเลอร์จะยังคงใช้การบรรจุเริ่มต้นหากคุณเลือกค่าแพ็คที่ใหญ่กว่าที่คอมไพลเลอร์เลือกไว้ดังนั้นหากคุณเลือกค่าการบรรจุขนาดใหญ่คลาสจะยังคงมีการบรรจุที่แตกต่างกันระหว่างคอมไพเลอร์ วิธีแก้ปัญหาสำหรับสิ่งนี้คือการใช้#pragma pack(1)ซึ่งจะบังคับให้คอมไพเลอร์จัดตำแหน่งสมาชิกข้อมูลในขอบเขตหนึ่งไบต์ (โดยพื้นฐานแล้วจะไม่มีการใช้การบรรจุ) นี่ไม่ใช่ความคิดที่ดีเนื่องจากอาจทำให้เกิดปัญหาด้านประสิทธิภาพหรือแม้แต่ระบบขัดข้องในบางระบบ อย่างไรก็ตามจะช่วยให้มั่นใจได้ว่าสมาชิกข้อมูลในชั้นเรียนของคุณจะถูกจัดแนวในหน่วยความจำอย่างสม่ำเสมอ

การจัดลำดับสมาชิกใหม่

ถ้าคลาสของคุณไม่ใช่โครงร่างมาตรฐานคอมไพลเลอร์สามารถจัดเรียงสมาชิกข้อมูลในหน่วยความจำใหม่ได้ ไม่มีมาตรฐานสำหรับวิธีการดำเนินการดังนั้นการจัดเรียงข้อมูลใหม่อาจทำให้เกิดความไม่ลงรอยกันระหว่างคอมไพเลอร์ การส่งข้อมูลกลับไปกลับมาไปยัง 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 ของคุณทั้งสองโมดูลจะใช้กองที่แตกต่างกัน นี่เป็นปัญหาที่เป็นไปได้โดยเฉพาะเนื่องจากโมดูลกำลังถูกคอมไพล์ด้วยคอมไพเลอร์ที่แตกต่างกัน

เพื่อลดปัญหานี้หน่วยความจำทั้งหมดจะต้องถูกจัดสรรลงในฮีปที่ใช้ร่วมกันและยกเลิกการจัดสรรจากฮีปเดียวกัน โชคดีที่ Windows มี API เพื่อช่วยในเรื่องนี้: GetProcessHeapจะช่วยให้คุณเข้าถึงฮีปของ EXE ของโฮสต์ได้และHeapAlloc / HeapFreeจะช่วยให้คุณจัดสรรและเพิ่มหน่วยความจำภายในฮีปนี้ได้ เป็นสิ่งสำคัญที่คุณจะต้องไม่ใช้แบบปกติmalloc/ freeเนื่องจากไม่มีการรับประกันว่าจะทำงานได้ตามที่คุณคาดหวัง

ปัญหา STL

ไลบรารีมาตรฐาน C ++ มีชุดปัญหา ABI ของตัวเอง มีการรับประกันว่าประเภท STL กำหนดจะวางในลักษณะเดียวกันในหน่วยความจำและไม่มีการรับประกันว่าระดับ STL ให้มีขนาดเท่ากันจากการดำเนินงานไปยังอีก (โดยเฉพาะการแก้ปัญหาสร้างอาจทำให้ข้อมูลการแก้ปัญหาเป็นพิเศษเป็น กำหนดประเภท STL) ดังนั้นคอนเทนเนอร์ STL ใด ๆ จะต้องถูกคลายออกเป็นประเภทพื้นฐานก่อนที่จะส่งผ่านขอบเขต DLL และบรรจุใหม่อีกด้านหนึ่ง

ชื่อมะม่วง

DLL ของคุณน่าจะส่งออกฟังก์ชันที่ EXE ของคุณต้องการเรียกใช้ อย่างไรก็ตามคอมไพเลอร์ C ++ ไม่มีวิธีมาตรฐานในการตั้งชื่อฟังก์ชันที่ซับซ้อน ซึ่งหมายความว่าฟังก์ชันที่ตั้งชื่อGetCCDLLอาจจะยุ่งเหยิง_Z8GetCCDLLvใน GCC และ?GetCCDLL@@YAPAUCCDLL_v1@@XZใน MSVC

คุณจะไม่สามารถรับประกันการลิงก์แบบคงที่ไปยัง DLL ของคุณได้เนื่องจาก DLL ที่สร้างด้วย GCC จะไม่สร้างไฟล์. lib และการเชื่อมโยง DLL แบบคงที่ใน MSVC จำเป็นต้องมี การเชื่อมโยงแบบไดนามิกดูเหมือนจะเป็นตัวเลือกที่สะอาดกว่ามาก แต่การโกงชื่อจะเข้ามาขวางคุณ: หากคุณลองGetProcAddressใช้ชื่อที่ผิดพลาดการโทรจะล้มเหลวและคุณจะไม่สามารถใช้ DLL ของคุณได้ สิ่งนี้ต้องใช้การแฮ็กเกอร์เล็กน้อยในการหลีกเลี่ยงและเป็นเหตุผลสำคัญพอสมควรว่าทำไมการส่งคลาส C ++ ข้ามขอบเขต DLL จึงเป็นความคิดที่ไม่ดี

คุณจะต้องสร้าง DLL ของคุณจากนั้นตรวจสอบไฟล์. deb ที่สร้างขึ้น (หากมีการสร้างขึ้นซึ่งจะแตกต่างกันไปตามตัวเลือกโครงการของคุณ) หรือใช้เครื่องมือเช่น Dependency Walker เพื่อค้นหาชื่อที่ยุ่งเหยิง จากนั้นคุณจะต้องเขียนไฟล์. dev ของคุณเองโดยกำหนดนามแฝงที่ไม่มีการเชื่อมต่อให้กับฟังก์ชันที่ยุ่งเหยิง ตัวอย่างเช่นลองใช้GetCCDLLฟังก์ชันที่ฉันกล่าวถึงเพิ่มเติม ในระบบของฉันไฟล์. dev ต่อไปนี้ใช้ได้กับ GCC และ MSVC ตามลำดับ:

GCC:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

MSVC:

EXPORTS
    GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1

สร้าง DLL ของคุณใหม่จากนั้นตรวจสอบฟังก์ชันที่ส่งออกอีกครั้ง ชื่อฟังก์ชันที่ไม่มีการเชื่อมควรอยู่ในหมู่พวกเขา โปรดทราบว่าคุณไม่สามารถใช้ฟังก์ชันที่โอเวอร์โหลดได้ด้วยวิธีนี้ : ชื่อฟังก์ชันที่ไม่มีการเชื่อมโยงเป็นนามแฝงสำหรับการโอเวอร์โหลดฟังก์ชันเฉพาะหนึ่งฟังก์ชันตามที่กำหนดโดยชื่อที่ถูกแยกออก โปรดทราบว่าคุณจะต้องสร้างไฟล์. dev ใหม่สำหรับ DLL ของคุณทุกครั้งที่คุณเปลี่ยนการประกาศฟังก์ชันเนื่องจากชื่อที่ยุ่งเหยิงจะเปลี่ยนไป สิ่งสำคัญที่สุดคือการข้ามชื่อที่ยุ่งเหยิงคุณกำลังลบล้างการป้องกันใด ๆ ที่ผู้เชื่อมโยงพยายามเสนอให้คุณเกี่ยวกับปัญหาความไม่ลงรอยกัน

กระบวนการทั้งหมดนี้จะง่ายกว่าถ้าคุณสร้างอินเทอร์เฟซสำหรับ DLL ของคุณที่จะทำตามเนื่องจากคุณจะมีฟังก์ชันเดียวในการกำหนดนามแฝงแทนที่จะต้องสร้างนามแฝงสำหรับทุกฟังก์ชันใน DLL ของคุณ อย่างไรก็ตามยังคงใช้คำเตือนเดียวกัน

การส่งผ่านคลาสอ็อบเจ็กต์ไปยังฟังก์ชัน

นี่อาจเป็นปัญหาที่ละเอียดอ่อนและอันตรายที่สุดในการส่งผ่านข้อมูลข้ามคอมไพเลอร์ แม้ว่าคุณจะจัดการทุกอย่างอื่นมีมาตรฐานสำหรับวิธีการขัดแย้งจะถูกส่งผ่านไปยังฟังก์ชันไม่มี ซึ่งอาจทำให้เกิดปัญหาที่ลึกซึ้งมีเหตุผลที่ชัดเจนและไม่มีวิธีที่ง่ายที่จะแก้ปัญหาให้พวกเขา คุณจะต้องส่งผ่านอาร์กิวเมนต์ทั้งหมดผ่านพอยน์เตอร์รวมถึงบัฟเฟอร์สำหรับค่าส่งคืนใด ๆ นี่เป็นเรื่องเงอะงะและไม่สะดวกและยังเป็นอีกหนึ่งวิธีแก้ปัญหาแฮ็กที่อาจได้ผลหรือไม่


การรวบรวมวิธีแก้ปัญหาเหล่านี้ทั้งหมดเข้าด้วยกันและสร้างงานสร้างสรรค์บางอย่างด้วยเทมเพลตและตัวดำเนินการเราสามารถพยายามส่งผ่านวัตถุข้ามขอบเขต DLL ได้อย่างปลอดภัย โปรดทราบว่าการสนับสนุน C ++ 11 เป็นสิ่งจำเป็นเช่นเดียวกับการสนับสนุน#pragma packและตัวแปรต่างๆ MSVC 2013 ให้การสนับสนุนนี้เช่นเดียวกับ GCC และเสียงดังรุ่นล่าสุด

//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ความเชี่ยวชาญมีตรรกะเพิ่มเติมเล็กน้อยเนื่องจากถูกแปลงเป็น a 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 และผู้โทรทุกคนสามารถใช้ได้ โปรดทราบว่าเรากำลังส่งตัวชี้ไปที่ a 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ไปยังปกติwstringและแสดงในกล่องข้อความ ท้ายที่สุดนี่เป็นเพียงPOCไม่ใช่ไลบรารียูทิลิตี้เต็มรูปแบบ

ตอนนี้เราสามารถสร้าง DLL อย่าลืมไฟล์. deb พิเศษเพื่อหลีกเลี่ยงการโกงชื่อผู้เชื่อมโยง (หมายเหตุ: โครงสร้าง CCDLL ที่ฉันสร้างและรันมีฟังก์ชันมากกว่าที่ฉันนำเสนอที่นี่ไฟล์. dev อาจไม่ทำงานตามที่คาดไว้)

ตอนนี้สำหรับ 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

ภาพที่แสดงผลลัพธ์หลังจากนั้น


สรุปได้ว่าหากคุณต้องส่งวัตถุ C ++ ข้ามขอบเขต DLL อย่างแน่นอนนี่คือวิธีที่คุณทำ อย่างไรก็ตามไม่มีการรับประกันว่าจะใช้ได้กับการตั้งค่าของคุณหรือของใครก็ตาม สิ่งเหล่านี้อาจพังได้ตลอดเวลาและอาจจะหยุดทำงานในวันก่อนที่ซอฟต์แวร์ของคุณจะมีกำหนดเปิดตัวครั้งใหญ่ เส้นทางนี้เต็มไปด้วยการแฮ็กความเสี่ยงและความโง่เขลาทั่วไปที่ฉันน่าจะถูกยิง หากคุณไปเส้นทางนี้โปรดทดสอบด้วยความระมัดระวังเป็นอย่างยิ่ง และจริงๆ ... อย่าทำอย่างนี้เลย


1
อืมไม่เลว! คุณดึงคอลเลกชันของอาร์กิวเมนต์ที่ค่อนข้างดีโดยใช้ประเภท c ++ มาตรฐานเพื่อโต้ตอบกับWindows DLLและติดแท็กตามนั้น ข้อ จำกัด ABI โดยเฉพาะเหล่านี้จะไม่ใช้กับโซ่เครื่องมืออื่นที่ไม่ใช่ MSVC ควรพูดถึงเรื่องนี้ด้วยซ้ำ ...
πάνταῥεῖ

12
@DavidHeffernan ขวา. แต่นี่เป็นผลมาจากการวิจัยเป็นเวลาหลายสัปดาห์สำหรับฉันดังนั้นฉันจึงคิดว่ามันคุ้มค่าที่จะบันทึกสิ่งที่ฉันได้เรียนรู้เพื่อให้คนอื่นไม่จำเป็นต้องทำวิจัยเดียวกันและความพยายามเดียวกันในการแฮ็กโซลูชันที่ใช้งานได้ ยิ่งไปกว่านั้นเนื่องจากสิ่งนี้ดูเหมือนจะเป็นคำถามกึ่ง ๆ ทั่วไปที่นี่
cf ยืนคู่กับ Monica

@ πάνταῥεῖ ข้อ จำกัด ABI เฉพาะเหล่านี้จะไม่ใช้กับโซ่เครื่องมืออื่นที่ไม่ใช่ MSVC ควรพูดถึงเรื่องนี้ด้วยซ้ำ ...ฉันไม่แน่ใจว่าเข้าใจถูกต้อง คุณกำลังระบุว่าปัญหา ABI เหล่านี้เป็นเอกสิทธิ์ของ MSVC หรือไม่และกล่าวได้ว่า DLL ที่สร้างด้วยเสียงดังก้องจะทำงานร่วมกับ EXE ที่สร้างด้วย GCC ได้สำเร็จหรือไม่ ฉันสับสนเล็กน้อยเนื่องจากดูเหมือนว่าจะขัดแย้งกับงานวิจัยทั้งหมดของฉัน ...
cf ย่อมาจาก Monica

@computerfreaker ไม่ฉันกำลังบอกว่า PE และ ELF ใช้รูปแบบ ABI ที่แตกต่างกัน ...
πάνταῥεῖ

3
@computerfreaker คอมไพเลอร์ C ++ ที่สำคัญส่วนใหญ่ (GCC, เสียงดัง, ICC, EDG ฯลฯ ) เป็นไปตาม Itanium C ++ ABI MSVC ไม่ได้ ใช่แล้วปัญหา ABI เหล่านี้ส่วนใหญ่เฉพาะสำหรับ MSVC แม้ว่าจะไม่ใช่เฉพาะแม้แต่คอมไพเลอร์ C บนแพลตฟอร์ม Unix (และแม้แต่คอมไพเลอร์รุ่นเดียวกัน!) ก็ยังประสบปัญหาความสามารถในการทำงานร่วมกันน้อยกว่าที่สมบูรณ์แบบ แม้ว่าพวกเขาจะอยู่ใกล้กันมากพอที่ฉันจะไม่แปลกใจเลยที่พบว่าคุณสามารถเชื่อมโยง DLL ที่สร้างเสียงดังก้องกับไฟล์ปฏิบัติการที่สร้างขึ้นด้วย GCC ได้สำเร็จ
Stuart Olsen

17

@computerfreaker ได้เขียนคำอธิบายที่ดีเยี่ยมว่าเหตุใดการขาด ABI จึงป้องกันไม่ให้ส่งผ่านวัตถุ C ++ ข้ามขอบเขต DLL ในกรณีทั่วไปแม้ว่าคำจำกัดความประเภทจะอยู่ภายใต้การควบคุมของผู้ใช้และใช้ลำดับโทเค็นเดียวกันในทั้งสองโปรแกรม (มีสองกรณีที่ใช้งานได้: คลาสโครงร่างมาตรฐานและอินเทอร์เฟซบริสุทธิ์)

สำหรับประเภทออบเจ็กต์ที่กำหนดไว้ในมาตรฐาน C ++ (รวมถึงประเภทที่ดัดแปลงมาจากไลบรารีเทมเพลตมาตรฐาน) สถานการณ์นั้นแย่กว่ามาก โทเค็นที่กำหนดประเภทเหล่านี้ไม่เหมือนกันในคอมไพเลอร์หลายตัวเนื่องจาก C ++ Standard ไม่ได้ระบุนิยามประเภทที่สมบูรณ์มีเพียงข้อกำหนดขั้นต่ำเท่านั้น นอกจากนี้การค้นหาชื่อของตัวระบุที่ปรากฏในคำจำกัดความประเภทเหล่านี้ไม่สามารถแก้ไขได้เหมือนกัน แม้ในระบบที่มี C ++ ABI การพยายามแชร์ประเภทดังกล่าวข้ามขอบเขตของโมดูลจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดจำนวนมากเนื่องจากการละเมิดกฎคำจำกัดความเดียว

นี่เป็นสิ่งที่โปรแกรมเมอร์ Linux ไม่คุ้นเคยกับการจัดการเนื่องจาก libstdc ++ ของ g ++ เป็นมาตรฐาน de-facto และแทบทุกโปรแกรมใช้มันจึงทำให้ ODR เป็นที่พอใจ libc ++ ของ clang ทำลายสมมติฐานนั้นและจากนั้น C ++ 11 ก็มาพร้อมกับการเปลี่ยนแปลงที่จำเป็นสำหรับไลบรารีมาตรฐานเกือบทุกประเภท

อย่าแชร์ประเภทไลบรารีมาตรฐานระหว่างโมดูล เป็นพฤติกรรมที่ไม่ได้กำหนด


17

คำตอบบางส่วนที่นี่ทำให้คลาส C ++ ที่ผ่านนั้นฟังดูน่ากลัวจริงๆ แต่ฉันต้องการแบ่งปันมุมมองอื่น วิธีการ C ++ เสมือนจริงที่กล่าวถึงในการตอบสนองอื่น ๆ บางส่วนกลับกลายเป็นว่าสะอาดกว่าที่คุณคิด ฉันได้สร้างระบบปลั๊กอินทั้งหมดตามแนวคิดและทำงานได้ดีมากเป็นเวลาหลายปี ฉันมีคลาส "PluginManager" ที่โหลด dll แบบไดนามิกจากไดเร็กทอรีที่ระบุโดยใช้ LoadLib () และ GetProcAddress () (และลินุกซ์ที่เทียบเท่าเพื่อให้สามารถเรียกใช้งานได้เพื่อทำให้ข้ามแพลตฟอร์ม)

เชื่อหรือไม่ว่าวิธีนี้สามารถให้อภัยได้แม้ว่าคุณจะทำสิ่งแปลก ๆ เช่นเพิ่มฟังก์ชันใหม่ที่ส่วนท้ายของอินเทอร์เฟซเสมือนจริงของคุณและพยายามโหลด dll ที่คอมไพล์กับอินเทอร์เฟซโดยไม่มีฟังก์ชั่นใหม่ก็จะโหลดได้ดี แน่นอน ... คุณจะต้องตรวจสอบหมายเลขเวอร์ชันเพื่อให้แน่ใจว่าไฟล์ปฏิบัติการของคุณเรียกใช้ฟังก์ชันใหม่สำหรับ dll รุ่นใหม่ที่ใช้ฟังก์ชันเท่านั้น แต่ข่าวดีก็คือมันได้ผล! ดังนั้นคุณมีวิธีการที่หยาบคายในการพัฒนาอินเทอร์เฟซของคุณเมื่อเวลาผ่านไป

สิ่งที่ยอดเยี่ยมอีกอย่างเกี่ยวกับอินเทอร์เฟซเสมือนจริง - คุณสามารถสืบทอดอินเทอร์เฟซได้มากเท่าที่คุณต้องการและคุณจะไม่มีวันเจอปัญหาเพชร!

ฉันจะบอกว่าข้อเสียที่ใหญ่ที่สุดของแนวทางนี้คือคุณต้องระวังให้มากว่าประเภทใดที่คุณส่งผ่านเป็นพารามิเตอร์ ไม่มีคลาสหรือออบเจ็กต์ STL โดยไม่ต้องห่อด้วยอินเทอร์เฟซเสมือนจริงก่อน ไม่มีโครงสร้าง (โดยไม่ต้องผ่าน pragma pack voodoo) เพียงประเภทดั้งเดิมและตัวชี้ไปยังอินเทอร์เฟซอื่น ๆ นอกจากนี้คุณไม่สามารถใช้งานฟังก์ชั่นมากเกินไปซึ่งเป็นความไม่สะดวก แต่ไม่ใช่ตัวหยุดการแสดง

ข่าวดีก็คือด้วยโค้ดเพียงไม่กี่บรรทัดคุณสามารถสร้างคลาสและอินเทอร์เฟซทั่วไปที่ใช้ซ้ำได้เพื่อรวมสตริง STL เวกเตอร์และคลาสคอนเทนเนอร์อื่น ๆ หรือคุณสามารถเพิ่มฟังก์ชันลงในอินเทอร์เฟซของคุณเช่น GetCount () และ GetVal (n) เพื่อให้ผู้คนวนซ้ำรายการ

ผู้คนสร้างปลั๊กอินให้เราพบว่ามันค่อนข้างง่าย พวกเขาไม่จำเป็นต้องเป็นผู้เชี่ยวชาญในขอบเขต ABI หรืออะไรเลยพวกเขาแค่สืบทอดอินเทอร์เฟซที่พวกเขาสนใจโค้ดฟังก์ชันที่รองรับและส่งคืนเท็จสำหรับสิ่งที่พวกเขาไม่ทำ

เทคโนโลยีที่ทำให้งานทั้งหมดนี้ไม่ได้ขึ้นอยู่กับมาตรฐานใด ๆ เท่าที่ฉันรู้ จากสิ่งที่ฉันรวบรวม Microsoft ตัดสินใจที่จะทำตารางเสมือนจริงเพื่อให้พวกเขาสามารถสร้าง COM ได้และนักเขียนคอมไพเลอร์คนอื่น ๆ ก็ตัดสินใจทำตาม ซึ่งรวมถึง GCC, Intel, Borland และคอมไพเลอร์ C ++ หลัก ๆ ส่วนใหญ่ หากคุณกำลังวางแผนที่จะใช้คอมไพเลอร์ฝังตัวที่คลุมเครือวิธีนี้อาจไม่เหมาะกับคุณ ในทางทฤษฎี บริษัท คอมไพเลอร์ใด ๆ สามารถเปลี่ยนตารางเสมือนได้ตลอดเวลาและทำลายสิ่งต่าง ๆ แต่เมื่อพิจารณาถึงโค้ดจำนวนมากที่เขียนขึ้นในช่วงหลายปีที่ผ่านมาซึ่งขึ้นอยู่กับเทคโนโลยีนี้ฉันจะแปลกใจมากหากผู้เล่นรายใหญ่ตัดสินใจที่จะทำลายอันดับ

ดังนั้นคุณธรรมของเรื่องราวก็คือ ... ยกเว้นสถานการณ์ที่รุนแรงบางประการคุณต้องมีคนหนึ่งคนที่รับผิดชอบอินเทอร์เฟซที่สามารถตรวจสอบให้แน่ใจว่าขอบเขต ABI ยังคงสะอาดอยู่กับประเภทดั้งเดิมและหลีกเลี่ยงการโอเวอร์โหลด หากคุณพอใจกับข้อกำหนดนั้นฉันจะไม่กลัวที่จะแชร์อินเทอร์เฟซกับคลาสใน DLL / SO ระหว่างคอมไพเลอร์ การแบ่งปันชั้นเรียนโดยตรง == ปัญหา แต่การแบ่งปันอินเทอร์เฟซเสมือนจริงนั้นไม่เลวร้ายนัก


นั่นเป็นประเด็นที่ดี ... ฉันควรจะบอกว่า "อย่ากลัวที่จะแชร์อินเทอร์เฟซกับชั้นเรียน" ฉันจะแก้ไขคำตอบของฉัน
Ph0t0 น

2
นี่เป็นคำตอบที่ดีขอบคุณ! สิ่งที่จะทำให้ดียิ่งขึ้นในความคิดของฉันคือลิงก์ไปยังการอ่านเพิ่มเติมที่แสดงตัวอย่างบางส่วนของสิ่งที่คุณกำลังพูดถึง (หรือแม้แต่โค้ดบางส่วน) เช่นการตัดคลาส STL เป็นต้นมิฉะนั้นฉันกำลังอ่าน คำตอบนี้ แต่ฉันก็หลงทางเล็กน้อยว่าสิ่งเหล่านี้จะมีลักษณะอย่างไรและจะค้นหาได้อย่างไร
Ela782

8

คุณไม่สามารถส่งออบเจ็กต์ STL ข้ามขอบเขต DLL ได้อย่างปลอดภัยเว้นแต่ว่าโมดูลทั้งหมด (.EXE และ.

หากคุณต้องการเปิดเผยอินเทอร์เฟซเชิงวัตถุจาก DLL ของคุณคุณควรแสดงอินเตอร์เฟสที่บริสุทธิ์ของ C ++ (ซึ่งคล้ายกับที่ COM ทำ) ลองอ่านบทความที่น่าสนใจเกี่ยวกับ CodeProject:

HowTo: ส่งออกคลาส C ++ จาก DLL

คุณอาจต้องการพิจารณาแสดงอินเทอร์เฟซ C แท้ที่ขอบเขต DLL จากนั้นสร้าง C ++ wrapper ที่ไซต์ผู้โทร
สิ่งนี้คล้ายกับสิ่งที่เกิดขึ้นใน Win32: โค้ดการใช้งาน Win32 เกือบจะเป็น C ++ แต่ Win32 API จำนวนมากจะแสดงอินเทอร์เฟซ C ที่บริสุทธิ์ (นอกจากนี้ยังมี API ที่แสดงอินเตอร์เฟส COM) จากนั้น ATL / WTL และ MFC จะรวมอินเทอร์เฟซ C บริสุทธิ์เหล่านี้เข้ากับคลาสและอ็อบเจ็กต์ C ++

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.