diff --git a/.github/workflows/cpp-examples.yml b/.github/workflows/cpp-examples.yml new file mode 100644 index 0000000..7dc8ed1 --- /dev/null +++ b/.github/workflows/cpp-examples.yml @@ -0,0 +1,48 @@ +name: C++ Examples + +# Builds the C++ example consumers and runs their tests on Linux, Windows and +# macOS. This guards the public headers' extern "C" guards: the examples +# include dedx.h / dedx_tools.h / dedx_wrappers.h from C++ translation units +# and link the C library, so a regression in the guards breaks this workflow. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +# Build/test only: the token needs no more than read access to the checkout. +permissions: + contents: read + +jobs: + cpp-examples: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Tool versions + run: | + cmake --version + cmake -E echo "Generator/compiler resolved at configure time" + + - name: Configure + run: > + cmake -S . -B build + -DCMAKE_BUILD_TYPE=Release + -DDEDX_BUILD_EXAMPLES=ON + -DDEDX_BUILD_CXX_EXAMPLES=ON + -DDEDX_BUILD_TESTS=OFF + + - name: Build + run: cmake --build build --config Release --parallel + + - name: Run C++ example tests + run: ctest --test-dir build -C Release -R dedx_cpp --output-on-failure diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b182d59..3ba4353 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -40,3 +40,9 @@ add_test(NAME getdedx_lookup COMMAND getdedx 2 1 276 100) add_test(NAME getdedx_list_programs COMMAND getdedx -1 1 276 100) install(TARGETS getdedx DESTINATION "${CMAKE_INSTALL_BINDIR}") + +# C++ example consumers (demonstrate the extern "C" public headers from C++). +option(DEDX_BUILD_CXX_EXAMPLES "Build C++ example consumers (requires a C++ compiler)" ON) +if(DEDX_BUILD_CXX_EXAMPLES) + add_subdirectory(cpp) +endif() diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..97234c7 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,106 @@ +# libdedx examples + +Small, self-contained programs that show how to call libdedx. The C programs +live in this directory; the C++ programs (which consume the public headers +through their `extern "C"` guards) live in [`cpp/`](cpp/). + +Every code block below is a **single command** — use the copy button and run +them one at a time, in order. All paths are relative to the repository root. + +## Prerequisites + +- CMake 3.21 or newer +- A C11 compiler (and a C++17 compiler for the `cpp/` examples) + +## 1. Configure + +```bash +cmake --preset debug +``` + +## 2. Build everything (library + examples) + +```bash +cmake --build --preset debug +``` + +## 3. Run the C examples + +```bash +./build/examples/dedx_example +``` + +```bash +./build/examples/dedx_list +``` + +```bash +./build/examples/dedx_use_wrappers +``` + +```bash +./build/examples/dedx_bethe +``` + +```bash +./build/examples/dedx_csda +``` + +```bash +./build/examples/dedx_custom_compound +``` + +## 4. Run the C++ examples + +```bash +./build/examples/cpp/dedx_cpp_core +``` + +```bash +./build/examples/cpp/dedx_cpp_wrappers +``` + +## 5. Use the `getdedx` command-line tool + +Look up PSTAR stopping power for a proton in water at 100 MeV/nucl: + +```bash +./build/examples/getdedx 2 1 276 100 +``` + +## Run examples as a test suite + +Run every example test: + +```bash +ctest --preset debug --output-on-failure +``` + +Run only the C++ example tests: + +```bash +ctest --preset debug --output-on-failure -R dedx_cpp +``` + +## Building without the C++ examples + +If you have no C++ compiler, turn the C++ examples off at configure time: + +```bash +cmake --preset debug -DDEDX_BUILD_CXX_EXAMPLES=OFF +``` + +## Windows note + +The Visual Studio generator is multi-config, so the binaries land in a +per-config subfolder. Build with an explicit config: + +```bash +cmake --build build --config Release +``` + +Then run from the matching folder, e.g.: + +```bash +./build/examples/cpp/Release/dedx_cpp_core.exe +``` diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt new file mode 100644 index 0000000..de99a3f --- /dev/null +++ b/examples/cpp/CMakeLists.txt @@ -0,0 +1,32 @@ +# C++ example consumers. +# +# These demonstrate that the public libdedx headers can be included and linked +# from C++ thanks to their extern "C" guards. The library remains C-only, so +# C++ is enabled lazily here and the whole subdirectory is skipped when no C++ +# compiler is available. +include(CheckLanguage) +check_language(CXX) +if(NOT CMAKE_CXX_COMPILER) + message(STATUS "No C++ compiler found; skipping C++ example consumers") + return() +endif() +enable_language(CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(cpp_examples dedx_cpp_core dedx_cpp_wrappers) + +foreach(example ${cpp_examples}) + add_executable(${example} ${example}.cpp) + # Linking the `dedx` target transitively pulls in its PUBLIC dependencies + # (e.g. the math library on Unix), so no per-platform handling is needed. + target_link_libraries(${example} PRIVATE dedx) + if(MSVC) + target_compile_options(${example} PRIVATE /W4 /permissive-) + else() + target_compile_options(${example} PRIVATE -Wall -Wextra) + endif() + add_test(NAME ${example} COMMAND ${example}) +endforeach() diff --git a/examples/cpp/dedx_cpp_core.cpp b/examples/cpp/dedx_cpp_core.cpp new file mode 100644 index 0000000..8922bb3 --- /dev/null +++ b/examples/cpp/dedx_cpp_core.cpp @@ -0,0 +1,51 @@ +// Core (stateful) libdedx API consumed from C++. +// +// Builds a proton-in-water stopping-power / CSDA-range table using the RAII +// helpers, std::vector and exception-based error handling. Demonstrates that +// dedx.h and dedx_tools.h link cleanly into a C++ program now that the public +// headers carry extern "C" guards. +#include +#include +#include +#include +#include +#include + +#include "dedx_raii.hpp" + +int main() { + try { + auto ws = dedx::make_workspace(1); + + auto cfg = dedx::make_config(); + cfg->program = DEDX_PSTAR; + cfg->ion = DEDX_PROTON; + cfg->ion_a = 1; // nucleon number, required by dedx_get_csda + cfg->target = DEDX_WATER; + + int err = DEDX_OK; + dedx_load_config(ws.get(), cfg.get(), &err); + dedx::check(err, "dedx_load_config"); + + const std::vector energies = {1.0f, 5.0f, 10.0f, 50.0f, 100.0f, 250.0f}; + + std::cout << "Proton stopping power & CSDA range in liquid water (PSTAR)\n"; + std::cout << " E [MeV/nucl] dE/dx [MeV cm^2/g] CSDA [g/cm^2]\n"; + std::cout << std::fixed << std::setprecision(4); + + for (const float e : energies) { + const float stp = dedx_get_stp(ws.get(), cfg.get(), e, &err); + dedx::check(err, "dedx_get_stp"); + + const double csda = dedx_get_csda(ws.get(), cfg.get(), e, &err); + dedx::check(err, "dedx_get_csda"); + + std::cout << std::setw(14) << e << std::setw(21) << stp << std::setw(16) << csda << '\n'; + } + + return EXIT_SUCCESS; + } catch (const dedx::error &ex) { + std::cerr << "libdedx error: " << ex.what() << '\n'; + return EXIT_FAILURE; + } +} diff --git a/examples/cpp/dedx_cpp_wrappers.cpp b/examples/cpp/dedx_cpp_wrappers.cpp new file mode 100644 index 0000000..ca5d056 --- /dev/null +++ b/examples/cpp/dedx_cpp_wrappers.cpp @@ -0,0 +1,74 @@ +// Stateless wrapper + tools libdedx APIs consumed from C++. +// +// Reads the program's default tabulated grid, evaluates a custom energy grid +// in one batched call, and converts mass stopping power to linear stopping +// power — all backed by std::vector. Exercises dedx_wrappers.h and +// dedx_tools.h from a C++ translation unit. +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dedx_raii.hpp" + +int main() { + try { + constexpr int program = DEDX_PSTAR; + constexpr int ion = DEDX_PROTON; + constexpr int target = DEDX_WATER_LIQUID; + + // 1) Default tabulated grid shipped with the program. + const int n = dedx_get_stp_table_size(program, ion, target); + if (n <= 0) { + std::cerr << "no tabulated data for this program/ion/target\n"; + return EXIT_FAILURE; + } + + std::vector grid_e(static_cast(n)); + std::vector grid_stp(static_cast(n)); + // Returns 0 on success / negative on error; the point count is `n`. + if (dedx_fill_default_energy_stp_table(program, ion, target, grid_e.data(), grid_stp.data()) < 0) { + std::cerr << "dedx_fill_default_energy_stp_table failed\n"; + return EXIT_FAILURE; + } + std::cout << "Default PSTAR grid: " << n << " points, " << grid_e.front() << " - " << grid_e.back() + << " MeV/nucl\n"; + + // 2) Evaluate an arbitrary energy grid in a single batched call. + const std::vector energies = {2.0f, 20.0f, 200.0f}; + std::vector stps(energies.size()); + dedx::check( + dedx_get_stp_table(program, ion, target, static_cast(energies.size()), energies.data(), stps.data()), + "dedx_get_stp_table"); + + // 3) Convert mass stopping power [MeV cm^2/g] to linear units. For + // liquid water (rho = 1 g/cm^3) the MeV/cm value coincides + // numerically with the mass value, so keV/um is shown too to make + // the unit change visible. + const auto convert = [&](int to_unit) { + std::vector out(stps.size()); + dedx::check( + convert_units(DEDX_MEVCM2G, to_unit, target, static_cast(stps.size()), stps.data(), out.data()), + "convert_units"); + return out; + }; + const std::vector stps_mevcm = convert(DEDX_MEVCM); + const std::vector stps_kevum = convert(DEDX_KEVUM); + + std::cout << " E [MeV/nucl] dE/dx [MeV cm^2/g] dE/dx [MeV/cm] dE/dx [keV/um]\n"; + std::cout << std::fixed << std::setprecision(4); + for (std::size_t i = 0; i < energies.size(); ++i) { + std::cout << std::setw(14) << energies[i] << std::setw(21) << stps[i] << std::setw(16) << stps_mevcm[i] + << std::setw(16) << stps_kevum[i] << '\n'; + } + + return EXIT_SUCCESS; + } catch (const dedx::error &ex) { + std::cerr << "libdedx error: " << ex.what() << '\n'; + return EXIT_FAILURE; + } +} diff --git a/examples/cpp/dedx_raii.hpp b/examples/cpp/dedx_raii.hpp new file mode 100644 index 0000000..20ae87a --- /dev/null +++ b/examples/cpp/dedx_raii.hpp @@ -0,0 +1,90 @@ +// Minimal modern-C++ RAII helpers around the libdedx C API. +// +// Their sole purpose is to demonstrate that the public headers — now wrapped +// in `extern "C"` linkage guards — can be consumed directly from a C++ +// translation unit with no consumer-side `extern "C"` wrapping. The helpers +// are intentionally tiny and header-only; libdedx itself stays a pure C +// library and these are *not* an official C++ binding. +#ifndef DEDX_EXAMPLES_CPP_RAII_HPP +#define DEDX_EXAMPLES_CPP_RAII_HPP + +#include +#include +#include +#include +#include + +namespace dedx { + +/// Exception carrying a libdedx error code plus the library's own message. +class error : public std::runtime_error { + public: + error(int code, const std::string &context) : std::runtime_error(build_message(code, context)), code_(code) { + } + + [[nodiscard]] int code() const noexcept { + return code_; + } + + private: + static std::string build_message(int code, const std::string &context) { + char buf[256] = {}; + dedx_get_error_code(buf, code); + return context + ": " + buf + " (code " + std::to_string(code) + ")"; + } + + int code_; +}; + +/// Throw dedx::error unless `err` is DEDX_OK. +inline void check(int err, const char *context) { + if (err != DEDX_OK) { + throw error(err, context); + } +} + +namespace detail { + +struct workspace_deleter { + void operator()(dedx_workspace *ws) const noexcept { + int err = DEDX_OK; + dedx_free_workspace(ws, &err); + } +}; + +struct config_deleter { + // dedx_free_config frees the struct's internal arrays *and* free()s the + // struct itself, so make_config() below allocates with the C allocator. + void operator()(dedx_config *cfg) const noexcept { + int err = DEDX_OK; + dedx_free_config(cfg, &err); + } +}; + +} // namespace detail + +using workspace = std::unique_ptr; +using config = std::unique_ptr; + +/// Allocate a workspace; throws dedx::error on failure. +[[nodiscard]] inline workspace make_workspace(unsigned int count) { + int err = DEDX_OK; + dedx_workspace *ws = dedx_allocate_workspace(count, &err); + check(err, "dedx_allocate_workspace"); + return workspace{ws}; +} + +/// Allocate a zero-initialised config. The deleter routes cleanup through +/// dedx_free_config (which calls free()), so the C allocator is used here to +/// keep the alloc/free pair consistent. +[[nodiscard]] inline config make_config() { + auto *cfg = static_cast(std::calloc(1, sizeof(dedx_config))); + if (cfg == nullptr) { + throw error(DEDX_ERR_NO_MEMORY, "make_config"); + } + return config{cfg}; +} + +} // namespace dedx + +#endif // DEDX_EXAMPLES_CPP_RAII_HPP diff --git a/include/dedx.h b/include/dedx.h index c424bd9..14bc5df 100644 --- a/include/dedx.h +++ b/include/dedx.h @@ -4,6 +4,10 @@ #include "dedx_elements.h" #include "dedx_error.h" +#ifdef __cplusplus +extern "C" { +#endif + /** * @file dedx.h * @brief Public API for libdedx — a stopping power (dE/dx) library. @@ -294,4 +298,8 @@ float dedx_get_simple_stp(int ion, int target, float energy, int *err); */ void dedx_free_config(dedx_config *config, int *err); +#ifdef __cplusplus +} // extern "C" +#endif + #endif // DEDX_H diff --git a/include/dedx_elements.h b/include/dedx_elements.h index 6f55de6..9680845 100644 --- a/include/dedx_elements.h +++ b/include/dedx_elements.h @@ -1,6 +1,10 @@ #ifndef DEDX_ELEMENTS_H #define DEDX_ELEMENTS_H +#ifdef __cplusplus +extern "C" { +#endif + /** Maximum number of tabulated energy points stored per dataset. */ #define DEDX_MAX_ELEMENTS 150 @@ -326,4 +330,8 @@ enum { #define DEDX_CAESIUM DEDX_CESIUM /** @} */ +#ifdef __cplusplus +} // extern "C" +#endif + #endif // DEDX_ELEMENTS_H diff --git a/include/dedx_error.h b/include/dedx_error.h index b69d7b7..19cfc06 100644 --- a/include/dedx_error.h +++ b/include/dedx_error.h @@ -1,6 +1,10 @@ #ifndef DEDX_ERROR_H #define DEDX_ERROR_H +#ifdef __cplusplus +extern "C" { +#endif + /** * @file dedx_error.h * @brief Error codes returned via the int *err output parameter. @@ -53,4 +57,8 @@ #define DEDX_ERR_NO_MEMORY 301 /**< memory allocation failed */ /** @} */ +#ifdef __cplusplus +} // extern "C" +#endif + #endif /* DEDX_ERROR_H */ diff --git a/include/dedx_tools.h b/include/dedx_tools.h index a453d4e..9f761f5 100644 --- a/include/dedx_tools.h +++ b/include/dedx_tools.h @@ -8,6 +8,10 @@ #include "dedx.h" +#ifdef __cplusplus +extern "C" { +#endif + /** * @brief Units for stopping power values. */ @@ -71,4 +75,8 @@ int convert_units(const int old_unit, const float *old_values, float *new_values); +#ifdef __cplusplus +} // extern "C" +#endif + #endif // DEDX_TOOLS_H diff --git a/include/dedx_wrappers.h b/include/dedx_wrappers.h index 31b59c2..edb85ba 100644 --- a/include/dedx_wrappers.h +++ b/include/dedx_wrappers.h @@ -11,6 +11,10 @@ #include "dedx.h" +#ifdef __cplusplus +extern "C" { +#endif + /** @brief Fill an array with all supported program identifiers. * @param[out] program_list Caller-allocated array; must be large enough to * hold all programs (use dedx_get_program_list() to @@ -97,4 +101,8 @@ int dedx_get_csda_range_table(const int program, const float *energies, double *csda_ranges); +#ifdef __cplusplus +} // extern "C" +#endif + #endif // DEDX_WRAPPERS_H