diff --git a/CMakeLists.txt b/CMakeLists.txt index 755e17a..21c1fe1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,7 +195,8 @@ if(SLED_BUILD_TESTS) sled_add_test(NAME sled_string_view_test SRCS src/sled/nonstd/string_view_test.cc) sled_add_test(NAME sled_expected_test SRCS src/sled/nonstd/expected_test.cc) - sled_add_test(NAME sled_config_test SRCS src/sled//config_test.cc) + sled_add_test(NAME sled_config_test SRCS src/sled/config_test.cc) + sled_add_test(NAME sled_ioc_test SRCS src/sled/ioc/ioc_test.cc) endif(SLED_BUILD_TESTS) if(SLED_BUILD_FUZZ) diff --git a/src/sled/ioc/ioc.h b/src/sled/ioc/ioc.h new file mode 100644 index 0000000..a387725 --- /dev/null +++ b/src/sled/ioc/ioc.h @@ -0,0 +1,933 @@ +#ifndef SLED_SLED_IOC_IOC_H +#define SLED_SLED_IOC_IOC_H + +#pragma once + +#include +#include +#include +#include +#include + +#define CINJECT_VERSION 1000000// 1.000.000 + +namespace cinject { + +///////////////////////////////////////////////////////// +// INJECTION HELPERS FOR PRODUCTION +///////////////////////////////////////////////////////// + +template +struct ConstructorType { + typedef T Type; +}; + +#define CINJECT(constructorFunction) \ + typedef cinject::ConstructorType ConstructorTypedef; \ + constructorFunction + +#define CINJECT_NAME(component_name) \ + static const char *name() { return component_name; } + +///////////////////////////////////////////////////////// +// TEMPLATE TYPE HELPERS +///////////////////////////////////////////////////////// +template +struct always_false { + enum { value = false }; +}; + +template +struct trim_shared_ptr; + +template +struct trim_shared_ptr> { + typedef T type; +}; + +template +struct is_shared_ptr : public std::false_type {}; + +template +struct is_shared_ptr> : public std::true_type {}; + +template +struct is_vector : public std::false_type {}; + +template +struct is_vector> : public std::true_type {}; + +template +struct trim_vector; + +template +struct trim_vector> { + typedef T type; +}; + +template +struct has_constructor_injection { + typedef char true_type[1]; + typedef char false_type[2]; + + template + static true_type &check(typename C::ConstructorTypedef *); + + template + static false_type &check(...); + + static const bool value = sizeof(check(0)) == sizeof(true_type); +}; + +template +struct has_name : std::false_type {}; + +template +struct has_name : std::true_type {}; + +///////////////////////////////////////////////////////// +// Type HELPER +///////////////////////////////////////////////////////// +struct cinject_unspecified_component {}; + +struct component_type { + explicit component_type(const std::type_info &t, const std::string &customName = "") + : typeInfo(t), + customName(customName) + {} + + const std::type_info &typeInfo; + const std::string customName; + + std::string name() const { return customName.empty() ? typeInfo.name() : customName; } + + bool specified() const { return typeInfo != typeid(cinject_unspecified_component); } +}; + +template +static component_type +make_component_type(const std::string &customName = "") +{ + return component_type(typeid(T), customName); +} + +inline bool +operator==(const component_type &first, const component_type &other) +{ + return first.typeInfo == other.typeInfo; +} + +struct component_type_hash { + size_t operator()(const component_type &type) const { return type.typeInfo.hash_code(); } +}; + +template +struct type_name { + static const char *value() { return typeid(T).name(); } +}; + +template +struct type_name::value>::type> { + static const char *value() { return T::name(); } +}; + +///////////////////////////////////////////////////////// +// EXCEPTIONS +///////////////////////////////////////////////////////// +class CircularDependencyFoundException : public std::logic_error { +public: + explicit CircularDependencyFoundException(const component_type &type) + : std::logic_error(std::string("Found circular dependency on object '") + type.name() + "'") + {} +}; + +class ComponentNotFoundException : public std::logic_error { +public: + explicit ComponentNotFoundException(const component_type &type) + : std::logic_error(std::string("Component for interface '") + type.name() + "' not found") + {} +}; + +class InvalidOperationException : public std::logic_error { +public: + explicit InvalidOperationException(const char *message) : std::logic_error(message) {} +}; + +///////////////////////////////////////////////////////// +// INJECTION CONTEXT +///////////////////////////////////////////////////////// + +class Container; + +class InjectionContext { +public: + InjectionContext(Container &container, component_type requesterComponent) : container_(container) + { + pushType(requesterComponent); + } + + ~InjectionContext() { popType(); } + + Container &getContainer() { return container_; } + + void pushType(component_type &type) { componentStack_.emplace_back(type); } + + void popType() { componentStack_.pop_back(); } + + const std::vector &getComponentStack() { return componentStack_; } + + const component_type &getRequester() + { + if (componentStack_.size() < 2) { throw InvalidOperationException("Context not valid."); } + + return componentStack_[componentStack_.size() - 2]; + } + + InjectionContext(const InjectionContext &) = delete; + InjectionContext(const InjectionContext &&) = delete; + void operator=(const InjectionContext &) = delete; + void operator=(const InjectionContext &&) = delete; + +private: + Container &container_; + std::vector componentStack_; +}; + +///////////////////////////////////////////////////////// +// CONTEXT GUARD +///////////////////////////////////////////////////////// + +class ContextGuard { +public: + ContextGuard(InjectionContext *context, component_type type) : context_(context), type_(type) + { + context_->pushType(type); + } + + ~ContextGuard() { context_->popType(); } + + void ensureNoCycle() + { + const std::vector &stack = context_->getComponentStack(); + + for (size_t i = 0; i < stack.size() - 1; ++i) { + if (stack[i] == stack.back()) { throw CircularDependencyFoundException(stack.back()); } + } + } + +private: + InjectionContext *context_; + component_type type_; +}; + +///////////////////////////////////////////////////////// +// INSTANCE RETRIEVERS +///////////////////////////////////////////////////////// +class IInstanceRetriever { +public: + virtual ~IInstanceRetriever() = default; +}; + +template +class InstanceRetriever : public IInstanceRetriever { +public: + virtual std::shared_ptr forwardInstance(InjectionContext *context) = 0; +}; + +template +class CastInstanceRetriever : public InstanceRetriever { +public: + explicit CastInstanceRetriever(std::shared_ptr storage) : storage_(storage) {} + + std::shared_ptr forwardInstance(InjectionContext *context) override + { + return std::dynamic_pointer_cast(storage_->getInstance(context)); + } + +private: + std::shared_ptr storage_; +}; + +///////////////////////////////////////////////////////// +// CONTAINER DECLARATION +///////////////////////////////////////////////////////// + +template +class ComponentBuilder; + +/// Container is used to configure bindings between interfaces and implementations. +/// +/// Start with this class to configure your application. +/// ### Sample usage +/// ``` +/// class IFoo +/// {}; +/// +/// class Foo : public IFoo +/// { +/// }; +/// +/// ... +/// +/// Container container; +/// container.bind().to(); +/// +/// std::shared_ptr foo = container.get() +/// ``` +class Container { + template + friend class ComponentBuilderBase; + +public: + Container() = default; + + explicit Container(const Container *parentContainer) : parentContainer_(parentContainer) {} + + /// Initiates binding configuration for the TArgs component type. + /// + /// Start with this class to configure your application. + /// @par Sample usage + /// ``` + /// Container c; + /// c.bind() + /// c.bind() + /// c.bind() + /// ``` + /// + /// @tparam TArgs One or many components used for registration. + /// @return ComponentBuilder instance used to specific binding configuration + template + ComponentBuilder Bind(); + + /// Attempts to resolve all available instances registered to the requested type. + /// + /// This function is used when the requested type is vector with simple type as the containing type + /// + /// @par Sample usage + /// ``` + /// container.get>() + /// ``` + /// + /// @param context [in] Injection context. + /// @tparam TVectorWithInterface Requested type. Usually an interface. + /// @returns vector of instances registered to the requested type identified by the TVectorWithInterface template argument. + template + typename std::enable_if::value + && !is_shared_ptr::type>::value + && !std::is_reference::value, + std::vector::type>>>::type + Get(InjectionContext *context = nullptr); + + /// Attempts to resolve all available instances registered to the requested type. + /// + /// This function is used when the requested type is vector with shared_ptr as the containing type + /// @par Sample usage + /// ``` + /// container.get>>() + /// ``` + /// + /// @param context [in] Injection context. + /// @tparam TVectorWithInterface Requested type. Usually an interface. + /// @returns vector of instances registered to the requested type identified by the TVectorWithInterface template argument. + template + typename std::enable_if::value + && is_shared_ptr::type>::value + && !std::is_reference::value, + std::vector::type>>::type + Get(InjectionContext *context = nullptr); + + /// Attempts to resolve an instance registered to the requested type. + /// + /// This function is used when the requested type shared_ptr + /// + /// @par Sample usage + /// ``` + /// container.get>() + /// ``` + /// + /// @param context [in] Injection context. + /// @tparam TInterfaceWithSharedPtr Requested type. Usually an interface. + /// @returns Instance registered to the requested type identified by the TInterfaceWithSharedPtr template argument. + template + typename std::enable_if::value && is_shared_ptr::value + && !std::is_reference::value, + TInterfaceWithSharedPtr>::type + Get(InjectionContext *context = nullptr); + + /// Attempts to resolve an instance registered to the requested type. + /// + /// This function is used when the requested type simple type + /// + /// @par Sample usage + /// ``` + /// container.get() + /// ``` + /// + /// @param context [in] Injection context. + /// @tparam TInterface Requested type. Usually an interface. + /// @returns Instance registered to the requested type identified by the TInterface template argument. + template + typename std::enable_if::value && !is_shared_ptr::value + && !std::is_reference::value, + std::shared_ptr>::type + Get(InjectionContext *context = nullptr); + + /// Attempts to resolve an instance registered to the requested type. + /// + /// This function is used when the requested type is reference typ + /// + /// @par Sample usage + /// ``` + /// container.get&>() + /// container.get>&>() + /// ``` + /// + /// @note Only vector is supported + /// @param context [in] Injection context. + /// @tparam TInterface Requested type. Usually an interface. + /// @returns Instance registered to the requested type identified by the TInterface template argument. + template + typename std::enable_if::value + && std::is_const::type>::value, + typename std::remove_reference::type>::type + Get(InjectionContext *context = nullptr); + +private: + void findInstanceRetrievers(std::vector> &instanceRetrievers, + const component_type &type) const; + + const Container *parentContainer_ = nullptr; + std::unordered_map>, component_type_hash> + registrations_; +}; + +///////////////////////////////////////////////////////// +// CONSTRUCTOR FACTORY +///////////////////////////////////////////////////////// + +struct ctor_arg_resolver { + explicit ctor_arg_resolver(InjectionContext *context) : context_(context) {} + + template::value, int>::type = 0> + operator TCtorArgument() + { + return context_->getContainer().Get(context_); + } + + InjectionContext *context_; +}; + +template +struct ctor_arg_resolver_1st { + explicit ctor_arg_resolver_1st(InjectionContext *context) : context_(context) {} + + template::value + && !std::is_same::value + && !std::is_pointer::value, + int>::type + = 0> + operator TCtorArgument() + { + return context_->getContainer().Get(context_); + } + + InjectionContext *context_; +}; + +template +class ConstructorFactory { + static_assert(always_false::value, "Could not deduce any ConstructorFactory"); +}; + +// Factory for trivial constructors with no arguments +template +class ConstructorFactory::value + && std::is_constructible::value>::type> { +public: + std::shared_ptr createInstance(InjectionContext *context) { return std::make_shared(); } +}; + +// Factory for automatic injection for one to ten arguments +template +class ConstructorFactory::value + && !std::is_constructible::value>::type> { +public: + std::shared_ptr createInstance(InjectionContext *context) + { + return try_instantiate( + ctor_arg_resolver(context), + ctor_arg_resolver(context), + ctor_arg_resolver(context), + ctor_arg_resolver(context), + ctor_arg_resolver(context), + ctor_arg_resolver(context), + ctor_arg_resolver(context), + ctor_arg_resolver(context), + ctor_arg_resolver(context), + ctor_arg_resolver_1st(context)); + } + +private: + template + typename std::enable_if::value, + std::shared_ptr>::type + try_instantiate(TArg a1, TNextArg a2, TRestArgs... args) + { + return std::make_shared(a1, a2, args...); + } + + template + typename std::enable_if::value, + std::shared_ptr>::type + try_instantiate(TArg a1, TNextArg a2, TRestArgs... args) + { + return try_instantiate(a2, args...); + } + + template + typename std::enable_if::value, std::shared_ptr>::type + try_instantiate(TArg arg) + { + return std::make_shared(arg); + } + + template + typename std::enable_if::value, std::shared_ptr>::type + try_instantiate(TArg arg) + { + static_assert(always_false::value, + "Could not find any suitable constructor for injection. Try explicitly mark the constructor " + "using CINJECT macro"); + } +}; + +template +struct ConstructorInvoker; + +template +struct ConstructorInvoker { + static std::shared_ptr invoke(InjectionContext *context) + { + Container &container = context->getContainer(); + + return std::make_shared(container.Get(context)...); + } +}; + +// Factory for injection using the CINJECT macro +template +class ConstructorFactory::value>::type> { +public: + std::shared_ptr createInstance(InjectionContext *context) + { + return ConstructorInvoker::invoke(context); + } +}; + +///////////////////////////////////////////////////////// +// FUNCTION FACTORY +///////////////////////////////////////////////////////// + +template +struct FunctionFactory { + typedef std::function(InjectionContext *)> FactoryMethodType; + + FunctionFactory(FactoryMethodType factoryMethod) : factoryMethod_(factoryMethod) {} + + std::shared_ptr createInstance(InjectionContext *context) { return factoryMethod_(context); } + + FactoryMethodType factoryMethod_; +}; + +///////////////////////////////////////////////////////// +// CONSTANT FACTORY +///////////////////////////////////////////////////////// + +template +struct ConstantFactory { + ConstantFactory(std::shared_ptr instance) : instance_(instance) {} + + std::shared_ptr createInstance(InjectionContext *context) { return instance_; } + + std::shared_ptr instance_; +}; + +///////////////////////////////////////////////////////// +// INSTANCE STORAGE +///////////////////////////////////////////////////////// + +template +class InstanceStorage { +public: + explicit InstanceStorage(TFactory factory) : factory_(factory) {} + + virtual std::shared_ptr getInstance(InjectionContext *context) + { + if (!isSingleton_) { return createInstance(context); } + + if (instance_ == nullptr) { instance_ = createInstance(context); } + + return instance_; + } + + void setSingleton(bool value) { isSingleton_ = value; } + + void setName(const std::string &name) { name_ = name; } + + void setName(std::string &&name) { name_ = name; } + +private: + std::shared_ptr createInstance(InjectionContext *context) + { + ContextGuard guard( + context, + make_component_type(!name_.empty() ? name_ : type_name::value())); + + guard.ensureNoCycle(); + + return factory_.createInstance(context); + } + + TFactory factory_; + bool isSingleton_ = false; + std::shared_ptr instance_; + std::string name_; +}; + +///////////////////////////////////////////////////////// +// STORAGE CONFIGURATION +///////////////////////////////////////////////////////// + +/// Configures instance storage. +/// +/// Instance can be either transient or singleton. If it's singleton, then the same instance is provided whenever it's requested. Otherwise +/// a new instance is always created. +/// @tparam TInstanceStorage Instance storage type. +template +class StorageConfiguration { +public: + explicit StorageConfiguration(std::shared_ptr storage) : storage_(storage) {} + + /// Configures the instance to be handled as singleton + /// + /// + StorageConfiguration &InSingletonScope() + { + storage_->setSingleton(true); + + return *this; + } + + /// Configures the instance name + /// + /// + StorageConfiguration &Alias(const std::string &name) + { + storage_->setName(name); + + return *this; + } + + /// Configures the instance name + /// + /// + StorageConfiguration &Alias(std::string &&name) + { + storage_->setName(name); + + return *this; + } + +private: + std::shared_ptr storage_; +}; + +/// Specialized Storage Configuration for Constant Factory +/// @tparam TInstance Instance type to be configured. +template +class StorageConfiguration>> { +public: + explicit StorageConfiguration(std::shared_ptr>> storage) + : storage_(storage) + {} + +private: + std::shared_ptr>> storage_; +}; + +///////////////////////////////////////////////////////// +// COMPONENT BUILDER +///////////////////////////////////////////////////////// + +/// Builds binding between interfaces and implementations. +/// +/// @par Sample usage +/// ``` +/// Container c; +/// +/// c.bind().to(); +/// c.bind().toFunction([](InjectionContext*) { return std::make_shared(); }); +/// c.bind().toContainer(cheetah); +/// c.bind().toSelf(); +/// ``` +/// @note The toSelf is available only when the list of interfaces constains exactly one item +/// @tparam TComponents List of interfaces used for binding +template +class ComponentBuilderBase { +public: + explicit ComponentBuilderBase(Container *container) : container_(container) {} + + /// Binds all interfaces to provided implementation identified by the TImplementation type. + /// + /// @tparam TImplementation Implementation type. + /// @return StoreConfiguration instance used to configure instance storage. + template + StorageConfiguration>> To() + { + typedef InstanceStorage> InstanceStorageType; + + // Create instance holder + auto instanceStorage = std::make_shared(ConstructorFactory()); + + registerType(instanceStorage); + + return StorageConfiguration(instanceStorage); + } + + /// Binds all interfaces to provided function used to create a new instance. + /// + /// @tparam TImplementation Implementation type. The provided function must return an instance that can be converted to TImplementation. + /// @param factoryMethod Function creating a new instance. + /// @return StoreConfiguration instance used to configure instance storage. + template + StorageConfiguration>> + ToFunction(typename FunctionFactory::FactoryMethodType factoryMethod) + { + typedef InstanceStorage> InstanceStorageType; + + // Create instance holder + auto instanceStorage = std::make_shared(factoryMethod); + + registerType(instanceStorage); + + return StorageConfiguration(instanceStorage); + } + + /// Binds all interfaces to already existing instance. + /// + /// @tparam TImplementation Implementation type. The provided instance must be convertible to TImplementation. + /// @param instance Instance + /// @return StoreConfiguration instance used to configure instance storage. + template + StorageConfiguration>> + ToConstant(std::shared_ptr instance) + { + typedef InstanceStorage> InstanceStorageType; + + // Create instance holder + auto instanceStorage = std::make_shared(instance); + + registerType(instanceStorage); + + return StorageConfiguration(instanceStorage); + } + +private: + template + void registerType(std::shared_ptr instanceStorage) + { + // register + addRegistration(instanceStorage); + + registerType(instanceStorage); + } + + template + void registerType(std::shared_ptr instanceStorage) + { + // register + addRegistration(instanceStorage); + } + + template + void addRegistration(std::shared_ptr instanceStorage) + { + static_assert(std::is_convertible::value, + "No conversion exists from TImplementation* to TComponent*"); + + container_->registrations_[make_component_type()].emplace_back(std::shared_ptr( + new CastInstanceRetriever(instanceStorage))); + } + +private: + Container *container_; +}; + +/// Basic builder used for two and more interfaces. +/// +/// @see ComponentBuilderBase +template +class ComponentBuilder : public ComponentBuilderBase { +public: + explicit ComponentBuilder(Container *container) : ComponentBuilderBase(container) {} +}; + +/// Specialization for single component registration that allows the toSelf. +/// +/// This class is used only when the number of interfaces is exactly one. +/// @tparam TComponent Interface used for registration. +template +class ComponentBuilder : public ComponentBuilderBase { +public: + explicit ComponentBuilder(Container *container) : ComponentBuilderBase(container) {} + + /// Registers interface to the same type. + /// + /// @par Sample usage + /// ``` + /// Container c; + /// + /// c.bind().toSelf() + /// ``` + /// @return StoreConfiguration instance used to configure instance storage. + StorageConfiguration>> ToSelf() + { + return ComponentBuilderBase::template To(); + } +}; + +///////////////////////////////////////////////////////// +// CONTAINER IMPLEMENTATION +///////////////////////////////////////////////////////// + +template +ComponentBuilder +Container::Bind() +{ + return ComponentBuilder(this); +} + +// container.get>() +template +typename std::enable_if::value + && !is_shared_ptr::type>::value + && !std::is_reference::value, + std::vector::type>>>::type +Container::Get(InjectionContext *context) +{ + typedef typename trim_vector::type InterfaceType; + + std::unique_ptr contextPtr; + + if (context == nullptr) { + contextPtr.reset(new InjectionContext(*this, make_component_type())); + context = contextPtr.get(); + } + + std::vector> retrievers; + findInstanceRetrievers(retrievers, make_component_type()); + + std::vector> instances; + + for (std::shared_ptr retrieverInterface : retrievers) { + std::shared_ptr> retriever + = std::dynamic_pointer_cast>(retrieverInterface); + + instances.emplace_back(retriever->forwardInstance(context)); + } + + return instances; +} + +// container.get>>() +template +typename std::enable_if::value + && is_shared_ptr::type>::value + && !std::is_reference::value, + std::vector::type>>::type +Container::Get(InjectionContext *context) +{ + return Get< + std::vector::type>::type>>( + context); +} + +// container.get>() +template +typename std::enable_if::value && is_shared_ptr::value + && !std::is_reference::value, + TInterfaceWithSharedPtr>::type +Container::Get(InjectionContext *context) +{ + return Get::type>(context); +} + +// container.get() +template +typename std::enable_if::value && !is_shared_ptr::value + && !std::is_reference::value, + std::shared_ptr>::type +Container::Get(InjectionContext *context) +{ + std::unique_ptr contextPtr; + + if (context == nullptr) { + contextPtr.reset( + new InjectionContext(*this, make_component_type("Unspecified"))); + context = contextPtr.get(); + } + + const component_type type = make_component_type(); + + std::vector> retrievers; + findInstanceRetrievers(retrievers, type); + + if (retrievers.size() == 0) { throw ComponentNotFoundException(type); } + + std::shared_ptr> retriever + = std::dynamic_pointer_cast>(retrievers[0]); + + return retriever->forwardInstance(context); +} + +// container.get() +template +typename std::enable_if::value + && std::is_const::type>::value, + typename std::remove_reference::type>::type +Container::Get(InjectionContext *context) +{ + return Get::type>::type>(context); +} + +inline void +Container::findInstanceRetrievers(std::vector> &instanceRetrievers, + const component_type &type) const +{ + auto iter = registrations_.find(type); + if (iter != registrations_.end()) { + const std::vector> ¤tRetrievers = iter->second; + + instanceRetrievers.insert(instanceRetrievers.end(), currentRetrievers.begin(), currentRetrievers.end()); + } + + if (parentContainer_ != nullptr) { parentContainer_->findInstanceRetrievers(instanceRetrievers, type); } +} + +}// namespace cinject + +namespace sled { +namespace ioc { +using Container = cinject::Container; +using InjectContext = cinject::InjectionContext; +}// namespace ioc +}// namespace sled + +#endif// SLED_SLED_IOC_IOC_H diff --git a/src/sled/ioc/ioc_test.cc b/src/sled/ioc/ioc_test.cc new file mode 100644 index 0000000..450b446 --- /dev/null +++ b/src/sled/ioc/ioc_test.cc @@ -0,0 +1,64 @@ +#include +#include + +class IRunner { +public: + virtual ~IRunner() = default; + virtual int RunSpeed() = 0; +}; + +class IWalker { +public: + virtual ~IWalker() {} + + virtual int WalkSpeed() = 0; +}; + +class Cheetah : public IRunner, public IWalker { +public: + int WalkSpeed() override { return 4; } + + int RunSpeed() override { return 100; } +}; + +class Formatter { +public: + std::string Format(int value) { return std::to_string(value); } +}; + +class Service { +public: + Service(std::shared_ptr formatter) : formatter_(formatter) {} + + std::string Format(int value) { return formatter_->Format(value); } + +private: + std::shared_ptr formatter_; +}; + +TEST_SUITE("Inversion Of Control") +{ + TEST_CASE("base") + { + sled::ioc::Container container; + container.Bind().To(); + auto runner = container.Get(); + auto walker = container.Get(); + CHECK(runner); + CHECK(walker); + CHECK_EQ(runner.use_count(), 1); + CHECK_EQ(walker.use_count(), 1); + CHECK_EQ(runner->RunSpeed(), 100); + CHECK_EQ(walker->WalkSpeed(), 4); + } + + TEST_CASE("deps") + { + sled::ioc::Container container; + container.Bind().ToSelf(); + container.Bind().ToSelf(); + auto service = container.Get(); + CHECK_EQ(service->Format(42), "42"); + CHECK_EQ(service->Format(44), "44"); + } +} diff --git a/src/sled/sled.h b/src/sled/sled.h index 450a9ae..3ee3c5d 100644 --- a/src/sled/sled.h +++ b/src/sled/sled.h @@ -24,6 +24,7 @@ // futures // #include "sled/futures/promise.h" +#include "sled/ioc/ioc.h" // lang #include "sled/lang/attributes.h"