Demonstração
IMPORTANTE: A partir do MacOS 10.14 A abordagem usada na biblioteca não funciona mais, parece que a Apple restringiu algumas das rotinas de baixo nível. No Linux, pelo menos no Ubuntu 20.04, ele ainda funciona bem.
O Jet-Live é uma biblioteca para C ++ "Recarrega de código quente". Ele funciona no Linux e no Modern MacOS (10.12+, eu acho) em sistemas de 64 bits alimentados pela CPU com o conjunto de instruções x86-64. Além de recarregar as funções, ele é capaz de manter o estado estático e global dos aplicativos inalterado depois que o código foi recarregado (consulte "Como funciona" para o que é e por que é importante). Testado no Ubuntu 18.04 com 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 e MacOS 10.13.6 com Xcode 8.3.3, cmake 3.8.2, faça 3.1.
IMPORTANTE: Esta biblioteca não o força a organizar seu código de alguma maneira especial (como no RCCPP ou CR), você não precisa separar o código recarregável em alguma biblioteca compartilhada, o Jet-Live deve trabalhar com qualquer projeto da maneira menos intrusiva.
Se você precisar de algo semelhante para o Windows, tente piscar, não tenho planos de oferecer suporte ao Windows.
Você precisa do compilador compilador c++11 . Também existem várias dependências que são agrupadas, a maioria delas é apenas a biblioteca de pares H/CPP única. Consulte o diretório lib para obter detalhes.
Esta biblioteca é mais adequada para projetos com base nos sistemas CMake e Make ou Ninja Build, os padrões são ajustados para essas ferramentas. O cmakelists.txt adicionará a opção set(CMAKE_EXPORT_COMPILE_COMMANDS ON) para compile_commands.json e sinalizadores de compilador e linker. Isso é importante e não é evitável. Para detalhes, consulte Cmakelists.txt. Se você usa Ninja, adicione -d keepdepfile Ninja Flag ao executar o Ninja, isso é necessário para rastrear dependências entre os arquivos de origem e cabeçalho
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 não é segura. Ele usa threads sob o capô para executar o compilador, mas você deve chamar todos os métodos da biblioteca do mesmo thread.
Também uso esta biblioteca apenas com compilações de depuração ( -O0 , não despojado, sem -fvisibility=hidden e coisas assim) para não lidar com funções e variáveis otimizadas e inlinadas. Não sei como funciona em construções despojadas altamente otimizadas, provavelmente não funcionará.
Pessoalmente, eu uso assim. Eu tenho um atalho Ctrl+r ao qual tryReload é atribuído no meu aplicativo. Além disso, meu aplicativo chama update no Main Runloop e ouve os eventos onCodePreLoad e onCodePostLoad para recriar alguns objetos ou reavaliar algumas funções:
Ctrl+r O Jet-Live monitorará as alterações de arquivo, o recompile alterou arquivos e somente quando tryReload for chamado, aguardará que todos os processos de compilação atuais terminem e recarreguem o novo código. Por favor, não ligue para tryReload em cada atualização, ele não funcionará como você está esperando, ligue apenas quando seu código -fonte estiver pronto para ser recarregado.
Se você não deseja alternar entre o seu editor de código e o aplicativo, poderá configurar um atalho de teclado que executa um comando shell kill -s USR1 $(pgrep <your_app_name>) , a biblioteca acionará o código recarregada quando o sinal SIGUSR1 for recebido. Funciona pelo menos em Emacs, Xcode, Clion e Vscode, mas tenho certeza de que é possível em outros editores e IDEs, basta pesquisar no Google. Se o seu depurador for LLDB e ele pega esse sinal e interrompe o aplicativo, adicione esses comandos ao arquivo ~/.lldbinit :
breakpoint set --name main
breakpoint command add
process handle -n true -p true -s false SIGUSR1
continue
DONE
No MacOS, você pode usar o gerador cmake -G Xcode além de Make e Ninja. Nesse caso, instale xcpretty gem:
gem install xcpretty
Há um aplicativo de exemplo simples, basta executar:
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 tente o comando hello . Não se esqueça de executar o comando reload depois de corrigir a função.
Há um conjunto de testes não muito abrangente, mas constantemente atualizado. Para executá -lo:
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 depois que o novo arquivo .cpp foi criado)Será implementado:
Não será implementado:
O Jet-Live está bem ajustado para trabalhar com as ferramentas CMake e Make/Ninja, mas se você quiser adotá-lo para outra ferramenta de construção, há uma maneira de personalizar seu comportamento em alguns aspectos. Consulte as fontes e a documentação. Também é uma boa ideia criar seu próprio ouvinte para receber eventos da biblioteca. Consulte a documentação do ILiveListener e LiveConfig .
IMPORTANTE: É altamente recomendável registrar todas as mensagens da biblioteca usando ILiveListener::onLog para ver se algo deu errado.
A biblioteca lê cabeçalhos de elfo e seções deste executável e de todas as bibliotecas compartilhadas carregadas, encontra todos os símbolos e tenta descobrir qual deles pode ser ligada (funções) ou deve ser transferida/realocada (variáveis estáticas/globais). Também encontra tamanho de símbolos e endereço "real".
Além desse jato, tenta encontrar compile_commands.json perto do seu executável ou em seus diretórios de pais recursivamente. Usando este arquivo, ele distingue:
.o (objeto).d (depfile) arquivos caminho Quando todas as unidades de compilação são analisadas, ele distingue o diretório mais comum para todos os arquivos de origem e começa a assistir a todos os diretórios com arquivos de origem, suas dependências e alguns arquivos de serviço como compile_commands.json .
Além disso, a biblioteca tenta encontrar todas as dependências para cada unidade de compilação. Por padrão, ele lerá os depfiles próximos aos arquivos do objeto (consulte -MD Compiler Option). Suponha que o arquivo de objeto esteja localizado em:
/home/coolhazker/projects/some_project/build/main.cpp.o
O Jet-Live tentará encontrar o depfile em:
/home/coolhazker/projects/some_project/build/main.cpp.o.d
or
/home/coolhazker/projects/some_project/build/main.cpp.d
Ele captará todas as dependências que estão nos diretórios de observação; portanto, coisas como /usr/include/elf.h não serão tratadas como dependência, mesmo que esse arquivo esteja realmente incluído em alguns dos seus arquivos .cpp.
Agora a biblioteca é inicializada.
Em seguida, ao editar algum arquivo de origem e salvá-lo, o Jet-Live inicia imediatamente a compilação de todos os arquivos dependentes em segundo plano. Por padrão, o número de processos de compilação simultânea é 4, mas você pode configurá -lo. Ele escreverá para registrar sobre sucessos e erros usando o método ILiveListener::onLog do ouvinte. Se você acionar a compilação de algum arquivo quando ele já estiver compilando (ou aguardando na fila), o processo de compilação antigo será morto e o novo será adicionado à fila, portanto, é um meio seguro para não esperar a compilação terminar e fazer novas alterações no código. Além disso, depois que cada arquivo foi compilado, ele atualizará dependências para o arquivo compilado, pois o compilador pode recriar o DePFile para ele se a nova versão da unidade de compilação tiver novas dependências.
Ao ligar para Live::tryReload , a biblioteca aguarda processos de compilação inacabada e, em seguida, todos os novos arquivos de objeto acumulados serão vinculados na biblioteca compartilhada e colocados perto do seu executável com o nome lib_reloadXXX.so , onde XXX é um número de "recarra" durante esta sessão. SO lib_reloadXXX.so CONTENSA TODOS O NOVO CÓDIGO.
O Jet-Live carrega esta biblioteca usando dlopen , lê cabeçalhos e seções ELF/Mach-O e encontra todos os símbolos. Também carrega informações de realocação dos arquivos de objeto que foram usados para construir esta nova biblioteca. Depois disso:
memcpy de local antigo para novo IMPORTANTE: O evento ILiveListener::onCodePreLoad é disparado logo antes de lib_reloadXXX.so ser carregado na memória do processo. O evento ILiveListener::onCodePostLoad é disparado logo após a conclusão de toda a máquina de relowaring.
Você pode ler mais sobre a função de conexão aqui. Esta biblioteca usa a Biblioteca Subhook incrível para redirecionar o fluxo de funções de antigas para novas. Você pode ver que, em plataformas de 32 bits, suas funções devem ter pelo menos 5 bytes para serem ligáveis. Em 64 bits, você precisa de pelo menos 14 bytes, o que é muito e, por exemplo, a função de stub vazia provavelmente não se encaixará em 14 bytes. A partir das minhas observações, Clang por padrão produz código com alinhamento de 16 bytes. O GCC não faz isso por padrão; portanto, para GCC, o sinalizador -falign-functions=16 é usado. Isso significa que o espaçamento entre inicia qualquer funções não é menor que 16 bytes, o que possibilita prejudicar qualquer função.
Novas versões de funções devem usar estática e globais que já estão vivendo no aplicativo. Por que é importante? Suponha que você tenha (um exemplo um pouco sintético, mas de qualquer maneira):
// 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 ;
} Então você deseja atualizar veryUsefulFunction para o SMTH como este:
int veryUsefulFunction ( int value)
{
return value * 3 ;
} Great, now it multiplies argument by 3. But since whole Singleton.cpp will be reloaded and Singleton::instance function will be hooked to call new version, lib_reloadXXX.so will contain new static variable static Singleton ins , which is not initialized, and if you call Singleton::instance() after code was reloaded, it will initialize this variable again which is not good cause we don't want to call its constructor de novo. É por isso que precisamos realocar todas as estáticas e globais para o novo código e transferir as variáveis de guarda de estática. A maioria das realocações de tempo de ligação relacionadas à estática e globais é de 32 bits. Portanto, se a biblioteca compartilhada com novo código será carregada muito longe na memória do aplicativo, não será possível realocar variáveis dessa maneira. Para resolver isso, a nova biblioteca compartilhada está vinculada usando sinalizadores especiais de vinculadores, o que nos permite carregá-lo em um local pré-calculado específico na memória virtual (consulte -image_base no Apple LD, --image-base em llvm lld e -Ttext-segment + -z max-page-size nos flags ld ld).
Além disso, seu aplicativo provavelmente falhará se você tentar alterar o layout de memória de seus tipos de dados no código recarregável.
Suponha que você tenha uma instância desta classe alocada em algum lugar na pilha ou na pilha:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
}
private:
int m_someVar1 = 0 ;
}Você edita e agora parece:
class SomeClass
{
public:
void calledEachUpdate () {
m_someVar1++;
m_someVar2++;
}
private:
int m_someVar1 = 0 ;
int m_someVar2 = 0 ;
} Depois que o código for recarregado, você provavelmente observará uma falha porque o objeto já alocado possui um layout de dados diferentes, ele não possui variável de instância m_someVar2 , mas a nova versão do calledEachUpdate tentará modificá -lo realmente modificando dados aleatórios. Nesses casos, você deve excluir esta instância no retorno de chamada onCodePreLoad e recriá -lo no retorno de chamada onCodePostLoad . A transferência correta de seu estado depende de você. O mesmo efeito ocorrerá se você tentar alterar o layout de estruturas de dados estáticos. O mesmo também correto para classes polimórficas (vtable) e lambdas com capturas (as capturas são armazenadas nos campos de dados de Lambdas).
Mit
Para licenças de bibliotecas usadas, consulte seus diretórios e código -fonte.