Skip to content

Commit 006a82a

Browse files
committed
added some explanation and corrected a readme file
1 parent f956b37 commit 006a82a

3 files changed

Lines changed: 239 additions & 18 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# LoadLibraries — Detailed explanation
2+
3+
This folder contains a small utility library and example that demonstrates loading shared libraries at runtime (plugins) using the POSIX `dlopen`/`dlclose` API and registering functions exposed by those libraries in a generic factory. The design focuses on simple, safe handling of dynamic libraries via RAII and on a straightforward mechanism for plugin registration using library constructors.
4+
5+
Files of interest
6+
- `LoadLibraries.hpp` / `LoadLibraries.cpp` — the loader class using `dlopen` and keeping handles in a map. Implements load-from-file, load-single-library, close, and get-handle operations.
7+
- `main.cpp` — example program that uses `LoadLibraries` and a small `FunctionFactory` to execute functions exported by the loaded libraries.
8+
- `libtraits.hpp` / `libFactory.cpp` — glue that defines the `FunFactory` type and provides the shared factory instance.
9+
- `lib1.cpp`, `lib2.cpp` — example plugin libraries exposing functions by registering them with `funFactory` in a constructor function.
10+
- `libraries.txt` — list of shared-object files to load (one per line).
11+
- `Makefile` / `README.md` — build/run notes.
12+
13+
High-level design and methodology
14+
15+
1. Dynamic loading with RAII
16+
- `apsc::LoadLibraries` wraps the `dlopen`/`dlclose` calls and keeps loaded handles in an internal `std::unordered_map<std::string, void*> loadedLibs`.
17+
- The `LoadLibraries` destructor calls `close()` to `dlclose` any opened handles (RAII): libraries are closed automatically when the loader object is destroyed.
18+
- The loader exposes `load(fileName)` to read a file with library names and `loadSingleLibrary(libName)` to open an individual library and add it to `loadedLibs`.
19+
20+
2. Plugin registration via constructors
21+
- Each plugin (e.g., `lib1.cpp`, `lib2.cpp`) declares some functions and registers them in a shared factory `funFactory` by using a constructor function annotated with `__attribute__((constructor))`.
22+
- Example in `lib1.cpp`:
23+
```cpp
24+
__attribute__((constructor)) void load() {
25+
funFactory.add("norm2", FunType{norm2});
26+
funFactory.add("norminf", FunType{norminf});
27+
}
28+
```
29+
- The constructor function runs automatically when the shared object is loaded with `dlopen`, inserting function wrappers into the global factory.
30+
31+
3. Shared factory for exported symbols
32+
- `libtraits.hpp` defines types used for the example: `FunType` (callable signature) and `FunFactory` (a `FunctionFactory` from `Factory.hpp`).
33+
- `libFactory.cpp` defines `FunFactory& funFactory = FunFactory::Instance();` — a global factory instance accessible by both the main program and the dynamically loaded plugins (they all link to the same symbol instance).
34+
- When plugin constructor functions call `funFactory.add(...)`, the main program later queries the factory to obtain std::function wrappers that call the plugin functions.
35+
36+
Step-by-step runtime flow
37+
38+
1. The main program constructs a `LoadLibraries` instance with the name of a file listing libraries (e.g., `LoadLibraries libraries("libraries.txt");`).
39+
- `LoadLibraries::load()` reads the file line-by-line (trimming blanks) and calls `loadSingleLibrary(libName)` for each entry.
40+
41+
2. For each library file name, `loadSingleLibrary` calls `dlopen(libName.c_str(), mode)` and stores the returned handle in `loadedLibs`.
42+
- `dlopen` runs library initializers which call the plugins' constructor functions. Those constructors register functions into `funFactory`.
43+
44+
3. After libraries are loaded, the main code queries `funFactory.registered()` to get the list of registered names and calls `funFactory.get(name)` to retrieve a `FunType` callable for each registered function. The example then invokes these callables on a sample vector.
45+
46+
4. Before closing the libraries the main code clears `funFactory` (factory entries) to ensure no pointers to plugin code remain. Finally, it calls `libraries.close()` or relies on the `LoadLibraries` destructor to `dlclose` the libraries.
47+
48+
Important implementation details
49+
50+
- `LoadLibraries::load` reads `fileName`, extracts non-blank tokens from each line and calls `loadSingleLibrary`. It prints a message for empty lines.
51+
- `LoadLibraries::loadSingleLibrary` checks if the library is already present in `loadedLibs`; if not it calls `dlopen` and stores the handle; on failure it prints `dlerror()` and returns false.
52+
- `LoadLibraries::close()` iterates loaded handles and calls `dlclose`; `close(libName)` closes a single library by name.
53+
- `LoadLibraries::getLibraryHandle(libName)` returns the raw `void*` handle (useful for `dlsym` or advanced operations).
54+
55+
Safety, lifetime and pitfalls
56+
57+
- Lifetime coupling: plugin objects and function pointers often refer to code/data in the loaded shared object. After `dlclose`, those addresses become invalid. Therefore:
58+
- Keep the `LoadLibraries` instance alive as long as you need plugin-provided code/data.
59+
- Do not keep callable wrappers (or objects) around after you `dlclose` the library; clear factory entries first.
60+
61+
- Symbol visibility: the plugins and the main program must agree on where the shared factory `funFactory` is defined. The typical patterns are:
62+
- Define `funFactory` in a shared library that both the main program and plugins link against.
63+
- Or define it in the main executable and ensure exported symbols are visible to dlopen-ed libraries (on Linux use `-rdynamic` when linking the main executable so the symbol is exported).
64+
- In this project `libFactory.cpp` creates `funFactory` using `FunFactory::Instance()`; ensure build/link settings make this symbol available to plugins.
65+
66+
- Constructor attribute portability:
67+
- The code uses GCC/Clang `__attribute__((constructor))` to run registration code at `dlopen` time. This is convenient but not standard C++.
68+
- Portable alternative: export a known C function (e.g. `extern "C" void register_plugin()`), and after `dlopen` call `dlsym(handle, "register_plugin")` and call it explicitly.
69+
70+
- Error handling and diagnostics:
71+
- `LoadLibraries` prints `dlerror()` on `dlopen` failure. You may want to propagate errors or throw exceptions in library code depending on how critical plugin loading is.
72+
73+
- Threading:
74+
- Neither `funFactory` nor `LoadLibraries` use internal synchronization. If loading plugins concurrently or registering from multiple threads, add mutexes.
75+
76+
Example usage (main.cpp flow)
77+
78+
1. Build shared libs (`lib1.so`, `lib2.so`) and place them relative to `libraries.txt` entries.
79+
2. Run the test program. It will load each .so, plugin constructors register function names (`norm2`, `norminf`, `norm1`, `norm0`) into `funFactory`.
80+
3. The example obtains the list of registered functions and executes each on a sample vector.
81+
82+
Commands (example, from inside `src/LoadLibraries`)
83+
84+
```bash
85+
# build shared libraries and example (project uses provided Makefile targets)
86+
make alllibs
87+
make exec
88+
./main # or the produced test executable
89+
```
90+
91+
Potential improvements and alternatives
92+
93+
- Replace constructor-based registration with an explicit `register_plugin` symbol + `dlsym` for portability and control.
94+
- Add logging and more robust error propagation instead of printing to `stderr`.
95+
- Make `LoadLibraries` thread-safe.
96+
- Allow versioning or plugin metadata parsing to avoid loading incompatible plugins.
97+
98+
Conclusion
99+
100+
The `LoadLibraries` folder implements a lightweight, clear demonstration of runtime plugin loading and registration via `dlopen` and constructor attributes. It is suitable for didactic purposes and small projects but should be adapted (explicit registration, symbol visibility checks, thread-safety) for production-level plugin systems.
101+
102+
---
103+
104+
If you want, I can:
105+
- Add a `dlsym`-based registration example (modify a plugin to provide `register_plugin()` and update `LoadLibraries::loadSingleLibrary` to call it), or
106+
- Add a short README snippet with exact build commands (Makefile commands) tailored to your environment. Which would you like next?

