diff --git a/phlex/CMakeLists.txt b/phlex/CMakeLists.txt index 7eaf8aca6..5c4851109 100644 --- a/phlex/CMakeLists.txt +++ b/phlex/CMakeLists.txt @@ -34,7 +34,24 @@ cet_make_library( phlex::configuration ) -install(FILES concurrency.hpp driver.hpp module.hpp source.hpp DESTINATION include/phlex) +cet_make_library( + LIBRARY_NAME + phlex_resource + EXPORT_NAME + resource + INTERFACE + NO_SOURCE + LIBRARIES + Boost::boost + Boost::json + phlex::core + phlex::configuration +) + +install( + FILES concurrency.hpp driver.hpp module.hpp resource.hpp source.hpp + DESTINATION include/phlex +) install(FILES detail/plugin_macros.hpp DESTINATION include/phlex/detail) cet_make_library( diff --git a/phlex/app/load_module.cpp b/phlex/app/load_module.cpp index 721e04a5f..83c212189 100644 --- a/phlex/app/load_module.cpp +++ b/phlex/app/load_module.cpp @@ -3,6 +3,7 @@ #include "phlex/core/framework_graph.hpp" #include "phlex/driver.hpp" #include "phlex/module.hpp" +#include "phlex/resource.hpp" #include "phlex/source.hpp" #include "boost/algorithm/string.hpp" @@ -38,12 +39,22 @@ namespace phlex::detail { boost::dll::shared_library lib; internal::source_creator_t* fn{}; - void operator()(source_bundle bundle, configuration const& config) const + void operator()(graph_registration_bundle bundle, configuration const& config) const { fn(bundle, config); } }; + struct resource_plugin { + boost::dll::shared_library lib; + internal::resource_creator_t* fn{}; + + void operator()(resources_graph_proxy const& proxy, configuration const& config) const + { + fn(proxy, config); + } + }; + struct driver_plugin { boost::dll::shared_library lib; internal::driver_shim_t* fn{}; @@ -56,12 +67,12 @@ namespace phlex::detail { } }; - // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) std::vector create_module; - // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::vector create_source; - // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + std::vector create_resources; std::optional create_driver; + // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) template std::pair plugin_loader(std::string const& spec, @@ -132,6 +143,24 @@ namespace phlex::detail { creator(g.module_proxy(config), config); } + void load_resource(framework_graph& g, std::string const& label, boost::json::object raw_config) + { + auto const adjusted_config = internal::adjust_config(label, std::move(raw_config)); + + auto const* cpp = adjusted_config.if_contains("cpp"); + if (not cpp) { + throw std::runtime_error( + fmt::format("Missing 'cpp' parameter for {} -- only C++ resources are supported.", label)); + } + auto const& spec = value_to(*cpp); + auto [lib, fn] = plugin_loader(spec, "create_resources"); + auto& creator = create_resources.emplace_back(resource_plugin{.lib = std::move(lib), .fn = fn}); + + configuration const config{adjusted_config}; + auto const proxy = resources_graph_proxy{g.registration_bundle(config)}; + creator(proxy, config); + } + void load_source(framework_graph& g, std::string const& label, boost::json::object raw_config) { auto const adjusted_config = internal::adjust_config(label, std::move(raw_config)); @@ -146,7 +175,7 @@ namespace phlex::detail { // adjusted_config["module_label"] = label; // already set by adjust_config configuration const config{adjusted_config}; - creator(g.source_proxy(config), config); + creator(g.registration_bundle(config), config); } void load_driver(framework_graph& g, boost::json::object const& raw_config) diff --git a/phlex/app/load_module.hpp b/phlex/app/load_module.hpp index 0c4f91c9c..1126168a8 100644 --- a/phlex/app/load_module.hpp +++ b/phlex/app/load_module.hpp @@ -19,6 +19,9 @@ namespace phlex::detail { RUN_PHLEX_EXPORT void load_module(framework_graph& g, std::string const& label, boost::json::object config); + RUN_PHLEX_EXPORT void load_resource(framework_graph& g, + std::string const& label, + boost::json::object config); RUN_PHLEX_EXPORT void load_source(framework_graph& g, std::string const& label, boost::json::object config); diff --git a/phlex/app/run.cpp b/phlex/app/run.cpp index 19cf4b2ed..c0777b6bb 100644 --- a/phlex/app/run.cpp +++ b/phlex/app/run.cpp @@ -19,6 +19,15 @@ namespace phlex::detail { { auto g = framework_graph::without_driver(max_parallelism); + boost::json::object resource_configs; + if (configurations.contains("resources")) { + resource_configs = object_decorate_exception(configurations, "resources"); + } + + for (auto const& [key, value] : resource_configs) { + load_resource(g, key, value.as_object()); + } + // It is allowed for users to not specify any modules boost::json::object module_configs; if (configurations.contains("modules")) { diff --git a/phlex/core/framework_graph.hpp b/phlex/core/framework_graph.hpp index da9661f84..ca7fbf964 100644 --- a/phlex/core/framework_graph.hpp +++ b/phlex/core/framework_graph.hpp @@ -76,7 +76,7 @@ namespace phlex::detail { return {config, graph_, nodes_, registration_errors_, resources_}; } - source_bundle source_proxy(configuration const& config) + graph_registration_bundle registration_bundle(configuration const& config) { return {.config = config, .graph = graph_, diff --git a/phlex/core/graph_proxy.hpp b/phlex/core/graph_proxy.hpp index 9f951b76d..d52493e49 100644 --- a/phlex/core/graph_proxy.hpp +++ b/phlex/core/graph_proxy.hpp @@ -22,6 +22,17 @@ namespace phlex { } namespace phlex::detail { + struct graph_registration_bundle { + // Non-owning references to framework-owned resources; this is a short-lived struct. + // NOLINTBEGIN(cppcoreguidelines-avoid-const-or-ref-data-members) + configuration const& config; + tbb::flow::graph& graph; + node_catalog& nodes; + resource_catalog& resources; + std::vector& registration_errors; + // NOLINTEND(cppcoreguidelines-avoid-const-or-ref-data-members) + }; + // ============================================================================== // Registering user functions diff --git a/phlex/detail/plugin_macros.hpp b/phlex/detail/plugin_macros.hpp index b4b054a4b..012bb55f5 100644 --- a/phlex/detail/plugin_macros.hpp +++ b/phlex/detail/plugin_macros.hpp @@ -36,7 +36,7 @@ // Source plugin entry-points cannot use extern "C" directly because the user-facing proxy types // (providers_graph_proxy, source_graph_proxy) are C++ templates. Instead we: // 1. Forward-declare the user's C++ implementation (takes the proxy by reference). -// 2. Define a thin extern "C" shim that accepts source_bundle by value (matching +// 2. Define a thin extern "C" shim that accepts graph_registration_bundle by value (matching // source_creator_t exactly), constructs the appropriate proxy from the bundle, // and calls the user's implementation. // 3. Open the user's implementation definition for the body that follows the macro. @@ -54,7 +54,7 @@ #define PHLEX_DETAIL_REGISTER_SOURCE_PLUGIN(token_type, func_name, dll_alias, ...) \ static PHLEX_DETAIL_SELECT_SOURCE_SIGNATURE(token_type, func_name, __VA_ARGS__); \ - extern "C" void dll_alias(phlex::detail::source_bundle __phlex_bundle, \ + extern "C" void dll_alias(phlex::detail::graph_registration_bundle __phlex_bundle, \ phlex::configuration const& __phlex_config) \ { \ func_name(token_type{__phlex_bundle}, __phlex_config); \ diff --git a/phlex/resource.hpp b/phlex/resource.hpp new file mode 100644 index 000000000..4feebf803 --- /dev/null +++ b/phlex/resource.hpp @@ -0,0 +1,53 @@ +#ifndef PHLEX_RESOURCE_HPP +#define PHLEX_RESOURCE_HPP + +#include "phlex/core/graph_proxy.hpp" +#include "phlex/detail/plugin_macros.hpp" + +#include + +namespace phlex::detail { + + /// @brief Proxy for registering resources. + /// + /// Passed to @c PHLEX_REGISTER_RESOURCES plugin entry points. Only resource + /// registration is accessible. Users never construct this type directly. + class resources_graph_proxy { + public: + explicit resources_graph_proxy(graph_registration_bundle bundle) : resources_{bundle.resources} + { + } + + template + requires unlimited_resource_registration + void add_unlimited_resource(Args&&... args) const + { + resources_.template add_unlimited(std::forward(args)...); + } + + template + requires serialized_resource_registration + void add_serialized_resource(Args&&... args) const + { + resources_.template add_serialized(std::forward(args)...); + } + + private: + resource_catalog& resources_; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + }; + + namespace internal { + using resource_creator_t = void(resources_graph_proxy const&, configuration const&); + + // The plugin mechanism requires a template, but resources_graph_proxy doesn't need to be one. + // The following template alias is the workaround. + template T> + using resources_graph_proxy_shim = resources_graph_proxy; + } +} + +#define PHLEX_REGISTER_RESOURCES(...) \ + PHLEX_DETAIL_REGISTER_PLUGIN( \ + phlex::detail::internal::resources_graph_proxy_shim, create, create_resources, __VA_ARGS__) + +#endif // PHLEX_RESOURCE_HPP diff --git a/phlex/source.hpp b/phlex/source.hpp index cbf20fcf9..b77e3c11e 100644 --- a/phlex/source.hpp +++ b/phlex/source.hpp @@ -10,18 +10,6 @@ #include namespace phlex::detail { - - struct source_bundle { - // Non-owning references to framework-owned resources; source_bundle is a short-lived struct. - // NOLINTBEGIN(cppcoreguidelines-avoid-const-or-ref-data-members) - configuration const& config; - tbb::flow::graph& graph; - node_catalog& nodes; - resource_catalog& resources; - std::vector& registration_errors; - // NOLINTEND(cppcoreguidelines-avoid-const-or-ref-data-members) - }; - /// @brief Proxy for registering explicit provider nodes. /// /// Passed to @c PHLEX_REGISTER_PROVIDERS plugin entry points. Only provide @@ -31,7 +19,7 @@ namespace phlex::detail { using base = graph_proxy; public: - providers_graph_proxy(source_bundle bundle) : + providers_graph_proxy(graph_registration_bundle bundle) : base{bundle.config, bundle.graph, bundle.nodes, bundle.registration_errors, bundle.resources} { } @@ -58,7 +46,7 @@ namespace phlex::detail { using base = graph_proxy; public: - source_graph_proxy(source_bundle bundle) : + source_graph_proxy(graph_registration_bundle bundle) : base{bundle.config, bundle.graph, bundle.nodes, bundle.registration_errors, bundle.resources} { } @@ -68,7 +56,7 @@ namespace phlex::detail { }; namespace internal { - using source_creator_t = void(source_bundle, configuration const&); + using source_creator_t = void(graph_registration_bundle, configuration const&); } } diff --git a/test/adjust_config_test.cpp b/test/adjust_config_test.cpp index 23e94bc31..046c51c13 100644 --- a/test/adjust_config_test.cpp +++ b/test/adjust_config_test.cpp @@ -1,4 +1,5 @@ #include "phlex/app/load_module.hpp" +#include "phlex/core/framework_graph.hpp" #include "catch2/catch_test_macros.hpp" #include "catch2/matchers/catch_matchers_string.hpp" @@ -59,3 +60,13 @@ TEST_CASE("Both py and cpp specified, cpp as string", "[config]") CHECK_THROWS_WITH(adjust_config("malformed3", std::move(obj)), Catch::Matchers::ContainsSubstring(err_msg)); } + +TEST_CASE("Loading resources requires a cpp parameter", "[config]") +{ + auto graph = phlex::detail::framework_graph::without_driver(); + + CHECK_THROWS_WITH( + phlex::detail::load_resource(graph, "my_resource", {}), + Catch::Matchers::ContainsSubstring( + "Missing 'cpp' parameter for my_resource -- only C++ resources are supported.")); +} diff --git a/test/plugins/CMakeLists.txt b/test/plugins/CMakeLists.txt index f5f326a15..50d8773ea 100644 --- a/test/plugins/CMakeLists.txt +++ b/test/plugins/CMakeLists.txt @@ -7,6 +7,12 @@ target_link_libraries(ij_source PRIVATE phlex::source) add_library(output MODULE output.cpp) target_link_libraries(output PRIVATE phlex::module) +add_library(register_resources_for_testing MODULE register_resources_for_testing.cpp) +target_link_libraries(register_resources_for_testing PRIVATE phlex::resource) + +add_library(verify_resources MODULE verify_resources.cpp) +target_link_libraries(verify_resources PRIVATE phlex::module) + cet_test( job:add HANDBUILT @@ -19,3 +25,16 @@ cet_test( ENVIRONMENT "PHLEX_PLUGIN_PATH=${PROJECT_BINARY_DIR}/${phlex_LIBRARY_DIR}" ) + +cet_test( + job:resources + HANDBUILT + TEST_EXEC + phlex::phlex + TEST_ARGS + -c + ${CMAKE_CURRENT_SOURCE_DIR}/test_resources.jsonnet + TEST_PROPERTIES + ENVIRONMENT + "PHLEX_PLUGIN_PATH=${PROJECT_BINARY_DIR}/${phlex_LIBRARY_DIR}" +) diff --git a/test/plugins/register_resources_for_testing.cpp b/test/plugins/register_resources_for_testing.cpp new file mode 100644 index 000000000..533c40e6a --- /dev/null +++ b/test/plugins/register_resources_for_testing.cpp @@ -0,0 +1,26 @@ +#include "phlex/configuration.hpp" +#include "phlex/resource.hpp" +#include "test/plugins/resources_for_testing.hpp" + +using namespace phlex; +using namespace test::plugins; + +namespace { + using proxy = detail::resources_graph_proxy; + + template + concept exposes_observe = + requires(T const& resource_proxy) { resource_proxy.observe("invalid", [](int) {}); }; + + static_assert(requires(proxy const& r) { + r.template add_unlimited_resource(1); + r.template add_serialized_resource(); + }); + static_assert(not exposes_observe); +} + +PHLEX_REGISTER_RESOURCES(r, config) +{ + r.add_unlimited_resource(config.get("value")); + r.add_serialized_resource(); +} diff --git a/test/plugins/resources_for_testing.hpp b/test/plugins/resources_for_testing.hpp new file mode 100644 index 000000000..914cc6f0f --- /dev/null +++ b/test/plugins/resources_for_testing.hpp @@ -0,0 +1,16 @@ +#ifndef TEST_PLUGINS_RESOURCES_FOR_TESTING_HPP +#define TEST_PLUGINS_RESOURCES_FOR_TESTING_HPP + +namespace test::plugins { + struct configured_resource { + explicit configured_resource(int value) : value{value} {} + + int value; + }; + + struct serialized_resource { + using token_type = serialized_resource const*; + }; +} + +#endif // TEST_PLUGINS_RESOURCES_FOR_TESTING_HPP diff --git a/test/plugins/test_resources.jsonnet b/test/plugins/test_resources.jsonnet new file mode 100644 index 000000000..627d287d5 --- /dev/null +++ b/test/plugins/test_resources.jsonnet @@ -0,0 +1,24 @@ +{ + resources: { + resources_for_testing: { + cpp: 'register_resources_for_testing', + value: 42, + }, + }, + driver: { + cpp: 'generate_layers', + layers: { + event: { parent: 'job', total: 10, starting_number: 1 }, + }, + }, + sources: { + provider: { + cpp: 'ij_source', + }, + }, + modules: { + verify_resources: { + cpp: 'verify_resources', + }, + }, +} diff --git a/test/plugins/verify_resources.cpp b/test/plugins/verify_resources.cpp new file mode 100644 index 000000000..7bb366725 --- /dev/null +++ b/test/plugins/verify_resources.cpp @@ -0,0 +1,22 @@ +#include "phlex/module.hpp" +#include "test/plugins/resources_for_testing.hpp" + +#include + +using namespace phlex; +using namespace test::plugins; + +PHLEX_REGISTER_ALGORITHMS(m) +{ + m.observe( + "verify_resources", + [](int const, configured_resource const* configured, serialized_resource const* serialized) { + assert(configured != nullptr); + assert(configured->value == 42); + assert(serialized != nullptr); + }, + concurrency::unlimited) + .input_family(product_selector{.creator = "input", .layer = "event", .suffix = "i"}, + resource{}, + resource{}); +}