演示
重要的是:从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的数据字段中)也是正确的。
麻省理工学院
有关二手库的许可,请参考其目录和源代码。