C++17 Dependency Injection header-only library
- Supports singleton1, transient2, shared3 and scoped4 services.
- Supports registering multiple services of the same type or multiple implementations of the same interface and resolving them all at once.
- Threadsafe (can be disabled if your program is single-threaded).
- Has runtime circular dependency checks.
- Works on Windows and Linux. Other platforms support is untested.
- You don't have to change your services in order to use them with solinject (except that they should accept and use their dependencies as
std::shared_ptr<>
). - Your services don't have to know about their dependencies' lifetime.
- All configuration is tied to the container.
- Container is mutable by default, but it will be immutable if you use it by a reference to const or by a pointer to const.
- Container can be configured in code or by a config file
#define SOLINJECT_NOTHREADSAFE // If your program is single-threaded
#include <solinject.hpp>
#include <solinject-macros.hpp>
solinject.hpp
contains the sol::di::Container
class, and solinject-macros.hpp
provides you some handy macros for registering services.
sol::di::Container container;
RegisterSingletonService(container, MyServiceClass/*, constructor params go here */);
// or
RegisterSingletonInterface(container, IMyServiceInterface, MyServiceClass/*, constructor params go here */);
If your service has constructor parameters, that should be injected from the container, use the FROM_DI()
macro, the FROM_DI_OPTIONAL()
macro or the FROM_DI_MULTIPLE()
macro:
RegisterSingletonService(container, MyServiceClass, FROM_DI(MyOtherServiceClass));
// or
RegisterSingletonInterface(container, IMyServiceInterface, MyServiceClass, FROM_DI(MyOtherServiceClass));
The FROM_DI()
and FROM_DI_OPTIONAL()
macros inject a single instance of the service as std::shared_ptr<T>
, while the FROM_DI_MULTIPLE()
macro injects multiple instances of the service or multiple implementations of the interface as std::vector<std::shared_ptr<T>>
.
The FROM_DI_OPTIONAL()
macro should be used when the injected service is optional.
Warning Optional here means that the service may or may not be registered and it doesn't mean that the service may be registered with
nullptr
or a factory function that returnsnullptr
.
If you have a std::shared_ptr<>
or a std::unique_ptr<>
to an instance of the service, you can register it as a singleton:
auto instance1 = std::make_shared<MyServiceClass>();
auto instance2 = std::make_unique<MyOtherServiceClass>();
container.template RegisterSingletonService<MyServiceClass>(instance1);
container.template RegisterSingletonService<MyOtherServiceClass>(std::move(instance2));
If for some reason you want to go the hard way, you can register the service directly using a lambda expression:
container.template RegisterSingletonService<MyServiceClass>([](const auto& container)
{
return std::make_shared<MyServiceClass>(
container.template GetRequiredService<MyOtherServiceClass>()
);
});
// or
container.template RegisterSingletonService<IMyServiceInterface>([](const auto& container)
{
return std::make_shared<MyServiceClass>(
container.template GetRequiredService<MyOtherServiceClass>()
);
});
// For a single instance:
std::shared_ptr<MyServiceClass> myService = container.template GetRequiredService<MyServiceClass>();
// or
std::shared_ptr<IMyServiceInterface> myService = container.template GetRequiredService<IMyServiceInterface>();
// For multiple instances or implementations:
std::vector<std::shared_ptr<MyServiceClass>> myServices = container.template GetServices<MyServiceClass>();
// or
std::vector<std::shared_ptr<IMyServiceInterface>> myServices = container.template GetServices<IMyServiceInterface>();
The GetRequiredService<>()
method will throw sol::di::exc::ServiceNotRegisteredException
if the requested service is not registered. If you prefer to get an empty std::shared_ptr<>
in such cases, use the GetService<>()
method.
If you want to use scoped services, then create a scope:
sol::di::Container scope = container.CreateScope();
and resolve your services from this scope just like you would from a container:
std::shared_ptr<MyServiceClass> myService1 = scope.template GetRequiredService<MyServiceClass>();
std::shared_ptr<IMyServiceInterface> myService2 = scope.template GetRequiredService<IMyServiceInterface>();
std::vector<std::shared_ptr<MyServiceClass>> myServices1 = scope.template GetServices<MyServiceClass>();
std::vector<std::shared_ptr<IMyServiceInterface>> myServices2 = scope.template GetServices<IMyServiceInterface>();
A scope container can do everything a regular sol::di::Container
can do. You can register services to it (even scoped services), resolve services from it etc. You can even create a scope from a scope, and then create a scope from that scope and so on.
If you want to use config files for configuring services, then registration looks a bit different:
// Create a builder
sol::di::ContainerBuilder builder;
// Register services
builder.RegisterService<MyServiceClass>(
"MyServiceClass",
FACTORY(MyServiceClass/*, constructor params go here*/)
);
builder.RegisterInterface<MyServiceInterface>("MyServiceInterface");
builder.RegisterService<MyServiceImpl, MyServiceInterface>(
"MyServiceImpl",
FACTORY(MyServiceImpl/*, constructor params go here*/)
);
builder.RegisterInterface<MyInterface>("MyInterface");
builder.RegisterService<MyImpl, MyInterface>(
"MyImpl",
FACTORY(MyImpl/*, constructor params go here*/)
);
builder.RegisterService<MyOtherImpl, MyInterface>(
"MyOtherImpl",
FACTORY(MyOtherImpl/*, constructor params go here*/)
);
// Open a config file
std::ifstream configFile("MyConfigFile.txt", std::ios::in | std::ios::binary);
// Read configuration from the file
sol::di::Configuration config;
configFile >> config;
// Build container
sol::di::Container container = builder.BuildContainer(config);
// Then resolve your services as usual
Config file syntax example:
# One-line comment
# Register MyServiceClass as self
# with singleton lifetime
MyServiceClass Self Singleton
# Register MyServiceImpl class as MyServiceInterface
# with transient lifetime
MyServiceInterface MyServiceImpl Transient
# Register MyImpl and MyOtherImpl
# as implementations of MyInterface
# with singleton and shared lifetimes
MyInterface {
MyImpl Singleton
MyOtherImpl Shared
}
There are a few ways to link solinject to your project. Here's an incomplete list of them (the most recommended first):
Add these lines to your top-level CMakeLists.txt
, replacing v1.0.0
with your preferred version:
include(FetchContent)
FetchContent_Declare(
solinject
GIT_REPOSITORY https://github.com/SemperSolus0x3d/solinject
GIT_TAG v1.0.0
)
FetchContent_MakeAvailable(solinject)
And then link the sol::solinject
target to your project's target:
target_link_libraries(YOUR_TARGET sol::solinject)
In your project folder run:
mkdir extern
git submodule add "https://github.com/SemperSolus0x3d/solinject" extern/solinject
cd extern/solinject
git checkout v1.0.0
cd ../..
Then in your top-level CMakeLists.txt
add this submodule as subdirectory and link sol::solinject
to your project's target:
add_subdirectory(extern/solinject)
target_link_libraries(YOUR_TARGET sol::solinject)
Build or download the latest release, install it to your CMake packages installation prefix and link it to your project's target:
find_package(solinject VERSION 1.0.0 REQUIRED)
target_link_libraries(YOUR_TARGET sol::solinject)
Download the latest release source code, extract it to your project's directory for dependencies (in my example it's lib
) and add it to your project as subdirectory:
add_subdirectory(lib/solinject)
target_link_libraries(YOUR_TARGET sol::solinject)
solinject is a header-only library, so you can just copy everything from this project's include
directory and paste it into your project's include
directory. Make sure to add the folder containing solinject.hpp
and solinject-macros.hpp
to your compiler include directories.
solinject is a header-only library, so building it is needed only to build tests and docs and install a cmake package, which then can be linked with find_package()
.
Clone the repository and cd
into it:
git clone --recurse-submodules https://github.com/SemperSolus0x3d/solinject/
cd solinject
At this point you can use the BuildForAllPlatformsVS.py
script for the Visual Studio generator:
scripts/BuildForAllPlatformsVS.py
or BuildForAllPlatformsUnixMakefiles.py
for the Unix Makefiles generator:
scripts/BuildForAllPlatformsUnixMakefiles.py
or just build the project manually for every platform you need:
cmake -S . -B build -DBUILD_TESTING=0
cmake --build build
cmake --install build --prefix install
The way how you should tell cmake the platform you want depends on the generator you are using.
SPDX-License-Identifier: LGPL-3.0-or-later
Copyright (C) 2022 SemperSolus0x3d
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
Footnotes
-
A singleton service is created once and used everywhere. ↩
-
A transient service is created each time it is requested. ↩
-
A shared service exists while it is used. When the shared service is requested, if an instance already exists, that instance is returned; otherwise a new instance is created. ↩
-
A scoped service behaves like a singleton inside its scope and derived scopes. ↩