Manifestación
IMPORTANTE: A partir de MacOS 10.14, el enfoque utilizado en la biblioteca ya no funciona, parece que Apple ha restringido algunas de las rutinas de bajo nivel. En Linux al menos en Ubuntu 20.04 todavía funciona bien.
Jet-Live es una biblioteca para C ++ "recarga de código caliente". Funciona en Linux y Modern MacOS (10.12+, supongo) en sistemas de 64 bits alimentados por CPU con un conjunto de instrucciones X86-64. Además de la recarga de funciones, es capaz de mantener el estado estático y global de las aplicaciones sin cambios después de que el código se volviera a cargar (consulte "cómo funciona" para lo que es y por qué es importante). Probado en Ubuntu 18.04 con 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 y MacOS 10.13.6 con Xcode 8.3.3, CMake 3.8.2, Make 3.81.
IMPORTANTE: Esta biblioteca no lo obliga a organizar su código de una manera especial (como en RCCPP o CR), no necesita separar el código recargable en alguna biblioteca compartida, Jet-Live debería trabajar con cualquier proyecto de la manera menos intrusiva.
Si necesita algo similar para Windows, intente Blink, no tengo planes de admitir Windows.
Necesita un compilador compatible con c++11 . También hay varias dependencias que están agrupadas, la mayoría de ellas son solo encabezadas o una sola biblioteca de par H/CPP. Consulte el directorio lib para más detalles.
Esta biblioteca es más adecuada para proyectos basados en sistemas CMake y Make o Ninja Build, los valores predeterminados están ajustados para estas herramientas. La opción CMAKELISTS.txt agregará set(CMAKE_EXPORT_COMPILE_COMMANDS ON) Opción para compile_commands.json y alterar las banderas de compiladores y enlazadores. Esto es importante y no es evitable. Para obtener más detalles, consulte cmakelists.txt. Si usa Ninja, agregue -d keepdepfile Ninja Flag al ejecutar Ninja, esto es necesario para rastrear las dependencias entre los archivos de fuente y encabezado
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()IMPORTANTE: Esta biblioteca no es segura. Utiliza hilos debajo del capó para ejecutar el compilador, pero debe llamar a todos los métodos de la biblioteca desde el mismo hilo.
También uso esta biblioteca solo con construcciones de depuración ( -O0 , no despojadas, sin -fvisibility=hidden y cosas así) para no tratar con funciones y variables optimizadas y en línea. No sé cómo funciona en construcciones de descuento altamente optimizadas, lo más probable es que no funcione en absoluto.
Personalmente lo uso así. Tengo un atajo Ctrl+r al que se asigna tryReload en mi aplicación. También la aplicación de mi aplicación llama update en el Runloop principal y escucha los eventos onCodePreLoad y onCodePostLoad para recrear algunos objetos o reevaluar algunas funciones:
Ctrl+r Jet-Live monitoreará los cambios de archivos, recompilará los archivos cambiados y solo cuando se llama tryReload , esperará que todos los procesos de compilación actuales finalicen y recargue un nuevo código. No llame tryReload en cada actualización, no funcionará como espere, llame solo cuando su código fuente esté listo para ser recargado.
Si no desea cambiar entre su editor de código y su aplicación, puede configurar un atajo de teclado que ejecuta un comando shell kill -s USR1 $(pgrep <your_app_name>) , la biblioteca activará el código recargar cuando se reciba la señal SIGUSR1 . Funciona al menos en emacs, xcode, clion y vscode, pero estoy seguro de que se puede lograr en otros editores e ides, solo google. Si su depurador es LLDB y capta esta señal y detiene la aplicación, agregue estos comandos al archivo ~/.lldbinit :
breakpoint set --name main
breakpoint command add
process handle -n true -p true -s false SIGUSR1
continue
DONE
En MacOS puede usar el generador cmake -G Xcode aparte de Make y Ninja. En este caso, instale xcpretty gem:
gem install xcpretty
Hay una aplicación de ejemplo simple, simplemente ejecute:
git clone https://github.com/ddovod/jet-live.git && cd jet-live
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug .. && make
./example/example e intente hello comando. No olvides ejecutar el comando reload después de arreglar la función.
No hay una suite de prueba no muy completa, pero constantemente actualizada. Para ejecutarlo:
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/Implementado:
compile_commands.json después de que se creara un nuevo archivo .cpp)Se implementará:
No se implementará en absoluto:
Jet-Live está ajustado para trabajar con CMake y Make/Ninja Tools, pero si desea adoptarlo a otra herramienta de compilación, hay una manera de personalizar su comportamiento en algunos aspectos. Consulte las fuentes y la documentación. También es una buena idea crear su propio oyente para recibir eventos de la biblioteca. Consulte la documentación de ILiveListener y LiveConfig .
IMPORTANTE: Se recomienda altamente registrar todos los mensajes de la biblioteca usando ILiveListener::onLog para ver si algo salió mal.
La biblioteca lee encabezados y secciones de ELF de este ejecutable y todas las bibliotecas compartidas cargadas, encuentra todos los símbolos e intenta averiguar cuáles de ellas se pueden enganchar (funciones) o deben transferirse/reubicarse (variables estáticas/globales). También encuentra el tamaño de los símbolos y la dirección "real".
Además de ese jet-Live, intenta encontrar compile_commands.json cerca de su ejecutable o en sus 'directorios de padres de manera recursiva. Usando este archivo, distingue:
.o (objeto).d (depfile) Cuando se analizan todas las unidades de compilación, distingue el directorio más común para todos los archivos de origen y comienza a observar todos los directorios con archivos de origen, sus dependencias y algunos archivos de servicio como compile_commands.json .
Aparte de eso, la biblioteca intenta encontrar todas las dependencias para cada unidad de compilación. De manera predeterminada, leerá Depfiles cerca de los archivos de objeto (consulte la opción del compilador -MD ). Supongamos que el archivo de objeto se encuentra en:
/home/coolhazker/projects/some_project/build/main.cpp.o
Jet-Live intentará encontrar Depfile en:
/home/coolhazker/projects/some_project/build/main.cpp.o.d
or
/home/coolhazker/projects/some_project/build/main.cpp.d
Recogerá todas las dependencias que están en los directorios de observación, por lo que cosas como /usr/include/elf.h no serán tratadas como dependencia incluso si este archivo realmente está incluido en algunos de sus archivos .cpp.
Ahora la biblioteca se inicializa.
Luego, cuando edita algún archivo fuente y lo guarda, Jet-Live inicia inmediatamente la compilación de todos los archivos dependientes en segundo plano. Por defecto, el número de procesos de compilación simultáneos es 4, pero puede configurarlo. Escribirá para iniciar sesión sobre éxitos y errores utilizando el método ILiveListener::onLog del oyente. Si desencadena la compilación de algún archivo cuando ya está compilando (o esperando en la cola), se matará el proceso de compilación anterior y se agregará uno nuevo a la cola, por lo que es un poco seguro no esperar a que la compilación finalice y hacer nuevos cambios del código. También después de que se compiló cada archivo, actualizará las dependencias para el archivo compilado ya que el compilador puede recrear Depfile para él si la nueva versión de la unidad de compilación tiene nuevas dependencias.
Cuando llame Live::tryReload , la biblioteca esperará los procesos de compilación sin terminar y luego todos los archivos de objetos nuevos acumulados se unirán en la biblioteca compartida y se colocan cerca de su ejecutable con el nombre lib_reloadXXX.so , donde XXX es una serie de "recargas" durante esta sesión. Entonces lib_reloadXXX.so contiene todo el código nuevo.
Jet-Live Cargue esta biblioteca usando dlopen , lee encabezados y secciones Elf/Mach-O y encuentra todos los símbolos. También carga la información de reubicación de los archivos de objetos que se utilizó para construir esta nueva biblioteca. Después:
memcpy de la ubicación antigua a una nueva Importante: el evento ILiveListener::onCodePreLoad se dispara justo antes de lib_reloadXXX.so se carga en la memoria del proceso. ILiveListener::onCodePostLoad se dispara justo después de que se termine toda la machinación de recuperación del código.
Puede leer más sobre el enganche de funciones aquí. Esta biblioteca utiliza la increíble biblioteca Subhook para redirigir el flujo de funciones de antiguos a nuevos. Puede ver que en las plataformas de 32 bits sus funciones deben tener al menos 5 bytes de largo para ser enganchables. En 64 bits necesita al menos 14 bytes, lo cual es mucho, y por ejemplo, la función de talón vacío probablemente no encajará en 14 bytes. De mis observaciones, Clang por defecto produce código con alineación de funciones de 16 bytes. GCC no hace esto de forma predeterminada, por lo que para GCC se usa el indicador -falign-functions=16 . Eso significa que el espacio entre los comienzos de las 2 funciones no es menor que 16 bytes, lo que hace posible enganchar cualquier función.
Las nuevas versiones de las funciones deben usar estadísticas y globales que ya viven en la aplicación. ¿Por qué es importante? Supongamos que tiene (un ejemplo sintético, pero de todos modos):
// 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 ;
} Entonces desea actualizar veryUsefulFunction de SMTH como esta:
int veryUsefulFunction ( int value)
{
return value * 3 ;
} Genial, ahora multiplica el argumento por 3. Pero dado que todo Singleton.cpp se volverá a cargar y la función Singleton::instance se conectará para llamar a una nueva versión, lib_reloadXXX.so contendrá una nueva variable estática estática static Singleton ins , que no se inicializa, y si usted llama Singleton::instance() después de que se relargue, se reló, se inicializará, lo que no lo convence, lo que no lo convence, no lo convence, no lo convence, lo que no lo convence, lo que no se llama. de nuevo. Es por eso que necesitamos reubicar todas las estadísticas y globales al nuevo código y transferir las variables de la estadística de guardia. La mayoría de las reubicaciones de tiempo de enlace relacionadas con la estadística y los globales son de 32 bits. Entonces, si la biblioteca compartida con un nuevo código se cargará demasiado lejos en la memoria de la aplicación, no será posible reubicar las variables de esta manera. Para resolver esto, la nueva biblioteca compartida está vinculada utilizando indicadores de enlazador especiales que nos permite cargarla en una ubicación precalculada específica en la memoria virtual (ver -image_base en Apple LD, --image-base en LLVM LLD y -Ttext-segment + -z max-page-size en GNU LD LDELLERS).
Además, su aplicación probablemente se bloqueará si intenta cambiar el diseño de memoria de sus tipos de datos en código recargable.
Supongamos que tiene una instancia de esta clase asignada en algún lugar del montón o en la pila:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
}
private:
int m_someVar1 = 0 ;
}Lo editas y ahora parece:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
m_someVar2++;
}
private:
int m_someVar1 = 0 ;
int m_someVar2 = 0 ;
} Después de que el código se vuelva a cargar, probablemente observará un bloqueo porque el objeto ya asignado tiene un diseño de datos diferente, no tiene una variable de instancia m_someVar2 , pero la nueva versión de calledEachUpdate intentará modificarlo realmente modificando datos aleatorios. En tales casos, debe eliminar esta instancia en onCodePreLoad Callback y recrearla en la devolución de llamada onCodePostLoad . La transferencia correcta de su estado depende de usted. El mismo efecto tendrá lugar si intenta cambiar el diseño de estructuras de datos estáticos. Lo mismo también es correcto para las clases polimórficas (VTABLE) y Lambdas con capturas (las capturas se almacenan dentro de los campos de datos de Lambdas).
MIT
Para obtener licencias de bibliotecas usadas, consulte sus directorios y código fuente.