การสาธิต
สำคัญ: เริ่มต้นจาก MacOS 10.14 วิธีการที่ใช้ในห้องสมุดไม่ทำงานอีกต่อไปดูเหมือนว่า Apple จะ จำกัด กิจวัตรระดับต่ำบางส่วน บน Linux อย่างน้อยบน Ubuntu 20.04 มันยังใช้งานได้ดี
Jet-Live เป็นไลบรารีสำหรับ C ++ "Hot Code Reloading" มันทำงานบน Linux และ Modern MacOS (10.12+ ฉันเดา) บนระบบ 64 บิตที่ขับเคลื่อนโดย CPU ด้วยชุดคำสั่ง 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, นินจา 1.8.2, ทำ 4.1 และ MacOS 10.13.6 ด้วย XCODE 8.3.3, CMAKE 3.8.2
สำคัญ: ห้องสมุดนี้ไม่ได้บังคับให้คุณจัดระเบียบรหัสของคุณด้วยวิธีพิเศษบางอย่าง (เช่นใน RCCPP หรือ CR) คุณไม่จำเป็นต้องแยกรหัสที่สามารถโหลดได้ออกเป็นห้องสมุดที่ใช้ร่วมกันบางส่วน Jet-Live ควรทำงานกับโครงการใด ๆ ในวิธีที่น่ารำคาญน้อยที่สุด
หากคุณต้องการสิ่งที่คล้ายกันสำหรับ Windows โปรดลอง Blink ฉันไม่มีแผนที่จะสนับสนุน Windows
คุณต้องการคอมไพเลอร์ที่สอดคล้องกับ c++11 นอกจากนี้ยังมีการพึ่งพาหลายอย่างที่รวมอยู่ในนั้นส่วนใหญ่เป็นส่วนหัวอย่างเดียวหรือห้องสมุดคู่ H/CPP เดี่ยว โปรดดูรายละเอียดของไดเรกทอรี lib
ห้องสมุดนี้เหมาะที่สุดสำหรับโครงการตาม CMake และ Make หรือ Ninja Build Systems ค่าเริ่มต้นได้รับการปรับแต่งสำหรับเครื่องมือเหล่านี้ cmakelists.txt จะเพิ่ม set(CMAKE_EXPORT_COMPILE_COMMANDS ON) ตัวเลือกสำหรับ compile_commands.json และ Alter Compiler และ Linker Flags นี่เป็นสิ่งสำคัญและไม่สามารถหลีกเลี่ยงได้ สำหรับรายละเอียดโปรดดู cmakelists.txt หากคุณใช้นินจาให้เพิ่ม -d keepdepfile Ninja Flag เมื่อเรียกใช้นินจาสิ่งนี้จำเป็นสำหรับการติดตามการพึ่งพาระหว่างไฟล์ต้นฉบับและไฟล์ส่วนหัว
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()สำคัญ: ไลบรารีนี้ไม่ปลอดภัย มันใช้เธรดภายใต้ฮูดเพื่อเรียกใช้คอมไพเลอร์ แต่คุณควรโทรหาวิธีไลบรารีทั้งหมดจากเธรดเดียวกัน
นอกจากนี้ฉันยังใช้ห้องสมุดนี้กับ Builds Debug เท่านั้น ( -O0 ไม่ได้ถอดออกโดยไม่มี -fvisibility=hidden และสิ่งต่าง ๆ เช่นนั้น) เพื่อไม่จัดการกับฟังก์ชั่นและตัวแปรที่ดีที่สุด ฉันไม่รู้ว่ามันทำงานอย่างไรกับการสร้างที่มีการปรับให้เหมาะสมมากเป็นไปได้ว่ามันจะไม่ทำงานเลย
โดยส่วนตัวแล้วฉันใช้มันแบบนี้ ฉันมีทางลัด Ctrl+r ที่ได้รับมอบหมายให้ tryReload ในแอปพลิเคชันของฉัน นอกจากนี้แอปแอพของฉันจะ update ใน Runloop หลักและฟังกิจกรรม onCodePreLoad และ onCodePostLoad เพื่อสร้างวัตถุบางส่วนหรือประเมินฟังก์ชั่นบางอย่างอีกครั้ง:
Ctrl+r Jet-Live จะตรวจสอบการเปลี่ยนแปลงของไฟล์การคอมไพล์ไฟล์ที่เปลี่ยนแปลงอีกครั้งและเฉพาะเมื่อ tryReload เรียกว่ามันจะรอให้กระบวนการรวบรวมปัจจุบันทั้งหมดเสร็จสิ้นและโหลดรหัสใหม่อีกครั้ง โปรดอย่าโทรหา tryReload ในการอัปเดตแต่ละครั้งมันจะไม่ทำงานตามที่คุณคาดหวังโทรหามันเฉพาะเมื่อซอร์สโค้ดของคุณพร้อมที่จะโหลดใหม่
หากคุณไม่ต้องการสลับไปมาระหว่างตัวแก้ไขรหัสและแอพของคุณคุณสามารถกำหนดค่าแป้นพิมพ์ลัดซึ่งเรียกใช้ kill -s USR1 $(pgrep <your_app_name>) ไลบรารีจะทริกเกอร์รหัสใหม่เมื่อได้รับสัญญาณ SIGUSR1 มันทำงานได้อย่างน้อยใน Emacs, Xcode, Clion และ VScode แต่ฉันแน่ใจว่าสามารถทำได้ในบรรณาธิการและ IDE อื่น ๆ หากดีบักเกอร์ของคุณเป็น LLDB และจับสัญญาณนี้และหยุดแอพให้เพิ่มคำสั่งนี้ในไฟล์ ~/.lldbinit :
breakpoint set --name main
breakpoint command add
process handle -n true -p true -s false SIGUSR1
continue
DONE
บน MacOS คุณสามารถใช้ cmake -G Xcode Generator นอกเหนือจาก 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/Make/Ninja เครื่องมือ แต่ถ้าคุณต้องการนำไปใช้กับเครื่องมือสร้างอื่นมีวิธีการปรับแต่งพฤติกรรมของมันในบางแง่มุม โปรดดูแหล่งที่มาและเอกสาร นอกจากนี้ยังเป็นความคิดที่ดีที่จะสร้างผู้ฟังของคุณเองเพื่อรับกิจกรรมจากห้องสมุด โปรดดูเอกสารของ ILiveListener และ LiveConfig
สิ่งสำคัญ: ขอแนะนำอย่างยิ่งให้บันทึกข้อความทั้งหมดจากห้องสมุดโดยใช้ ILiveListener::onLog เพื่อดูว่ามีอะไรผิดพลาดหรือไม่
ห้องสมุดอ่านส่วนหัวของเอลฟ์และส่วนของไลบรารีที่ใช้งานได้และโหลดทั้งหมดที่โหลดทั้งหมดค้นหาสัญลักษณ์ทั้งหมดและพยายามที่จะค้นหาว่าสามารถเชื่อมต่อได้ (ฟังก์ชั่น) หรือควรถ่ายโอน/ย้าย (ตัวแปรแบบคงที่/ทั่วโลก) นอกจากนี้ยังพบขนาดสัญลักษณ์และที่อยู่ "จริง"
นอกเหนือจาก เจ็ทไลฟ์ ที่พยายามค้นหา compile_commands.json ใกล้กับการปฏิบัติการของคุณหรือใน 'ไดเรกทอรีหลักของมันซ้ำ ๆ การใช้ไฟล์นี้แตกต่าง:
.o (วัตถุ).d (depfile) เมื่อหน่วยรวบรวมทั้งหมดถูกแยกวิเคราะห์มันจะแยกความแตกต่างของไดเรกทอรีที่พบบ่อยที่สุดสำหรับไฟล์ต้นฉบับทั้งหมดและเริ่มดูไดเรกทอรีทั้งหมดด้วยไฟล์ต้นฉบับการพึ่งพาและไฟล์บริการบางไฟล์เช่น compile_commands.json
นอกเหนือจากนั้นห้องสมุดพยายามค้นหาการพึ่งพาทั้งหมดสำหรับแต่ละหน่วยการรวบรวม โดยค่าเริ่มต้นจะอ่าน depfiles ใกล้กับไฟล์วัตถุ (ดู -MD Compiler 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 ไลบรารีจะรอกระบวนการรวบรวมที่ยังไม่เสร็จจากนั้นไฟล์วัตถุใหม่ที่สะสมทั้งหมดจะเชื่อมโยงเข้าด้วยกันในไลบรารีที่ใช้ร่วมกันและวางไว้ใกล้กับการปฏิบัติการของคุณด้วย XXX lib_reloadXXX.so ดังนั้น lib_reloadXXX.so มีรหัสใหม่ทั้งหมด
Jet-Live โหลดไลบรารีนี้โดยใช้ dlopen อ่านส่วนหัวและส่วนของ Elf/Mach-O และค้นหาสัญลักษณ์ทั้งหมด นอกจากนี้ยังโหลดข้อมูลการย้ายถิ่นฐานจากไฟล์วัตถุที่ใช้ในการสร้างไลบรารีใหม่นี้ หลังจากนั้น:
memcpy จากตำแหน่งเก่าไปใหม่ สำคัญ: ILiveListener::onCodePreLoad เหตุการณ์ถูกยิงทันทีที่ lib_reloadXXX.so ถูกโหลดลงในหน่วยความจำกระบวนการ ILiveListener::onCodePostLoad เหตุการณ์ถูกยิงทันทีหลังจากการทำงานของการเชื่อมโยงรหัสทั้งหมดเสร็จสิ้น
คุณสามารถอ่านเพิ่มเติมเกี่ยวกับฟังก์ชั่นการเชื่อมต่อได้ที่นี่ ไลบรารีนี้ใช้ไลบรารี Subhook ที่ยอดเยี่ยมเพื่อเปลี่ยนเส้นทางการไหลของฟังก์ชั่นจากเก่าไปยังใหม่ คุณจะเห็นได้ว่าบนแพลตฟอร์ม 32 บิตฟังก์ชั่นของคุณควรมีความยาวอย่างน้อย 5 ไบต์ที่จะเชื่อมต่อได้ ใน 64 บิตคุณต้องการอย่างน้อย 14 ไบต์ซึ่งมีมากและตัวอย่างเช่นฟังก์ชั่น Stub ที่ว่างเปล่าอาจไม่พอดีกับ 14 ไบต์ จากการสังเกตของฉัน Clang โดยค่าเริ่มต้นจะสร้างรหัสที่มีการจัดตำแหน่งฟังก์ชั่น 16 ไบต์ GCC ไม่ได้ทำสิ่งนี้โดยค่าเริ่มต้นดังนั้นสำหรับ GCC จะใช้ -falign-functions=16 Flag นั่นหมายถึงระยะห่างระหว่างการเริ่มต้นของฟังก์ชั่น 2 รายการไม่น้อยกว่าที่ 16 ไบต์ซึ่งทำให้สามารถเชื่อมต่อฟังก์ชั่นใด ๆ ได้
ฟังก์ชั่นเวอร์ชันใหม่ควรใช้สถิติและ globals ซึ่งอาศัยอยู่ในแอปพลิเคชันแล้ว ทำไมจึงสำคัญ? สมมติว่าคุณมี (ตัวอย่างสังเคราะห์เล็กน้อย แต่อย่างไรก็ตาม):
// 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 จะถูกโหลดซ้ำและ Singleton::instance() Singleton::instance จะถูกเชื่อมต่อเพื่อเรียกเวอร์ชันใหม่ lib_reloadXXX.so จะมีตัวแปรคงที่ตัวแปร static Singleton ins แบบคงที่ นั่นคือเหตุผลที่เราจำเป็นต้องย้ายสถิติและ globals ทั้งหมดไปยังรหัสใหม่และถ่ายโอนตัวแปรยามของสถิติ การย้ายถิ่นฐานเวลาลิงค์ส่วนใหญ่ที่เกี่ยวข้องกับสถิติและ globals คือ 32 บิต ดังนั้นหากไลบรารีที่ใช้ร่วมกันกับรหัสใหม่จะถูกโหลดไกลเกินไปในหน่วยความจำจากแอปพลิเคชันมันจะเป็นไปไม่ได้ที่จะย้ายตัวแปรด้วยวิธีนี้ ในการแก้ปัญหานี้ไลบรารีที่ใช้ร่วมกันใหม่นั้นเชื่อมโยงโดยใช้ธง linker พิเศษซึ่งช่วยให้เราสามารถโหลดลงในตำแหน่งที่คำนวณล่วงหน้าเฉพาะในหน่วยความจำเสมือนจริง (ดู -image_base ใน Apple LD, --image-base ใน LLVM LLD และ -Ttext-segment + -z max-page-size ใน GNU LD Linker)
แอพของคุณอาจจะล่มหากคุณพยายามเปลี่ยนเค้าโครงหน่วยความจำของประเภทข้อมูลของคุณในรหัสที่โหลดได้
สมมติว่าคุณมีอินสแตนซ์ของคลาสนี้จัดสรรที่ไหนสักแห่งในกองหรือบนสแต็ก:
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 การถ่ายโอนสถานะที่ถูกต้องขึ้นอยู่กับคุณ เอฟเฟกต์เดียวกันนี้จะเกิดขึ้นหากคุณพยายามเปลี่ยน เค้าโครง โครงสร้างข้อมูลแบบคงที่ สิ่งเดียวกันก็ถูกต้องสำหรับคลาส polymorphic (vtable) และแลมบ์ดาที่มีการจับภาพ (การจับจะถูกเก็บไว้ในเขตข้อมูลของแลมบ์ดาส)
มิกซ์
สำหรับใบอนุญาตของไลบรารีที่ใช้แล้วโปรดดูไดเรกทอรีและซอร์สโค้ดของพวกเขา