gracicot / kangaru

🦘 A dependency injection container for C++11, C++14 and later
MIT License
478 stars 40 forks source link

Service mapping using runtime values for the container #127

Closed fran6co closed 1 month ago

fran6co commented 1 month ago

It's useful to map services using runtime values, for example I have an application that creates media pipelines and each pipeline can be created at runtime and each has their own services. Mapping right now is done via templating, allowing to use strings or some other values that can be hashed would allow for this.

gracicot commented 1 month ago

@fran6co Does external services work for you? It allows you to declare services, but the container will let you set the instance for it.

If different parts of your application needs a different pipeline, then would external services + container forking works for your use case?

fran6co commented 1 month ago

Not really, because I have multiple services with the same type but that belong to different sources in the pipeline. If I could add a runtime map based on a string then each could query for the correct one. I was thinking a way to add the std::hash of a runtime variable to the type is hash used to store the service

gracicot commented 1 month ago

You can create many services for the same type like this:

// Three mappings for type dep
struct dependency1_service : kgr::single_service<dep> {};
struct dependency2_service : kgr::single_service<dep> {};
struct dependency3_service : kgr::single_service<dep> {};

Then use the right dependency at the right parts:

struct pipeline_part1_service : kgr::single_service<part1, kgr::dependency<dependency1_service>> {};
struct pipeline_part2_service : kgr::single_service<part2, kgr::dependency<dependency2_service>> {};
struct pipeline_part3_service : kgr::single_service<part3, kgr::dependency<dependency3_service>> {};

If you have a known amount of parts (or types of part) it should work. This is really because you can create multiple service type for one type, then use those services as dependency to others.

If on the other hand you have a unknown or a dynamic amount of parts, where the amount of types are unknown at any point except runtime, you could create your own runtime mapping with strings and hash, then pass that container to a custom service definition that picks the right one when building your service. It's quite a bit more involved though.

fran6co commented 1 month ago

Yeah, the problem is that the pipeline is dynamically constructed so I can't do that. What we have now is a std::map container with the services as a poor man's id and I wanted to replace it with kangaru. I was hoping for an easy way to achieve this

gracicot commented 1 month ago

If you don't load pipepile parts from dynamic libraires you should be able to create as much services for a type as the amount of types that needs it. I don't think assembling the pipeline at runtime blocks that. Maybe you can provide me a short code snippet that reproduced what you're trying to do?

fran6co commented 1 month ago

It's very simplified, but it would be something like this. Have some mapped_ variants that take something that can be std::hash.


struct CameraService : kgr::shared_service<Camera> {};

kgr::container container;

for (const auto& [sourceId, source] : sources) {
     auto camera = container.mapped_shared_service<CameraService>(sourceId, other_args_to_init_camera);
     // Wire the camera to pipeline to pass images to it
}

// .... do other things

// Start the cameras
for (const auto& [sourceId, source] : sources) {
    container.mapped_invoke(sourceId, [](std::shared_ptr<Camera> camera){
          camera->start();
    });   
}
gracicot commented 1 month ago

You can achieve this example code with a forked container in a map:

std::unordered_map<std::string, kgr::container> mapped;
for (const auto& [sourceId, source] : sources) {
    auto fork = container.fork();
    container.emplace<CameraService>(other_args_to_init_camera);
    mapped.emplace_or_assign(sourceId, std::move(fork));

     // Wire the camera to pipeline to pass images to it
}

// .... do other things

// Start the cameras
for (const auto& [sourceId, source] : sources) {
    mapped.at(sourceId).invoke([](std::shared_ptr<Camera> camera) {
          camera->start();
    });   
}

Unless this code don't represent what you're trying to do well enough, it should be achieving what you're looking for.

fran6co commented 1 month ago

That might work, I didn't think about forking containers. Thanks

gracicot commented 1 month ago

Let me know how it goes! If it works well enough for your needs, feel free to close the issue after you verified that it work 😄