Examples/src/Parallel/MPI/PMatrix/README.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
*(if you have a compilation error see the note at the bottom)*
44

5-
In this example we illustrate a class template that represents a parallel (full) matrix and is able to perform matrix-vector multiplication in parallel in an MPI environment. It supports two types of matrix partitioning: `block row` and `block column`. I recall that they consist in distibuting the rows of the matrix, respectively the columns, to the different processes so that the local number of rows (columns) is (nearly) the same.
5+
In this example we illustrate a class template that represents a parallel (full) matrix and is able to perform matrix-vector multiplication in parallel in an MPI environment. It supports two types of matrix partitioning: `block row` and `block column`. I recall that they consist in distributing the rows of the matrix, respectively the columns, to the different processes so that the local number of rows (columns) is (nearly) the same.
66

7-
The choice of making row-based or column-based partitioning is automatically determined by the type of the `Matrix` template parameter. If it is a row-wise ordered matrix the row block partition is adopted, and viceversa. The class has a default constructor and you can pass the global matrix to be partitioned and the MPI commuinicator using the method `setup`. Indeed, we have adopted the strategy where the master process (by default of rank 0) has the global matrix (a different technique is to have each process set the local matrix, maybe by reading from a file, but here it is not implemented).
7+
The choice of making row-based or column-based partitioning is automatically determined by the type of the `Matrix` template parameter. If it is a row-wise ordered matrix the row block partition is adopted, and vice versa. The class has a default constructor and you can pass the global matrix to be partitioned and the MPI communicator using the method `setup`. Indeed, we have adopted the strategy where the master process (by default of rank 0) has the global matrix (a different technique is to have each process set the local matrix, maybe by reading from a file, but here it is not implemented).
88

