デモ
重要:MacOS 10.14から始まるライブラリで使用されているアプローチは機能しなくなり、Appleが低レベルのルーチンの一部を制限しているようです。 Linuxでは、少なくともubuntu 20.04では、それでも正常に機能します。
Jet-Liveは、 C ++の「ホットコードリロード」のライブラリです。 X86-64命令セットでCPUを搭載した64ビットシステムでLinuxおよびModern MacOS(10.12+私は推測する)で動作します。関数のリロードとは別に、コードがリロードされた後、アプリの静的状態とグローバル状態を変更しておくことができます(それが何であり、なぜ重要なのかについては「機能する方法」を参照してください)。 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、ninja 1.8.2、Xcode 8.3.3、cmake 3.8.2で4.1、およびMacos 10.13.6を作成して、Clang 6.0.1/7.0.1、LLD-7でテストしました。
重要:このライブラリは、特別な方法でコードを整理することを強制しません( RCCPPやCRなど)。リロード可能なコードを共有ライブラリに分離する必要はありません。
Windowsに似たようなものが必要な場合は、Blinkを試してみてください。Windowsをサポートする予定はありません。
c++11コンプライアントコンパイラが必要です。また、バンドルされたいくつかの依存関係があり、それらのほとんどはヘッダーのみまたは単一のH/CPPペアライブラリです。詳細については、 libディレクトリを参照してください。
このライブラリは、CmakeおよびMakeまたはNinja Build Systemsに基づいたプロジェクトに最適です。デフォルトはこれらのツールに微調整されています。 cmakelists.txtは、 compile_commands.jsonのset(CMAKE_EXPORT_COMPILE_COMMANDS ON)オプションを追加し、コンパイラとリンカーフラグを変更します。これは重要であり、回避できません。詳細については、cmakelists.txtを参照してください。 Ninjaを使用する場合は、Ninjaを実行するときに-d keepdepfile 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クラスのインスタンスを作成しますliveInstance->update()呼び出しますliveInstance->tryReload()を呼び出します重要:このライブラリは安全ではありません。フードの下のスレッドを使用してコンパイラを実行しますが、同じスレッドのすべてのライブラリメソッドを呼び出す必要があります。
また、このライブラリは、最適化された関数と変数を扱わないように、デバッグビルド( -O0 、 -fvisibility=hidden 、そのようなものなしで剥がれない)でのみ使用します。高度に最適化された剥がれたビルドでどのように機能するかはわかりませんが、まったく機能しない可能性があります。
個人的に私はこのようにそれを使用します。アプリケーションにtryReloadが割り当てられているCtrl+rショートカットがあります。また、私のアプリアプリはメインのRunloopでupdate呼び出し、 onCodePreLoadおよびonCodePostLoadイベントのリッスンを行い、いくつかのオブジェクトを再現したり、いくつかの機能を再評価したりします。
Ctrl+rを押しますJet-Liveはファイルの変更を監視し、変更されたファイルを再コンパイルし、 tryReloadが呼び出された場合にのみ、現在のすべてのコンピレーションプロセスが新しいコードを完了してリロードするのを待ちます。各アップデートでtryReload電話しないでください。期待どおりに機能しません。ソースコードをリロードする準備ができた場合にのみ電話してください。
コードエディターとアプリを前後に切り替えたくない場合は、シェルコマンドkill -s USR1 $(pgrep <your_app_name>) SIGUSR1実行するキーボードショートカットを構成できます。少なくともemacs、xcode、clion、vscodeで動作しますが、他の編集者やidesで達成できると確信しています。デバッガーがLLDBであり、この信号をキャッチしてアプリを停止する場合、このコマンドを~/.lldbinitファイルに追加します。
breakpoint set --name main
breakpoint command add
process handle -n true -p true -s false SIGUSR1
continue
DONE
MacOSでは、MakeとNinja以外のcmake -G Xcodeジェネレーターを使用できます。この場合、 xcprettyジェムをインストールしてください:
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を使用してライブラリからすべてのメッセージをログに記録して、何かがうまくいかなかったかどうかを確認することを強くお勧めします。
ライブラリは、この実行可能ファイルのELFヘッダーとセクションとすべてのロードされた共有ライブラリを読み取り、すべてのシンボルを見つけて、それらのどれをフック(関数)または転送/移転(静的/グローバル変数)のいずれかを見つけようとします。また、シンボルのサイズと「実際の」アドレスが見つかります。
それとは別に、Jet-Liveは、実行可能ファイルの近くにcompile_commands.json見つけようとします。このファイルを使用して、次のことを区別します。
.o (オブジェクト)ファイルパス.d (depfile)ファイルパスすべてのコンピレーションユニットが解析されると、すべてのソースファイルの最も一般的なディレクトリを区別し、ソースファイル、その依存関係、 compile_commands.jsonなどの一部のサービスファイルを使用してすべてのディレクトリを監視し始めます。
それとは別に、ライブラリは各コンパイルユニットのすべての依存関係を見つけようとします。デフォルトでは、オブジェクトファイルの近くのdepfilesを読み取ります( -MDコンパイラオプションを参照)。オブジェクトファイルが次の場所にあるとします。
/home/coolhazker/projects/some_project/build/main.cpp.o
jet-liveは、次のことを見つけようとします:
/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に電話すると、ライブラリは未完成のコンピレーションプロセスを待ち、その後、すべての蓄積された新しいオブジェクトファイルが共有ライブラリにリンクされ、このセッション中にXXXが多数の「リロード」である名前lib_reloadXXX.soで実行可能ファイルの近くに配置されます。したがって、 lib_reloadXXX.soにはすべての新しいコードが含まれています。
Jet-Liveは、 dlopenを使用してこのライブラリをロードし、ELF/MACH-Oヘッダーとセクションを読み取り、すべてのシンボルを見つけます。また、この新しいライブラリを構築するために使用されたオブジェクトファイルからの再配置情報をロードします。その後:
memcpy変数について重要: ILiveListener::onCodePreLoadイベントは、 lib_reloadXXX.soがプロセスメモリにロードされる直前に発射されます。 ILiveListener::onCodePostLoadイベントは、すべてのコードリロードマチナリーが完成した直後に解雇されます。
関数のフックをこちらをご覧ください。このライブラリは、素晴らしいサブフックライブラリを使用して、古いものから新しいものに機能の流れをリダイレクトします。 32のビットプラットフォームでは、機能がフックできるように少なくとも5バイトの長さである必要があることがわかります。 64ビットでは、少なくとも14バイトが必要ですが、たとえば空のスタブ関数はおそらく14バイトに収まりません。私の観察から、Clangはデフォルトで16バイト関数アラインメントを備えたコードを生成します。 GCCはデフォルトでこれを行わないため、GCCの場合-falign-functions=16フラグが使用されます。つまり、2つの関数の開始間の間隔は16バイトほどではなく、機能をフックすることができます。
関数の新しいバージョンでは、すでにアプリケーションに住んでいるstatics and Globalsを使用する必要があります。なぜそれが重要なのですか?あなたが持っていると仮定します(少し合成例ですが、とにかく):
// 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 ;
}次に、このようなSMTHにveryUsefulFunctionを更新したいと思います。
int veryUsefulFunction ( int value)
{
return value * 3 ;
}素晴らしい、今では議論に3を掛けます。しかし、 Singleton.cpp全体がリロードされ、シングルトンSingleton::instance() Singleton::instance関数は新しいバージョンを呼び出すためにフックされますlib_reloadXXX.soには、初期化されていない新しい静的変数static Singleton insが含まれます。 また。そのため、すべてのstatics and Globalsを新しいコードに再配置し、staticsのガード変数を転送する必要があります。スタチックとグローバルに関連するリンク時間リンクの再配置のほとんどは32ビットです。したがって、新しいコードを使用して共有ライブラリがアプリケーションからメモリにロードされすぎる場合、この方法で変数を再配置することはできません。これを解決するために、新しい共有ライブラリは、仮想メモリの特定の事前に計算された場所にロード-Ttext-segment特別なリンカーフラグを使用してリンクさ--image-baseます(Apple LDの-image_base -z max-page-sizeしてください。
また、リロード可能なコードでデータ型のメモリレイアウトを変更しようとすると、おそらくアプリがクラッシュするでしょう。
このクラスのインスタンスがヒープ内またはスタックのどこかに割り当てられているとします。
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)とキャプチャ付きのラムダにも正しい(キャプチャはラムダスのデータフィールドに保存されます)。
mit
中古ライブラリのライセンスについては、ディレクトリとソースコードを参照してください。