Démo
IMPORTANT: À partir de MacOS 10.14, l'approche utilisée dans la bibliothèque ne fonctionne plus, on dirait qu'Apple a restreint certaines des routines de bas niveau. Sur Linux au moins sur Ubuntu 20.04, cela fonctionne toujours bien.
Jet-Live est une bibliothèque pour C ++ "Hot Code Recharger". Il fonctionne sur Linux et MacOS moderne (10.12+ je suppose) sur des systèmes 64 bits alimentés par CPU avec un ensemble d'instructions x86-64. Outre le rechargement des fonctions, il est en mesure de garder l'inchangé d'état statique et mondial des applications après le rechargement du code (veuillez vous référer à "comment cela fonctionne" pour ce qui est et pourquoi il est important). Testé sur Ubuntu 18.04 avec 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 et MacOS 10.13.6 avec Xcode 8.3.3, CMake 3.8.2, Make 3.81.
Important: cette bibliothèque ne vous oblige pas à organiser votre code d'une manière particulière (comme dans RCCPP ou CR), vous n'avez pas besoin de séparer le code de rechargenable dans une bibliothèque partagée, Jet-Live devrait fonctionner avec un projet de la manière la moins intrusive.
Si vous avez besoin de quelque chose de similaire pour Windows, essayez Blink, je n'ai pas l'intention de prendre en charge Windows.
Vous avez besoin d'un compilateur conforme c++11 . Il y a également plusieurs dépendances qui sont introduites, la plupart d'entre elles sont une bibliothèque de paires H / CPP unique en tête uniquement ou unique. Veuillez vous référer au répertoire lib pour plus de détails.
Cette bibliothèque est la mieux adaptée aux projets basés sur les systèmes CMake et MakE ou Ninja, les défauts sont affinés pour ces outils. L'option CMAKELIST.TXT ajoutera set(CMAKE_EXPORT_COMPILE_COMMANDS ON) pour compile_commands.json et modifier les drapeaux du compilateur et des liens. Ceci est important et non évitable. Pour plus de détails, consultez CMakelists.txt. Si vous utilisez ninja, ajoutez -d keepdepfile ninja drapeau lors de l'exécution de ninja, cela est nécessaire pour suivre les dépendances entre les fichiers source et les fichiers d'en-tête
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::LiveliveInstance->update()liveInstance->tryReload()Important: cette bibliothèque n'est pas sûre de fil. Il utilise des threads sous le capot pour exécuter le compilateur, mais vous devez appeler toutes les méthodes de bibliothèque à partir du même thread.
J'utilise également cette bibliothèque uniquement avec des builds de débogage ( -O0 , non dépouillés, sans -fvisibility=hidden et des choses comme ça) pour ne pas gérer les fonctions et variables optimisées et incaiinées. Je ne sais pas comment cela fonctionne sur les constructions dépouillées hautement optimisées, il ne fonctionnera probablement pas du tout.
Personnellement, je l'utilise comme ça. J'ai un raccourci Ctrl+r auquel tryReload est attribué dans mon application. De plus, mon application applique les appels update dans le Runloop principal et écoute les événements onCodePreLoad et onCodePostLoad pour recréer certains objets ou réévaluer certaines fonctions:
Ctrl+r Jet-Live surveillera les modifications de fichiers, recompile les fichiers modifiés et uniquement lorsque tryReload est appelé, il attendra que tous les processus de compilation actuels se terminent et rechargeont un nouveau code. Veuillez ne pas appeler tryReload sur chaque mise à jour, cela ne fonctionnera pas comme vous l'attendez, appelez-le uniquement lorsque votre code source est prêt à être rechargé.
Si vous ne souhaitez pas basculer entre votre éditeur de code et votre application, vous pouvez configurer un raccourci clavier qui exécute une commande shell kill -s USR1 $(pgrep <your_app_name>) , la bibliothèque déclenchera le rechargement du code lorsque le signal SIGUSR1 sera reçu. Il fonctionne au moins dans EMACS, XCODE, CLION et VSCODE, mais je suis sûr qu'il est réalisable dans d'autres éditeurs et IDE, il suffit de Google. Si votre débogueur est LLDB et qu'il attrape ce signal et arrête l'application, ajoutez ces commandes au fichier ~/.lldbinit :
breakpoint set --name main
breakpoint command add
process handle -n true -p true -s false SIGUSR1
continue
DONE
Sur macOS, vous pouvez utiliser le générateur cmake -G Xcode en dehors de Make et Ninja. Dans ce cas, veuillez installer xcpretty Gem:
gem install xcpretty
Il y a un exemple simple d'application, il suffit d'exécuter:
git clone https://github.com/ddovod/jet-live.git && cd jet-live
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug .. && make
./example/example et essayez la commande hello . N'oubliez pas d'exécuter la commande reload après avoir fixé la fonction.
Il y a une suite de tests non très complète, mais à jour constante. Pour l'exécuter:
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/Mis en œuvre:
compile_commands.json Fichier après la création du nouveau fichier .cpp)Sera mis en œuvre:
Ne sera pas du tout mis en œuvre:
Jet-Live est affiné pour travailler avec CMake et Make / Ninja Tools, mais si vous souhaitez l'adopter à un autre outil de construction, il existe un moyen de personnaliser son comportement à certains aspects. Veuillez vous référer aux sources et à la documentation. C'est également une bonne idée de créer votre propre auditeur pour recevoir des événements de la bibliothèque. Veuillez vous référer à la documentation d' ILiveListener et LiveConfig .
Important: il est fortement recommandé de enregistrer tous les messages de la bibliothèque à l'aide d' ILiveListener::onLog pour voir si quelque chose a mal tourné.
La bibliothèque lit les en-têtes ELF et les sections de cet exécutable et toutes les bibliothèques partagées chargées, trouve tous les symboles et essaie de découvrir lesquels peuvent être accrochés (fonctions) ou doivent être transférés / relocalisés (variables statiques / globales). Il trouve également la taille des symboles et l'adresse "réelle".
En dehors de ce jet-live essaie de trouver compile_commands.json près de votre exécutable ou dans ses répertoires parentaux récursivement. En utilisant ce fichier, il distingue:
.o (objet).d (depFile) Lorsque toutes les unités de compilation sont analysées, elle distingue le répertoire le plus courant pour tous les fichiers source et commence à surveiller tous les répertoires avec des fichiers source, leurs dépendances et certains fichiers de service comme compile_commands.json .
En dehors de cela, la bibliothèque essaie de trouver toutes les dépendances pour chaque unité de compilation. Par défaut, il lira DepFiles près des fichiers d'objet (voir l'option du compilateur -MD ). Supposons que le fichier d'objet soit situé à:
/home/coolhazker/projects/some_project/build/main.cpp.o
Jet-Live essaiera de trouver DepFile à:
/home/coolhazker/projects/some_project/build/main.cpp.o.d
or
/home/coolhazker/projects/some_project/build/main.cpp.d
Il ramassera toutes les dépendances qui sont sous les répertoires d'observation, de sorte que des choses comme /usr/include/elf.h ne seront pas traitées comme une dépendance même si ce fichier est vraiment inclus dans certains de vos fichiers .cpp.
Maintenant, la bibliothèque est initialisée.
Ensuite, lorsque vous modifiez un fichier source et l'enregistrez, Jet-Live démarre immédiatement la compilation de tous les fichiers dépendants en arrière-plan. Par défaut, le nombre de processus de compilation simultanés est de 4, mais vous pouvez le configurer. Il écrira pour se connecter sur les succès et les erreurs à l'aide de la méthode d' ILiveListener::onLog de l'écoute. Si vous déclenchez la compilation d'un fichier lorsqu'il compile déjà (ou en attente dans la file d'attente), l'ancien processus de compilation sera tué et un nouveau sera ajouté à la file d'attente, donc il est sûr de ne pas attendre la compilation pour terminer et apporter de nouvelles modifications du code. De plus, une fois chaque fichier compilé, il mettra à jour les dépendances du fichier compilé, car le compilateur peut recréer DepFile pour celui-ci si la nouvelle version de l'unité de compilation a de nouvelles dépendances.
Lorsque vous appelez Live::tryReload , la bibliothèque attendra les processus de compilation inachevés, puis tous les nouveaux fichiers d'objets accumulés seront liés ensemble dans la bibliothèque partagée et placée près de votre exécutable avec le nom lib_reloadXXX.so , où XXX est un certain nombre de "reloads" pendant cette session. Donc lib_reloadXXX.so contient tous les nouveaux code.
Jet-Live charge cette bibliothèque à l'aide de dlopen , lit des en-têtes ELF / Mach-O et des sections et trouve tous les symboles. Il charge également les informations de relocalisation à partir des fichiers d'objet qui ont été utilisés pour construire cette nouvelle bibliothèque. Après cela:
memcpy la mémoire de l'ancien emplacement à un nouveau IMPORTANT: L'événement ILiveListener::onCodePreLoad est tiré juste avant lib_reloadXXX.so est chargé dans la mémoire du processus. L'événement ILiveListener::onCodePostLoad est licencié juste une fois que la machine de téléchargement de code est terminée.
Vous pouvez en savoir plus sur l'accrochage des fonctions ici. Cette bibliothèque utilise une bibliothèque Subhook impressionnante pour rediriger le flux de fonctions de nouveaux à de nouveaux. Vous pouvez voir que sur les plates-formes 32 bits, vos fonctions devraient du moins 5 octets pour être accrochables. Sur 64 bits, vous avez besoin d'au moins 14 octets, ce qui est beaucoup, et par exemple la fonction de stub vide ne rentrera probablement pas dans 14 octets. D'après mes observations, Clang par défaut produit du code avec l'alignement des fonctions de 16 octets. GCC ne le fait pas par défaut, donc pour GCC, le drapeau -falign-functions=16 est utilisé. Cela signifie que l'espacement entre les commencements de 2 fonctions n'est pas moins que 16 octets, ce qui permet d'accrocher n'importe quelle fonction.
Les nouvelles versions des fonctions devraient utiliser des statistiques et des globaux qui vivent déjà dans l'application. Pourquoi est-ce important? Supposons que vous ayez (un peu d'exemple synthétique, mais de toute façon):
// 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 ;
} Ensuite, vous voulez mettre à jour veryUsefulFunction terme à SMTH comme ceci:
int veryUsefulFunction ( int value)
{
return value * 3 ;
} Génial, maintenant il multiplie l'argument par 3. Mais comme Whole Singleton.cpp sera reloade et Singleton::instance sera accrochée pour appeler une nouvelle version, lib_reloadXXX.so contiendra une nouvelle variable statique static Singleton ins , qui n'est pas initialisée, et si vous appellerez Singleton::instance() , après que nous ne voulons pas être rélocculé, il ne voulait pas être à la construction. C'est pourquoi nous devons déplacer toutes les statistiques et les globaux vers le nouveau code et transférer les variables de garde de la statistique. La plupart des délocalisations en temps de liaison liées aux statistiques et aux globaux sont 32 bits. Donc, si la bibliothèque partagée avec un nouveau code sera chargée trop loin en mémoire de l'application, il ne sera pas possible de déplacer les variables de cette manière. Pour résoudre ce problème, la nouvelle bibliothèque partagée est liée à l'aide de drapeaux de liaison spéciaux qui nous permet de le charger dans un emplacement pré-calculé spécifique dans la mémoire virtuelle (voir -image_base dans Apple LD, --image-base dans LLVM LLD et -Ttext-segment + -z max-page-size dans les drapeaux de liaison GNU LD).
De plus, votre application se bloquera probablement si vous essayez de modifier la disposition de la mémoire de vos types de données dans le code de rechargement.
Supposons que vous ayez une instance de cette classe allouée quelque part dans le tas ou sur la pile:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
}
private:
int m_someVar1 = 0 ;
}Vous le modifiez et maintenant il ressemble:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
m_someVar2++;
}
private:
int m_someVar1 = 0 ;
int m_someVar2 = 0 ;
} Une fois le code rechargé, vous observerez probablement un crash car l'objet déjà alloué a une disposition de données différente, il n'a pas de variable d'instance m_someVar2 , mais la nouvelle version de calledEachUpdate essaiera de la modifier en réellement la modification des données aléatoires. Dans de tels cas, vous devez supprimer cette instance dans le rappel onCodePreLoad et le recréer dans un rappel onCodePostLoad . Le transfert correct de son état dépend de vous. Le même effet aura lieu si vous essayez de modifier la disposition des structures de données statiques. La même chose est également correcte pour les classes polymorphes (VTable) et les lambdas avec des captures (les captures sont stockées dans les champs de données de Lambdas).
Mit
Pour les licences des bibliothèques d'occasion, veuillez vous référer à leurs répertoires et code source.