Демо
ВАЖНО: Начиная с MacOS 10.14, подход, используемый в библиотеке, больше не работает, похоже, что Apple ограничила некоторые из процедур низкого уровня. На Linux, по крайней мере, на Ubuntu 20.04 он все еще работает нормально.
Jet-Live -это библиотека для C ++ "Hot Code Reloading". Он работает на Linux и Modern 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, Defaults по умолчанию готовы к этим инструментам. Cmakelists.txt добавит опцию set(CMAKE_EXPORT_COMPILE_COMMANDS ON) для compile_commands.json и FALCE COMPILER и FLAGS LINGER. Это важно и невозможно. Подробнее, см. Cmakelists.txt. Если вы используете Ninja, добавьте -d keepdepfile Ninja Flag при запуске Ninja, это необходимо для отслеживания зависимостей между файлами источника и заголовка
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, но я уверен, что это достижимо в других редакторах и 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 кроме 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 , чтобы посмотреть, пошло ли что -то не так.
Библиотека считывает заголовки эльфов и разделы этого исполняемого файла и все загруженные общие библиотеки, находят все символы и пытаются выяснить, какие из них можно зацепить (функции), либо должны быть переданы/перенесены (статические/глобальные переменные). Также он находит размер символов и «реальный» адрес.
Помимо этой реактивной жизни пытается найти compile_commands.json рядом с вашим исполняемым файлом или в его родительских каталогах рекурсивно. Используя этот файл, он различает:
.o (объект) Путь файла.d (depfile) paly paly Когда все компиляционные единицы проанализированы, он различает наиболее распространенный каталог для всех исходных файлов и начинает наблюдать за всеми каталогами с исходными файлами, их зависимостями и некоторыми файлами службы, такими как compile_commands.json .
Кроме того, библиотека пытается найти все зависимости для каждого компиляционного блока. По умолчанию он будет читать DepFiles вблизи объектных файлов (см. Параметром -MD Compiler). Предположим, что объектный файл расположен по адресу:
/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 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 Event запускается сразу после того, как все кодовая загружающая машина завершена.
Вы можете прочитать больше о подключении функций здесь. Эта библиотека использует потрясающую библиотеку Subhook, чтобы перенаправить поток функций от старых на новые. Вы можете видеть, что на 32 -битных платформах ваши функции должны составлять не менее 5 байтов длиной, чтобы быть подключенными. На 64 -битах вам нужно не менее 14 байтов, что много, и, например, функция пустой заглушки, вероятно, не будет вписаться в 14 байтов. Из моих наблюдений, Clang по умолчанию создает код с 16-байтовым выравниванием функций. GCC не делайте этого по умолчанию, поэтому для GCC используется флаг -falign-functions=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 ;
} Отлично, теперь он умножает аргумент на 3. Но, поскольку Whate Singleton.cpp будет перезагружен, а функция Singleton::instance будет подключен к новой версии, lib_reloadXXX.so будет содержать новые статические переменные static Singleton ins , которые не инициализируются, и если вы назвали бы Singleton::instance() после того, как код был перезагружен, это будет инициализируется, что инициализируется. Вот почему нам нужно перенести все статику и глобальные значения в новый код и перенести гвардические переменные статики. Большинство перемещений времени ссылки, связанных со статикой и глобалами, составляют 32-разрядные. Поэтому, если общая библиотека с новым кодом будет загружена слишком далеко в памяти из приложения, таким образом будет невозможно переехать переменные. Чтобы решить это, новая общая библиотека связана с использованием специальных флагов линкеров, которые позволяют нам загружать его в конкретное предварительно рассчитанное местоположение в виртуальной памяти (см. -image_base в Apple LD, --image-base в LLVM LLD и -Ttext-segment + -z max-page-size в флагах LD 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).
Грань
Для получения лицензий на использованные библиотеки обратитесь к их каталогам и исходному коду.