Demo
WICHTIG: Ab macOS 10.14 funktioniert der in der Bibliothek verwendete Ansatz nicht mehr, so sieht Apple ein, wie es einige der Routinen auf niedriger Ebene eingeschränkt hat. Zumindest auf Ubuntu 20.04 funktioniert es immer noch gut.
Jet-Live ist eine Bibliothek für C ++ "Hot Code Reloading". Es funktioniert unter Linux und Modern MacOS (10.12+) auf 64-Bit-Systemen, die von CPU mit X86-64-Befehlssatz betrieben werden. Abgesehen vom Nachladen von Funktionen kann es nach dem Nachladen von Apps die statische und globale Staat unverändert halten (siehe "Wie es funktioniert" für das, was es ist und warum es wichtig ist). Getestet auf Ubuntu 18.04 mit Klang 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 und MacOS 10.13.6 mit Xcode 8.3.3, CMake 3.8.2, Make 3.81.
Wichtig: Diese Bibliothek zwingt Sie nicht, Ihren Code auf besondere Weise zu organisieren (wie in RCCPP oder CR), Sie müssen keinen neu loadbaren Code in eine gemeinsam genutzte Bibliothek trennen. Jet-Live sollte mit jedem Projekt in der am wenigsten aufdringlichen Weise zusammenarbeiten.
Wenn Sie für Windows etwas Ähnliches benötigen, versuchen Sie bitte Blink, ich habe keine Pläne, Windows zu unterstützen.
Sie benötigen c++11 konforme Compiler. Außerdem gibt es mehrere Abhängigkeiten, die gebündelt sind. Die meisten sind nur eine Header- oder einzelne H/CPP-Paarbibliothek. Weitere Informationen finden Sie im lib -Verzeichnis.
Diese Bibliothek eignet sich am besten für Projekte, die auf CMAKE- und Maschinen- oder Ninja-Build-Systemen basieren. Standardeinstellungen sind für diese Tools fein abgestimmt. set(CMAKE_EXPORT_COMPILE_COMMANDS ON) Option " compile_commands.json Dies ist wichtig und nicht vermeidbar. Weitere Informationen finden Sie unter cmakelists.txt. Wenn Sie Ninja verwenden, fügen Sie beim Ausführen von Ninja -d keepdepfile -Ninja -Flag hinzu.
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 -KlasseliveInstance->update()liveInstance->tryReload()Wichtig: Diese Bibliothek ist nicht sicher. Es verwendet Threads unter der Motorhaube, um den Compiler auszuführen. Sie sollten jedoch alle Bibliotheksmethoden aus demselben Thread aufrufen.
Außerdem benutze ich diese Bibliothek nur mit Debug -Builds ( -O0 , nicht gestrippt, ohne -fvisibility=hidden und ähnliches), um nicht mit optimierten und eingeführten Funktionen und Variablen umzugehen. Ich weiß nicht, wie es bei hoch optimierten Stripp -Builds funktioniert, höchstwahrscheinlich wird es überhaupt nicht funktionieren.
Persönlich benutze ich es so. Ich habe eine Ctrl+r -Verknüpfung, zu der tryReload in meiner Anwendung zugewiesen ist. Auch meine App-App-Anrufe update im Haupt-Runloop und hört auf onCodePreLoad und onCodePostLoad -Ereignisse zu, um einige Objekte neu zu erstellen oder einige Funktionen neu zu bewerten:
Ctrl+r Jet-Live wird auf Dateiänderungen überwacht, die Dateien geändert und nur dann, wenn tryReload aufgerufen wird. Es wartet auf alle aktuellen Kompilierungsprozesse, um neuen Code zu beenden und neu zu laden. Bitte rufen Sie bei jedem Update tryReload nicht an, es funktioniert nicht, wie Sie es erwarten, sondern nur dann, wenn Ihr Quellcode für die Neu geladen werden kann.
Wenn Sie nicht zwischen Ihrem Code -Editor und Ihrer App hin und her wechseln möchten, können Sie eine Tastaturverknüpfung konfigurieren, bei der ein Shell -Befehl kill -s USR1 $(pgrep <your_app_name>) ausgeführt wird, löst die Bibliothek Code Reload aus, wenn SIGUSR1 -Signal empfangen wird. Es funktioniert zumindest in EMACs, Xcode, Clion und VSCode, aber ich bin sicher, dass es in anderen Redakteuren und IDES erreichbar ist, nur googeln Sie es. Wenn Ihr Debugger LLDB ist und dieses Signal fängt und die App gestoppt, fügen Sie diese Befehle zur ~/.lldbinit -Datei hinzu:
breakpoint set --name main
breakpoint command add
process handle -n true -p true -s false SIGUSR1
continue
DONE
Auf macOS können Sie cmake -G Xcode -Generator abgesehen von Make und Ninja verwenden. In diesem Fall installieren Sie bitte xcpretty GEM:
gem install xcpretty
Es gibt eine einfache Beispiel -App, einfach ausführen:
git clone https://github.com/ddovod/jet-live.git && cd jet-live
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug .. && make
./example/example Und versuchen Sie es mit hello . Vergessen Sie nicht, den Befehl reload auszuführen, nachdem Sie die Funktion behoben haben.
Es gibt eine nicht sehr umfassende, aber ständig aktualisierte Testsuite. Um es auszuführen:
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/Implementiert:
compile_commands.json -Datei nach einer neuen .cpp -Datei erstellt zu werden).Wird implementiert:
Wird überhaupt nicht implementiert:
Jet-Live ist gut abgestimmt, um mit CMake und Made/Ninja-Tools zu arbeiten. Wenn Sie es jedoch für ein anderes Build-Tool übernehmen möchten, können Sie sein Verhalten in einigen Aspekten anpassen. Bitte beachten Sie Quellen und Dokumentation. Es ist auch eine gute Idee, Ihren eigenen Hörer zu erstellen, um Ereignisse aus der Bibliothek zu erhalten. Bitte beachten Sie die Dokumentation von ILiveListener und LiveConfig .
WICHTIG: Es wird dringend empfohlen, alle Nachrichten aus der Bibliothek mit ILiveListener::onLog zu protokollieren, um zu sehen, ob etwas schief gelaufen ist.
Die Bibliothek liest ELF -Header und -abschnitte dieser ausführbaren Datei und alle geladenen gemeinsam genutzten Bibliotheken, findet alle Symbole und versucht herauszufinden, welche von ihnen entweder begeistert sein kann (Funktionen) oder übertragen/verlegt werden sollten (statische/globale Variablen). Außerdem findet es die Größe der Symbole und die "echte" Adresse.
Abgesehen von diesem Jet-Live versucht es, compile_commands.json in der Nähe Ihrer ausführbaren Datei oder in seinen übergeordneten Verzeichnissen rekursiv zu finden. Mit dieser Datei werden sie unterscheidet:
.o (Object) Dateipfad.d (Deepfile) Dateien Pfad Wenn alle Kompilierungseinheiten analysiert werden, unterscheidet es das häufigste Verzeichnis für alle Quelldateien und beginnt, alle Verzeichnisse mit Quelldateien, ihren Abhängigkeiten und einigen Servicedateien wie compile_commands.json zu beobachten.
Abgesehen davon versucht die Bibliothek, alle Abhängigkeiten für jede Kompilierungseinheit zu finden. Standardmäßig wird DeeTfiles in der Nähe der Objektdateien gelesen (siehe Option -MD Compiler). Angenommen, die Objektdatei befindet sich unter:
/home/coolhazker/projects/some_project/build/main.cpp.o
Jet-Live wird versuchen, Deepfile zu finden unter:
/home/coolhazker/projects/some_project/build/main.cpp.o.d
or
/home/coolhazker/projects/some_project/build/main.cpp.d
Es wird alle Abhängigkeiten aufnehmen, die sich unter den Beobachtungsverzeichnissen befinden, also werden Dinge wie /usr/include/elf.h include/elf nicht als Abhängigkeit behandelt, auch wenn diese Datei wirklich in einigen Ihrer .CPP -Dateien enthalten ist.
Jetzt wird die Bibliothek initialisiert.
Wenn Sie als Nächstes eine Quelldatei bearbeiten und speichern, startet Jet-Live sofort die Kompilierung aller abhängigen Dateien im Hintergrund. Standardmäßig beträgt die Anzahl der gleichzeitigen Kompilierungsprozesse 4, aber Sie können es konfigurieren. Es wird geschrieben, um mit ILiveListener::onLog -Methode des Hörers über Erfolge und Fehler zu protokollieren. Wenn Sie die Kompilierung einer Datei auslösen, wenn sie bereits kompiliert (oder in der Warteschlange wartet), wird der alte Kompilierungsprozess getötet und neu in der Warteschlange hinzugefügt, sodass es irgendwie sicher ist, nicht auf die Kompilierung zu warten, um fertig zu werden und neue Änderungen des Codes vorzunehmen. Auch nachdem jede Datei zusammengestellt wurde, aktualisiert sie Abhängigkeiten für kompilierte Datei, da Compiler DeeTfile dafür neu erstellen kann, wenn eine neue Version der Compilation Unit neue Abhängigkeiten hat.
Wenn Sie Live::tryReload anrufen, wartet die Bibliothek auf unvollendete Kompilierungsprozesse und dann werden alle akkumulierten neuen Objektdateien in der gemeinsamen Bibliothek miteinander verknüpft und in der Nähe Ihrer ausführbaren Datei mit Namen lib_reloadXXX.so platziert, wobei XXX während dieser Sitzung eine Reihe von "Nachladen" ist. So enthält lib_reloadXXX.so alle neuen Code.
Jet-Live lädt diese Bibliothek mit dlopen , liest ELF/Mach-O-Header und -abschnitte und findet alle Symbole. Außerdem lädt es Umzugsinformationen aus den Objektdateien, mit denen diese neue Bibliothek erstellt wurde. Danach:
memcpy wurden Wichtig: ILiveListener::onCodePreLoad -Ereignis wird kurz vor lib_reloadXXX.so in den Prozessspeicher geladen. ILiveListener::onCodePostLoad Ereignis wird direkt abgefeuert, nachdem alle Code-Reloading-Maschinerie beendet sind.
Weitere Informationen zum Funktionsanschluss finden Sie hier. Diese Bibliothek verwendet eine fantastische Subhook -Bibliothek, um den Funktionsfluss von alten zu neuen umzuleiten. Sie können sehen, dass Ihre Funktionen auf 32 -Bit -Plattformen mindestens 5 Bytes dauern sollten, um hakenbar zu sein. Bei 64 Bit benötigen Sie mindestens 14 Bytes, was viel ist, und beispielsweise wird die leere Stubfunktion wahrscheinlich nicht in 14 Bytes passen. Aus meinen Beobachtungen erzeugt Clang standardmäßig Code mit 16-Byte-Funktionen. GCC tun dies nicht standardmäßig, daher wird für GCC das Flag -falign-functions=16 verwendet. Das bedeutet, dass der Abstand zwischen Beginn von 2 -Funktionen nicht weniger als 16 Bytes ist, was es ermöglicht, eine Funktion zu hängen.
Neue Versionen von Funktionen sollten Statik und Globale verwenden, die bereits in der Anwendung leben. Warum ist es wichtig? Angenommen, Sie haben (ein bisschen synthetisches Beispiel, aber trotzdem):
// 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 ;
} Dann möchten Sie veryUsefulFunction auf SMTH aktualisieren:
int veryUsefulFunction ( int value)
{
return value * 3 ;
} Großartig, jetzt multipliziert es das Argument mit 3., aber da der static Singleton ins Singleton.cpp neu geladen wird und Singleton::instance Singleton::instance() neue Version nennen wird, enthält lib_reloadXXX.so wieder. Deshalb müssen wir alle Statik und Globalen in den neuen Code verlegen und die Schutzvariablen von Statik übertragen. Die meisten Verschiebungen der Link-Zeit im Zusammenhang mit Statik und Globalen sind 32-Bit. Wenn also die gemeinsam genutzte Bibliothek mit neuem Code aus der Anwendung zu weit geladen wird, ist es nicht möglich, Variablen auf diese Weise zu verlagern. Um dies zu lösen, wird eine neue freigegebene Bibliothek mit speziellen Linker-Flags verknüpft, mit denen wir sie in einen spezifischen vorkalkulierten Speicherort im virtuellen Speicher laden können (siehe -image_base in Apple Ld, --image-base in LLVM LLD- und -Ttext-segment + -z max-page-size in GNU-LD-Linker-Flags).
Auch Ihre App stürzt wahrscheinlich zum Absturz, wenn Sie versuchen, das Speicherlayout Ihrer Datentypen im Nachladecode zu ändern.
Angenommen, Sie haben eine Instanz dieser Klasse, die irgendwo auf dem Haufen oder auf dem Stapel zugewiesen wurde:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
}
private:
int m_someVar1 = 0 ;
}Sie bearbeiten es und jetzt sieht es aus wie:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
m_someVar2++;
}
private:
int m_someVar1 = 0 ;
int m_someVar2 = 0 ;
} Nachdem der Code neu geladen wurde, werden Sie wahrscheinlich einen Absturz beobachten, da bereits zugewiesene Objekte ein anderes Datenlayout enthält. Es hat keine m_someVar2 -Instanzvariable, aber eine neue Version von calledEachUpdate versucht, sie tatsächlich zu ändern, um zufällige Daten zu ändern. In solchen Fällen sollten Sie diese Instanz im onCodePreLoad -Callback löschen und in onCodePostLoad -Callback neu erstellen. Die korrekte Übertragung seines Staates liegt bei Ihnen. Der gleiche Effekt erfolgt, wenn Sie versuchen, das Layout der statischen Datenstrukturen zu ändern. Gleiches gilt auch für polymorphe Klassen (VTABLE) und Lambdas mit Erfassungen (Aufnahmen werden in den Datenfeldern von Lambdas gespeichert).
MIT
Für Lizenzen von gebrauchten Bibliotheken finden Sie unter ihren Verzeichnissen und Quellencode.