العرض التوضيحي
هام: بدءًا من MacOS 10.14 ، لم يعد النهج المستخدم في المكتبة يعمل ، يبدو أن Apple قد تقيد بعض الإجراءات المنخفضة المستوى. على Linux على الأقل على Ubuntu 20.04 لا يزال يعمل بشكل جيد.
Jet-Live هي مكتبة لـ C ++ "إعادة تحميل الرمز الساخن". إنه يعمل على Linux و MacOS الحديثة (10.12+ أعتقد) على أنظمة 64 بت مدعومة بواسطة وحدة المعالجة المركزية مع مجموعة تعليمات X86-64. بصرف النظر عن إعادة تحميل الوظائف ، فإنه قادر على الاحتفاظ بالدولة الثابتة والعالمية التي لم تتغير بعد إعادة التحميل (يرجى الرجوع إلى "كيف تعمل" لما هو ولماذا هو مهم). تم اختباره على Ubuntu 18.04 مع Clang 6.0.1/7.0.1 ، LLD-7 ، GCC 6.4.0/7.3.0 ، GNU LD 2.30 ، CMAKE 3.10.2 ، Ninja 1.8.2 ، Make 4.1 و MacOS 10.13.6 مع XCODE 8.3.3 ، CMAKE 3.8.2 ، Make 3.81.
هام: لا تجبرك هذه المكتبة على تنظيم الكود الخاص بك بطريقة خاصة (كما هو الحال في RCCPP أو CR) ، لا تحتاج إلى فصل التعليمات البرمجية القابلة للتحميل في بعض المكتبات المشتركة ، يجب أن تعمل Jet-Live مع أي مشروع بأقل طريقة.
إذا كنت بحاجة إلى شيء مشابه لنظام التشغيل Windows ، فيرجى تجربة Blink ، ليس لدي أي خطط لدعم Windows.
تحتاج إلى c++11 مترجم متوافق. كما أن هناك العديد من التبعيات التي يتم تجميعها ، معظمها عبارة عن مكتبة زوج H/CPP واحدة فقط أو واحدة. يرجى الرجوع إلى دليل lib للحصول على التفاصيل.
هذه المكتبة هي الأنسب للمشاريع القائمة على أنظمة CMake و Make أو Ninja Build ، يتم ضبط التخفيضات على هذه الأدوات. سيقوم Cmakelists.txt بإضافة set(CMAKE_EXPORT_COMPILE_COMMANDS ON) لـ compile_commands.json و ALTER Compiler و Linker. هذا مهم ولا يمكن تجنبه. للحصول على التفاصيل ، يرجى الاطلاع على cmakelists.txt. إذا كنت تستخدم Ninja ، -d keepdepfile .
include ( path /to/jet-live/cmake/jet_live_setup.cmake) # setup needed compiler and linker flags, include this file in your root CMakeLists.txt
set (JET_LIVE_BUILD_EXAMPLE OFF )
set (JET_LIVE_SHARED ON ) # if you want to
add_subdirectory ( path /to/jet-live)
target_link_libraries (your-app- target jet-live)jet::Live ClassliveInstance->update()liveInstance->tryReload()هام: هذه المكتبة ليست آمنة. يستخدم مؤشرات الترابط أسفل الغطاء لتشغيل المترجم ، ولكن يجب عليك الاتصال بجميع أساليب المكتبة من نفس الموضوع.
كما أنني أستخدم هذه المكتبة فقط مع تصميمات التصحيح ( -O0 ، ولم يتم تجريدها ، دون أن -fvisibility=hidden وأشياء من هذا القبيل) لعدم التعامل مع الوظائف والمتغيرات المحسنة والمضطر. لا أعرف كيف يعمل على بنيات محسّنة للغاية ، على الأرجح لن يعمل على الإطلاق.
أنا شخصياً أستخدمه هكذا. لدي اختصار Ctrl+r الذي تم تعيين tryReload في طلبي. أيضًا ، يقوم تطبيق التطبيق الخاص بي update في RunLoop الرئيسي ويستمع إلى أحداث onCodePreLoad و onCodePostLoad لإعادة إنشاء بعض الكائنات أو إعادة تقييم بعض الوظائف:
Ctrl+r ستقوم Jet-Live بمراقبة تغييرات الملفات ، وملفات إعادة الترجمة ، وفقط عندما يتم تسمية tryReload ، ستنتظر جميع عمليات التجميع الحالية لإنهاء وإعادة تحميل التعليمات البرمجية الجديدة. يرجى عدم الاتصال tryReload في كل تحديث ، ولن يعمل كما تتوقع ، اتصل به فقط عندما يكون رمز المصدر جاهزًا لإعادة تحميله.
إذا كنت لا ترغب في التبديل ذهابًا وإيابًا بين محرر الرمز والتطبيق ، فيمكنك تكوين اختصار لوحة المفاتيح الذي يقوم بتشغيل أمر Shell kill -s USR1 $(pgrep <your_app_name>) ، ستؤدي المكتبة إلى إعادة تحميل الكود عند استلام إشارة SIGUSR1 . إنه يعمل على الأقل في Emacs و Xcode و Clion و Vscode ، لكنني متأكد من أنه يمكن تحقيقه في المحررين والمعاصفات الأخرى ، فقط Google. إذا كان مصحح الأخطاء الخاص بك هو LLDB وأمسك هذه الإشارة وأوقف التطبيق ، فأضف هذه الأوامر إلى ملف ~/.lldbinit :
breakpoint set --name main
breakpoint command add
process handle -n true -p true -s false SIGUSR1
continue
DONE
على MacOS ، يمكنك استخدام مولد cmake -G Xcode بصرف النظر عن Make و Ninja. في هذه الحالة ، يرجى تثبيت xcpretty Gem:
gem install xcpretty
هناك تطبيق مثال بسيط ، فقط قم بتشغيله:
git clone https://github.com/ddovod/jet-live.git && cd jet-live
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug .. && make
./example/example وجرب hello الأمر. لا تنس تشغيل أمر reload بعد إصلاح الوظيفة.
هناك مجموعة اختبار ليست شاملة للغاية ، ولكنها تحديث باستمرار. لتشغيله:
git clone https://github.com/ddovod/jet-live.git && cd jet-live
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DJET_LIVE_BUILD_TESTS=ON .. && make
../tools/tests/test_runner.py -b . -s ../tests/src/مُنفّذ:
compile_commands.json بعد إنشاء ملف .cpp جديد)سيتم تنفيذها:
لن يتم تنفيذها على الإطلاق:
تم ضبط Jet-Live للعمل مع أدوات Cmake و Make/Ninja ، ولكن إذا كنت ترغب في تبنيها إلى أداة بناء أخرى ، فهناك طريقة لتخصيص سلوكها في بعض الجوانب. يرجى الرجوع إلى المصادر والوثائق. من الجيد أيضًا إنشاء مستمع خاص بك لتلقي الأحداث من المكتبة. يرجى الرجوع إلى وثائق ILiveListener و LiveConfig .
هام: يوصى بشدة بتسجيل جميع الرسائل من المكتبة باستخدام ILiveListener::onLog لمعرفة ما إذا كان هناك خطأ ما.
تقرأ المكتبة رؤوس القزم وأقسام هذه المكتبات القابلة للتنفيذ وجميع المكتبات المشتركة ، وتجد جميع الرموز ويحاول معرفة أي منها يمكن أن يكون مدمن مخدرات (وظائف) أو يجب نقله/نقله (المتغيرات الثابتة/العالمية). كما يجد حجم الرموز وعنوان "حقيقي".
بصرف النظر عن ذلك ، تحاول Jet-Live العثور على compile_commands.json بالقرب من قابلة للتنفيذ أو في أدلة الوالدين بشكل متكرر. باستخدام هذا الملف يميز:
.o (كائن) مسار الملف.d (depfile) مسار الملفات عندما يتم تحليل جميع وحدات التجميع ، فإنه يميز الدليل الأكثر شيوعًا لجميع ملفات المصدر ويبدأ في المشاهدة لجميع الدلائل مع ملفات المصدر وتبعياتها وبعض ملفات الخدمة مثل compile_commands.json .
بصرف النظر عن ذلك ، تحاول المكتبة العثور على جميع التبعيات لكل وحدة تجميع. بشكل افتراضي ، ستقرأ DepFiles بالقرب من ملفات الكائن (انظر -MD Option Option). افترض أن ملف الكائن موجود في:
/home/coolhazker/projects/some_project/build/main.cpp.o
ستحاول Jet-Live العثور على Depfile على:
/home/coolhazker/projects/some_project/build/main.cpp.o.d
or
/home/coolhazker/projects/some_project/build/main.cpp.d
سوف تلتقط جميع التبعيات التي تقع تحت أدلة المشاهدة ، لذلك لن يتم التعامل مع أشياء مثل /usr/include/elf.h على أنها تبعية حتى لو تم تضمين هذا الملف حقًا في بعض ملفات .cpp الخاصة بك.
الآن تتم تهيئة المكتبة.
بعد ذلك ، عند تحرير بعض الملفات المصدر وحفظه ، يبدأ Jet-Live على الفور في تجميع جميع الملفات التابعة في الخلفية. افتراضيًا ، يكون عدد عمليات التجميع المتزامنة 4 ، ولكن يمكنك تكوينه. سوف تكتب لتسجيل النجاحات والأخطاء باستخدام طريقة المستمع ILiveListener::onLog . إذا قمت بتوجيه تجميع لبعض الملفات عند تجميعها بالفعل (أو تنتظر في قائمة الانتظار) ، فسيتم قتل عملية التجميع القديمة وسيتم إضافة نسخة جديدة إلى قائمة الانتظار ، وبالتالي فإنها آمنة نوعًا ما حتى لا تنتظر التجميع وإجراء تغييرات جديدة في الرمز. أيضًا بعد تجميع كل ملف ، سيتم تحديث التبعيات للملف المترجم حيث يمكن للمترجم إعادة إنشاء DepFile لذلك إذا كان للإصدار الجديد من وحدة التجميع تبعيات جديدة.
عند الاتصال على Live::tryReload ، ستنتظر المكتبة عمليات تجميع غير مكتملة ، ثم سيتم ربط جميع ملفات الكائنات الجديدة المتراكمة معًا في مكتبة مشتركة ووضعها بالقرب من قابلة للتنفيذ مع اسم lib_reloadXXX.so ، حيث يكون XXX عدد من "إعادة التحميل" خلال هذه الجلسة. لذلك lib_reloadXXX.so يحتوي على جميع التعليمات البرمجية الجديدة.
تقوم Jet-Live بتحميل هذه المكتبة باستخدام dlopen ، يقرأ رؤوس وأقسام ELF/Mach-O ويجد جميع الرموز. كما أنه يقوم بتحميل معلومات النقل من ملفات الكائنات التي تم استخدامها لإنشاء هذه المكتبة الجديدة. بعد ذلك:
memcpy من الموقع القديم إلى واحد جديد هام: يتم إطلاق حدث ILiveListener::onCodePreLoad قبل lib_reloadXXX.so في ذاكرة العملية. يتم إطلاق حدث ILiveListener::onCodePostLoad مباشرة بعد الانتهاء من جميع عمليات التحميل في الكود.
يمكنك قراءة المزيد عن وظيفة التثبيت هنا. تستخدم هذه المكتبة مكتبة Subhook Awesome لإعادة توجيه تدفق الوظيفة من قديمة إلى جديدة. يمكنك أن ترى أنه على 32 بت من منصاتك ، يجب أن تكون وظائفك ما لا يقل عن 5 بايت لتكون قابلة للتوصيل. في 64 بت ، تحتاج إلى ما لا يقل عن 14 بايت وهو الكثير ، وعلى سبيل المثال ، لن تتناسب وظيفة الكعب الفارغة في 14 بايت. من ملاحظاتي ، ينتج Clang افتراضيًا رمزًا مع محاذاة وظائف 16 بايت. لا تقوم GCC بذلك افتراضيًا ، لذا بالنسبة إلى GCC ، يتم استخدام العلم -falign-functions=16 . هذا يعني أن التباعد بين أي وظائف 2 لا يقل عن 16 بايت ، مما يجعل من الممكن ربط أي وظيفة.
يجب أن تستخدم الإصدارات الجديدة من الوظائف الإحصائيات والكرات التي تعيش بالفعل في التطبيق. لماذا هو مهم؟ افترض أن لديك (مثال اصطناعي بعض الشيء ، ولكن على أي حال):
// Singleton.hpp
class Singleton
{
public:
static Singleton& instance ();
};
int veryUsefulFunction ( int value);
// Singleton.cpp
Singleton& Singleton::instance ()
{
static Singleton ins;
return ins;
}
int veryUsefulFunction ( int value)
{
return value * 2 ;
} ثم تريد تحديث veryUsefulFunction إلى SMTH مثل هذا:
int veryUsefulFunction ( int value)
{
return value * 3 ;
} رائع ، الآن يضاعف الوسيطة بحلول 3. ولكن منذ أن تم إعادة تحميل Singleton.cpp static Singleton ins وسيتم ربط وظيفة Singleton::instance() Singleton::instance للاتصال بإصدار جديد ، lib_reloadXXX.so مرة أخرى. ولهذا السبب نحتاج إلى نقل جميع الإحصائيات والكراتية إلى الكود الجديد ونقل متغيرات الحرس للإحصائيات. معظم عمليات نقل وقت الارتباط المتعلقة بالإحصائيات والكراتية هي 32 بت. لذا ، إذا تم تحميل المكتبة المشتركة التي تحتوي على رمز جديد بعيدًا في الذاكرة من التطبيق ، فلن يكون من الممكن نقل المتغيرات بهذه الطريقة. لحل هذا ، يتم ربط المكتبة المشتركة الجديدة باستخدام أعلام الارتباط الخاصة التي تتيح لنا تحميلها في موقع محدد محدد مسبقًا في الذاكرة الافتراضية (انظر -image_base في Apple LD ، --image-base في LLVM LLD و -Ttext-segment + -z max-page-size في علامات رابط GNU LD).
من المحتمل أيضًا أن ينهار تطبيقك إذا حاولت تغيير تخطيط الذاكرة لأنواع البيانات الخاصة بك في رمز قابل لإعادة التحميل.
لنفترض أن لديك مثيلًا لهذه الفئة مخصصة في مكان ما في الكومة أو على المكدس:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
}
private:
int m_someVar1 = 0 ;
}يمكنك تحريره والآن يبدو:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
m_someVar2++;
}
private:
int m_someVar1 = 0 ;
int m_someVar2 = 0 ;
} بعد إعادة تحميل الكود ، من المحتمل أن تلاحظ تصادمًا لأن الكائن المخصص بالفعل لديه تخطيط مختلف للبيانات ، فهو لا يحتوي على متغير مثيل m_someVar2 ، ولكن سيحاول إصدار جديد من calledEachUpdate تعديل البيانات العشوائية فعليًا. في مثل هذه الحالات ، يجب عليك حذف هذا المثيل في رد الاتصال onCodePreLoad وإعادة إنشائه في رد اتصال onCodePostLoad . النقل الصحيح لدولتها متروك لك. سيحدث نفس التأثير إذا حاولت تغيير تصميم هياكل البيانات الثابتة. نفس الشيء الصحيح أيضًا بالنسبة للفئات المتعددة الأشكال (VTable) و Lambdas مع التقاطات (يتم تخزين الأسر داخل حقول بيانات Lambdas).
معهد ماساتشوستس للتكنولوجيا
للحصول على تراخيص المكتبات المستعملة ، يرجى الرجوع إلى الدلائل ورمز المصدر.