ASIO Service Registration and Dependency Injection: A Complete Guide
ASIO's execution_context provides a built-in service registry that enables dependency injection through use_service, add_service, and has_service APIs, allowing modular components to be registered once and retrieved on-demand across asynchronous operations.
The chriskohlhoff/asio library implements a sophisticated service registration and dependency injection system centered around the execution_context class. This architecture allows developers to register custom services that can be shared across strands, timers, and user-defined components without explicit constructor wiring. Understanding ASIO service registration and dependency injection is essential for building extensible asynchronous applications that leverage the library's internal resource management patterns.
Core Architecture Components
ASIO's dependency injection mechanism relies on three primary classes that work together to provide type-safe service management.
execution_context and the Service Base Class
The asio::execution_context class defined in include/asio/execution_context.hpp serves as the foundation for all execution contexts including io_context. This class provides the service abstract base class that all services must extend:
class my_service : public asio::execution_context::service {
public:
using key_type = my_service; // Unique identifier for the registry
// ...
};
Every service must declare a key_type that the registry uses to identify the service type. The base class provides lifecycle hooks including shutdown and notify_fork for proper resource management during context destruction or process forking.
service_registry and service_maker
The asio::detail::service_registry class in include/asio/detail/service_registry.hpp maintains a type-safe map of service keys to instances. During context construction, an optional service_maker functor (defined in include/asio/execution_context.hpp) can pre-populate the registry with default services. The registry ensures that each service type exists as a singleton per execution context, creating services on-demand using the service maker pattern or default construction.
Service Registration Flow
The lifecycle of service registration follows three distinct phases:
-
Construction – When an
execution_context(or derivedio_context) is constructed, it may receive a service maker that pre-creates a set of services. The registry is initially empty if no service maker is provided. -
On-Demand Creation – The first call to
use_service<MyService>(ctx)triggers the registry to look up the service's key. If the key is missing, the registry constructs the service via its default constructor and stores the instance. -
Dependency Injection – User code obtains a reference to the service simply by calling
use_service. The returned reference can be stored, passed to other objects, or used directly, achieving constructor-less dependency injection.
Service Registration APIs
ASIO provides three primary templates for service interaction that form the dependency injection interface.
use_service Template Function
The asio::use_service<Service>(context) function defined in include/asio/execution_context.hpp implements lazy initialization. When called, the function queries the internal service_registry for the service key, constructs the service using its constructor if not present, and returns a reference to the existing or newly created instance:
asio::io_context ioc;
auto& service = asio::use_service<custom_logger>(ioc);
This pattern eliminates the need for factories or dependency injection containers, allowing components to request services directly from the execution context.
add_service and has_service
For explicit service registration, asio::add_service<Service>(context, service_ptr) in include/asio/execution_context.hpp inserts a pre-constructed service into the registry. This function throws asio::service_already_exists if the service type is already registered:
auto* logger = new logger_service(ioc);
asio::add_service(ioc, logger);
The asio::has_service<Service>(context) function provides a safe check to determine if a service exists before attempting retrieval or addition, preventing exceptions in conditional initialization logic.
Practical Implementation Examples
Creating a Custom Logger Service
The following implementation demonstrates a complete custom service based on the reference in src/examples/cpp11/services/logger_service.hpp. This service inherits from execution_context::service and implements the required interface:
// logger_service.hpp
#include <asio.hpp>
#include <fstream>
#include <thread>
#include <sstream>
struct logger_impl {
explicit logger_impl(const std::string& id) : identifier(id) {}
std::string identifier;
};
class logger_service : public asio::execution_context::service {
public:
using key_type = logger_service;
using impl_type = logger_impl*;
explicit logger_service(asio::execution_context& ctx)
: asio::execution_context::service(ctx),
work_(asio::make_work_guard(work_io_context_)),
work_thread_([this]{ work_io_context_.run(); }) {}
impl_type null() const { return nullptr; }
void create(impl_type& impl, const std::string& id) {
impl = new logger_impl(id);
}
void destroy(impl_type& impl) {
delete impl;
impl = null();
}
void log(impl_type& impl, const std::string& msg) {
std::ostringstream os;
os << impl->identifier << ": " << msg;
asio::post(work_io_context_, std::bind(&logger_service::log_impl, this, os.str()));
}
private:
void log_impl(const std::string& text) {
ofstream_ << text << std::endl;
}
asio::io_context work_io_context_;
asio::executor_work_guard<asio::io_context::executor_type> work_;
std::thread work_thread_;
std::ofstream ofstream_;
};
This service encapsulates its own io_context for asynchronous logging operations, demonstrating how services can own their own execution resources while being managed by the parent context.
Accessing Services via use_service
Client code retrieves the logger service without explicit construction, achieving dependency injection:
#include <asio.hpp>
#include "logger_service.hpp"
int main() {
asio::io_context ioc;
// Service is created automatically on first call
logger_service& logger = asio::use_service<logger_service>(ioc);
logger_service::impl_type log_impl;
logger.create(log_impl, "application");
logger.log(log_impl, "Service registration complete");
logger.destroy(log_impl);
return 0;
}
The asio::use_service<logger_service>(ioc) call transparently handles service instantiation, returning a reference that can be stored or passed to other components.
Explicit Service Registration with add_service
For scenarios requiring custom initialization parameters, use add_service:
int main() {
asio::io_context ioc;
// Manual construction with custom parameters
logger_service* custom_logger = new logger_service(ioc);
// Explicit registration
asio::add_service(ioc, custom_logger);
// Verify presence
if (asio::has_service<logger_service>(ioc)) {
logger_service& ref = asio::use_service<logger_service>(ioc);
// ref points to *custom_logger
}
}
This pattern is essential when services require constructor arguments that cannot be provided by the default service creation mechanism.
Key Source Files in chriskohlhoff/asio
include/asio/execution_context.hpp: Definesexecution_context, theservicebase class, and the public API templatesuse_service,add_service, andhas_service.include/asio/detail/service_registry.hpp: Implements the internal type-safe registry that maps service keys to concrete instances.src/examples/cpp11/services/logger_service.hpp: Reference implementation demonstrating custom service patterns including asynchronous operation handling.include/asio/io_context.hpp: Concrete execution context that inherits service registration capabilities.include/asio/strand.hpp: Shows how built-in services likestrand_executor_serviceutilize the registration system.
Summary
- ASIO service registration centers on
execution_context, which owns aservice_registryfor managing service lifetimes and singleton instances per context. - The
use_servicetemplate provides lazy initialization, automatically creating services on first access and returning references that enable dependency injection. add_serviceenables explicit registration with pre-constructed objects, whilehas_serviceallows safe existence checks to avoid duplicate registration exceptions.- Custom services must inherit from
execution_context::serviceand declare akey_typefor registry identification. - This architecture underpins ASIO's built-in components including strands, timers, and thread pools, providing a consistent pattern for extensible asynchronous design.
Frequently Asked Questions
What is the purpose of the key_type declaration in ASIO services?
The key_type serves as a unique identifier for the service type within the registry. When you call use_service<MyService>, the registry uses MyService::key_type to look up the instance. This design allows the same service implementation to potentially operate with different keys, though typically key_type is typedef'd to the service class itself. The separation between the service class and its key enables flexible service composition while maintaining type safety.
Can I register multiple instances of the same service type in one io_context?
No, the service_registry enforces a singleton pattern per service type within a single execution_context. If you attempt to call add_service with a service type that already exists, the library throws asio::service_already_exists. To manage multiple similar resources, you should extend your service implementation to handle multiple internal instances, or create distinct service types for each resource variant.
What happens to services when the io_context is destroyed?
When an execution_context is destroyed, it automatically invokes shutdown on all registered services in reverse order of creation. This mechanism, implemented in service_registry, ensures that services release resources and stop their internal io_context or threads before the parent context completes destruction. Services must implement the shutdown virtual function to clean up any pending operations or background threads.
Is the ASIO service registry thread-safe?
Service creation through use_service is thread-safe—the registry ensures that only one thread constructs the service even if multiple threads simultaneously request the same service type. However, once retrieved, the service object itself is not inherently thread-safe unless documented otherwise. Custom services must implement their own synchronization (such as strands or mutexes) for thread-safe operation, as the registry only guarantees safe initialization and singleton access.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →