Idle is an asynchronous, hot-reloadable, and highly reactive dynamic component framework similar to OSGI that is:
- 📦 Modular: Your program logic is encapsulated into services that provide interfaces for usage
- 🌀 Dynamic: Services can come and go whenever they want, Idle will keep your application stable
- 🚀 Progressive: Your code is recompiled automatically asynchronously into your application
Therefore Idle can improve your workflow significantly through decreased code iteration time and cleaner code-bases based on the SOLID principles, especially interface segregation. Idle is a C++ engine that is capable of hot-reloading large multi-module codebases on the fly through a scheduled dependency graph. What a game engine is to graphics, that is Idle for C++, without targeting a specific domain. Idle can be used for almost any C++ application type e.g. servers, GUIs, or graphic engines.
Traditional solutions to C++ hot-reloading, such as binary patching, usually do not work when headers are modified, files are added, or code is modified out-of-function or even on optimized release builds. Idle solves this by using shared library loading/unloading techniques that are far more powerful than existing solutions, which are usually limited to simple classes and re-loadable loops. Therefore, Idle can automatically reload headers, compilation units, embedded resources and also takes changes of dependencies and build-system files into account.
Additionally, as C++ becomes more asynchronous in the future, the difficult question of how we can effectively use asynchrony in large applications arises. Idle provides simple patterns and solutions to solve such problems. Furthermore, C++ applications can be prototyped rapidly through Idle's asynchronous ecosystem that provides a CLI, logging, configuration, and persistency.
🔧 Idle is currently in development and therefore not ready for production yet. But, Idle is highly suited already for side-projects, coding playgrounds, and case studies.
The following demonstration shows how a multi-module GUI application can be hot-reloaded with Idle:
idle-hotswap-gl.mp4
The actual application shown in this demo is not included in this repository
An application in Idle is based on multiple fundamental concepts:
- idle::Interface: Provides an arbitrary API for implementation
- idle::Service: Provides the actual implementation of a running entity inside the system.
- Clusters that describe a direct relation of an idle::Service to another to allow local configuration.
- Usages that express a relation of an idle::Service to cluster-external entities.
Therefore Idle provides many improvements over traditional dynamic component systems:
- Zero run-time overhead and while your system is stable: references to connected interfaces are cached and not mutated while you possibly could access it (except on request, to ensure your service is always thread-safe).
- Declarative dependencies allow simple dependency configuration without manifests (no XML/JSON).
- For the locale configuration of services idle uses strongly typed property classes instead of bug-prone string key-value pairs. You fetch the global configuration from your dedicated idle::Config service and propagate these back to nested components!
To build an asynchronous application, Idle provides a feature-rich and ready to use asynchronous ecosystem based on components and interfaces:
- Concurrency abstractions:
- continuable as future/task primitive with optional C++20 coroutine support
- (event-loop) executor and programmer-friendly custom threads.
- Asynchronous PluginHotswap:
- CMake-driven - can reload code in < 2s based on your machine.
- Compatible with MSBuild and Ninja on Windows and make and Ninja on Posix.
- Asynchronous FileWatcher (based on efsw)
- Asynchronous ProcessGroup (base on boost::process)
- Asynchronous Command interface and a default command processor
- A terminal repl with command auto-completion and activity progress indicators (based on replxx).
- Uniform logger facade for pluggable log message sinks (spdlog for example)
- A configuration system based on a custom reflection system that can adapt to any configuration format (TOML currently implemented).
- Changes to the configuration file are detected automatically and dependent services are updated accordingly.
- The configuration file is kept in sync automatically with recently added configuration values and their description.
- A storage system to persist data across hot-swaps using our reflection system or a user-provided byte buffer.
Networking/HTTP abstractions are considered out-of-scope for this project but might be provided through an external module later
This short introduction covers only a small part of the functionality of Idle. To get into the framework we provide you various examples that you can try out dynamically inside the Idle CLI.
Because Idle understands the dependencies of your code and data, it can manage its lifetime much better than you could express it manually. Services are lazy by design and provide an asynchronous onStart
and onStop
override-able method for managing its lifetime. Thus the application start is automatically parallelized and fast, whereas stopping service cleans up resources safely:
class HelloWorldService : public idle::Service {
public:
using Super::Super;
continuable<> onStart() override {
return idle::async([] {
puts("HelloWorldService service is being started!");
});
}
continuable<> onStop() override {
return idle::async([] {
puts("HelloWorldService service is being stopped!");
});
}
};
For initialization, services also provide a thread-safe onInit
and onDestroy
method that is always dispatched on the same event loop (regardless of the thread that initialized or destroyed the service).
class HelloWorldService : public idle::Service {
public:
using Super::Super;
void onInit() override {
IDLE_ASSERT(root().is_on_event_loop());
}
void sayHello() {
puts("Hello!");
}
};
Initialization always happens automatically before starting the service:
Ref<Context> context = Context::create();
Ref<HelloWorldService> hello_world = idle::spawn<HelloWorldService>(*context);
hello_world->start().then([=] {
hello_world->sayHello();
});
Services not referenced anymore are stopped and garbage-collected automatically.
Because an idle::Service is lazy by design, we have to start it manually through service->start()
. Starting a service automatically is supported by the ecosystem itself, rather than the core system. We can export an idle::Autostarted
interface from any service, which causes the service to be started automatically (because a global idle::Autostarter
service will create a dependency on it).
class AutostartedService : public idle::Implements<idle::Autostarted> {
public:
using Super::Super;
};
IDLE_DECLARE(AutostartedService)
The IDLE_DECLARE
macro introduces the AutostartedService
to the system and can be used in any executable or plugin directly.
Dependencies in idle are expressed in two ways: parent-child relationships and usages.
idle provides many helper classes for expressing dependencies between services, specialized in cardinality (1:1, 1:n), and flexibility (statically or changeable at run-time).
Every helper class creates Usage
objects in different ways and can also be created for a custom purpose.
The Dependency
class models a 1:1 non-runtime-changeable dependency, for example, we can make our HelloWorldService
depend on the idle::Log
as following:
class HelloWorldService : public idle::Service {
public:
using Super::Super;
continuable<> onStart() override {
return idle::async([this] {
IDLE_LOG_INFO(log_, "{} is being started!", this->name());
});
}
private:
idle::Dependency<idle::Log> log_{*this};
};
IDLE_DECLARE(HelloWorldService)
The Dependency
always ensures that the service we depend on is started before the depending service. Additionally, the dependent service stays running until all depending services have stopped.
Idle services encapsulate popular open-source libraries with best practices under the hood.
For example, the log message above is formatted with fmtlib (compile-time fmt) and can be dispatched to your favorite log sink (spdlog, for example).
Declaring an Interface
is as easy as it gets. Idle supports default created services and exporting multiple Interface
classes from the same Service
:
class MyInterface : public idle::Interface {
public:
using Super::Super;
virtual void doSomething() = 0;
IDLE_INTERFACE //< Optional
};
class MyInterfaceProvider public : idle::Implements<MyInterface> {
public:
using Super::Super;
void doSomething() override {
// ...
}
IDLE_SERVICE //< Optional
};
With the included support of weak dependencies, Idle can schedule weakly cyclic dependency graphs, which represent the strongest constrained graph type, that can be scheduled. Cycles are allowed to appear, as long as a dependency can be attached and detached at run-time to and from a depending idle::Service.
In this example a nested component is configured from its parent idle::Service.
A configuration file is maintained automatically from the reflected configurations in the system. Services depending on a specific config key, are restarted automatically if a depending value is changed inside the file.
struct ConfigurableConfig {
bool flag{false};
int value{0};
};
IDLE_REFLECT(ConfigurableConfig, flag, (value, "An optional comment"));
class ConfigurableService : public idle::Service {
public:
using Super::Super;
void setup(ConfigurableConfig conf) {
config_ = std::move(conf);
}
void onSetup() override {
my_component_->setup(config_.value);
}
private:
idle::Var<ConfigurableConfig> var_{*this, "config.hello"};
idle::Component<MyComponent> my_component_{*this};
};
IDLE_DECLARE(ConfigurableService)
For a more detailed insight into the framework, further examples are provided.
Requirements:
- Windows, Linux, or macOS (not fully tested yet)
- A C++14 capable compiler: MSVC 2019 (16.7+), Clang 5+, GCC 9+
- CMake 3.17+
- Idle uses custom CMake scripts to download and set up its source dependencies automatically.
- A CMake install will build and install a ready-to-use distribution to your preferred install location.
git clone https://github.com/Naios/idle.git
cd idle
mkdir build
mkdir install
cd build
# MSBuild (Visual Studio)
cmake .. -DCMAKE_INSTALL_PREFIX=install
cmake --build . --config Debug --target INSTALL
# Ninja/make
cmake .. -DCMAKE_INSTALL_PREFIX=install -DCMAKE_BUILD_TYPE=Debug
cmake --build . --target install
../install/bin/idle
# Start editing the project files in `install/project/src`
# Ready-to-use example source files are installed in `install/project/examples`
A ready-to-build Dockerfile
is available inside the docker
subdirectory:
./docker/build.sh
: Builds the image with name idle./docker/make.sh
: Builds idle intobuild/docker
(through mounting the source directory) Make sure that Docker is allowed to mount theidle
source directory../docker/shell.sh ./idle
: Invokesidle
(frombuild/docker
) inside the Docker container
In Docker, Idle supports the automatic rebuilding of source files from a mounted file system due to switching to a polling filewatcher in such cases (controlled with the IDLE_VM
environment variable).
Therefore it is possible to edit one project that can be automatically tested and live reloaded inside multiple Docker containers at the same time.
-
Internally services and their imports and exports are represented as a weakly cyclic directed graph (click to enlarge):
You can generate your own Graphviz graph as shown above, through the
idle graph show [service | cluster]
command, which is invocable from the Idle CLI. -
Based on this graph, service starts and stops are scheduled, and preferred dependencies are selected
-
Because we know the topology of the whole application, we can change services effectively at run-time
At its current state, Idle will work mostly out of the box when everything works as expected. The code-base currently lacks proper error/exception handling which will be reworked in a future iteration. For instance, throwing an exception from an idle::Service::onStart
method currently leads to an application shutdown, but will properly be handled in the future.
The first step for troubleshooting is to enable the internal logs that are separated from the logger of the ecosystem:
- If you are building in
Release
mode make sure that the CMake option isIDLE_WITH_INTERNAL_LOG=ON
is set. InDebug
builds Idle is always compiled with internal logging support built-in. - Enable the internal Idle logs by defining the
IDLE_INTERNAL_LOGLEVEL
environment variable (system-wide or per invocation):IDLE_INTERNAL_LOGLEVEL=0 ./idle
: Runs Idle with trace logs (most detailed but might print sensitive information)IDLE_INTERNAL_LOGLEVEL=1 ./idle
: Runs Idle with debug logs (prints less than trace)IDLE_INTERNAL_LOGLEVEL=2 ./idle
: Runs Idle with error logs (non-fatal errors which need to be handled properly)
The entire graph of your current Idle system can be exported through GraphViz to many visual formats (pdf, SVG) for explanation purposes, to help debugging issues with resolved dependencies, and to analyze the overall system. This requires that the GraphViz dot
program can be found through your systems PATH
environment variable.
A graph (in various granularities) can be generated with the idle graph show [service | cluster]
command, which can be invoked from the Idle CLI directly.
If you are reporting an issue make sure to attach the graph of your current system as a pdf, an internal trace log as well as any available stack trace logs to your bug report.
If you are running into issues with rebuilds, try to delete the build directory first. In its default configuration, the build directory is created in idle/project/build
. Idle detects a missing build directory on start, and will automatically re-create it for you.
- Is Idle related to an entity-component framework (ECS)?
- No, services in Idle are meant to provide logical functionality and heavyweight data. Services require a unique position in memory and are mostly dynamically or statically allocated. They are not cache-optimized nor recommended for representing an entity where you expect to have thousands of instances alive or in iteration simultaneously. We recommend using an ECS from inside a service instead of making a service part of an ECS.
- Does Idle hot-swap require a specific platform, compiler flag, or toolchain?
- No, theoretically Idle hot-swap can be used on every platform that supports dynamic runtime linking. Because dependencies are tracked and swapped on a per-service level, this will even work on heavily optimized release builds and can be used theoretically in production as well.
- Is it possible to ship my application with static instead of dynamic linkage?
- You can statically link your plugins fully into your idle application without any change in your source code. Idle can even read your
IDLE_DECLARE
definitions from your statical linked executable and instantiate the exported services as they were exported from a plugin. A recommended workflow is to develop your application by using dynamic linking and automatic hot-swapping for a short code iteration while distributing it with static linking and no hot-swap. Through that, static lifetime differences between platforms and build types will belong to the past.
- You can statically link your plugins fully into your idle application without any change in your source code. Idle can even read your
- Is it possible to disable RTTI?
- Idle uses RTTI for improved naming of your services only, if available, and can be used without RTTI. Naming support through the
IDLE_SERVICE
,IDLE_INTERFACE
, andIDLE_PART
macros are implemented without RTTI through constexpr type reflection (__PRETTY_FUNCTION__
parsing).
- Idle uses RTTI for improved naming of your services only, if available, and can be used without RTTI. Naming support through the
- Why is this software licensed under AGPL v3? Is it planned to relicense it?
- Idle is licensed under the AGPL until the project has grown in maturity and we can narrow the project's direction. Different parts of the project might be relicensed/sublicensed to a more permissive license to make it available to a broader audience. I'm currently looking into possibilities to fund this project properly, and therefore licensing it now for open commercial use, would limit my options.