99
Let's see an example, taken from the test program present in this directory.
1010

@@ -30,56 +30,56 @@ All processes must have a copy of the global vector with which we want to multip
3030
The result may be extracted with the method `collectGlobal()`
3131

3232
```
33-
std::vector<douuble> result;
33+
std::vector<double> result;
3434
pmr.collectGlobal(result);
3535
```
3636
In this form, **only the master process obtains the result**, in the other processes `result` is an empty vector. If you want all processes to get the result (more costly since you need an all-to-all communication), you do
3737

3838
```
39-
std::vector<douuble> result;
39+
std::vector<double> result;
4040
pmr.AllCollectGlobal(result);
4141
```
4242
That's it.
4343

4444
## Note on the procedure for parallel matrix-vector product ##
45-
Let the (global) matrix dimensions be `m X n` , `p` the number of processes, and `v` the vector to be multiplied with, of dimension `n`.
46-
We need to distinguish the two cases
45+
Let the (global) matrix dimensions be `m x n`, `p` the number of processes, and `v` the vector to be multiplied with, of dimension `n`.
46+
We need to distinguish the two cases:
4747

48-
** Row block partition case**
48+
**Row block partition case**
4949

5050
- After the partition, each process `i` owns a local matrix `A_i` of dimension `l x n`, where `l` is at most `m/p +1`;
5151
- The local matrix-vector multiplication is carried out normally: `r_i=A_i v`, and `r_i` has dimension `l`;
52-
- The global solution vector is obtained by gathering the contribution `r_i` of each process using `MPI_gatherv` or `MPI_Allgaterv`.
52+
- The global solution vector is obtained by gathering the contribution `r_i` of each process using `MPI_Gatherv` or `MPI_Allgatherv`.
5353

54-
** Column block partition case**
54+
**Column block partition case**
5555

