Skip to content

Commit 2536e09

Browse files
committed
Pre-parse plugin options hook for dynamic plugin option registration
1 parent c5e743b commit 2536e09

5 files changed

Lines changed: 176 additions & 1 deletion

File tree

gemc.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424

2525
int main(int argc, char* argv[]) {
2626

27-
auto gopts = std::make_shared<GOptions>(argc, argv, gemc::defineOptions());
27+
auto base_schema = gemc::defineOptions();
28+
base_schema += gemc::collectPluginOptions(argc, argv);
29+
auto gopts = std::make_shared<GOptions>(argc, argv, base_schema);
2830
auto log = std::make_shared<GLogger>(gopts, SFUNCTION_NAME, GENERAL_LOGGER);
2931

3032
auto gui = gopts->getSwitch("gui");

gemc_options.cc

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@
33
#include "gemc_options.h"
44
#include "gemcConventions.h"
55

6+
// yaml-cpp — needed by collectPluginOptions for the bootstrap YAML scan
7+
#include "yaml-cpp/yaml.h"
8+
9+
// POSIX dynamic loading
10+
#include <dlfcn.h>
11+
12+
// c++
13+
#include <filesystem>
14+
#include <set>
15+
#include <sstream>
16+
617
// options definitions
718
#include "dbselect_options.h"
819
#include "gstreamer_options.h"
@@ -83,4 +94,103 @@ namespace gemc {
8394
}
8495

8596

97+
GOptions collectPluginOptions(int argc, char* argv[]) {
98+
GOptions merged;
99+
100+
// Build the plugin search path from three sources, checked in priority order:
101+
// 1. -plugin_path=... on the command line (not yet parsed)
102+
// 2. plugin_path: value in any YAML file listed in argv
103+
// 3. GEMC_PLUGIN_PATH environment variable
104+
// Source 1 is extracted first; source 2 is accumulated while scanning YAML files
105+
// below; source 3 is appended after both.
106+
std::string search_path;
107+
for (int i = 1; i < argc; ++i) {
108+
std::string_view arg = argv[i];
109+
if (arg.size() > 13 && arg.substr(0, 13) == "-plugin_path=") {
110+
const auto val = std::string(arg.substr(13));
111+
if (!search_path.empty()) search_path += ':';
112+
search_path += val;
113+
}
114+
}
115+
116+
// Scan every YAML file present in argv. For each file:
117+
// - collect any plugin_path value declared there
118+
// - collect gsystem names to probe for a definePluginOptions symbol
119+
std::set<std::string> seen_names; // deduplicate across multiple YAML files
120+
for (int i = 1; i < argc; ++i) {
121+
const std::string arg = argv[i];
122+
if (!std::filesystem::exists(arg)) continue;
123+
124+
YAML::Node root;
125+
try { root = YAML::LoadFile(arg); } catch (...) { continue; }
126+
127+
// Pick up plugin_path declared in this YAML file (prepend so YAML paths
128+
// take priority over the env var but not over command-line paths).
129+
if (root["plugin_path"]) {
130+
try {
131+
const auto yaml_pp = root["plugin_path"].as<std::string>();
132+
if (!yaml_pp.empty()) {
133+
search_path = search_path.empty()
134+
? yaml_pp
135+
: search_path + ':' + yaml_pp;
136+
}
137+
} catch (...) {}
138+
}
139+
140+
if (!root["gsystem"]) continue;
141+
for (const auto& entry : root["gsystem"]) {
142+
if (!entry["name"]) continue;
143+
std::string name;
144+
try { name = entry["name"].as<std::string>(); } catch (...) { continue; }
145+
if (name.empty() || !seen_names.insert(name).second) continue;
146+
147+
// Resolve <name>.gplugin using the accumulated search path plus
148+
// GEMC_PLUGIN_PATH and, as a last resort, the current directory.
149+
std::string full_search = search_path;
150+
if (const char* env = std::getenv("GEMC_PLUGIN_PATH")) {
151+
if (!full_search.empty()) full_search += ':';
152+
full_search += env;
153+
}
154+
155+
const std::string basename = name + ".gplugin";
156+
std::string lib_path;
157+
158+
if (!full_search.empty()) {
159+
std::istringstream ss(full_search);
160+
std::string dir;
161+
while (std::getline(ss, dir, ':')) {
162+
if (dir.empty()) continue;
163+
const auto candidate = dir + "/" + basename;
164+
if (std::filesystem::exists(candidate)) { lib_path = candidate; break; }
165+
}
166+
}
167+
if (lib_path.empty() && std::filesystem::exists(basename)) lib_path = basename;
168+
if (lib_path.empty()) continue; // no plugin for this system — normal
169+
170+
// Open the library. On Linux, RTLD_NODELETE keeps it resident so that the
171+
// later GManager dlopen gets the exact same handle without re-executing
172+
// static initialisers. On macOS the reference count achieves the same effect.
173+
#if defined(__linux__) && defined(RTLD_NODELETE)
174+
const auto h = dlopen(lib_path.c_str(), RTLD_NOW | RTLD_NODELETE);
175+
#else
176+
const auto h = dlopen(lib_path.c_str(), RTLD_NOW);
177+
#endif
178+
if (!h) continue; // library not loadable — skip silently
179+
180+
// The definePluginOptions symbol is optional. Plugins that need no custom
181+
// options simply omit it and are silently skipped here.
182+
// The symbol returns GOptions* (heap-allocated) so that extern "C" linkage
183+
// is not applied to a by-value C++ return type.
184+
using opt_fptr = GOptions* (*)();
185+
const auto sym = reinterpret_cast<opt_fptr>(dlsym(h, "definePluginOptions"));
186+
if (!sym) continue;
187+
188+
std::unique_ptr<GOptions> plugin_opts(sym());
189+
merged += *plugin_opts;
190+
}
191+
}
192+
193+
return merged;
194+
}
195+
86196
}

