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