5656
- After the partition, each process `i` owns a local matrix `A_i` of dimension `m x l`, where `l` is at most `n/p +1`;
5757
- The local matrix-vector multiplication is carried out by extracting from `v` the `l` rows corresponding to the global index of the columns of
5858
`A` contained in `A_i`. The local solution vector `r_i=A_i v_i` has dimension `m`;
5959
- The global solution vector is obtained by summing the local solution `r_i` of each process using `MPI_Reduce` or `MPI_Allreduce`.
6060

6161
## Note on the result of the test program ##
62-
First of all in the compilation I have not activated openMP. If you do, by adding `-fopenmp` to the `CXXFLAGS` in `Makefile.inc` you have an hybrid parallelization, since the class `Matrix` uses multithreading in the local matrix-vector product. However, not always hybrid parallelism works well in a computer not meant for hybrid parallel computing like a normal PC, that's why I have taken it away by default. Remember to compile with
62+
First of all, in the compilation I have not activated OpenMP. If you do, by adding `-fopenmp` to the `CXXFLAGS` in `Makefile.inc` you have a hybrid parallelization, since the class `Matrix` uses multithreading in the local matrix-vector product. However, hybrid parallelism does not always work well on a computer not meant for hybrid parallel computing like a normal PC, that's why I have taken it away by default. Remember to compile with
6363

6464
```
6565
make DEBUG=no
6666
```
67-
if you want to get full optimization (do a `make clean` before, to be sure you recompile everithing).
67+
if you want to get full optimization (do a `make clean` before, to be sure you recompile everything).
6868

69-
Try to run the code using different number of processes (you are clearly limited by your machine...)
69+
Try to run the code using a different number of processes (you are clearly limited by your machine...):
7070

7171
```
7272
mpirun -n <NP> ./main_PMatrix
7373
```
7474

75-
You may note that the setup time, i.e. the time taken to partition the matrix and distribute it, is dominant (at least in a normal PC computer), while the time for the actual matrix-vector parallel execution is rather satisfactory, showing a reasonable speedup even on a simple multicore PC. The conclusions are
75+
You may note that the setup time, i.e. the time taken to partition the matrix and distribute it, is dominant (at least on a normal PC computer), while the time for the actual matrix-vector parallel execution is rather satisfactory, showing a reasonable speedup even on a simple multicore PC. The conclusions are:
7676

77-
- The technique where a master process distributes the global matrix partitiones to the processes implies a certain overhead due to the need of communicating large amount of data during the setup phase. In prectical situations, it's better to have each processor build its own local matrix, whenever possible.
78-
- the parallel matrix-vector computation becomes more advantageous if repeated many times with the same matrix. Indeed, in this case the setup phase overhed may become irrelevant. Luckily, this is the most common situation, for instance in iterative techniques for the solution of linear systems.
77+
- The technique where a master process distributes the global matrix partitions to the processes implies a certain overhead due to the need of communicating large amounts of data during the setup phase. In practical situations, it's better to have each processor build its own local matrix, whenever possible.
78+
- The parallel matrix-vector computation becomes more advantageous if repeated many times with the same matrix. Indeed, in this case the setup phase overhead may become irrelevant. Luckily, this is the most common situation, for instance in iterative techniques for the solution of linear systems.
7979

8080
## Note on the use of the class ##
8181

82-
The `PMatrix.hpp` file includes other header files, in particular `partitioner.hpp`, `mpi_utils.h` and `Matrix.hpp`. To have them in the include directory of the Examples and thus available for the compilation of the test program using `make`, you need to
82+
The `PMatrix.hpp` file includes other header files, in particular `partitioner.hpp`, `mpi_utils.h` and `Matrix.hpp`. To have them in the include directory of the Examples and thus available for the compilation of the test program using `make`, you need to:
8383

84-
- go in the `Parallel/Utilities/` forlder and type `make install`
85-
- go in the `Matrix/` folder and type `make install`
84+
- go to the `Parallel/Utilities/` folder and type `make install`
85+
- go to the `Matrix/` folder and type `make install`

0 commit comments

Comments
 (0)