Compiling is done through the help of CMake. Ichor requires at least gcc 12, clang 15 or MSVC 2022, and is tested with gcc 12.4, clang 15, clang 18 and MSVC 2022.
The easiest is to build it with the provided Dockerfile:
docker build -f Dockerfile -t ichor . && docker run -v $(pwd):/opt/ichor/src -it ichor # for a release build
docker build -f Dockerfile-musl -t ichor-musl . && docker run -v $(pwd):/opt/ichor/src -it ichor-musl # for a release + musl build
docker build -f Dockerfile-asan -t ichor-asan . && docker run -v $(pwd):/opt/ichor/src -it ichor-asan # for a debug build with sanitizersThe binaries will then be in $(pwd)/bin. The musl build statically compiles Ichor and should be able to run on all platforms, otherwise the binaries need at least glibc 2.35 on the system that's running them.
To cross-compile, install the latest qemu-user-static package for your distribution and binfmt_support. Then reboot and you should have qemu binaries doing emulation. With that, use one of the dockerfiles as an example to create your own builds.
sudo apt install libisl-dev libmpfrc++-dev libmpc-dev libgmp-dev build-essential cmake g++
wget http://mirror.koddos.net/gcc/releases/gcc-12.4.0/gcc-12.4.0.tar.xz
tar xf gcc-12.4.0.tar.xz
mkdir gcc-build
cd gcc-build
../gcc-12.4.0/configure --prefix=/opt/gcc-12.4 --enable-languages=c,c++ --disable-multilib --with-system-zlib --enable-default-pie --enable-default-ssp --enable-host-pie
make -j$(nproc)
sudo make install
Then with cmake, use
CXX=/opt/gcc-12.4.0/bin/g++ cmake $PATH_TO_ICHOR_SOURCE
sudo apt install g++ build-essential cmake
Some features are behind feature flags and have their own dependencies.
If using the Boost.BEAST (recommended boost 1.70 or newer):
Ubuntu 20.04:
sudo apt install libboost1.71-all-dev libssl-dev
Ubuntu 22.04:
sudo apt install libboost1.74-all-dev libssl-dev
Install MSVC 17.4 or newer. Open Ichor in MSVC and configure CMake according to your wishes. Build and install and you should find an out directory in Ichor's top level directory.
Use that directory in your personal project, preferably with cmake as Ichor exports compile-time definitions in it.
If boost is desired, please download the windows prebuilt packages (boost_1_81_0-msvc-14.3-64.exe is the latest at time of writing).
Then add the following system variables, with the path you've extracted boost into:
BOOST_INCLUDEDIR C:\SDKs\boost_1_81_0\
BOOST_LIBRARYDIR C:\SDKs\boost_1_81_0\lib64-msvc-14.3
BOOST_ROOT C:\SDKs\boost_1_81_0\boost
To run the examples/tests that use boost, copy the dlls in C:\SDKs\boost_1_81_0\lib64-msvc-14.3 (or where you installed boost) into the generated bin folder.
Something similar goes for openssl. Download the latest prebuilt binaries here and unpack it into C:\SDKs\openssl_3.1.3 so that C:\SDKs\openssl_3.1.3´\include exists, skipping a few subdirectories. Then add the following environment variables:
OPENSSL_INCLUDE_DIR C:\SDKs\openssl_3.1.3\include
OPENSSL_LIBRARYDIR C:\SDKs\openssl_3.1.3\lib
OPENSSL_ROOT C:\SDKs\openssl_3.1.3
Don't forget to copy C:\SDKs\openssl_3.1.3\bin\*.dll to the Ichor bin directory after compiling Ichor.
Work in progress, initial support available, sanitizers with boost seem to get false positives.
brew install llvm
brew install ninja
brew install boost
brew install cmakeThen with cmake, set the CC and CXX variables explicitly:
CC=$(brew --prefix llvm)/bin/clang CXX=$(brew --prefix llvm)/bin/clang++ cmake -GNinja -DICHOR_USE_SANITIZERS=OFF -DICHOR_USE_BOOST_BEAST=ON ..
ninjaTo use Ichor, compile and install it in a location that cmake can find (e.g. /usr) and use the following CMakeLists.txt:
cmake_minimum_required(VERSION 3.12)
project(my_project)
set(CMAKE_CXX_STANDARD 20)
find_package(Ichor CONFIG REQUIRED)
add_executable(my_exe main.cpp)
target_link_libraries(my_exe Ichor::ichor)Starting a dependency manager is quite easy. Instantiate it, tell it which services to register and start it.
// main.cpp
#include <ichor/DependencyManager.h>
#include <ichor/event_queues/PriorityQueue.h>
int main() {
auto queue = std::make_unique<PriorityQueue>(); // use a priority queue based on std::priority_queue
auto &dm = queue->createManager(); // create the dependency manager
dm.start(CaptureSigInt);
return 0;
}Just starting a manager without any custom services is not very interesting. So let's start populating it!
// SomeDependency.h
struct ISomeDependency {
virtual void hello_world() = 0;
};
struct SomeDependency final : public ISomeDependency {
SomeDependency() = default;
void hello_world() final;
}; // a minimal implementation// SomeDependency.cpp
#include <fmt/format.h>
#include "SomeDependency.h"
void SomeDependency::hello_world() {
fmt::print("Hello, world!\n");
}// MyService.h
#include "SomeDependency.h"
struct MyService final { // Don't need an interface here, nothing has a dependency on MyService
MyService(ScopedServiceProxy<ISomeDependency> dependency) {
dependency->hello_world();
}
}; // a minimal implementation// main.cpp
#include <ichor/DependencyManager.h>
#include <ichor/event_queues/PriorityQueue.h>
#include "SomeDependency.h"
#include "MyService.h"
int main() {
auto queue = std::make_unique<PriorityQueue>();
auto &dm = queue->createManager();
dm.createServiceManager<SomeDependency, ISomeDependency>(); // register SomeDependency as providing an ISomeDependency
dm.createServiceManager<MyService>(); // register MyService (requested dependencies get registered automatically)
dm.start(CaptureSigInt);
return 0;
}For more information on Dependency Injection, please see the relevant doc.
In general, the arguments in a constructor are reflected upon on compile-time and are all considered to be requests. That means that there are no custom arguments possible. e.g.
struct CompileErrorService final {
CompileErrorService(int i) { // `int` is not a struct/class nor is `i` a pointer.
}
};The following, however, is completely fine:
struct MyService final {
MyService(ScopedServiceProxy<IService1>, ScopedServiceProxy<IService2>, ScopedServiceProxy<IService3>, ScopedServiceProxy<IService4> /* and so on */) {
}
};The limit is compiler dependent, but it is >100 as far as the author is aware on all compilers.
There are a couple of special dependency requests you can make in a service:
struct MyService final {
MyService(IEventQueue *, DependencyManager *, IService *) {
}
};IEventQueue is always the underlying queue registered with the DependencyManager, providing a way to manipulate the event loop
DependencyManager is self-explanatory
IService is the underlying service created by the DependencyManager when the instance got created. This contains things like the service Id and service Properties.
// EventManipulationService.h
#include <ichor/event_queues/IEventQueue.h>
#include <ichor/dependency_management/IService.h>
#include <fmt/format.h>
struct EventManipulationService final { // Don't need an interface here, nothing has a dependency on EventManipulationService
MyService(IEventQueue *q, IService *self) {
// push an event on the queue, to be executed later.
// Events use the service id of the originating service to identify where it came from.
q->pushEvent<RunFunctionEvent>(self->getServiceId(), [&]() {
fmt::print("Hello, world!\n");
q->pushEvent<QuitEvent>(self->getServiceId()); // this event, when run, tells the Dependency Manager to stop, releasing the thread.
});
}
}; // a minimal implementation// main.cpp
#include <ichor/DependencyManager.h>
#include <ichor/event_queues/PriorityQueue.h>
#include "EventManipulationService.h"
int main() {
auto queue = std::make_unique<PriorityQueue>();
auto &dm = queue->createManager();
dm.createServiceManager<EventManipulationService>();
dm.start(CaptureSigInt);
fmt::print("This runs after the QuitEvent has successfully stopped the manager\n");
return 0;
}Before we get to the point where we are able to start and stop the program, let's showcase one more service:
// MyTimerService.h
#include <ichor/services/timer/ITimerFactory.h>
struct MyTimerService final {
MyTimerService(ScopedServiceProxy<ITimerFactory> factory) {
auto timer = factory->createTimer();
timer.setChronoInterval(std::chrono::seconds(1));
timer.setCallback([this]() {
fmt::print("Timer callback\n"); // this line gets executed once every second
});
timer.startTimer();
}
};// main.cpp
#include <ichor/DependencyManager.h>
#include <ichor/event_queues/PriorityQueue.h>
#include <ichor/services/timer/TimerFactoryFactory.h> // Add this
#include "MyService.h"
#include "MyDependencyService.h"
#include "MyTimerService.h" // Add this
int main() {
auto queue = std::make_unique<PriorityQueue>();
auto &dm = queue->createManager();
dm.createServiceManager<MyService, IMyService>();
dm.createServiceManager<MyDependencyService, IMyDependencyService>();
dm.createServiceManager<MyTimerService>(); // Add this
dm.createServiceManager<TimerFactoryFactory>(); // Add this
dm.start();
return 0;
}The newly added TimerFactoryFactory listens for any services requesting a ITimerFactory and creates one on-the-fly. Timers impersonate the requesting service when inserting events into the queue and therefore need the underlying service id of the requesting service. The FactoryFactory seemlessly solves this without the requesting service ever knowing.
The flipside is that if the TimerFactoryFactory is not instantiated, the MyTimerService never starts, as its dependency never gets created.
Now that we have a timer, we've got everything necessary to setup and use coroutines, a fancy new c++20 feature.
// AwaitService.h
#include <ichor/coroutines/AsyncManualResetEvent.h>
#include <ichor/coroutines/Task.h>
#include <ichor/event_queues/IEventQueue.h>
#include <thread>
using namespace std::chrono_literals;
struct IAwaitService {
virtual Ichor::Task<int> WaitOneSecond() = 0;
};
struct AwaitService final : public IAwaitService {
AwaitService(IEventQueue *queue) : _queue(queue) {
}
Ichor::Task<int> WaitOneSecond() final {
Ichor::AsyncManualResetEvent evt{}; // storage for calling coroutine frame
std::thread t([&]() {
std::this_thread::sleep_for(1s);
// If we want coroutines waiting on this function to resume on the same thread,
// we have to call the `set` function from the queue, not our current thread.
_queue->pushEvent<RunFunctionEvent>(ServiceIdType{0}, [&]() {
evt.set(); // resume waiting coroutines
});
});
co_await evt; // pause execution until evt.set() is called
co_return 5; // return 5 when we're done waiting
}
IEventQueue *_queue{};
};// MyCoroutineTimerService.h
#include <ichor/services/timer/ITimerFactory.h>
#include "AwaitService.h"
struct MyCoroutineTimerService final {
MyCoroutineTimerService(ScopedServiceProxy<ITimerFactory> factory, ScopedServiceProxy<IAwaitService> awaitService) {
auto timer = factory->createTimer();
timer.setChronoInterval(std::chrono::seconds(1));
timer.setCallbackAsync([awaitService]() {
fmt::print("Timer callback\n");
uint64_t i = co_await awaitService->waitOneSecond(); // the callback goes into waiting here, but other events can still be processed
fmt::print("Waiting finished, ret {}\n", i);
co_return {};
});
timer.startTimer();
auto timer2 = factory->createTimer();
timer2.setChronoInterval(std::chrono::milliseconds(500));
timer2.setCallbackAsync([]() {
fmt::print("Timer2 callback\n"); // this will print a couple times until
co_return {};
});
timer2.startTimer();
}
};// main.cpp
#include <ichor/DependencyManager.h>
#include <ichor/event_queues/PriorityQueue.h>
#include <ichor/services/timer/TimerFactoryFactory.h>
#include "AwaitService.h"
#include "MyCoroutineTimerService.h"
int main() {
auto queue = std::make_unique<PriorityQueue>();
auto &dm = queue->createManager();
dm.createServiceManager<AwaitService, IAwaitService>();
dm.createServiceManager<MyCoroutineTimerService>();
dm.createServiceManager<TimerFactoryFactory>();
dm.start();
return 0;
}At some point in your program, the only thing left to do is tell Ichor to stop. This can easily be done by pushing a QuitEvent, like so:
// MyQuittingTimerService.h
#include <ichor/services/timer/ITimerFactory.h>
#include <ichor/event_queues/IEventQueue.h>
struct MyQuittingTimerService final {
MyQuittingTimerService(ScopedServiceProxy<ITimerFactory> factory, IEventQueue *queue) {
auto timer = factory->createTimer();
timer.setChronoInterval(std::chrono::seconds(1));
timer.setCallback([queue]() {
queue->pushEvent<QuitEvent>(ServiceIdType{0});
});
timer.startTimer();
}
};And there you have it, the basic building blocks of Ichor!
There are a couple more advanced features that Ichor has:
The biggest reason that Ichor is a runtime dependency manager rather than compile time, unlike say Boost.DI, is to have the option to decide which services to create, at runtime.
The number one use case here is to create a per-service-instance specific logger. Let's try to create a smaller LoggerFactory than is included in Ichor:
// includes and so on omitted for brevity
struct ILogger {
virtual void Log(std::string_view msg) = 0;
};
struct Logger final : public ILogger {
void Log(std::string_view msg) final {
fmt::print("{}\n", msg);
}
};
struct LoggerFactory final {
LoggerFactory(DependencyManager *dm, IService *self) : _dm(dm) {
_loggerTrackerRegistration = _dm->registerDependencyTracker<ILogger>(this, self);
}
AsyncGenerator<IchorBehaviour> handleDependencyRequest(v1::AlwaysNull<ILogger*>, DependencyRequestEvent const &evt) {
auto logger = _loggers.find(evt.originatingService);
if (logger != end(_loggers)) {
co_return {}; // already created a logger for this service!
}
Properties props{};
// Filter is a special property in Ichor, if this is detected, it gets checked before asking another service if they're interested in it.
// In this case, we apply a filter specifically so that the requesting service id is the only one that will match.
props.template emplace<>("Filter", Ichor::v1::make_any<Filter>(ServiceIdFilterEntry{evt.originatingService}));
auto *newLogger = _dm->createServiceManager<Logger, ILogger>(std::move(props));
_loggers.emplace(evt.originatingService, newLogger->getServiceId());
co_return {};
}
AsyncGenerator<IchorBehaviour> handleDependencyUndoRequest(v1::AlwaysNull<ILogger*>, DependencyUndoRequestEvent const &evt) {
auto service = _loggers.find(evt.originatingService);
if(service != end(_loggers)) {
// the true at the end tells Ichor to remove the service immediately after being stopped.
_dm->pushPrioritisedEvent<StopServiceEvent>(AdvancedService<LoggerFactory<LogT>>::getServiceId(), INTERNAL_DEPENDENCY_EVENT_PRIORITY, service->second, true);
_loggers.erase(service);
}
co_return {};
}
DependencyTrackerRegistration _loggerTrackerRegistration{};
unordered_map<ServiceIdType, ServiceIdType, ServiceIdHash> _loggers;
DependencyManager *_dm;
};
struct SomeServiceUsingLogger final {
SomeServiceUsingLogger(ScopedServiceProxy<ILogger> logger) {
logger->Log("Logged!");
}
};
int main() {
auto queue = std::make_unique<PriorityQueue>();
auto &dm = queue->createManager();
dm.createServiceManager<LoggerFactory>(); // LoggerFactory will end up resolving the ILogger request from SomeServiceUsingLogger
dm.createServiceManager<SomeServiceUsingLogger>();
queue->start(CaptureSigInt);
return 0;
}It is possible to intercept all events before they're handled by registered services (or Ichor itself!). An example:
// MyInterceptorService.h
#include <ichor/DependencyManager.h>
#include <ichor/events/Events.h>
#include <ichor/events/RunFunctionEvent>
struct MyInterceptorService final {
MyInterceptorService(DependencyManager *dm, IService *self) {
_interceptor = dm->registerEventInterceptor<Ichor::RunFunctionEvent>(this, self); // Can change RunFunctionEvent to just Event if you want to intercept *all* events
}
bool preInterceptEvent(Ichor::RunFunctionEvent const &) {
return AllowOthersHandling; //PreventOthersHandling if this event should be discarded
}
void postInterceptEvent(Ichor::RunFunctionEvent const &, bool processed) {
// Can use this to track how long the processing took
}
Ichor::EventInterceptorRegistration _interceptor{};
};Events are easy to add, they need a constexpr TYPE and NAME and some fields as required by the constructor of Event. For the rest you're free to add any fields you like (though your event needs to be creatable by std::unique_ptr).
Your events can then be inserted, intercepted or handled as you would e.g. a QuitEvent.
struct MyEvent final : public Event {
constexpr MyEvent(uint64_t _id, ServiceIdType _originatingService, uint64_t _priority, uint64_t _someData) noexcept : Event(_id, _originatingService, _priority), someData(_someData) {}
constexpr ~MyEvent() final = default;
[[nodiscard]] ICHOR_CONST_FUNC_ATTR constexpr std::string_view get_name() const noexcept final {
return NAME;
}
[[nodiscard]] ICHOR_CONST_FUNC_ATTR constexpr NameHashType get_type() const noexcept final {
return TYPE;
}
uint64_t someData;
static constexpr NameHashType TYPE = typeNameHash<MyEvent>();
static constexpr std::string_view NAME = typeName<MyEvent>();
};
// inserting them is easy
dm->pushEvent<MyEvent>(0, 5); //creates a MyEvent with a unique id, 0 as the originating service, standard priority and 5 as someData.
struct MyHandleService final {
MyHandleService(DependencyManager *dm, IService *self) {
_handler = dm->registerEventHandler<MyEvent>(this, self); // Can change RunFunctionEvent to just Event if you want to intercept *all* events
}
AsyncGenerator<IchorBehaviour> handleEvent(MyEvent const &e) {
fmt::print("Handling MyEvent {}\n", e.someData); // prints 5, if using the insertion above.
return AllowOthersHandling; //PreventOthersHandling if this event should not be handled by other event handlers
}
Ichor::EventHandlerRegistration _handler{};
};Ichor gives every event a default priority, but if necessary, you can push events with a higher priority so that they get processed before others. By default, the priority given to events is 1000, where lower numbers are higher priority. The lowest priority given is std::numeric_limits<uint64_t>::max().
Pushing an event with a priority is done with the pushPrioritisedEvent function:
getManager().pushPrioritisedEvent<TimerEvent>(getServiceId(), 10u);The default priority for events is 1000. For dependency related things (like start service, dependency online events) it is 100.
Ichor used to provide std::pmr::memory_resource based allocation, however that had a big impact on the ergonomy of the code. Moreover, clang 15 does not support <memory_resource> at all. Instead, Ichor recommends using mimalloc to reduce the resource contention when using multiple threads.
All the effort into thread-safety would be for naught if it weren't possible to use Ichor with multiple threads.
Starting up two manager is easy, but allowing services to communicate to services in another thread requires some setup:
// main.cpp
#include <ichor/DependencyManager.h>
#include <ichor/event_queues/PriorityQueue.h>
#include <ichor/CommunicationChannel.h>
int main() {
Ichor::CommunicationChannel channel{};
auto queueOne = std::make_unique<PriorityQueue>();
auto &dmOne = queue->createManager(); // ID = 0
auto queueTwo = std::make_unique<PriorityQueue>();
auto &dmTwo = queue->createManager(); // ID = 1
// Register the manager to the channel
channel.addManager(&dmOne);
channel.addManager(&dmTwo);
std::thread t1([&dmOne] {
// your services here
dmOne.start();
});
std::thread t2([&dmTwo] {
// your services here
dmTwo.start();
});
t1.join();
t2.join();
return 0;
}This then allows services to send events to other manager:
// MyCommunicatingService.h
#include <ichor/DependencyManager.h>
#include <ichor/Events.h>
struct MyCommunicatingService final : public Ichor::Service<MyCommunicatingService> {
Ichor::AsyncGenerator<tl::expected<void, Ichor::StartError>> start() final {
getManager().getCommunicationChannel()->broadcastEvent<QuitEvent>(getManager(), getServiceId()); // sends to all other managers, except the one this service is registered to
co_return {};
}
};The communication channel also has a sendEventTo function, which allows sending to a specific manager. Manager IDs are deterministic, the ID starts at 0 and increments by one for every created manager. See the comments above for main.cpp for an example.