Architecture Overview¶
rgpot follows a metatensor-style layered architecture: a Rust core defines the
fundamental types and RPC infrastructure, exposed via a C ABI (generated by
cbindgen), with hand-written C++ RAII wrappers on top. Existing C++ potentials
remain callable through an FFI callback mechanism.
Layer Diagram¶
+---------------------------------------------+
| C++ Consumer Layer (RAII wrappers) |
| include/rgpot/*.hpp |
+---------------------------------------------+
| C ABI (generated by cbindgen) |
| rgpot-core/include/rgpot.h |
+---------------------------------------------+
| Rust Core |
| rgpot-core/src/ |
| - Types (ForceInput, ForceOut) |
| - Potential trait + callback dispatch |
| - Cap'n Proto RPC client/server |
| - Error handling (status codes + TLS msg) |
+---------------------------------------------+
| FFI callbacks
+---------------------------------------------+
| Existing C++ Potentials (LJ, CuH2) |
| CppCore/rgpot/LennardJones/, CuH2/ |
+---------------------------------------------+
Layers in Detail¶
Rust Core (rgpot-core/)¶
The Rust crate is the single source of truth for the public API. It defines:
rgpot_force_input_tandrgpot_force_out_t#[repr(C)]structs mirroring the legacy C++ForceInput/ForceOutfromForceStructs.hpp. These are the canonical data exchange types across the FFI boundary.rgpot_status_tAn enum of status codes (
RGPOT_SUCCESS,RGPOT_INVALID_PARAMETER,RGPOT_INTERNAL_ERROR,RGPOT_RPC_ERROR,RGPOT_BUFFER_SIZE_ERROR). Everyextern "C"function returns one of these.rgpot_potential_tAn opaque handle wrapping a C function pointer callback plus a
void* user_data. This is how C++ potentials are registered into the Rust core without the core knowing the concrete type.rgpot_last_error()Thread-local last error message. On any non-success return, the caller can retrieve a human-readable string describing the failure.
catch_unwindwrapperEvery
extern "C"function boundary catches Rust panics and converts them toRGPOT_INTERNAL_ERRORplus an error message. This prevents undefined behaviour from unwinding across the FFI boundary.
Feature Gates¶
The crate uses Cargo feature flags to keep the core minimal:
rpcEnables Cap’n Proto RPC client and server. Adds dependencies on
capnp,capnp-rpc, andtokio. The RPC module reuses the existingPotentials.capnpschema shared with the C++ server.cacheReserved for future caching integration.
gen-headerEnables C header generation via
cbindgen. Used only during development; runpixi r gen-headerto regenerateinclude/rgpot.h.
Cap’n Proto Schema Sharing¶
The Cap’n Proto schema (Potentials.capnp) is bundled inside the Rust crate at
rgpot-core/schema/Potentials.capnp. This is the canonical copy used when
building from crates.io. In the monorepo, build.rs also checks
CppCore/rgpot/rpc/Potentials.capnp as a fallback. The Cap’n Proto wire format
is stable across language implementations, so Rust clients and C++ servers (or
vice versa) are fully interoperable.
C ABI (rgpot-core/include/rgpot.h)¶
The C header is auto-generated by cbindgen from the Rust source. Run pixi r gen-header (which invokes cargo build --features gen-header) to regenerate it.
This header is the contract between the Rust core and all C/C++ consumers. It
contains:
Type definitions for all
#[repr(C)]structs and enums.Forward declarations for opaque handles (
rgpot_potential_t,rgpot_rpc_client_t).Function declarations for the entire public API.
Doxygen-style documentation extracted from Rust doc comments.
#ifdef RGPOT_HAS_RPCguards around RPC-specific declarations.
This header is never edited by hand; any changes to the public API are made in the Rust source and regenerated.
C++ RAII Wrappers (include/rgpot/)¶
Hand-written C++ headers provide idiomatic wrappers around the C API:
errors.hppMaps
rgpot_status_tcodes torgpot::Errorexceptions viadetails::check_status(). Internally callsrgpot_last_error()to populate the exception message.types.hppProvides
rgpot::InputSpec(a lightweight view over borrowed input data) andrgpot::CalcResult(owns the force buffer and stores energy/variance). These are distinct from the legacyrgpot::ForceInput/rgpot::ForceOuttypes inForceStructs.hppto avoid name collisions.potential.hppThe
rgpot::PotentialHandleclass is a move-only RAII wrapper aroundrgpot_potential_t*. Key methods:calculate(const InputSpec&)Invokes the potential and returns a
CalcResult. Throwsrgpot::Erroron failure.from_impl<Impl>(impl)Template factory that wraps any C++ potential object with a compatible
forceImplmethod. Generates a type-safe trampoline function automatically.from_callback(fn, user_data, free_fn)Low-level factory taking an explicit C function pointer.
rpc_client.hppRAII wrapper around
rgpot_rpc_client_t*. Connects to a remote server on construction and providescalculate()with the sameInputSpec/CalcResultinterface. Guarded by#ifdef RGPOT_HAS_RPC.rgpot.hppAggregate header that includes everything.
Existing C++ Potentials¶
The CppCore/rgpot/ directory contains the original potential implementations
(LJPot, CuH2Pot) using the CRTP-based Potential<Derived> template. These
remain fully functional and unmodified. To use them through the new API, pass
them to PotentialHandle::from_impl<>(), which generates a type-safe trampoline
that converts between the C ABI structs and the legacy ForceInput / ForceOut
types automatically.
Error Handling Conventions¶
The project uses a two-tier error strategy:
Across the FFI boundary :: Status codes (
rgpot_status_t) plus a thread-local error message (rgpot_last_error()). No exceptions cross FFI. Every Rustextern "C"function wraps its body incatch_unwindto convert panics toRGPOT_INTERNAL_ERROR.Within C++ :: The RAII wrappers call
details::check_status()which maps non-success codes torgpot::Errorexceptions. C++ consumers work with exceptions as usual.Within Rust :: Standard
Resulttypes internally, converted to status codes at theextern "C"boundary.
Adding a New Potential¶
To register a new C++ potential with the Rust core:
Implement the potential class inheriting from
Potential<MyPot>as usual. The class must providevoid forceImpl(const ForceInput&, ForceOut*) const.Use the RAII wrapper to create a handle:
#include "rgpot/potential.hpp"
#include "my_pot/MyPot.hpp"
rgpot::MyPot my_pot;
auto handle = rgpot::PotentialHandle::from_impl(my_pot);
auto result = handle.calculate(input_spec);
double energy = result.energy();
Alternatively, write a standalone
extern "C"trampoline and callrgpot_potential_new()directly from C:
rgpot_status_t my_trampoline(void *ud, const rgpot_force_input_t *in,
rgpot_force_out_t *out) {
/* ... forward to your implementation ... */
return RGPOT_SUCCESS;
}
rgpot_potential_t *pot = rgpot_potential_new(my_trampoline, my_data, NULL);
Build System Integration¶
Meson¶
The Rust crate is built via a custom_target in meson.build, gated behind the
with_rust_core option:
meson setup bbdir -Dwith_rust_core=true
meson compile -C bbdir
Meson invokes cargo build and links the resulting librgpot_core.a into the
project. The feature flags (rpc, cache) are forwarded from the corresponding
Meson options.
Standalone Rust¶
The crate can also be built and tested independently:
cd rgpot-core
cargo build
cargo test
cargo build --features rpc # with RPC support
Directory Reference¶
Path |
Purpose |
|---|---|
|
Rust crate configuration. |
|
capnp compilation (+ cbindgen when |
|
cbindgen configuration. |
|
Rust source (types, status, potential, capi, rpc). |
|
Auto-generated C header. |
|
Hand-written C++ RAII wrapper headers. |
|
Original C++ potentials and helpers. |
|
Bundled Cap’n Proto schema (canonical for crates.io). |
|
Shared Cap’n Proto schema (monorepo fallback). |