gemc_options.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,39 @@ namespace gemc {
99

1010
GOptions defineOptions();
1111

12+
/**
13+
* \brief Scans YAML files in argv for gsystem names and probes each corresponding
14+
* .gplugin for an optional \c definePluginOptions symbol.
15+
*
16+
* Called before the main GOptions parsing constructor so that plugin-specific
17+
* options are registered in the schema and appear in --help, saved config, and
18+
* command-line parsing alongside core GEMC options.
19+
*
20+
* The search path for plugins is assembled from (in priority order):
21+
* -# \c -plugin_path=... command-line arguments found in argv
22+
* -# \c plugin_path: entries found in YAML files listed in argv
23+
* -# the \c GEMC_PLUGIN_PATH environment variable
24+
* -# the current working directory
25+
*
26+
* Failures (missing plugin, missing symbol, bad YAML) are silently skipped;
27+
* not every system has a plugin and not every plugin declares custom options.
28+
*
29+
* \param argc Argument count from \c main().
30+
* \param argv Argument vector from \c main().
31+
* Plugin contract: the symbol must be declared as
32+
* \code{.cpp}
33+
* extern "C" GOptions* definePluginOptions();
34+
* \endcode
35+
* The returned pointer is heap-allocated and owned by the caller (deleted immediately
36+
* after merging). Returning a pointer rather than a value avoids the C-linkage /
37+
* non-trivial-return-type warning that clang emits for by-value C++ returns across
38+
* an \c extern "C" boundary.
39+
*
40+
* \return A \c GOptions containing only the option schema contributions from plugins.
41+
* Values are not parsed yet; that happens in the main constructor.
42+
*/
43+
GOptions collectPluginOptions(int argc, char* argv[]);
44+
1245
}
1346

1447

releases/0.3.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This version includes:
99
- Volume inspection from the geometry tree
1010
- Unified `value*unit` notation for `gparticle` kinematic fields
1111
- `GEMC_PLUGIN_PATH` and `-plugin_path` for external plugin discovery
12+
- Pre-parse plugin options hook for dynamic plugin option registration
1213
- ROOT gstreamer and event-action bug fixes
1314

