演示
重要的是:從MACOS 10.14開始,庫中使用的方法不再起作用,看起來Apple限制了一些低級例程。至少在ubuntu上的Linux上,它仍然可以正常工作。
Jet Live是C ++“熱代碼重新加載”的庫。它可以在Linux和Modern MacOS(我猜10.12+)上使用64個位系統,由CPU提供X86-64指令集。除了重新加載函數外,還可以在重新加載代碼後保持應用程序的靜態和全球狀態(請參閱“它的工作原理”以及它很重要的原因)。用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.8.2,MAKE 3.8.2,MAKE 3.8.81,在Ubuntu 18.04上測試。
重要的是:該庫不強迫您以某種特殊的方式組織代碼(例如在RCCPP或CR中),您不需要將可重新加載的代碼分離為某些共享的庫, Jet Live應該以最小的侵入性方式與任何項目一起使用。
如果您需要Windows類似的內容,請嘗試眨眼,我沒有計劃支持Windows。
您需要c++11兼容的編譯器。另外,有幾個被捆綁在一起的依賴項,其中大多數是僅標頭或單個H/CPP對庫。有關詳細信息,請參考lib目錄。
該庫最適合基於CMAKE和MAKE或NINJA構建系統的項目,用於這些工具的默認設置。 cmakelists.txt將添加compile_commands.json和alter編譯器和鏈接器標誌的選項添加set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 。這很重要,不可避免。有關詳細信息,請參閱cmakelists.txt。如果您使用忍者,則在運行忍者時添加-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課程的實例liveInstance->update()liveInstance->tryReload()重要的是:此庫不是安全的。它使用引擎蓋下的線程來運行編譯器,但是您應該從同一線程調用所有庫方法。
另外,我僅將此庫與調試構建( -O0 ,未剝離,沒有-fvisibility=hidden and there)不使用優化和內線的函數和變量。我不知道它如何在高度優化的剝離版本上工作,很可能根本無法使用。
我個人是這樣使用的。我有一個Ctrl+r快捷方式,在我的應用程序中分配了tryReload 。另外,我的App Call呼叫update在主Runloop中,並聽onCodePreLoad和onCodePostLoad事件來重新創建某些對像或重新評估某些功能:
Ctrl+r Jet-Live將監視文件更改,重新編譯更改的文件,並且只有在稱呼tryReload時,才會等待所有當前的編譯過程完成並重新加載新代碼。請不要在每次更新中調用tryReload ,它將無法正常工作,只有在準備重新加載源代碼時,它才會稱其為調用。
如果您不想在代碼編輯器和應用程序之間來回切換,則可以配置運行shell命令kill -s USR1 $(pgrep <your_app_name>)的鍵盤快捷鍵,當接收到SIGUSR1信號時,庫將觸發代碼重新加載。它至少可以在Emacs,Xcode,clion和Vscode中起作用,但是我敢肯定,它在其他編輯和IDE中可以實現,只需Google IT。如果您的調試器是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 and 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文件)將實施:
根本不會實施:
Jet-Live經過微調即可使用CMake並製造/Ninja工具,但是如果您想將其採用到另一個構建工具中,則有一種方法可以在某些方面自定義其行為。請參考來源和文檔。同樣,最好創建自己的聽眾從圖書館接收事件。請參閱ILiveListener和LiveConfig的文檔。
重要的是:強烈建議使用ILiveListener::onLog Onlog記錄庫中的所有消息,以查看是否出現了問題。
該庫讀取此可執行文件和所有已加載的共享庫的精靈標題和部分,找到所有符號並試圖找出可以掛接的符號(函數)(函數)或應該轉移/重新定位(靜態/全局變量)。它還找到符號大小和“真實”地址。
除了該噴氣式飛機外,還試圖在您的可執行文件附近或其“父級目錄”附近找到compile_commands.json 。使用此文件可以區分:
.o (對象)文件路徑.d (depfile)文件路徑當解析所有編譯單元時,它會區分所有源文件的最常見目錄,並開始觀看使用源文件,其依賴項和某些服務文件(例如compile_commands.json )的所有目錄。
除此之外,圖書館試圖找到每個彙編單元的所有依賴項。默認情況下,它將在對象文件附近讀取Depfiles(請參閱-MD編譯器選項)。假設對象文件位於:
/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
它將拾取觀看目錄下方的所有依賴項,因此,即使該文件確實包含在您的某些.cpp文件中,也不會將諸如/usr/include/elf.h之類的依賴項視為依賴關係。
現在庫是初始化的。
接下來,當您編輯某些源文件並保存時, Jet-Live立即開始在後台彙編所有相關文件。默認情況下,同時編譯過程的數量為4,但是您可以配置它。它將使用ILiveListener::onLog的偵聽器方法來登錄成功和錯誤。如果您觸發某些文件已經編譯時(或在隊列中等待)時,舊的彙編過程將被殺死,並將新的彙編過程添加到隊列中,因此,有點安全地不僅要等待彙編完成並進行代碼的新更改。同樣,在編譯每個文件後,它將更新編譯文件的依賴項,因為編譯器可以為其重新創建DepFile,如果新版本的編譯單元具有新的依賴項。
當您調用Live::tryReload時,庫將等待未完成的編譯過程,然後所有累積的新對象文件都將在共享庫中鏈接在一起,並將其放置在您的可執行lib_reloadXXX.so附近,並在本次會議期間XXX是許多“ Reloads”。因此, lib_reloadXXX.so包含所有新代碼。
Jet Live使用dlopen加載此庫,讀取Elf/Mach-O標題和部分,並找到所有符號。此外,它還從用於構建此新庫的對象文件中加載重定位信息。在那之後:
memcpy安置的本地靜態變量,只有從舊位置到新位置重要的是: ILiveListener::onCodePreLoad事件在將lib_reloadXXX.so加載到過程內存中之前就觸發了。 ILiveListener::onCodePostLoad事件在所有代碼重新加載機器人完成後立即發射。
您可以在此處閱讀有關功能掛鉤的更多信息。該庫使用Awesome Subhook庫將功能從舊功能重定向到新的函數。您會看到,在32位平台上,您的功能應至少長5個字節可連接。在64位,您至少需要14個字節,例如,空的存根功能可能不適合14個字節。從我的觀察結果,Clang默認情況下會產生具有16字節函數對齊的代碼。 GCC默認情況下不要這樣做,因此對於GCC,使用-falign-functions=16標誌。這意味著任何2個函數的開始之間的間距與16個字節的距離不少於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 ;
} Singleton::instance static Singleton ins ,現在lib_reloadXXX.so參數乘以Singleton::instance() Singleton.cpp這就是為什麼我們需要將所有靜態和全球範圍重新定位到新代碼並轉移靜態的後衛變量。與靜態和全球儀有關的大多數鏈接時間搬遷都是32位。因此,如果將帶有新代碼的共享庫與應用程序的內存中加載得太遠,則不可能以這種方式重新分配變量。為了解決此問題,使用特殊的鏈接標誌將新的共享庫鏈接到虛擬內存中的特定預定位置(請參見Apple LD中的-image_base , --image-base ,llvm lld和-Ttext-segment + -z max-page-size in GNU ld Linker flags中的image-base)。
另外,如果您嘗試在可重新加載代碼中更改數據類型的內存佈局,那麼您的應用程序可能會崩潰。
假設您有一個在堆或堆棧中分配的該類的實例:
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實例變量,但是NEW版本calledEachUpdate將嘗試修改其實際修改隨機數據。在這種情況下,您應該在onCodePreLoad回調中刪除此實例,並在onCodePostLoad回調中重新創建它。正確轉移其狀態取決於您。如果您嘗試更改靜態數據結構佈局,也會發生相同的效果。對於帶有捕獲的多態性類(VTable)和Lambdas(捕獲存儲在Lambdas的數據字段中)也是正確的。
麻省理工學院
有關二手庫的許可,請參考其目錄和源代碼。