1415
<br/>
@@ -349,3 +350,18 @@ Both x86_64 and ARM64 platforms are supported.
349350
the fallback warning. The Lund file reader pre-converts its computed doubles
350351
via `gutilities::getG4Number(value, unit)` before passing them to the
351352
constructor.
353+
- Added `gemc::collectPluginOptions(int argc, char* argv[])` declared in `gemc_options.h` and
354+
implemented in `gemc_options.cc`. Before the main `GOptions` parsing constructor is called in
355+
`main()`, GEMC scans every YAML file listed in `argv` for `gsystem` entries, resolves each
356+
`<name>.gplugin` using the plugin search path, and probes for an optional
357+
`extern "C" GOptions* definePluginOptions()` symbol. Options declared there are merged into
358+
the schema so plugin-specific options appear in `gemc <config>.yaml -h`, are saved to the
359+
configuration snapshot, and can be set from the command line or YAML alongside core options.
360+
The pre-parse search path honours `-plugin_path=` CLI arguments, `plugin_path:` YAML keys,
361+
and the `GEMC_PLUGIN_PATH` environment variable in that priority order. On Linux,
362+
`RTLD_NODELETE` keeps the probed library resident so the later `GManager::dlopen` during
363+
detector construction gets the same OS handle without re-executing static initialisers; on
364+
macOS, reference counting achieves the same effect. Plugin verbosity keys are registered by
365+
passing the channel name to the single-argument `GOptions` constructor inside
366+
`definePluginOptions`, adding `verbosity.<plugin>` to the schema independently of the global
367+
`gdigitization` verbosity.

releases/dev.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11

22
## Commits on main since 2026-05-21
33

4+
- 2026-06-09 **c5e743b** — fixed field options loading plugin path _(by Maurizio Ungaro)_
5+
- 2026-06-09 **fde83ef** — utility functions _(by Maurizio Ungaro)_
6+
- 2026-06-09 **9c8ddec** — documenting processGTouchableModifiersImpl _(by Maurizio Ungaro)_
7+
- 2026-06-08 **f6ec9c2** — added GEMC_PLUGIN_PATH and fixed case where nullptr for GDigitizedData was not picked up in gEventAction.cc _(by Maurizio Ungaro)_
8+
- 2026-06-08 **81da0ee** — less aggressive images cleanup and added explicit units in gparticle _(by Maurizio Ungaro)_
9+
- 2026-06-06 **e0aa4e4** — testing new test deploy and binary workflow _(by Maurizio Ungaro)_
10+
- 2026-06-06 **5cc9897** — Merge pull request #140 from Jah-yee/main _(by Mauri)_
11+
- 2026-06-06 **32a25c2** — fix: remove dead birkConstant branch in getMaterialPropertyFromString _(by Jah-yee)_
12+
- 2026-06-05 **b44333f** — added passive and active g4placements, default to active _(by Maurizio Ungaro)_
13+
- 2026-06-05 **1f5d536** — gui tweaks and added inspection button _(by Maurizio Ungaro)_
14+
- 2026-06-05 **096ce27** — replaced view buttons with svg _(by Maurizio Ungaro)_
15+
- 2026-06-05 **2fb4662** — added color buttons for scale and frame, and cleaned up ui _(by Maurizio Ungaro)_
16+
- 2026-06-05 **dd4e36f** — added description to gvolume _(by Maurizio Ungaro)_
17+
- 2026-06-05 **32227f9** — added center and twinkle button _(by Maurizio Ungaro)_
418
- 2026-06-05 **ccd929f** — moved g4dialog at the bottom in the src _(by Maurizio Ungaro)_
519
- 2026-06-05 **af0bfc3** — moved g4dialog at the bottom _(by Maurizio Ungaro)_
620
- 2026-06-05 **686151c** — doxygen style was missing _(by Maurizio Ungaro)_

0 commit comments

Comments
 (0)