From 9ed36718dbbdb8b05895748674cb7bcd0ba13f9f Mon Sep 17 00:00:00 2001 From: Vladimir Pankratov Date: Thu, 30 Apr 2026 23:51:46 +0200 Subject: [PATCH 01/22] Initial commit of -sys crate --- .cargo/config.toml | 15 + .github/workflows/ci-rust.yml | 79 ++++ .gitignore | 4 + rust/Cargo.toml | 28 ++ rust/crates/eckit-sys/Cargo.toml | 88 ++++ rust/crates/eckit-sys/README.md | 81 ++++ rust/crates/eckit-sys/build.rs | 351 ++++++++++++++ rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 511 +++++++++++++++++++++ rust/crates/eckit-sys/cpp/eckit_bridge.h | 356 ++++++++++++++ rust/crates/eckit-sys/src/lib.rs | 352 ++++++++++++++ 10 files changed, 1865 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .github/workflows/ci-rust.yml create mode 100644 rust/Cargo.toml create mode 100644 rust/crates/eckit-sys/Cargo.toml create mode 100644 rust/crates/eckit-sys/README.md create mode 100644 rust/crates/eckit-sys/build.rs create mode 100644 rust/crates/eckit-sys/cpp/eckit_bridge.cpp create mode 100644 rust/crates/eckit-sys/cpp/eckit_bridge.h create mode 100644 rust/crates/eckit-sys/src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..b4069aba2 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,15 @@ +[build] +jobs = -1 + +[target.'cfg(all())'] +rustflags = [ + "-Wclippy::all", + "-Wclippy::pedantic", + "-Wclippy::nursery", + "-Wclippy::unwrap_used", + "-Aclippy::module_name_repetitions", + "-Aclippy::missing_errors_doc", +] + +[net] +git-fetch-with-cli = true diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml new file mode 100644 index 000000000..48efac09a --- /dev/null +++ b/.github/workflows/ci-rust.yml @@ -0,0 +1,79 @@ +name: rust + +on: + push: + branches: + - 'master' + - 'develop' + - 'rust-bindings' + tags-ignore: + - '**' + paths: + - 'rust/**' + - '.github/workflows/ci-rust.yml' + + pull_request: + paths: + - 'rust/**' + - '.github/workflows/ci-rust.yml' + + workflow_dispatch: ~ + +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + +jobs: + fmt: + name: fmt + runs-on: ubuntu-latest + defaults: + run: + working-directory: rust + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Format check + run: cargo fmt --check + + clippy: + name: clippy + if: ${{ !github.event.pull_request.head.repo.fork }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: rust + steps: + - uses: actions/checkout@v4 + + - name: Configure git for private repos + run: git config --global url."https://x-access-token:${{ secrets.GH_REPO_READ_TOKEN }}@github.com/".insteadOf "ssh://git@github.com/" + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Clippy + run: cargo clippy --features vendored --all-targets -- -D warnings + + test: + name: test + if: ${{ !github.event.pull_request.head.repo.fork }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: rust + steps: + - uses: actions/checkout@v4 + + - name: Configure git for private repos + run: git config --global url."https://x-access-token:${{ secrets.GH_REPO_READ_TOKEN }}@github.com/".insteadOf "ssh://git@github.com/" + + - uses: dtolnay/rust-toolchain@stable + + - name: Test + run: cargo test --features vendored diff --git a/.gitignore b/.gitignore index fec77aad6..64f9a434c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ build/ tests/geo/eckit_geo_cache _build *.ccls-cache + +# Rust +rust/target/ +rust/Cargo.lock diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 000000000..a82848168 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,28 @@ +[workspace] +resolver = "2" +members = ["crates/eckit-sys"] + +[workspace.package] +edition = "2024" +license = "Apache-2.0" +repository = "https://github.com/ecmwf/eckit" +rust-version = "1.90" +readme = "README.md" +keywords = ["ecmwf", "weather", "meteorology", "geo"] +categories = ["science"] + +[workspace.dependencies] +# Internal +eckit-sys = { path = "crates/eckit-sys" } + +# Build tools +bindman = { git = "ssh://git@github.com/ecmwf/bindman.git", branch = "generate_exception_bridge" } +bindman-build = { git = "ssh://git@github.com/ecmwf/bindman.git", branch = "generate_exception_bridge" } +bindman-utils = { git = "ssh://git@github.com/ecmwf/bindman.git", branch = "generate_exception_bridge" } + +# External +cxx = "1.0" +cxx-build = "1.0" +log = "0.4" +thiserror = "2" +tempfile = "3" diff --git a/rust/crates/eckit-sys/Cargo.toml b/rust/crates/eckit-sys/Cargo.toml new file mode 100644 index 000000000..92d6cb681 --- /dev/null +++ b/rust/crates/eckit-sys/Cargo.toml @@ -0,0 +1,88 @@ +[package] +name = "eckit-sys" +version = "2.0.7" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +description = "FFI bindings to ECMWF eckit C++ library" +links = "eckit_sys" +build = "build.rs" + +[features] +# Defaults: core features that don't require external libraries +default = ["vendored", "eckit-codec", "eckit-spec", "eckit-geo", "unicode", "aio"] + +# Build strategy (mutually exclusive) +vendored = [] +system = [] + +# Core eckit libraries (CMake default: ON) +eckit-codec = ["eckit-spec"] # eckit::codec encoding/decoding library (requires eckit-spec) +eckit-spec = [] # eckit::spec metadata library +eckit-geo = [] # eckit::geo geometry library +eckit-sql = [] # eckit::sql SQL engine (required by odc) + +# MPI support (CMake default: ON, requires MPI) +mpi = [] # Use system MPI libraries + +# Compression codecs (CMake default: OFF, require external libs) +bzip2 = [] # BZip2 support for compression +snappy = [] # Snappy support for compression +lz4 = [] # LZ4 support for compression +aec = [] # AEC support for compression +zip = [] # ZIP support for compression + +# Hashing (CMake default: OFF) +xxhash = [] # xxHash support for hashing + +# Linear algebra (CMake default: OFF, require external libs) +eigen = [] # Eigen linear algebra library +lapack = [] # Linear Algebra PACKage +mkl = [] # MKL linear algebra library +omp = [] # OpenMP linear algebra backend + +# Network (CMake default: OFF, require external libs) +curl = [] # Curl library for transfering data with URLs +ssl = [] # OpenSSL support + +# Other features +unicode = [] # Unicode support (CMake default: ON) +aio = [] # Async IO support (CMake default: ON) +proj = [] # PROJ-based projections support +rados = [] # Ceph/Rados storage support +jemalloc = [] # Link against jemalloc memory allocator +rsync = [] # librsync implementation of the rsync algorithm + +# GPU support (CMake default: OFF) +cuda = [] # CUDA GPU linear algebra operations +hip = [] # HIP GPU linear algebra operations + +# Geo sub-features (CMake default: OFF, except geo-area-shapefile) +geo-codec-grids = ["eckit-codec", "lz4", "eckit-geo"] # ORCA/FESOM/ICON grid types (requires codec + LZ4) +geo-caching = [] # eckit::geo default caching behaviour +geo-bitreproducible = [] # eckit::geo bit reproducibility tests +geo-projection-proj-default = [] # eckit::geo default to PROJ-based projections +geo-area-shapefile = [] # Shapefile support (CMake default: ON) + +# Advanced (CMake default: OFF) +convex-hull = [] # eckit::maths convex hull/Delaunay triangulation + +# Experimental (CMake default: OFF) +experimental = [] # Experimental features +sandbox = [] # Sandbox playground for prototyping + +[dependencies] +bindman.workspace = true +cxx.workspace = true +log.workspace = true + +[build-dependencies] +bindman-utils.workspace = true +bindman-build.workspace = true +cxx-build.workspace = true + +[package.metadata.docs.rs] diff --git a/rust/crates/eckit-sys/README.md b/rust/crates/eckit-sys/README.md new file mode 100644 index 000000000..7113623bc --- /dev/null +++ b/rust/crates/eckit-sys/README.md @@ -0,0 +1,81 @@ +# eckit-sys + +Low-level Rust bindings to ECMWF's [eckit](https://github.com/ecmwf/eckit) C++ +library. + +This crate provides raw FFI bindings using [cxx](https://cxx.rs/). For a safe, +ergonomic API, use the higher-level `eckit` crate (forthcoming). + +## Features + +### Build strategy (mutually exclusive) + +- `vendored` (default) - Clone and build eckit (and ecbuild) from source. +- `system` - Link against a system-installed eckit, located via CMake + `find_package(eckit)`. Honours `ECKIT_DIR` and `CMAKE_PREFIX_PATH`. + +### Core eckit libraries (enabled by default) + +- `eckit-codec` - `eckit::codec` encoding/decoding library (implies `eckit-spec`). +- `eckit-spec` - `eckit::spec` metadata library. +- `eckit-geo` - `eckit::geo` geometry library. +- `unicode` - Unicode support. +- `aio` - Async I/O support. + +### Optional eckit libraries + +- `eckit-sql` - `eckit::sql` SQL engine (required by `odc`). + +### MPI + +- `mpi` - Use system MPI libraries. + +### Compression codecs (off by default; require external libraries) + +- `bzip2`, `snappy`, `lz4`, `aec`, `zip` + +### Hashing (off by default) + +- `xxhash` + +### Linear algebra (off by default; require external libraries) + +- `eigen`, `lapack`, `mkl`, `omp` + +### Networking (off by default; require external libraries) + +- `curl` - libcurl support for `eckit::URLHandle`. +- `ssl` - OpenSSL support. + +### GPU (off by default) + +- `cuda`, `hip` + +### Geo sub-features + +- `geo-codec-grids` - ORCA/FESOM/ICON grid types (implies `eckit-codec`, + `eckit-geo`, `lz4`). +- `geo-caching` - Default eckit::geo caching behaviour. +- `geo-bitreproducible` - Bit-reproducibility tests. +- `geo-projection-proj-default` - Default to PROJ-based projections. +- `geo-area-shapefile` - Shapefile support. + +### Other (off by default) + +- `proj` - PROJ-based projection support. +- `rados` - Ceph/RADOS storage support. +- `jemalloc` - Link against jemalloc. +- `rsync` - librsync. +- `convex-hull` - `eckit::maths` convex hull / Delaunay triangulation. +- `experimental` - Experimental upstream features. +- `sandbox` - Sandbox builds. + +## Environment variables + +- `ECKIT_DIR` - Install prefix of an eckit build, used by `system` mode. +- `CMAKE_PREFIX_PATH` - Additional CMake search paths. +- `DOCS_RS` - When set, the build script becomes a no-op (for docs.rs). + +## License + +Apache-2.0 diff --git a/rust/crates/eckit-sys/build.rs b/rust/crates/eckit-sys/build.rs new file mode 100644 index 000000000..ee30cbabb --- /dev/null +++ b/rust/crates/eckit-sys/build.rs @@ -0,0 +1,351 @@ +//! Build script for eckit-sys +//! +//! Supports two build modes: +//! - `vendored` (default): Clone and build eckit from source using ecbuild +//! - `system`: Use `CMake` `find_package` to find system-installed eckit + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-env-changed=ECKIT_DIR"); + println!("cargo:rerun-if-env-changed=CMAKE_PREFIX_PATH"); + println!("cargo:rerun-if-env-changed=DOCS_RS"); + + if bindman_utils::is_docs_rs() { + return; + } + + bindman_utils::validate_build_mode(cfg!(feature = "system"), cfg!(feature = "vendored")); + + let include = if cfg!(feature = "system") { + build_system() + } else { + build_vendored() + }; + + generate_exceptions(&include); + build_cxx_bridge(&include); + + let crate_dir = + std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); + bindman_build::check_cpp_api(&include, &crate_dir.join("src/lib.rs")); + + // Export OUT_DIR for downstream crates that need eckit_exceptions.h + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR")); + println!("cargo:out_dir={}", out_dir.display()); + + // Export cpp directory for downstream crates that include eckit_bridge.h + println!("cargo:cpp_dir={}", crate_dir.join("cpp").display()); +} + +/// Compile the CXX bridge +fn build_cxx_bridge(include: &std::path::Path) { + let crate_dir = std::path::PathBuf::from( + std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"), + ); + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set")); + + println!("cargo:rerun-if-changed=cpp/eckit_bridge.h"); + println!("cargo:rerun-if-changed=cpp/eckit_bridge.cpp"); + + cxx_build::bridge("src/lib.rs") + .file(crate_dir.join("cpp/eckit_bridge.cpp")) + .include(include) + .include(crate_dir.join("cpp")) + .include(&out_dir) // for eckit_exceptions.h + .flag_if_supported("-std=c++17") + .compile("eckit_sys_bridge"); + + bindman_utils::link_cpp_stdlib(); +} + +/// Generate exception bridge files from eckit's `Exceptions.h`. +fn generate_exceptions(include: &std::path::Path) { + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set")); + let header = include.join("eckit/exception/Exceptions.h"); + + bindman_build::generate_exception_bridge(&bindman_build::ExceptionBridgeConfig { + header: &header, + base_class: "Exception", + namespace: "eckit", + out_dir: &out_dir, + }); + + // Export path to generated header for downstream -sys crates + println!( + "cargo:exceptions_header={}", + out_dir.join("eckit_exceptions.h").display() + ); +} + +/// Minimum eckit version this crate's bridge is known to compile against. +/// `system` builds require >= this version; `vendored` builds clone exactly +/// this tag — keeping both modes pinned to the same source revision so +/// downstream code can rely on the same API surface either way. +const ECKIT_VERSION: &str = "2.0.7"; + +#[cfg(feature = "system")] +fn build_system() -> std::path::PathBuf { + let (root, include, lib_dir) = bindman_utils::cmake_find_package("eckit", ECKIT_VERSION); + + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + println!("cargo:rustc-link-lib=dylib=eckit"); + bindman_utils::link_cpp_stdlib(); + + // Export for downstream crates + println!("cargo:root={}", root.display()); + println!("cargo:include={}", include.display()); + + include +} + +#[cfg(not(feature = "system"))] +fn build_system() -> std::path::PathBuf { + unreachable!("build_system called without system feature"); +} + +/// Build eckit from source using ecbuild +#[cfg(feature = "vendored")] +#[allow(clippy::too_many_lines)] +fn build_vendored() -> std::path::PathBuf { + use std::env; + use std::fs; + use std::path::PathBuf; + use std::process::Command; + + const ECBUILD_REPO: &str = "https://github.com/ecmwf/ecbuild.git"; + const ECBUILD_TAG: &str = "3.13.1"; + const ECKIT_REPO: &str = "https://github.com/ecmwf/eckit.git"; + // Pinned via the shared `ECKIT_VERSION` const above — `system` mode + // requires the same minimum so both modes give the same API surface. + const ECKIT_TAG: &str = ECKIT_VERSION; + + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set")); + let src_dir = out_dir.join("src"); + let build_dir = out_dir.join("build"); + let install_dir = out_dir.join("install"); + + fs::create_dir_all(&src_dir).expect("Failed to create src directory"); + fs::create_dir_all(&build_dir).expect("Failed to create build directory"); + + // Clone ecbuild and eckit + let ecbuild_src = bindman_utils::git_clone(ECBUILD_REPO, ECBUILD_TAG, &src_dir.join("ecbuild")); + let eckit_src = bindman_utils::git_clone(ECKIT_REPO, ECKIT_TAG, &src_dir.join("eckit")); + + // Configure with ecbuild + let ecbuild_bin = ecbuild_src.join("bin/ecbuild"); + + let mut cmd = Command::new(&ecbuild_bin); + cmd.current_dir(&build_dir) + .arg(format!("--prefix={}", install_dir.display())) + .arg("--") + .arg(&eckit_src) + .arg(format!( + "-DCMAKE_BUILD_TYPE={}", + bindman_utils::cmake_build_type() + )) + // Always disabled (no features) + .arg("-DENABLE_TESTS=OFF") + .arg("-DENABLE_DOCS=OFF") + .arg("-DENABLE_BUILD_TOOLS=OFF") + .arg(format!( + "-DENABLE_ECKIT_SQL={}", + bindman_utils::on_off(cfg!(feature = "eckit-sql")) + )) + .arg("-DENABLE_ECKIT_CMD=OFF") + .arg("-DENABLE_EXTRA_TESTS=OFF"); + + // Core libraries + cmd.arg(format!( + "-DENABLE_ECKIT_CODEC={}", + bindman_utils::on_off(cfg!(feature = "eckit-codec")) + )); + cmd.arg(format!( + "-DENABLE_ECKIT_SPEC={}", + bindman_utils::on_off(cfg!(feature = "eckit-spec")) + )); + cmd.arg(format!( + "-DENABLE_ECKIT_GEO={}", + bindman_utils::on_off(cfg!(feature = "eckit-geo")) + )); + + // MPI + cmd.arg(format!( + "-DENABLE_MPI={}", + bindman_utils::on_off(cfg!(feature = "mpi")) + )); + + // Compression codecs + cmd.arg(format!( + "-DENABLE_BZIP2={}", + bindman_utils::on_off(cfg!(feature = "bzip2")) + )); + cmd.arg(format!( + "-DENABLE_SNAPPY={}", + bindman_utils::on_off(cfg!(feature = "snappy")) + )); + cmd.arg(format!( + "-DENABLE_LZ4={}", + bindman_utils::on_off(cfg!(feature = "lz4")) + )); + cmd.arg(format!( + "-DENABLE_AEC={}", + bindman_utils::on_off(cfg!(feature = "aec")) + )); + cmd.arg(format!( + "-DENABLE_ZIP={}", + bindman_utils::on_off(cfg!(feature = "zip")) + )); + + // Hashing + cmd.arg(format!( + "-DENABLE_XXHASH={}", + bindman_utils::on_off(cfg!(feature = "xxhash")) + )); + + // Linear algebra + cmd.arg(format!( + "-DENABLE_EIGEN={}", + bindman_utils::on_off(cfg!(feature = "eigen")) + )); + cmd.arg(format!( + "-DENABLE_LAPACK={}", + bindman_utils::on_off(cfg!(feature = "lapack")) + )); + cmd.arg(format!( + "-DENABLE_MKL={}", + bindman_utils::on_off(cfg!(feature = "mkl")) + )); + cmd.arg(format!( + "-DENABLE_OMP={}", + bindman_utils::on_off(cfg!(feature = "omp")) + )); + + // Network + cmd.arg(format!( + "-DENABLE_CURL={}", + bindman_utils::on_off(cfg!(feature = "curl")) + )); + cmd.arg(format!( + "-DENABLE_SSL={}", + bindman_utils::on_off(cfg!(feature = "ssl")) + )); + + // Other features + cmd.arg(format!( + "-DENABLE_UNICODE={}", + bindman_utils::on_off(cfg!(feature = "unicode")) + )); + cmd.arg(format!( + "-DENABLE_AIO={}", + bindman_utils::on_off(cfg!(feature = "aio")) + )); + cmd.arg(format!( + "-DENABLE_PROJ={}", + bindman_utils::on_off(cfg!(feature = "proj")) + )); + cmd.arg(format!( + "-DENABLE_RADOS={}", + bindman_utils::on_off(cfg!(feature = "rados")) + )); + cmd.arg(format!( + "-DENABLE_JEMALLOC={}", + bindman_utils::on_off(cfg!(feature = "jemalloc")) + )); + cmd.arg(format!( + "-DENABLE_RSYNC={}", + bindman_utils::on_off(cfg!(feature = "rsync")) + )); + + // GPU support + cmd.arg(format!( + "-DENABLE_CUDA={}", + bindman_utils::on_off(cfg!(feature = "cuda")) + )); + cmd.arg(format!( + "-DENABLE_HIP={}", + bindman_utils::on_off(cfg!(feature = "hip")) + )); + + // Geo sub-features + cmd.arg(format!( + "-DENABLE_GEO_CACHING={}", + bindman_utils::on_off(cfg!(feature = "geo-caching")) + )); + cmd.arg(format!( + "-DENABLE_GEO_BITREPRODUCIBLE={}", + bindman_utils::on_off(cfg!(feature = "geo-bitreproducible")) + )); + cmd.arg(format!( + "-DENABLE_GEO_PROJECTION_PROJ_DEFAULT={}", + bindman_utils::on_off(cfg!(feature = "geo-projection-proj-default")) + )); + cmd.arg(format!( + "-DENABLE_GEO_AREA_SHAPEFILE={}", + bindman_utils::on_off(cfg!(feature = "geo-area-shapefile")) + )); + + // Advanced + cmd.arg(format!( + "-DENABLE_CONVEX_HULL={}", + bindman_utils::on_off(cfg!(feature = "convex-hull")) + )); + + // Experimental + cmd.arg(format!( + "-DENABLE_EXPERIMENTAL={}", + bindman_utils::on_off(cfg!(feature = "experimental")) + )); + cmd.arg(format!( + "-DENABLE_SANDBOX={}", + bindman_utils::on_off(cfg!(feature = "sandbox")) + )); + + // Use @rpath install names — the leaf binary sets rpaths via bindman_utils::emit_rpaths() + #[cfg(target_os = "macos")] + cmd.arg("-DCMAKE_INSTALL_NAME_DIR=@rpath"); + + #[cfg(target_os = "linux")] + { + cmd.arg("-DCMAKE_INSTALL_RPATH=$ORIGIN:$ORIGIN/../lib"); + cmd.arg("-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON"); + } + + bindman_utils::run_command(&mut cmd, "ecbuild configure"); + + // Build + let num_jobs = bindman_utils::build_parallelism(); + + bindman_utils::run_command( + Command::new("cmake") + .args(["--build", ".", "--parallel", &num_jobs]) + .current_dir(&build_dir), + "cmake build", + ); + + // Install + bindman_utils::run_command( + Command::new("cmake") + .args(["--install", "."]) + .current_dir(&build_dir), + "cmake install", + ); + + // Link directives + let lib_dir = bindman_utils::resolve_lib_dir(&install_dir); + + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + println!("cargo:rustc-link-lib=dylib=eckit"); + bindman_utils::link_cpp_stdlib(); + + // Export for downstream crates + let include = install_dir.join("include"); + println!("cargo:root={}", install_dir.display()); + println!("cargo:include={}", include.display()); + + include +} + +#[cfg(not(feature = "vendored"))] +fn build_vendored() -> std::path::PathBuf { + unreachable!("build_vendored called without vendored feature"); +} diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp new file mode 100644 index 000000000..56d771f5d --- /dev/null +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -0,0 +1,511 @@ +// eckit C++ bridge implementation +#include "eckit_bridge.h" +#include "eckit-sys/src/lib.rs.h" // cxx-generated — provides LogLevel values + +namespace eckit_bridge { + +// ==================== Logging ==================== + +void RustLogTarget::write(const char *start, const char *end) { + buffer_.append(start, end); + + std::string::size_type pos; + while ((pos = buffer_.find('\n')) != std::string::npos) { + std::string line = buffer_.substr(0, pos); + while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) { + line.pop_back(); + } + if (!line.empty()) { + rust_log(level_, rust::Str(line.data(), line.size())); + } + buffer_.erase(0, pos + 1); + } +} + +void RustLogTarget::flush() { + if (!buffer_.empty()) { + while (!buffer_.empty() && + (buffer_.back() == '\r' || buffer_.back() == ' ')) { + buffer_.pop_back(); + } + if (!buffer_.empty()) { + rust_log(level_, rust::Str(buffer_.data(), buffer_.size())); + buffer_.clear(); + } + } +} + +RustMain::RustMain(int argc, char **argv) : Main(argc, argv) {} + +eckit::LogTarget *RustMain::createInfoLogTarget() const { + return new RustLogTarget(LogLevel::Info); +} +eckit::LogTarget *RustMain::createWarningLogTarget() const { + return new RustLogTarget(LogLevel::Warn); +} +eckit::LogTarget *RustMain::createErrorLogTarget() const { + return new RustLogTarget(LogLevel::Error); +} +eckit::LogTarget *RustMain::createDebugLogTarget() const { + return new RustLogTarget(LogLevel::Debug); +} +eckit::LogTarget *RustMain::createMetricsLogTarget() const { + return new RustLogTarget(LogLevel::Trace); +} + +void init() { + if (!eckit::Main::ready()) { + static const char *argv[] = {"eckit-rs", nullptr}; + static auto *main = new RustMain(1, const_cast(argv)); + (void)main; + } +} + +// ==================== Configuration ==================== + +bool ConfigWrapper::has(rust::Str key) const { + return config_.has(std::string(key)); +} + +bool ConfigWrapper::is_list(rust::Str key) const { + return config_.isList(std::string(key)); +} + +bool ConfigWrapper::is_empty() const { return config_.empty(); } + +rust::String ConfigWrapper::get_string(rust::Str key, + rust::Str default_val) const { + return rust::String( + config_.getString(std::string(key), std::string(default_val))); +} + +int64_t ConfigWrapper::get_long(rust::Str key, int64_t default_val) const { + return config_.getLong(std::string(key), default_val); +} + +int32_t ConfigWrapper::get_int(rust::Str key, int32_t default_val) const { + return config_.getInt(std::string(key), default_val); +} + +bool ConfigWrapper::get_bool(rust::Str key, bool default_val) const { + return config_.getBool(std::string(key), default_val); +} + +double ConfigWrapper::get_double(rust::Str key, double default_val) const { + return config_.getDouble(std::string(key), default_val); +} + +rust::Vec ConfigWrapper::get_string_vector( + rust::Str key, const rust::Vec &default_val) const { + std::vector def; + def.reserve(default_val.size()); + for (const auto &s : default_val) { + def.emplace_back(std::string(s)); + } + auto vec = config_.getStringVector(std::string(key), def); + rust::Vec result; + result.reserve(vec.size()); + for (const auto &s : vec) { + result.push_back(rust::String(s)); + } + return result; +} + +std::unique_ptr ConfigWrapper::get_sub(rust::Str key) const { + return std::make_unique( + config_.getSubConfiguration(std::string(key))); +} + +size_t ConfigWrapper::sub_count(rust::Str key) const { + return config_.getSubConfigurations(std::string(key)).size(); +} + +std::unique_ptr ConfigWrapper::sub_at(rust::Str key, + size_t index) const { + auto subs = config_.getSubConfigurations(std::string(key)); + return std::make_unique(subs.at(index)); +} + +size_t ConfigWrapper::root_sub_count() const { + return config_.getSubConfigurations().size(); +} + +std::unique_ptr ConfigWrapper::root_sub_at(size_t index) const { + auto subs = config_.getSubConfigurations(); + return std::make_unique(subs.at(index)); +} + +void ConfigWrapper::set_string(rust::Str key, rust::Str value) { + config_.set(std::string(key), std::string(value)); +} + +void ConfigWrapper::set_long(rust::Str key, int64_t value) { + config_.set(std::string(key), static_cast(value)); +} + +void ConfigWrapper::set_int(rust::Str key, int32_t value) { + config_.set(std::string(key), value); +} + +void ConfigWrapper::set_bool(rust::Str key, bool value) { + config_.set(std::string(key), value); +} + +void ConfigWrapper::set_double(rust::Str key, double value) { + config_.set(std::string(key), value); +} + +void ConfigWrapper::remove(rust::Str key) { config_.remove(std::string(key)); } + +std::unique_ptr create() { + return std::make_unique(); +} + +std::unique_ptr from_path(rust::Str path) { + auto p = eckit::PathName{std::string(path)}; + auto yaml = eckit::YAMLConfiguration{p}; + return std::make_unique(yaml); +} + +std::unique_ptr from_yaml(rust::Str yaml) { + auto str = std::string(yaml); + auto parsed = eckit::YAMLConfiguration{str}; + return std::make_unique(parsed); +} + +std::unique_ptr clone(const ConfigWrapper &src) { + return std::make_unique(src.inner()); +} + +// ==================== DataHandle ==================== + +int64_t DataHandleWrapper::open_for_read() { + return static_cast(handle_->openForRead()); +} + +void DataHandleWrapper::open_for_write(int64_t estimated_length) { + handle_->openForWrite(eckit::Length(estimated_length)); +} + +int64_t DataHandleWrapper::read(rust::Slice buf) { + return handle_->read(buf.data(), static_cast(buf.size())); +} + +int64_t DataHandleWrapper::write(rust::Slice buf) { + return handle_->write(buf.data(), static_cast(buf.size())); +} + +void DataHandleWrapper::close() { handle_->close(); } + +int64_t DataHandleWrapper::position() const { + return static_cast(handle_->position()); +} + +int64_t DataHandleWrapper::seek(int64_t offset) { + return static_cast(handle_->seek(eckit::Offset(offset))); +} + +bool DataHandleWrapper::can_seek() const { return handle_->canSeek(); } + +int64_t DataHandleWrapper::estimate() const { + return static_cast(handle_->estimate()); +} + +int64_t DataHandleWrapper::save_into(DataHandleWrapper &target) { + return static_cast(handle_->saveInto(*target.handle_)); +} + +std::unique_ptr data_handle_from_file(rust::Str path) { + auto p = eckit::PathName{std::string(path)}; + return std::make_unique(p.fileHandle()); +} + +std::unique_ptr +data_handle_from_part(rust::Str path, int64_t offset, int64_t length) { + return std::make_unique( + new eckit::PartFileHandle(eckit::PathName{std::string(path)}, + eckit::Offset(offset), eckit::Length(length))); +} + +std::unique_ptr +data_handle_from_buffer(rust::Slice data) { + return std::make_unique( + new eckit::MemoryHandle(data.data(), data.size())); +} + +std::unique_ptr +data_handle_from_multi(rust::Slice paths) { + auto *mh = new eckit::MultiHandle(); + for (const auto &p : paths) { + (*mh) += eckit::PathName(std::string(p)).fileHandle(); + } + return std::make_unique(mh); +} + +std::unique_ptr +data_handle_tee(rust::Slice paths) { + std::vector handles; + handles.reserve(paths.size()); + for (const auto &p : paths) { + handles.push_back(eckit::PathName(std::string(p)).fileHandle()); + } + return std::make_unique(new eckit::TeeHandle(handles)); +} + +// ==================== Message + Reader ==================== + +bool MessageWrapper::is_valid() const { return static_cast(msg_); } + +size_t MessageWrapper::length() const { return msg_.length(); } + +int64_t MessageWrapper::offset() const { + // eckit::Offset has operator long long(), so cast through that. + return static_cast(static_cast(msg_.offset())); +} + +rust::String MessageWrapper::get_string(rust::Str key) const { + return rust::String(msg_.getString(std::string(key))); +} + +int64_t MessageWrapper::get_long(rust::Str key) const { + return msg_.getLong(std::string(key)); +} + +double MessageWrapper::get_double(rust::Str key) const { + return msg_.getDouble(std::string(key)); +} + +rust::Slice MessageWrapper::data() const { + return rust::Slice(static_cast(msg_.data()), + msg_.length()); +} + +void MessageWrapper::write_to(DataHandleWrapper &handle) const { + msg_.write(handle.inner()); +} + +std::unique_ptr MessageWrapper::clone() const { + return std::make_unique(msg_); +} + +ReaderWrapper::ReaderWrapper(DataHandleWrapper &handle) + : reader_(std::make_unique(handle.inner(), true)) {} + +std::unique_ptr ReaderWrapper::next() { + auto msg = reader_->next(); + return std::make_unique(std::move(msg)); +} + +std::unique_ptr new_reader(DataHandleWrapper &handle) { + return std::make_unique(handle); +} + +// ==================== Library registration ==================== + +RustLibrary::RustLibrary(rust::Box lib) + : eckit::system::Library(std::string(library_name(*lib))), + rust_(std::move(lib)) {} + +std::string RustLibrary::version() const { + return std::string(library_version(*rust_)); +} + +std::string RustLibrary::gitsha1(unsigned int count) const { + return std::string(library_git_sha1(*rust_, count)); +} + +std::string RustLibrary::home() const { + if (library_home_is_set(*rust_)) { + return std::string(library_home(*rust_)); + } + return Library::home(); +} + +std::string RustLibrary::libraryHome() const { + if (library_library_home_is_set(*rust_)) { + return std::string(library_library_home(*rust_)); + } + return Library::libraryHome(); +} + +std::string RustLibrary::prefixDirectory() const { + if (library_prefix_directory_is_set(*rust_)) { + return std::string(library_prefix_directory(*rust_)); + } + return Library::prefixDirectory(); +} + +std::string RustLibrary::expandPath(const std::string &path) const { + rust::Str rpath(path.data(), path.size()); + if (library_expand_path_is_set(*rust_, rpath)) { + return std::string(library_expand_path(*rust_, rpath)); + } + return Library::expandPath(path); +} + +bool RustLibrary::debug() const { + if (library_debug_is_set(*rust_)) { + return library_debug(*rust_); + } + return Library::debug(); +} + +const void *RustLibrary::addr() const { + // Return a pointer inside the code segment so dladdr can resolve the + // binary path. `this` is heap-allocated and dladdr cannot resolve it. + return reinterpret_cast(®ister_library); +} + +void register_library(rust::Box lib) { + // Heap-allocate and leak — Library registers itself with LibraryManager + // in its constructor and must live for the process lifetime. + new RustLibrary(std::move(lib)); +} + +std::unique_ptr library_configuration(rust::Str name) { + auto &lib = eckit::system::LibraryManager::lookup(std::string(name)); + const auto &cfg = lib.configuration(); + return std::make_unique(eckit::LocalConfiguration(cfg)); +} + +// ==================== Stream (base class) ==================== + +void StreamWrapper::write_char(uint8_t c) { *stream_ << static_cast(c); } +void StreamWrapper::write_bool(bool v) { *stream_ << v; } +void StreamWrapper::write_int(int32_t v) { *stream_ << v; } +void StreamWrapper::write_long(int64_t v) { + *stream_ << static_cast(v); +} +void StreamWrapper::write_unsigned_long(uint64_t v) { + *stream_ << static_cast(v); +} +void StreamWrapper::write_double(double v) { *stream_ << v; } +void StreamWrapper::write_string(rust::Str v) { + *stream_ << std::string(v.data(), v.size()); +} +void StreamWrapper::write_blob(rust::Slice data) { + stream_->writeBlob(data.data(), data.size()); +} + +uint8_t StreamWrapper::read_char() { + char c{}; + *stream_ >> c; + return static_cast(c); +} +bool StreamWrapper::read_bool() { + bool v{}; + *stream_ >> v; + return v; +} +int32_t StreamWrapper::read_int() { + int v{}; + *stream_ >> v; + return v; +} +int64_t StreamWrapper::read_long() { + long long v{}; + *stream_ >> v; + return static_cast(v); +} +uint64_t StreamWrapper::read_unsigned_long() { + unsigned long long v{}; + *stream_ >> v; + return static_cast(v); +} +double StreamWrapper::read_double() { + double v{}; + *stream_ >> v; + return v; +} +rust::String StreamWrapper::read_string() { + std::string v; + *stream_ >> v; + return rust::String(v); +} + +int64_t StreamWrapper::read_bytes(rust::Slice buf) { + throw eckit::SeriousBug("read_bytes not supported on this stream type"); +} + +rust::Slice StreamWrapper::buffer() { + throw eckit::SeriousBug("buffer() not supported on this stream type"); +} + +int64_t StreamWrapper::bytes_written() const { return stream_->bytesWritten(); } + +// ==================== TcpStreamWrapper ==================== + +TcpStreamWrapper::TcpStreamWrapper(const std::string &host, int port) + : socket_(eckit::net::TCPClient().connect(host, port)), tcp_(socket_) { + stream_ = &tcp_; +} + +int64_t TcpStreamWrapper::read_bytes(rust::Slice buf) { + // The connection lives on `tcp_.socket()` — `socket_` was emptied by the + // TCPSocket "copy" ctor in `tcp_(socket_)` (ownership transfer). + return tcp_.socket().read(buf.data(), static_cast(buf.size())); +} + +std::unique_ptr TcpStreamWrapper::into_data_handle() { + // Steal the live connection from `tcp_` and wrap it as an owning + // `eckit::TCPSocketHandle` DataHandle. The TCPSocketHandle ctor copies + // (ownership transfer) the socket into its own member, so after this call + // both `socket_` and `tcp_.socket()` are detached (fd = -1) and only the + // returned DataHandle holds the connection. + return std::make_unique( + new eckit::TCPSocketHandle(tcp_.socket())); +} + +std::unique_ptr StreamWrapper::into_data_handle() { + throw eckit::NotImplemented( + "StreamWrapper::into_data_handle is only supported on TCP streams", + Here()); +} + +rust::Vec library_versions() { + rust::Vec out; + constexpr size_t sha1len = 8; + for (const auto &name : eckit::system::LibraryManager::list()) { + const auto &lib = eckit::system::LibraryManager::lookup(name); + out.push_back(LibraryVersion{ + rust::String(lib.name()), rust::String(lib.version()), + rust::String(lib.gitsha1(sha1len)), rust::String(lib.libraryHome())}); + } + return out; +} + +// ==================== MemoryWriteStreamWrapper ==================== + +MemoryWriteStreamWrapper::MemoryWriteStreamWrapper() : buf_(4096), mem_(buf_) { + stream_ = &mem_; +} + +rust::Slice MemoryWriteStreamWrapper::buffer() { + return {static_cast(buf_.data()), + static_cast(mem_.bytesWritten())}; +} + +// ==================== MemoryReadStreamWrapper ==================== + +MemoryReadStreamWrapper::MemoryReadStreamWrapper( + rust::Slice data) + : buf_(data.data(), data.size()), mem_(buf_) { + stream_ = &mem_; +} + +// ==================== Factory functions ==================== + +std::unique_ptr stream_connect(rust::Str host, int32_t port) { + return std::make_unique(std::string(host), port); +} + +std::unique_ptr stream_memory_write() { + return std::make_unique(); +} + +std::unique_ptr +stream_memory_read(rust::Slice data) { + return std::make_unique(data); +} + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.h b/rust/crates/eckit-sys/cpp/eckit_bridge.h new file mode 100644 index 000000000..1d0afe3dc --- /dev/null +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.h @@ -0,0 +1,356 @@ +// eckit C++ bridge for Rust FFI +#pragma once + +// Include auto-generated trycatch handler FIRST — before cxx generates its +// default +#include "eckit_exceptions.h" + +#include "eckit/config/LocalConfiguration.h" +#include "eckit/config/YAMLConfiguration.h" +#include "eckit/filesystem/PathName.h" +#include "eckit/io/DataHandle.h" +#include "eckit/io/MemoryHandle.h" +#include "eckit/io/MultiHandle.h" +#include "eckit/io/PartFileHandle.h" +#include "eckit/io/TeeHandle.h" +#include "eckit/log/Log.h" +#include "eckit/log/LogTarget.h" +#include "eckit/message/Message.h" +#include "eckit/message/Reader.h" +#include "eckit/io/TCPSocketHandle.h" +#include "eckit/net/TCPClient.h" +#include "eckit/net/TCPStream.h" +#include "eckit/runtime/Main.h" +#include "eckit/serialisation/MemoryStream.h" +#include "eckit/serialisation/ResizableMemoryStream.h" +#include "eckit/serialisation/Stream.h" +#include "eckit/system/Library.h" +#include "eckit/system/LibraryManager.h" + +#include "rust/cxx.h" + +#include +#include +#include + +namespace eckit_bridge { + +// Forward declarations — full definitions in cxx-generated code +enum class LogLevel : std::uint8_t; +void rust_log(LogLevel level, rust::Str msg) noexcept; + +// ==================== Logging ==================== + +/// LogTarget that routes all output to Rust's log crate. +/// Accumulates writes until a newline or flush, then emits a single log call. +class RustLogTarget : public eckit::LogTarget { +public: + explicit RustLogTarget(LogLevel level) : level_(level) {} + + void write(const char *start, const char *end) override; + void flush() override; + +private: + LogLevel level_; + std::string buffer_; +}; + +/// Main subclass that installs `RustLogTarget` on all channels. +/// Every new thread automatically gets `RustLogTarget` via the factory methods. +class RustMain : public eckit::Main { +public: + RustMain(int argc, char **argv); + + eckit::LogTarget *createInfoLogTarget() const override; + eckit::LogTarget *createWarningLogTarget() const override; + eckit::LogTarget *createErrorLogTarget() const override; + eckit::LogTarget *createDebugLogTarget() const override; + eckit::LogTarget *createMetricsLogTarget() const override; +}; + +/// Initialize eckit runtime with Rust log bridge. +void init(); + +// ==================== Configuration ==================== + +/// Wraps `eckit::LocalConfiguration` for Rust FFI. +class ConfigWrapper { + eckit::LocalConfiguration config_; + +public: + ConfigWrapper() = default; + explicit ConfigWrapper(const eckit::Configuration &other) : config_(other) {} + + // Read + bool has(rust::Str key) const; + bool is_list(rust::Str key) const; + bool is_empty() const; + rust::String get_string(rust::Str key, rust::Str default_val) const; + int64_t get_long(rust::Str key, int64_t default_val) const; + int32_t get_int(rust::Str key, int32_t default_val) const; + bool get_bool(rust::Str key, bool default_val) const; + double get_double(rust::Str key, double default_val) const; + rust::Vec + get_string_vector(rust::Str key, + const rust::Vec &default_val) const; + + // Sub-configurations (by key) + std::unique_ptr get_sub(rust::Str key) const; + size_t sub_count(rust::Str key) const; + std::unique_ptr sub_at(rust::Str key, size_t index) const; + + // Sub-configurations (root-level list) + size_t root_sub_count() const; + std::unique_ptr root_sub_at(size_t index) const; + + // Write + void set_string(rust::Str key, rust::Str value); + void set_long(rust::Str key, int64_t value); + void set_int(rust::Str key, int32_t value); + void set_bool(rust::Str key, bool value); + void set_double(rust::Str key, double value); + void remove(rust::Str key); + + // Access underlying for other C++ bridge code + const eckit::LocalConfiguration &inner() const { return config_; } + eckit::LocalConfiguration &inner() { return config_; } +}; + +// Factory functions — names match cxx bridge declarations +std::unique_ptr create(); +std::unique_ptr from_path(rust::Str path); +std::unique_ptr from_yaml(rust::Str yaml); +std::unique_ptr clone(const ConfigWrapper &src); + +// ==================== DataHandle ==================== + +/// Wraps `eckit::DataHandle*` for Rust FFI. Takes ownership. +class DataHandleWrapper { + std::unique_ptr handle_; + +public: + /// Takes ownership of a raw DataHandle pointer. + explicit DataHandleWrapper(eckit::DataHandle *h) : handle_(h) {} + + int64_t open_for_read(); + void open_for_write(int64_t estimated_length); + int64_t read(rust::Slice buf); + int64_t write(rust::Slice buf); + void close(); + int64_t position() const; + int64_t seek(int64_t offset); + bool can_seek() const; + int64_t estimate() const; + + /// Copy all data from this handle into target (both must be open). + int64_t save_into(DataHandleWrapper &target); + + /// Access underlying for other C++ bridge code. + eckit::DataHandle &inner() { return *handle_; } + const eckit::DataHandle &inner() const { return *handle_; } + + /// Release ownership — caller takes responsibility. + eckit::DataHandle *release() { return handle_.release(); } +}; + +// ==================== Message + Reader ==================== + +/// Wraps `eckit::message::Message` for Rust FFI. +/// Message is a value type with internal reference counting. +class MessageWrapper { + eckit::message::Message msg_; + +public: + MessageWrapper() = default; + explicit MessageWrapper(eckit::message::Message m) : msg_(std::move(m)) {} + + bool is_valid() const; + size_t length() const; + /// Byte offset of this message within the source data handle. Mirrors + /// `eckit::message::Message::offset()` — populated by the Reader when + /// scanning a file/stream. + int64_t offset() const; + rust::String get_string(rust::Str key) const; + int64_t get_long(rust::Str key) const; + double get_double(rust::Str key) const; + rust::Slice data() const; + void write_to(DataHandleWrapper &handle) const; + + /// Clone (Message is ref-counted internally). + std::unique_ptr clone() const; + + /// Access underlying for other C++ bridge code. + const eckit::message::Message &inner() const { return msg_; } + eckit::message::Message &inner() { return msg_; } +}; + +/// Wraps `eckit::message::Reader` for Rust FFI. +/// Reads messages from a `DataHandle`. +class ReaderWrapper { + std::unique_ptr reader_; + +public: + explicit ReaderWrapper(DataHandleWrapper &handle); + + /// Returns next message, or an invalid message when exhausted. + std::unique_ptr next(); +}; + +// Factory +std::unique_ptr new_reader(DataHandleWrapper &handle); + +/// Open a file as a DataHandle for reading. +std::unique_ptr data_handle_from_file(rust::Str path); + +/// Open a byte range of a file as a DataHandle. +std::unique_ptr +data_handle_from_part(rust::Str path, int64_t offset, int64_t length); + +/// Create a DataHandle from an in-memory buffer (copies the data). +std::unique_ptr +data_handle_from_buffer(rust::Slice data); + +/// Create a MultiHandle from multiple file paths. +std::unique_ptr +data_handle_from_multi(rust::Slice paths); + +/// Create a TeeHandle from multiple file paths — writes all targets in +/// parallel. +std::unique_ptr +data_handle_tee(rust::Slice paths); + +// ==================== Library registration ==================== + +// Forward declaration — defined on the Rust side, cxx generates the type. +struct LibraryBox; + +/// eckit::system::Library subclass that delegates to a Rust `dyn Library`. +class RustLibrary : public eckit::system::Library { + rust::Box rust_; + +public: + explicit RustLibrary(rust::Box lib); + + std::string version() const override; + std::string gitsha1(unsigned int count) const override; + +protected: + std::string home() const override; + std::string libraryHome() const override; + std::string prefixDirectory() const override; + std::string expandPath(const std::string &path) const override; + bool debug() const override; + const void *addr() const override; +}; + +/// Register a Rust library with eckit's LibraryManager. +void register_library(rust::Box lib); + +/// Get configuration for a registered library by name. +std::unique_ptr library_configuration(rust::Str name); + +// `LibraryVersion` is a shared struct defined by cxx in this same namespace +// (see the bridge in lib.rs). Forward-declare it here so the function +// signature below compiles when this header is included before the cxx +// definition. +struct LibraryVersion; + +/// Snapshot of every ECMWF library currently registered with +/// `eckit::system::LibraryManager`. Mirrors C++ `Environment::library_versions`. +rust::Vec library_versions(); + +// ==================== Stream ==================== + +/// Base wrapper for `eckit::Stream`. Subclasses own the transport-specific +/// resources (socket, buffer, etc.). All read/write methods delegate to the +/// `eckit::Stream*` set by the subclass. +class StreamWrapper { +protected: + eckit::Stream *stream_ = nullptr; + +public: + virtual ~StreamWrapper() = default; + + StreamWrapper(const StreamWrapper &) = delete; + StreamWrapper &operator=(const StreamWrapper &) = delete; + + // Write operations (eckit::Stream operator<<) + void write_char(uint8_t c); + void write_bool(bool v); + void write_int(int32_t v); + void write_long(int64_t v); + void write_unsigned_long(uint64_t v); + void write_double(double v); + void write_string(rust::Str v); + void write_blob(rust::Slice data); + + // Read operations (eckit::Stream operator>>) + uint8_t read_char(); + bool read_bool(); + int32_t read_int(); + int64_t read_long(); + uint64_t read_unsigned_long(); + double read_double(); + rust::String read_string(); + + // Raw byte read (for data transfer after protocol handshake) + virtual int64_t read_bytes(rust::Slice buf); + + /// Access underlying stream for other bridge code. + eckit::Stream &inner() { return *stream_; } + const eckit::Stream &inner() const { return *stream_; } + + /// Number of bytes written so far. + int64_t bytes_written() const; + + /// Get buffer contents (memory write streams only). + virtual rust::Slice buffer(); + + /// Hand off the underlying connection as a DataHandle for streaming reads. + /// + /// Only meaningful for TCP streams; the default throws `NotImplemented`. + /// After this call the stream is in an unspecified state and must not be + /// used; the returned `DataHandleWrapper` owns the connection. + virtual std::unique_ptr into_data_handle(); + +protected: + StreamWrapper() = default; +}; + +/// TCP stream — connects to host:port via `eckit::net::TCPClient`. +class TcpStreamWrapper : public StreamWrapper { + eckit::net::TCPSocket socket_; + eckit::net::TCPStream tcp_; + +public: + TcpStreamWrapper(const std::string &host, int port); + int64_t read_bytes(rust::Slice buf) override; + std::unique_ptr into_data_handle() override; +}; + +/// Resizable memory stream — for writing, buffer grows as needed. +class MemoryWriteStreamWrapper : public StreamWrapper { + eckit::Buffer buf_; + eckit::ResizableMemoryStream mem_; + +public: + MemoryWriteStreamWrapper(); + rust::Slice buffer() override; +}; + +/// Fixed memory stream — for reading from existing data. +class MemoryReadStreamWrapper : public StreamWrapper { + eckit::Buffer buf_; + eckit::MemoryStream mem_; + +public: + MemoryReadStreamWrapper(rust::Slice data); +}; + +// Factory functions +std::unique_ptr stream_connect(rust::Str host, int32_t port); +std::unique_ptr stream_memory_write(); +std::unique_ptr +stream_memory_read(rust::Slice data); + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/src/lib.rs b/rust/crates/eckit-sys/src/lib.rs new file mode 100644 index 000000000..865a35757 --- /dev/null +++ b/rust/crates/eckit-sys/src/lib.rs @@ -0,0 +1,352 @@ +//! FFI bindings to ECMWF eckit C++ library. +//! +//! This crate builds eckit and provides: +//! - Auto-generated exception types from `eckit/exception/Exceptions.h` +//! - Logging bridge (C++ `eckit::Log` → Rust `log` crate) +//! - Build paths for downstream -sys crates + +use bindman::track_cpp_api; + +// Auto-generated exception Error enum + From impl +include!(concat!(env!("OUT_DIR"), "/eckit_exceptions.rs")); + +#[track_cpp_api( + ("eckit/config/LocalConfiguration.h", class = "LocalConfiguration"), + ("eckit/io/DataHandle.h", class = "DataHandle"), + ("eckit/message/Message.h", class = "Message"), + ("eckit/message/Reader.h", class = "Reader"), +)] +#[cxx::bridge(namespace = "eckit_bridge")] +mod ffi { + /// Log levels shared between C++ and Rust. + /// Compile-time guarantee that both sides match. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum LogLevel { + Error = 1, + Warn = 2, + Info = 3, + Debug = 4, + Trace = 5, + } + + /// One ECMWF library reported by `eckit::system::LibraryManager`. + #[derive(Debug, Clone)] + struct LibraryVersion { + name: String, + version: String, + gitsha1: String, + home: String, + } + + unsafe extern "C++" { + include!("eckit_bridge.h"); + + /// Initialize eckit runtime with Rust log bridge. + /// Safe to call multiple times — only the first call has effect. + fn init(); + + // ==================== Configuration ==================== + + type ConfigWrapper; + + /// Create empty configuration. + #[rust_name = "config_create"] + #[must_use] + fn create() -> UniquePtr; + + /// Load from YAML file path. + #[rust_name = "config_from_path"] + fn from_path(path: &str) -> Result>; + + /// Parse from YAML string. + #[rust_name = "config_from_yaml"] + fn from_yaml(yaml: &str) -> Result>; + + /// Copy a configuration. + #[rust_name = "config_clone"] + fn clone(src: &ConfigWrapper) -> UniquePtr; + + // Read + fn has(self: &ConfigWrapper, key: &str) -> bool; + fn is_list(self: &ConfigWrapper, key: &str) -> bool; + fn is_empty(self: &ConfigWrapper) -> bool; + fn get_string(self: &ConfigWrapper, key: &str, default_val: &str) -> Result; + fn get_long(self: &ConfigWrapper, key: &str, default_val: i64) -> Result; + fn get_int(self: &ConfigWrapper, key: &str, default_val: i32) -> Result; + fn get_bool(self: &ConfigWrapper, key: &str, default_val: bool) -> Result; + fn get_double(self: &ConfigWrapper, key: &str, default_val: f64) -> Result; + fn get_string_vector( + self: &ConfigWrapper, + key: &str, + default_val: &Vec, + ) -> Result>; + + // Sub-configurations (by key) + fn get_sub(self: &ConfigWrapper, key: &str) -> Result>; + fn sub_count(self: &ConfigWrapper, key: &str) -> Result; + fn sub_at( + self: &ConfigWrapper, + key: &str, + index: usize, + ) -> Result>; + + // Sub-configurations (root-level list — no-arg getSubConfigurations()) + fn root_sub_count(self: &ConfigWrapper) -> Result; + fn root_sub_at(self: &ConfigWrapper, index: usize) -> Result>; + + // Write + fn set_string(self: Pin<&mut ConfigWrapper>, key: &str, value: &str); + fn set_long(self: Pin<&mut ConfigWrapper>, key: &str, value: i64); + fn set_int(self: Pin<&mut ConfigWrapper>, key: &str, value: i32); + fn set_bool(self: Pin<&mut ConfigWrapper>, key: &str, value: bool); + fn set_double(self: Pin<&mut ConfigWrapper>, key: &str, value: f64); + fn remove(self: Pin<&mut ConfigWrapper>, key: &str); + + // ==================== DataHandle ==================== + + type DataHandleWrapper; + + fn open_for_read(self: Pin<&mut DataHandleWrapper>) -> Result; + fn open_for_write(self: Pin<&mut DataHandleWrapper>, estimated_length: i64) -> Result<()>; + fn read(self: Pin<&mut DataHandleWrapper>, buf: &mut [u8]) -> Result; + fn write(self: Pin<&mut DataHandleWrapper>, buf: &[u8]) -> Result; + fn close(self: Pin<&mut DataHandleWrapper>) -> Result<()>; + fn position(self: &DataHandleWrapper) -> Result; + fn seek(self: Pin<&mut DataHandleWrapper>, offset: i64) -> Result; + fn can_seek(self: &DataHandleWrapper) -> bool; + fn estimate(self: &DataHandleWrapper) -> Result; + + /// Copy all data from this handle into another (both must be open). + fn save_into( + self: Pin<&mut DataHandleWrapper>, + target: Pin<&mut DataHandleWrapper>, + ) -> Result; + + /// Open a file as a DataHandle for reading. + fn data_handle_from_file(path: &str) -> Result>; + + /// Open a byte range of a file as a DataHandle. + fn data_handle_from_part( + path: &str, + offset: i64, + length: i64, + ) -> Result>; + + /// Create a DataHandle from an in-memory buffer (copies the data). + fn data_handle_from_buffer(data: &[u8]) -> Result>; + + /// Create a MultiHandle from multiple file paths. + fn data_handle_from_multi(paths: &[String]) -> Result>; + + /// Create a TeeHandle from multiple file paths — writes all targets in parallel. + fn data_handle_tee(paths: &[String]) -> Result>; + + // ==================== Message + Reader ==================== + + type MessageWrapper; + + fn is_valid(self: &MessageWrapper) -> bool; + fn length(self: &MessageWrapper) -> usize; + /// Byte offset of this message within the source data handle, as + /// reported by `eckit::message::Reader`. + fn offset(self: &MessageWrapper) -> i64; + fn get_string(self: &MessageWrapper, key: &str) -> Result; + fn get_long(self: &MessageWrapper, key: &str) -> Result; + fn get_double(self: &MessageWrapper, key: &str) -> Result; + fn data(self: &MessageWrapper) -> &[u8]; + fn write_to(self: &MessageWrapper, handle: Pin<&mut DataHandleWrapper>) -> Result<()>; + #[rust_name = "clone_message"] + fn clone(self: &MessageWrapper) -> Result>; + + type ReaderWrapper; + + fn new_reader(handle: Pin<&mut DataHandleWrapper>) -> Result>; + fn next(self: Pin<&mut ReaderWrapper>) -> Result>; + + // ==================== Stream ==================== + + type StreamWrapper; + + // Write + fn write_char(self: Pin<&mut StreamWrapper>, c: u8) -> Result<()>; + fn write_bool(self: Pin<&mut StreamWrapper>, v: bool) -> Result<()>; + fn write_int(self: Pin<&mut StreamWrapper>, v: i32) -> Result<()>; + fn write_long(self: Pin<&mut StreamWrapper>, v: i64) -> Result<()>; + fn write_unsigned_long(self: Pin<&mut StreamWrapper>, v: u64) -> Result<()>; + fn write_double(self: Pin<&mut StreamWrapper>, v: f64) -> Result<()>; + fn write_string(self: Pin<&mut StreamWrapper>, v: &str) -> Result<()>; + fn write_blob(self: Pin<&mut StreamWrapper>, data: &[u8]) -> Result<()>; + + // Read + fn read_char(self: Pin<&mut StreamWrapper>) -> Result; + fn read_bool(self: Pin<&mut StreamWrapper>) -> Result; + fn read_int(self: Pin<&mut StreamWrapper>) -> Result; + fn read_long(self: Pin<&mut StreamWrapper>) -> Result; + fn read_unsigned_long(self: Pin<&mut StreamWrapper>) -> Result; + fn read_double(self: Pin<&mut StreamWrapper>) -> Result; + fn read_string(self: Pin<&mut StreamWrapper>) -> Result; + + // Raw byte read from socket + fn read_bytes(self: Pin<&mut StreamWrapper>, buf: &mut [u8]) -> Result; + + /// Hand off the underlying connection as a streaming `DataHandle`. + /// + /// Only TCP streams support this; memory streams throw. After the + /// call the source stream is left in an unspecified state and must + /// be dropped — only the returned `DataHandleWrapper` holds the + /// connection. + fn into_data_handle(self: Pin<&mut StreamWrapper>) -> Result>; + + // Buffer access (memory streams) + fn buffer(self: Pin<&mut StreamWrapper>) -> Result<&[u8]>; + fn bytes_written(self: &StreamWrapper) -> i64; + + /// Connect to a TCP host:port and return a stream. + fn stream_connect(host: &str, port: i32) -> Result>; + + /// Create a resizable memory stream for writing. + #[must_use] + fn stream_memory_write() -> UniquePtr; + + /// Create a fixed memory stream for reading from existing data. + #[must_use] + fn stream_memory_read(data: &[u8]) -> UniquePtr; + + // ==================== Library registration ==================== + + /// Register a Rust-implemented library with eckit's LibraryManager. + /// Enables `~name` tilde expansion via `$NAME_HOME` env var. + fn register_library(lib: Box); + + /// Get configuration for a registered library by name. + fn library_configuration(name: &str) -> Result>; + + /// Snapshot of every ECMWF library registered with + /// `eckit::system::LibraryManager` (e.g. eckit, metkit, fdb5, mir). + /// Mirrors C++ `Environment::library_versions()` (mars-client). + #[must_use] + fn library_versions() -> Vec; + } + + extern "Rust" { + /// Called from C++ RustLogTarget to emit log messages via Rust's log crate. + fn rust_log(level: LogLevel, msg: &str); + + /// Opaque Rust box holding a `dyn Library` trait object. + type LibraryBox; + + // Callbacks from C++ RustLibrary into Rust trait methods + fn library_name(lib: &LibraryBox) -> &str; + fn library_version(lib: &LibraryBox) -> String; + fn library_git_sha1(lib: &LibraryBox, count: u32) -> String; + fn library_home(lib: &LibraryBox) -> String; + fn library_home_is_set(lib: &LibraryBox) -> bool; + fn library_library_home(lib: &LibraryBox) -> String; + fn library_library_home_is_set(lib: &LibraryBox) -> bool; + fn library_prefix_directory(lib: &LibraryBox) -> String; + fn library_prefix_directory_is_set(lib: &LibraryBox) -> bool; + fn library_expand_path(lib: &LibraryBox, path: &str) -> String; + fn library_expand_path_is_set(lib: &LibraryBox, path: &str) -> bool; + fn library_debug(lib: &LibraryBox) -> bool; + fn library_debug_is_set(lib: &LibraryBox) -> bool; + } +} + +// Public re-exports for the safe wrapper crate +pub use cxx::{Exception, UniquePtr}; +pub use ffi::*; + +/// Called from C++ `RustLogTarget::write()` — routes to Rust `log` crate. +fn rust_log(level: ffi::LogLevel, msg: &str) { + match level { + ffi::LogLevel::Error => log::error!(target: "eckit", "{msg}"), + ffi::LogLevel::Warn => log::warn!(target: "eckit", "{msg}"), + ffi::LogLevel::Info => log::info!(target: "eckit", "{msg}"), + ffi::LogLevel::Debug => log::debug!(target: "eckit", "{msg}"), + // Trace + wildcard for cxx non-exhaustive enum + _ => log::trace!(target: "eckit", "{msg}"), + } +} + +// ==================== Library registration (internal plumbing) ==================== + +type OptStringFn = Box Option + Send + Sync>; +type OptStringArgFn = Box Option + Send + Sync>; +type OptBoolFn = Box Option + Send + Sync>; + +/// Opaque box holding library callbacks for FFI. Constructed by the `eckit` crate. +pub struct LibraryBox { + pub name: String, + pub version_fn: Box String + Send + Sync>, + pub git_sha1_fn: Box String + Send + Sync>, + pub home_fn: OptStringFn, + pub library_home_fn: OptStringFn, + pub prefix_directory_fn: OptStringFn, + pub expand_path_fn: OptStringArgFn, + pub debug_fn: OptBoolFn, +} + +// Callbacks from C++ RustLibrary into Rust closures + +fn library_name(lib: &LibraryBox) -> &str { + &lib.name +} + +fn library_version(lib: &LibraryBox) -> String { + (lib.version_fn)() +} + +fn library_git_sha1(lib: &LibraryBox, count: u32) -> String { + (lib.git_sha1_fn)(count) +} + +fn library_home(lib: &LibraryBox) -> String { + (lib.home_fn)().unwrap_or_default() +} + +fn library_home_is_set(lib: &LibraryBox) -> bool { + (lib.home_fn)().is_some() +} + +fn library_library_home(lib: &LibraryBox) -> String { + (lib.library_home_fn)().unwrap_or_default() +} + +fn library_library_home_is_set(lib: &LibraryBox) -> bool { + (lib.library_home_fn)().is_some() +} + +fn library_prefix_directory(lib: &LibraryBox) -> String { + (lib.prefix_directory_fn)().unwrap_or_default() +} + +fn library_prefix_directory_is_set(lib: &LibraryBox) -> bool { + (lib.prefix_directory_fn)().is_some() +} + +fn library_expand_path(lib: &LibraryBox, path: &str) -> String { + (lib.expand_path_fn)(path).unwrap_or_default() +} + +fn library_expand_path_is_set(lib: &LibraryBox, path: &str) -> bool { + (lib.expand_path_fn)(path).is_some() +} + +fn library_debug(lib: &LibraryBox) -> bool { + (lib.debug_fn)().unwrap_or(false) +} + +fn library_debug_is_set(lib: &LibraryBox) -> bool { + (lib.debug_fn)().is_some() +} + +/// Initialize eckit runtime with Rust log bridge. +/// +/// Must be called before any eckit API usage. Safe to call multiple times. +/// +/// This creates a `RustMain` C++ object that overrides eckit's log target +/// factory methods, ensuring every thread gets log output routed through +/// Rust's `log` crate. +pub fn init() { + ffi::init(); +} From 9a25e98d395013fcb3e16a24e8f694c9acc296ac Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Thu, 30 Apr 2026 23:58:42 +0200 Subject: [PATCH 02/22] Apply clang format --- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 502 +++++++++++---------- rust/crates/eckit-sys/cpp/eckit_bridge.h | 364 +++++++-------- 2 files changed, 438 insertions(+), 428 deletions(-) diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index 56d771f5d..9ae300191 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -1,511 +1,513 @@ // eckit C++ bridge implementation #include "eckit_bridge.h" -#include "eckit-sys/src/lib.rs.h" // cxx-generated — provides LogLevel values +#include "eckit-sys/src/lib.rs.h" // cxx-generated — provides LogLevel values namespace eckit_bridge { // ==================== Logging ==================== -void RustLogTarget::write(const char *start, const char *end) { - buffer_.append(start, end); - - std::string::size_type pos; - while ((pos = buffer_.find('\n')) != std::string::npos) { - std::string line = buffer_.substr(0, pos); - while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) { - line.pop_back(); - } - if (!line.empty()) { - rust_log(level_, rust::Str(line.data(), line.size())); +void RustLogTarget::write(const char* start, const char* end) { + buffer_.append(start, end); + + std::string::size_type pos; + while ((pos = buffer_.find('\n')) != std::string::npos) { + std::string line = buffer_.substr(0, pos); + while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) { + line.pop_back(); + } + if (!line.empty()) { + rust_log(level_, rust::Str(line.data(), line.size())); + } + buffer_.erase(0, pos + 1); } - buffer_.erase(0, pos + 1); - } } void RustLogTarget::flush() { - if (!buffer_.empty()) { - while (!buffer_.empty() && - (buffer_.back() == '\r' || buffer_.back() == ' ')) { - buffer_.pop_back(); - } if (!buffer_.empty()) { - rust_log(level_, rust::Str(buffer_.data(), buffer_.size())); - buffer_.clear(); + while (!buffer_.empty() && (buffer_.back() == '\r' || buffer_.back() == ' ')) { + buffer_.pop_back(); + } + if (!buffer_.empty()) { + rust_log(level_, rust::Str(buffer_.data(), buffer_.size())); + buffer_.clear(); + } } - } } -RustMain::RustMain(int argc, char **argv) : Main(argc, argv) {} +RustMain::RustMain(int argc, char** argv) : Main(argc, argv) {} -eckit::LogTarget *RustMain::createInfoLogTarget() const { - return new RustLogTarget(LogLevel::Info); +eckit::LogTarget* RustMain::createInfoLogTarget() const { + return new RustLogTarget(LogLevel::Info); } -eckit::LogTarget *RustMain::createWarningLogTarget() const { - return new RustLogTarget(LogLevel::Warn); +eckit::LogTarget* RustMain::createWarningLogTarget() const { + return new RustLogTarget(LogLevel::Warn); } -eckit::LogTarget *RustMain::createErrorLogTarget() const { - return new RustLogTarget(LogLevel::Error); +eckit::LogTarget* RustMain::createErrorLogTarget() const { + return new RustLogTarget(LogLevel::Error); } -eckit::LogTarget *RustMain::createDebugLogTarget() const { - return new RustLogTarget(LogLevel::Debug); +eckit::LogTarget* RustMain::createDebugLogTarget() const { + return new RustLogTarget(LogLevel::Debug); } -eckit::LogTarget *RustMain::createMetricsLogTarget() const { - return new RustLogTarget(LogLevel::Trace); +eckit::LogTarget* RustMain::createMetricsLogTarget() const { + return new RustLogTarget(LogLevel::Trace); } void init() { - if (!eckit::Main::ready()) { - static const char *argv[] = {"eckit-rs", nullptr}; - static auto *main = new RustMain(1, const_cast(argv)); - (void)main; - } + if (!eckit::Main::ready()) { + static const char* argv[] = {"eckit-rs", nullptr}; + static auto* main = new RustMain(1, const_cast(argv)); + (void)main; + } } // ==================== Configuration ==================== bool ConfigWrapper::has(rust::Str key) const { - return config_.has(std::string(key)); + return config_.has(std::string(key)); } bool ConfigWrapper::is_list(rust::Str key) const { - return config_.isList(std::string(key)); + return config_.isList(std::string(key)); } -bool ConfigWrapper::is_empty() const { return config_.empty(); } +bool ConfigWrapper::is_empty() const { + return config_.empty(); +} -rust::String ConfigWrapper::get_string(rust::Str key, - rust::Str default_val) const { - return rust::String( - config_.getString(std::string(key), std::string(default_val))); +rust::String ConfigWrapper::get_string(rust::Str key, rust::Str default_val) const { + return rust::String(config_.getString(std::string(key), std::string(default_val))); } int64_t ConfigWrapper::get_long(rust::Str key, int64_t default_val) const { - return config_.getLong(std::string(key), default_val); + return config_.getLong(std::string(key), default_val); } int32_t ConfigWrapper::get_int(rust::Str key, int32_t default_val) const { - return config_.getInt(std::string(key), default_val); + return config_.getInt(std::string(key), default_val); } bool ConfigWrapper::get_bool(rust::Str key, bool default_val) const { - return config_.getBool(std::string(key), default_val); + return config_.getBool(std::string(key), default_val); } double ConfigWrapper::get_double(rust::Str key, double default_val) const { - return config_.getDouble(std::string(key), default_val); + return config_.getDouble(std::string(key), default_val); } -rust::Vec ConfigWrapper::get_string_vector( - rust::Str key, const rust::Vec &default_val) const { - std::vector def; - def.reserve(default_val.size()); - for (const auto &s : default_val) { - def.emplace_back(std::string(s)); - } - auto vec = config_.getStringVector(std::string(key), def); - rust::Vec result; - result.reserve(vec.size()); - for (const auto &s : vec) { - result.push_back(rust::String(s)); - } - return result; +rust::Vec ConfigWrapper::get_string_vector(rust::Str key, + const rust::Vec& default_val) const { + std::vector def; + def.reserve(default_val.size()); + for (const auto& s : default_val) { + def.emplace_back(std::string(s)); + } + auto vec = config_.getStringVector(std::string(key), def); + rust::Vec result; + result.reserve(vec.size()); + for (const auto& s : vec) { + result.push_back(rust::String(s)); + } + return result; } std::unique_ptr ConfigWrapper::get_sub(rust::Str key) const { - return std::make_unique( - config_.getSubConfiguration(std::string(key))); + return std::make_unique(config_.getSubConfiguration(std::string(key))); } size_t ConfigWrapper::sub_count(rust::Str key) const { - return config_.getSubConfigurations(std::string(key)).size(); + return config_.getSubConfigurations(std::string(key)).size(); } -std::unique_ptr ConfigWrapper::sub_at(rust::Str key, - size_t index) const { - auto subs = config_.getSubConfigurations(std::string(key)); - return std::make_unique(subs.at(index)); +std::unique_ptr ConfigWrapper::sub_at(rust::Str key, size_t index) const { + auto subs = config_.getSubConfigurations(std::string(key)); + return std::make_unique(subs.at(index)); } size_t ConfigWrapper::root_sub_count() const { - return config_.getSubConfigurations().size(); + return config_.getSubConfigurations().size(); } std::unique_ptr ConfigWrapper::root_sub_at(size_t index) const { - auto subs = config_.getSubConfigurations(); - return std::make_unique(subs.at(index)); + auto subs = config_.getSubConfigurations(); + return std::make_unique(subs.at(index)); } void ConfigWrapper::set_string(rust::Str key, rust::Str value) { - config_.set(std::string(key), std::string(value)); + config_.set(std::string(key), std::string(value)); } void ConfigWrapper::set_long(rust::Str key, int64_t value) { - config_.set(std::string(key), static_cast(value)); + config_.set(std::string(key), static_cast(value)); } void ConfigWrapper::set_int(rust::Str key, int32_t value) { - config_.set(std::string(key), value); + config_.set(std::string(key), value); } void ConfigWrapper::set_bool(rust::Str key, bool value) { - config_.set(std::string(key), value); + config_.set(std::string(key), value); } void ConfigWrapper::set_double(rust::Str key, double value) { - config_.set(std::string(key), value); + config_.set(std::string(key), value); } -void ConfigWrapper::remove(rust::Str key) { config_.remove(std::string(key)); } +void ConfigWrapper::remove(rust::Str key) { + config_.remove(std::string(key)); +} std::unique_ptr create() { - return std::make_unique(); + return std::make_unique(); } std::unique_ptr from_path(rust::Str path) { - auto p = eckit::PathName{std::string(path)}; - auto yaml = eckit::YAMLConfiguration{p}; - return std::make_unique(yaml); + auto p = eckit::PathName{std::string(path)}; + auto yaml = eckit::YAMLConfiguration{p}; + return std::make_unique(yaml); } std::unique_ptr from_yaml(rust::Str yaml) { - auto str = std::string(yaml); - auto parsed = eckit::YAMLConfiguration{str}; - return std::make_unique(parsed); + auto str = std::string(yaml); + auto parsed = eckit::YAMLConfiguration{str}; + return std::make_unique(parsed); } -std::unique_ptr clone(const ConfigWrapper &src) { - return std::make_unique(src.inner()); +std::unique_ptr clone(const ConfigWrapper& src) { + return std::make_unique(src.inner()); } // ==================== DataHandle ==================== int64_t DataHandleWrapper::open_for_read() { - return static_cast(handle_->openForRead()); + return static_cast(handle_->openForRead()); } void DataHandleWrapper::open_for_write(int64_t estimated_length) { - handle_->openForWrite(eckit::Length(estimated_length)); + handle_->openForWrite(eckit::Length(estimated_length)); } int64_t DataHandleWrapper::read(rust::Slice buf) { - return handle_->read(buf.data(), static_cast(buf.size())); + return handle_->read(buf.data(), static_cast(buf.size())); } int64_t DataHandleWrapper::write(rust::Slice buf) { - return handle_->write(buf.data(), static_cast(buf.size())); + return handle_->write(buf.data(), static_cast(buf.size())); } -void DataHandleWrapper::close() { handle_->close(); } +void DataHandleWrapper::close() { + handle_->close(); +} int64_t DataHandleWrapper::position() const { - return static_cast(handle_->position()); + return static_cast(handle_->position()); } int64_t DataHandleWrapper::seek(int64_t offset) { - return static_cast(handle_->seek(eckit::Offset(offset))); + return static_cast(handle_->seek(eckit::Offset(offset))); } -bool DataHandleWrapper::can_seek() const { return handle_->canSeek(); } +bool DataHandleWrapper::can_seek() const { + return handle_->canSeek(); +} int64_t DataHandleWrapper::estimate() const { - return static_cast(handle_->estimate()); + return static_cast(handle_->estimate()); } -int64_t DataHandleWrapper::save_into(DataHandleWrapper &target) { - return static_cast(handle_->saveInto(*target.handle_)); +int64_t DataHandleWrapper::save_into(DataHandleWrapper& target) { + return static_cast(handle_->saveInto(*target.handle_)); } std::unique_ptr data_handle_from_file(rust::Str path) { - auto p = eckit::PathName{std::string(path)}; - return std::make_unique(p.fileHandle()); + auto p = eckit::PathName{std::string(path)}; + return std::make_unique(p.fileHandle()); } -std::unique_ptr -data_handle_from_part(rust::Str path, int64_t offset, int64_t length) { - return std::make_unique( - new eckit::PartFileHandle(eckit::PathName{std::string(path)}, - eckit::Offset(offset), eckit::Length(length))); +std::unique_ptr data_handle_from_part(rust::Str path, int64_t offset, int64_t length) { + return std::make_unique( + new eckit::PartFileHandle(eckit::PathName{std::string(path)}, eckit::Offset(offset), eckit::Length(length))); } -std::unique_ptr -data_handle_from_buffer(rust::Slice data) { - return std::make_unique( - new eckit::MemoryHandle(data.data(), data.size())); +std::unique_ptr data_handle_from_buffer(rust::Slice data) { + return std::make_unique(new eckit::MemoryHandle(data.data(), data.size())); } -std::unique_ptr -data_handle_from_multi(rust::Slice paths) { - auto *mh = new eckit::MultiHandle(); - for (const auto &p : paths) { - (*mh) += eckit::PathName(std::string(p)).fileHandle(); - } - return std::make_unique(mh); +std::unique_ptr data_handle_from_multi(rust::Slice paths) { + auto* mh = new eckit::MultiHandle(); + for (const auto& p : paths) { + (*mh) += eckit::PathName(std::string(p)).fileHandle(); + } + return std::make_unique(mh); } -std::unique_ptr -data_handle_tee(rust::Slice paths) { - std::vector handles; - handles.reserve(paths.size()); - for (const auto &p : paths) { - handles.push_back(eckit::PathName(std::string(p)).fileHandle()); - } - return std::make_unique(new eckit::TeeHandle(handles)); +std::unique_ptr data_handle_tee(rust::Slice paths) { + std::vector handles; + handles.reserve(paths.size()); + for (const auto& p : paths) { + handles.push_back(eckit::PathName(std::string(p)).fileHandle()); + } + return std::make_unique(new eckit::TeeHandle(handles)); } // ==================== Message + Reader ==================== -bool MessageWrapper::is_valid() const { return static_cast(msg_); } +bool MessageWrapper::is_valid() const { + return static_cast(msg_); +} -size_t MessageWrapper::length() const { return msg_.length(); } +size_t MessageWrapper::length() const { + return msg_.length(); +} int64_t MessageWrapper::offset() const { - // eckit::Offset has operator long long(), so cast through that. - return static_cast(static_cast(msg_.offset())); + // eckit::Offset has operator long long(), so cast through that. + return static_cast(static_cast(msg_.offset())); } rust::String MessageWrapper::get_string(rust::Str key) const { - return rust::String(msg_.getString(std::string(key))); + return rust::String(msg_.getString(std::string(key))); } int64_t MessageWrapper::get_long(rust::Str key) const { - return msg_.getLong(std::string(key)); + return msg_.getLong(std::string(key)); } double MessageWrapper::get_double(rust::Str key) const { - return msg_.getDouble(std::string(key)); + return msg_.getDouble(std::string(key)); } rust::Slice MessageWrapper::data() const { - return rust::Slice(static_cast(msg_.data()), - msg_.length()); + return rust::Slice(static_cast(msg_.data()), msg_.length()); } -void MessageWrapper::write_to(DataHandleWrapper &handle) const { - msg_.write(handle.inner()); +void MessageWrapper::write_to(DataHandleWrapper& handle) const { + msg_.write(handle.inner()); } std::unique_ptr MessageWrapper::clone() const { - return std::make_unique(msg_); + return std::make_unique(msg_); } -ReaderWrapper::ReaderWrapper(DataHandleWrapper &handle) - : reader_(std::make_unique(handle.inner(), true)) {} +ReaderWrapper::ReaderWrapper(DataHandleWrapper& handle) : + reader_(std::make_unique(handle.inner(), true)) {} std::unique_ptr ReaderWrapper::next() { - auto msg = reader_->next(); - return std::make_unique(std::move(msg)); + auto msg = reader_->next(); + return std::make_unique(std::move(msg)); } -std::unique_ptr new_reader(DataHandleWrapper &handle) { - return std::make_unique(handle); +std::unique_ptr new_reader(DataHandleWrapper& handle) { + return std::make_unique(handle); } // ==================== Library registration ==================== -RustLibrary::RustLibrary(rust::Box lib) - : eckit::system::Library(std::string(library_name(*lib))), - rust_(std::move(lib)) {} +RustLibrary::RustLibrary(rust::Box lib) : + eckit::system::Library(std::string(library_name(*lib))), rust_(std::move(lib)) {} std::string RustLibrary::version() const { - return std::string(library_version(*rust_)); + return std::string(library_version(*rust_)); } std::string RustLibrary::gitsha1(unsigned int count) const { - return std::string(library_git_sha1(*rust_, count)); + return std::string(library_git_sha1(*rust_, count)); } std::string RustLibrary::home() const { - if (library_home_is_set(*rust_)) { - return std::string(library_home(*rust_)); - } - return Library::home(); + if (library_home_is_set(*rust_)) { + return std::string(library_home(*rust_)); + } + return Library::home(); } std::string RustLibrary::libraryHome() const { - if (library_library_home_is_set(*rust_)) { - return std::string(library_library_home(*rust_)); - } - return Library::libraryHome(); + if (library_library_home_is_set(*rust_)) { + return std::string(library_library_home(*rust_)); + } + return Library::libraryHome(); } std::string RustLibrary::prefixDirectory() const { - if (library_prefix_directory_is_set(*rust_)) { - return std::string(library_prefix_directory(*rust_)); - } - return Library::prefixDirectory(); + if (library_prefix_directory_is_set(*rust_)) { + return std::string(library_prefix_directory(*rust_)); + } + return Library::prefixDirectory(); } -std::string RustLibrary::expandPath(const std::string &path) const { - rust::Str rpath(path.data(), path.size()); - if (library_expand_path_is_set(*rust_, rpath)) { - return std::string(library_expand_path(*rust_, rpath)); - } - return Library::expandPath(path); +std::string RustLibrary::expandPath(const std::string& path) const { + rust::Str rpath(path.data(), path.size()); + if (library_expand_path_is_set(*rust_, rpath)) { + return std::string(library_expand_path(*rust_, rpath)); + } + return Library::expandPath(path); } bool RustLibrary::debug() const { - if (library_debug_is_set(*rust_)) { - return library_debug(*rust_); - } - return Library::debug(); + if (library_debug_is_set(*rust_)) { + return library_debug(*rust_); + } + return Library::debug(); } -const void *RustLibrary::addr() const { - // Return a pointer inside the code segment so dladdr can resolve the - // binary path. `this` is heap-allocated and dladdr cannot resolve it. - return reinterpret_cast(®ister_library); +const void* RustLibrary::addr() const { + // Return a pointer inside the code segment so dladdr can resolve the + // binary path. `this` is heap-allocated and dladdr cannot resolve it. + return reinterpret_cast(®ister_library); } void register_library(rust::Box lib) { - // Heap-allocate and leak — Library registers itself with LibraryManager - // in its constructor and must live for the process lifetime. - new RustLibrary(std::move(lib)); + // Heap-allocate and leak — Library registers itself with LibraryManager + // in its constructor and must live for the process lifetime. + new RustLibrary(std::move(lib)); } std::unique_ptr library_configuration(rust::Str name) { - auto &lib = eckit::system::LibraryManager::lookup(std::string(name)); - const auto &cfg = lib.configuration(); - return std::make_unique(eckit::LocalConfiguration(cfg)); + auto& lib = eckit::system::LibraryManager::lookup(std::string(name)); + const auto& cfg = lib.configuration(); + return std::make_unique(eckit::LocalConfiguration(cfg)); } // ==================== Stream (base class) ==================== -void StreamWrapper::write_char(uint8_t c) { *stream_ << static_cast(c); } -void StreamWrapper::write_bool(bool v) { *stream_ << v; } -void StreamWrapper::write_int(int32_t v) { *stream_ << v; } +void StreamWrapper::write_char(uint8_t c) { + *stream_ << static_cast(c); +} +void StreamWrapper::write_bool(bool v) { + *stream_ << v; +} +void StreamWrapper::write_int(int32_t v) { + *stream_ << v; +} void StreamWrapper::write_long(int64_t v) { - *stream_ << static_cast(v); + *stream_ << static_cast(v); } void StreamWrapper::write_unsigned_long(uint64_t v) { - *stream_ << static_cast(v); + *stream_ << static_cast(v); +} +void StreamWrapper::write_double(double v) { + *stream_ << v; } -void StreamWrapper::write_double(double v) { *stream_ << v; } void StreamWrapper::write_string(rust::Str v) { - *stream_ << std::string(v.data(), v.size()); + *stream_ << std::string(v.data(), v.size()); } void StreamWrapper::write_blob(rust::Slice data) { - stream_->writeBlob(data.data(), data.size()); + stream_->writeBlob(data.data(), data.size()); } uint8_t StreamWrapper::read_char() { - char c{}; - *stream_ >> c; - return static_cast(c); + char c{}; + *stream_ >> c; + return static_cast(c); } bool StreamWrapper::read_bool() { - bool v{}; - *stream_ >> v; - return v; + bool v{}; + *stream_ >> v; + return v; } int32_t StreamWrapper::read_int() { - int v{}; - *stream_ >> v; - return v; + int v{}; + *stream_ >> v; + return v; } int64_t StreamWrapper::read_long() { - long long v{}; - *stream_ >> v; - return static_cast(v); + long long v{}; + *stream_ >> v; + return static_cast(v); } uint64_t StreamWrapper::read_unsigned_long() { - unsigned long long v{}; - *stream_ >> v; - return static_cast(v); + unsigned long long v{}; + *stream_ >> v; + return static_cast(v); } double StreamWrapper::read_double() { - double v{}; - *stream_ >> v; - return v; + double v{}; + *stream_ >> v; + return v; } rust::String StreamWrapper::read_string() { - std::string v; - *stream_ >> v; - return rust::String(v); + std::string v; + *stream_ >> v; + return rust::String(v); } int64_t StreamWrapper::read_bytes(rust::Slice buf) { - throw eckit::SeriousBug("read_bytes not supported on this stream type"); + throw eckit::SeriousBug("read_bytes not supported on this stream type"); } rust::Slice StreamWrapper::buffer() { - throw eckit::SeriousBug("buffer() not supported on this stream type"); + throw eckit::SeriousBug("buffer() not supported on this stream type"); } -int64_t StreamWrapper::bytes_written() const { return stream_->bytesWritten(); } +int64_t StreamWrapper::bytes_written() const { + return stream_->bytesWritten(); +} // ==================== TcpStreamWrapper ==================== -TcpStreamWrapper::TcpStreamWrapper(const std::string &host, int port) - : socket_(eckit::net::TCPClient().connect(host, port)), tcp_(socket_) { - stream_ = &tcp_; +TcpStreamWrapper::TcpStreamWrapper(const std::string& host, int port) : + socket_(eckit::net::TCPClient().connect(host, port)), tcp_(socket_) { + stream_ = &tcp_; } int64_t TcpStreamWrapper::read_bytes(rust::Slice buf) { - // The connection lives on `tcp_.socket()` — `socket_` was emptied by the - // TCPSocket "copy" ctor in `tcp_(socket_)` (ownership transfer). - return tcp_.socket().read(buf.data(), static_cast(buf.size())); + // The connection lives on `tcp_.socket()` — `socket_` was emptied by the + // TCPSocket "copy" ctor in `tcp_(socket_)` (ownership transfer). + return tcp_.socket().read(buf.data(), static_cast(buf.size())); } std::unique_ptr TcpStreamWrapper::into_data_handle() { - // Steal the live connection from `tcp_` and wrap it as an owning - // `eckit::TCPSocketHandle` DataHandle. The TCPSocketHandle ctor copies - // (ownership transfer) the socket into its own member, so after this call - // both `socket_` and `tcp_.socket()` are detached (fd = -1) and only the - // returned DataHandle holds the connection. - return std::make_unique( - new eckit::TCPSocketHandle(tcp_.socket())); + // Steal the live connection from `tcp_` and wrap it as an owning + // `eckit::TCPSocketHandle` DataHandle. The TCPSocketHandle ctor copies + // (ownership transfer) the socket into its own member, so after this call + // both `socket_` and `tcp_.socket()` are detached (fd = -1) and only the + // returned DataHandle holds the connection. + return std::make_unique(new eckit::TCPSocketHandle(tcp_.socket())); } std::unique_ptr StreamWrapper::into_data_handle() { - throw eckit::NotImplemented( - "StreamWrapper::into_data_handle is only supported on TCP streams", - Here()); + throw eckit::NotImplemented("StreamWrapper::into_data_handle is only supported on TCP streams", Here()); } rust::Vec library_versions() { - rust::Vec out; - constexpr size_t sha1len = 8; - for (const auto &name : eckit::system::LibraryManager::list()) { - const auto &lib = eckit::system::LibraryManager::lookup(name); - out.push_back(LibraryVersion{ - rust::String(lib.name()), rust::String(lib.version()), - rust::String(lib.gitsha1(sha1len)), rust::String(lib.libraryHome())}); - } - return out; + rust::Vec out; + constexpr size_t sha1len = 8; + for (const auto& name : eckit::system::LibraryManager::list()) { + const auto& lib = eckit::system::LibraryManager::lookup(name); + out.push_back(LibraryVersion{rust::String(lib.name()), rust::String(lib.version()), + rust::String(lib.gitsha1(sha1len)), rust::String(lib.libraryHome())}); + } + return out; } // ==================== MemoryWriteStreamWrapper ==================== MemoryWriteStreamWrapper::MemoryWriteStreamWrapper() : buf_(4096), mem_(buf_) { - stream_ = &mem_; + stream_ = &mem_; } rust::Slice MemoryWriteStreamWrapper::buffer() { - return {static_cast(buf_.data()), - static_cast(mem_.bytesWritten())}; + return {static_cast(buf_.data()), static_cast(mem_.bytesWritten())}; } // ==================== MemoryReadStreamWrapper ==================== -MemoryReadStreamWrapper::MemoryReadStreamWrapper( - rust::Slice data) - : buf_(data.data(), data.size()), mem_(buf_) { - stream_ = &mem_; +MemoryReadStreamWrapper::MemoryReadStreamWrapper(rust::Slice data) : + buf_(data.data(), data.size()), mem_(buf_) { + stream_ = &mem_; } // ==================== Factory functions ==================== std::unique_ptr stream_connect(rust::Str host, int32_t port) { - return std::make_unique(std::string(host), port); + return std::make_unique(std::string(host), port); } std::unique_ptr stream_memory_write() { - return std::make_unique(); + return std::make_unique(); } -std::unique_ptr -stream_memory_read(rust::Slice data) { - return std::make_unique(data); +std::unique_ptr stream_memory_read(rust::Slice data) { + return std::make_unique(data); } -} // namespace eckit_bridge +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.h b/rust/crates/eckit-sys/cpp/eckit_bridge.h index 1d0afe3dc..81c85f3c5 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.h +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.h @@ -12,12 +12,12 @@ #include "eckit/io/MemoryHandle.h" #include "eckit/io/MultiHandle.h" #include "eckit/io/PartFileHandle.h" +#include "eckit/io/TCPSocketHandle.h" #include "eckit/io/TeeHandle.h" #include "eckit/log/Log.h" #include "eckit/log/LogTarget.h" #include "eckit/message/Message.h" #include "eckit/message/Reader.h" -#include "eckit/io/TCPSocketHandle.h" #include "eckit/net/TCPClient.h" #include "eckit/net/TCPStream.h" #include "eckit/runtime/Main.h" @@ -45,27 +45,30 @@ void rust_log(LogLevel level, rust::Str msg) noexcept; /// Accumulates writes until a newline or flush, then emits a single log call. class RustLogTarget : public eckit::LogTarget { public: - explicit RustLogTarget(LogLevel level) : level_(level) {} - void write(const char *start, const char *end) override; - void flush() override; + explicit RustLogTarget(LogLevel level) : level_(level) {} + + void write(const char* start, const char* end) override; + void flush() override; private: - LogLevel level_; - std::string buffer_; + + LogLevel level_; + std::string buffer_; }; /// Main subclass that installs `RustLogTarget` on all channels. /// Every new thread automatically gets `RustLogTarget` via the factory methods. class RustMain : public eckit::Main { public: - RustMain(int argc, char **argv); - eckit::LogTarget *createInfoLogTarget() const override; - eckit::LogTarget *createWarningLogTarget() const override; - eckit::LogTarget *createErrorLogTarget() const override; - eckit::LogTarget *createDebugLogTarget() const override; - eckit::LogTarget *createMetricsLogTarget() const override; + RustMain(int argc, char** argv); + + eckit::LogTarget* createInfoLogTarget() const override; + eckit::LogTarget* createWarningLogTarget() const override; + eckit::LogTarget* createErrorLogTarget() const override; + eckit::LogTarget* createDebugLogTarget() const override; + eckit::LogTarget* createMetricsLogTarget() const override; }; /// Initialize eckit runtime with Rust log bridge. @@ -75,82 +78,82 @@ void init(); /// Wraps `eckit::LocalConfiguration` for Rust FFI. class ConfigWrapper { - eckit::LocalConfiguration config_; + eckit::LocalConfiguration config_; public: - ConfigWrapper() = default; - explicit ConfigWrapper(const eckit::Configuration &other) : config_(other) {} - - // Read - bool has(rust::Str key) const; - bool is_list(rust::Str key) const; - bool is_empty() const; - rust::String get_string(rust::Str key, rust::Str default_val) const; - int64_t get_long(rust::Str key, int64_t default_val) const; - int32_t get_int(rust::Str key, int32_t default_val) const; - bool get_bool(rust::Str key, bool default_val) const; - double get_double(rust::Str key, double default_val) const; - rust::Vec - get_string_vector(rust::Str key, - const rust::Vec &default_val) const; - - // Sub-configurations (by key) - std::unique_ptr get_sub(rust::Str key) const; - size_t sub_count(rust::Str key) const; - std::unique_ptr sub_at(rust::Str key, size_t index) const; - - // Sub-configurations (root-level list) - size_t root_sub_count() const; - std::unique_ptr root_sub_at(size_t index) const; - - // Write - void set_string(rust::Str key, rust::Str value); - void set_long(rust::Str key, int64_t value); - void set_int(rust::Str key, int32_t value); - void set_bool(rust::Str key, bool value); - void set_double(rust::Str key, double value); - void remove(rust::Str key); - - // Access underlying for other C++ bridge code - const eckit::LocalConfiguration &inner() const { return config_; } - eckit::LocalConfiguration &inner() { return config_; } + + ConfigWrapper() = default; + explicit ConfigWrapper(const eckit::Configuration& other) : config_(other) {} + + // Read + bool has(rust::Str key) const; + bool is_list(rust::Str key) const; + bool is_empty() const; + rust::String get_string(rust::Str key, rust::Str default_val) const; + int64_t get_long(rust::Str key, int64_t default_val) const; + int32_t get_int(rust::Str key, int32_t default_val) const; + bool get_bool(rust::Str key, bool default_val) const; + double get_double(rust::Str key, double default_val) const; + rust::Vec get_string_vector(rust::Str key, const rust::Vec& default_val) const; + + // Sub-configurations (by key) + std::unique_ptr get_sub(rust::Str key) const; + size_t sub_count(rust::Str key) const; + std::unique_ptr sub_at(rust::Str key, size_t index) const; + + // Sub-configurations (root-level list) + size_t root_sub_count() const; + std::unique_ptr root_sub_at(size_t index) const; + + // Write + void set_string(rust::Str key, rust::Str value); + void set_long(rust::Str key, int64_t value); + void set_int(rust::Str key, int32_t value); + void set_bool(rust::Str key, bool value); + void set_double(rust::Str key, double value); + void remove(rust::Str key); + + // Access underlying for other C++ bridge code + const eckit::LocalConfiguration& inner() const { return config_; } + eckit::LocalConfiguration& inner() { return config_; } }; // Factory functions — names match cxx bridge declarations std::unique_ptr create(); std::unique_ptr from_path(rust::Str path); std::unique_ptr from_yaml(rust::Str yaml); -std::unique_ptr clone(const ConfigWrapper &src); +std::unique_ptr clone(const ConfigWrapper& src); // ==================== DataHandle ==================== /// Wraps `eckit::DataHandle*` for Rust FFI. Takes ownership. class DataHandleWrapper { - std::unique_ptr handle_; + std::unique_ptr handle_; public: - /// Takes ownership of a raw DataHandle pointer. - explicit DataHandleWrapper(eckit::DataHandle *h) : handle_(h) {} - - int64_t open_for_read(); - void open_for_write(int64_t estimated_length); - int64_t read(rust::Slice buf); - int64_t write(rust::Slice buf); - void close(); - int64_t position() const; - int64_t seek(int64_t offset); - bool can_seek() const; - int64_t estimate() const; - - /// Copy all data from this handle into target (both must be open). - int64_t save_into(DataHandleWrapper &target); - - /// Access underlying for other C++ bridge code. - eckit::DataHandle &inner() { return *handle_; } - const eckit::DataHandle &inner() const { return *handle_; } - - /// Release ownership — caller takes responsibility. - eckit::DataHandle *release() { return handle_.release(); } + + /// Takes ownership of a raw DataHandle pointer. + explicit DataHandleWrapper(eckit::DataHandle* h) : handle_(h) {} + + int64_t open_for_read(); + void open_for_write(int64_t estimated_length); + int64_t read(rust::Slice buf); + int64_t write(rust::Slice buf); + void close(); + int64_t position() const; + int64_t seek(int64_t offset); + bool can_seek() const; + int64_t estimate() const; + + /// Copy all data from this handle into target (both must be open). + int64_t save_into(DataHandleWrapper& target); + + /// Access underlying for other C++ bridge code. + eckit::DataHandle& inner() { return *handle_; } + const eckit::DataHandle& inner() const { return *handle_; } + + /// Release ownership — caller takes responsibility. + eckit::DataHandle* release() { return handle_.release(); } }; // ==================== Message + Reader ==================== @@ -158,66 +161,64 @@ class DataHandleWrapper { /// Wraps `eckit::message::Message` for Rust FFI. /// Message is a value type with internal reference counting. class MessageWrapper { - eckit::message::Message msg_; + eckit::message::Message msg_; public: - MessageWrapper() = default; - explicit MessageWrapper(eckit::message::Message m) : msg_(std::move(m)) {} - - bool is_valid() const; - size_t length() const; - /// Byte offset of this message within the source data handle. Mirrors - /// `eckit::message::Message::offset()` — populated by the Reader when - /// scanning a file/stream. - int64_t offset() const; - rust::String get_string(rust::Str key) const; - int64_t get_long(rust::Str key) const; - double get_double(rust::Str key) const; - rust::Slice data() const; - void write_to(DataHandleWrapper &handle) const; - - /// Clone (Message is ref-counted internally). - std::unique_ptr clone() const; - - /// Access underlying for other C++ bridge code. - const eckit::message::Message &inner() const { return msg_; } - eckit::message::Message &inner() { return msg_; } + + MessageWrapper() = default; + explicit MessageWrapper(eckit::message::Message m) : msg_(std::move(m)) {} + + bool is_valid() const; + size_t length() const; + /// Byte offset of this message within the source data handle. Mirrors + /// `eckit::message::Message::offset()` — populated by the Reader when + /// scanning a file/stream. + int64_t offset() const; + rust::String get_string(rust::Str key) const; + int64_t get_long(rust::Str key) const; + double get_double(rust::Str key) const; + rust::Slice data() const; + void write_to(DataHandleWrapper& handle) const; + + /// Clone (Message is ref-counted internally). + std::unique_ptr clone() const; + + /// Access underlying for other C++ bridge code. + const eckit::message::Message& inner() const { return msg_; } + eckit::message::Message& inner() { return msg_; } }; /// Wraps `eckit::message::Reader` for Rust FFI. /// Reads messages from a `DataHandle`. class ReaderWrapper { - std::unique_ptr reader_; + std::unique_ptr reader_; public: - explicit ReaderWrapper(DataHandleWrapper &handle); - /// Returns next message, or an invalid message when exhausted. - std::unique_ptr next(); + explicit ReaderWrapper(DataHandleWrapper& handle); + + /// Returns next message, or an invalid message when exhausted. + std::unique_ptr next(); }; // Factory -std::unique_ptr new_reader(DataHandleWrapper &handle); +std::unique_ptr new_reader(DataHandleWrapper& handle); /// Open a file as a DataHandle for reading. std::unique_ptr data_handle_from_file(rust::Str path); /// Open a byte range of a file as a DataHandle. -std::unique_ptr -data_handle_from_part(rust::Str path, int64_t offset, int64_t length); +std::unique_ptr data_handle_from_part(rust::Str path, int64_t offset, int64_t length); /// Create a DataHandle from an in-memory buffer (copies the data). -std::unique_ptr -data_handle_from_buffer(rust::Slice data); +std::unique_ptr data_handle_from_buffer(rust::Slice data); /// Create a MultiHandle from multiple file paths. -std::unique_ptr -data_handle_from_multi(rust::Slice paths); +std::unique_ptr data_handle_from_multi(rust::Slice paths); /// Create a TeeHandle from multiple file paths — writes all targets in /// parallel. -std::unique_ptr -data_handle_tee(rust::Slice paths); +std::unique_ptr data_handle_tee(rust::Slice paths); // ==================== Library registration ==================== @@ -226,21 +227,23 @@ struct LibraryBox; /// eckit::system::Library subclass that delegates to a Rust `dyn Library`. class RustLibrary : public eckit::system::Library { - rust::Box rust_; + rust::Box rust_; public: - explicit RustLibrary(rust::Box lib); - std::string version() const override; - std::string gitsha1(unsigned int count) const override; + explicit RustLibrary(rust::Box lib); + + std::string version() const override; + std::string gitsha1(unsigned int count) const override; protected: - std::string home() const override; - std::string libraryHome() const override; - std::string prefixDirectory() const override; - std::string expandPath(const std::string &path) const override; - bool debug() const override; - const void *addr() const override; + + std::string home() const override; + std::string libraryHome() const override; + std::string prefixDirectory() const override; + std::string expandPath(const std::string& path) const override; + bool debug() const override; + const void* addr() const override; }; /// Register a Rust library with eckit's LibraryManager. @@ -266,91 +269,96 @@ rust::Vec library_versions(); /// `eckit::Stream*` set by the subclass. class StreamWrapper { protected: - eckit::Stream *stream_ = nullptr; + + eckit::Stream* stream_ = nullptr; public: - virtual ~StreamWrapper() = default; - - StreamWrapper(const StreamWrapper &) = delete; - StreamWrapper &operator=(const StreamWrapper &) = delete; - - // Write operations (eckit::Stream operator<<) - void write_char(uint8_t c); - void write_bool(bool v); - void write_int(int32_t v); - void write_long(int64_t v); - void write_unsigned_long(uint64_t v); - void write_double(double v); - void write_string(rust::Str v); - void write_blob(rust::Slice data); - - // Read operations (eckit::Stream operator>>) - uint8_t read_char(); - bool read_bool(); - int32_t read_int(); - int64_t read_long(); - uint64_t read_unsigned_long(); - double read_double(); - rust::String read_string(); - - // Raw byte read (for data transfer after protocol handshake) - virtual int64_t read_bytes(rust::Slice buf); - - /// Access underlying stream for other bridge code. - eckit::Stream &inner() { return *stream_; } - const eckit::Stream &inner() const { return *stream_; } - - /// Number of bytes written so far. - int64_t bytes_written() const; - - /// Get buffer contents (memory write streams only). - virtual rust::Slice buffer(); - - /// Hand off the underlying connection as a DataHandle for streaming reads. - /// - /// Only meaningful for TCP streams; the default throws `NotImplemented`. - /// After this call the stream is in an unspecified state and must not be - /// used; the returned `DataHandleWrapper` owns the connection. - virtual std::unique_ptr into_data_handle(); + + virtual ~StreamWrapper() = default; + + StreamWrapper(const StreamWrapper&) = delete; + StreamWrapper& operator=(const StreamWrapper&) = delete; + + // Write operations (eckit::Stream operator<<) + void write_char(uint8_t c); + void write_bool(bool v); + void write_int(int32_t v); + void write_long(int64_t v); + void write_unsigned_long(uint64_t v); + void write_double(double v); + void write_string(rust::Str v); + void write_blob(rust::Slice data); + + // Read operations (eckit::Stream operator>>) + uint8_t read_char(); + bool read_bool(); + int32_t read_int(); + int64_t read_long(); + uint64_t read_unsigned_long(); + double read_double(); + rust::String read_string(); + + // Raw byte read (for data transfer after protocol handshake) + virtual int64_t read_bytes(rust::Slice buf); + + /// Access underlying stream for other bridge code. + eckit::Stream& inner() { return *stream_; } + const eckit::Stream& inner() const { return *stream_; } + + /// Number of bytes written so far. + int64_t bytes_written() const; + + /// Get buffer contents (memory write streams only). + virtual rust::Slice buffer(); + + /// Hand off the underlying connection as a DataHandle for streaming reads. + /// + /// Only meaningful for TCP streams; the default throws `NotImplemented`. + /// After this call the stream is in an unspecified state and must not be + /// used; the returned `DataHandleWrapper` owns the connection. + virtual std::unique_ptr into_data_handle(); protected: - StreamWrapper() = default; + + StreamWrapper() = default; }; /// TCP stream — connects to host:port via `eckit::net::TCPClient`. class TcpStreamWrapper : public StreamWrapper { - eckit::net::TCPSocket socket_; - eckit::net::TCPStream tcp_; + eckit::net::TCPSocket socket_; + eckit::net::TCPStream tcp_; public: - TcpStreamWrapper(const std::string &host, int port); - int64_t read_bytes(rust::Slice buf) override; - std::unique_ptr into_data_handle() override; + + TcpStreamWrapper(const std::string& host, int port); + int64_t read_bytes(rust::Slice buf) override; + std::unique_ptr into_data_handle() override; }; /// Resizable memory stream — for writing, buffer grows as needed. class MemoryWriteStreamWrapper : public StreamWrapper { - eckit::Buffer buf_; - eckit::ResizableMemoryStream mem_; + eckit::Buffer buf_; + eckit::ResizableMemoryStream mem_; public: - MemoryWriteStreamWrapper(); - rust::Slice buffer() override; + + MemoryWriteStreamWrapper(); + rust::Slice buffer() override; }; /// Fixed memory stream — for reading from existing data. class MemoryReadStreamWrapper : public StreamWrapper { - eckit::Buffer buf_; - eckit::MemoryStream mem_; + eckit::Buffer buf_; + eckit::MemoryStream mem_; public: - MemoryReadStreamWrapper(rust::Slice data); + + MemoryReadStreamWrapper(rust::Slice data); }; // Factory functions std::unique_ptr stream_connect(rust::Str host, int32_t port); std::unique_ptr stream_memory_write(); -std::unique_ptr -stream_memory_read(rust::Slice data); +std::unique_ptr stream_memory_read(rust::Slice data); -} // namespace eckit_bridge +} // namespace eckit_bridge From b6c048146d96c39c796f1e863053df7d2e1a4912 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Tue, 12 May 2026 21:49:13 +0200 Subject: [PATCH 03/22] Refactor exception handling integration for downstream `-sys` crates --- rust/crates/eckit-sys/build.rs | 31 ++++++++++++---------- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 5 ++++ rust/crates/eckit-sys/cpp/eckit_bridge.h | 8 +++--- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/rust/crates/eckit-sys/build.rs b/rust/crates/eckit-sys/build.rs index ee30cbabb..ba165780c 100644 --- a/rust/crates/eckit-sys/build.rs +++ b/rust/crates/eckit-sys/build.rs @@ -29,10 +29,6 @@ fn main() { std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); bindman_build::check_cpp_api(&include, &crate_dir.join("src/lib.rs")); - // Export OUT_DIR for downstream crates that need eckit_exceptions.h - let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR")); - println!("cargo:out_dir={}", out_dir.display()); - // Export cpp directory for downstream crates that include eckit_bridge.h println!("cargo:cpp_dir={}", crate_dir.join("cpp").display()); } @@ -58,23 +54,30 @@ fn build_cxx_bridge(include: &std::path::Path) { bindman_utils::link_cpp_stdlib(); } -/// Generate exception bridge files from eckit's `Exceptions.h`. +/// Generate exception bridge files from eckit's `Exceptions.h`. Publishes the +/// source list so downstream `-sys` crates can re-parse and inherit eckit's +/// catch blocks into their own generated bridge. fn generate_exceptions(include: &std::path::Path) { let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set")); - let header = include.join("eckit/exception/Exceptions.h"); + + let own = vec![bindman_build::ExceptionSource { + header: include.join("eckit/exception/Exceptions.h"), + include_path: "eckit/exception/Exceptions.h".to_string(), + cpp_namespace: "eckit".to_string(), + message_prefix: "eckit".to_string(), + base_class: "Exception".to_string(), + recursive: true, + }]; bindman_build::generate_exception_bridge(&bindman_build::ExceptionBridgeConfig { - header: &header, - base_class: "Exception", - namespace: "eckit", + primary_namespace: "eckit", out_dir: &out_dir, + own: &own, + inherited: &[], }); - // Export path to generated header for downstream -sys crates - println!( - "cargo:exceptions_header={}", - out_dir.join("eckit_exceptions.h").display() - ); + // Publish for downstream `-sys` crates that inherit eckit's exceptions. + bindman_build::publish_exception_sources(&own, &out_dir); } /// Minimum eckit version this crate's bridge is known to compile against. diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index 9ae300191..4fa8f8430 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -1,4 +1,9 @@ // eckit C++ bridge implementation + +// trycatch handler — must come before the cxx-generated header so the +// generated wrappers' Result handling picks up our specialization. +#include "eckit_exceptions.h" + #include "eckit_bridge.h" #include "eckit-sys/src/lib.rs.h" // cxx-generated — provides LogLevel values diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.h b/rust/crates/eckit-sys/cpp/eckit_bridge.h index 81c85f3c5..f5baaba46 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.h +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.h @@ -1,9 +1,11 @@ // eckit C++ bridge for Rust FFI #pragma once -// Include auto-generated trycatch handler FIRST — before cxx generates its -// default -#include "eckit_exceptions.h" +// Note: the auto-generated `rust::behavior::trycatch` lives in +// `eckit_exceptions.h`, which is included by `eckit_bridge.cpp` directly +// (not from this header). Downstream `-sys` crates have their own generated +// `_exceptions.h` and must not see eckit's transitively through here, +// or they would have two `trycatch` specializations in one translation unit. #include "eckit/config/LocalConfiguration.h" #include "eckit/config/YAMLConfiguration.h" From 77834c3c3bc3cb74a56bdc32298a04868e441280 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Tue, 12 May 2026 21:56:31 +0200 Subject: [PATCH 04/22] Fix include order in eckit_bridge.cpp --- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index 4fa8f8430..94dbfb7f6 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -4,8 +4,8 @@ // generated wrappers' Result handling picks up our specialization. #include "eckit_exceptions.h" -#include "eckit_bridge.h" #include "eckit-sys/src/lib.rs.h" // cxx-generated — provides LogLevel values +#include "eckit_bridge.h" namespace eckit_bridge { From 1fcd143935084e324ef3009048d63d0da3b73f12 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 5 Jun 2026 15:28:32 +0200 Subject: [PATCH 05/22] Enhance logging by adding target parameter to RustLogTarget and rust_log function --- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 25 ++++++++++++++++------ rust/crates/eckit-sys/cpp/eckit_bridge.h | 8 +++++-- rust/crates/eckit-sys/src/lib.rs | 16 ++++++++------ 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index 94dbfb7f6..4adf1477a 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -21,7 +21,7 @@ void RustLogTarget::write(const char* start, const char* end) { line.pop_back(); } if (!line.empty()) { - rust_log(level_, rust::Str(line.data(), line.size())); + rust_log(level_, rust::Str(target_.data(), target_.size()), rust::Str(line.data(), line.size())); } buffer_.erase(0, pos + 1); } @@ -33,7 +33,7 @@ void RustLogTarget::flush() { buffer_.pop_back(); } if (!buffer_.empty()) { - rust_log(level_, rust::Str(buffer_.data(), buffer_.size())); + rust_log(level_, rust::Str(target_.data(), target_.size()), rust::Str(buffer_.data(), buffer_.size())); buffer_.clear(); } } @@ -42,19 +42,29 @@ void RustLogTarget::flush() { RustMain::RustMain(int argc, char** argv) : Main(argc, argv) {} eckit::LogTarget* RustMain::createInfoLogTarget() const { - return new RustLogTarget(LogLevel::Info); + return new RustLogTarget(LogLevel::Info, "eckit"); } eckit::LogTarget* RustMain::createWarningLogTarget() const { - return new RustLogTarget(LogLevel::Warn); + return new RustLogTarget(LogLevel::Warn, "eckit"); } eckit::LogTarget* RustMain::createErrorLogTarget() const { - return new RustLogTarget(LogLevel::Error); + return new RustLogTarget(LogLevel::Error, "eckit"); } eckit::LogTarget* RustMain::createDebugLogTarget() const { - return new RustLogTarget(LogLevel::Debug); + return new RustLogTarget(LogLevel::Debug, "eckit"); } eckit::LogTarget* RustMain::createMetricsLogTarget() const { - return new RustLogTarget(LogLevel::Trace); + return new RustLogTarget(LogLevel::Trace, "eckit"); +} + +/// Install a per-library `RustLogTarget` on every registered library's debug +/// channel. Each library's debug output is then tagged with its own name as +/// the tracing/log target. Idempotent — `Channel::setTarget` replaces. +static void install_per_library_targets() { + for (const auto& libname : eckit::system::LibraryManager::list()) { + const auto& lib = eckit::system::LibraryManager::lookup(libname); + lib.debugChannel().setTarget(new RustLogTarget(LogLevel::Debug, libname)); + } } void init() { @@ -62,6 +72,7 @@ void init() { static const char* argv[] = {"eckit-rs", nullptr}; static auto* main = new RustMain(1, const_cast(argv)); (void)main; + install_per_library_targets(); } } diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.h b/rust/crates/eckit-sys/cpp/eckit_bridge.h index f5baaba46..dfd6dfa3f 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.h +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.h @@ -39,16 +39,19 @@ namespace eckit_bridge { // Forward declarations — full definitions in cxx-generated code enum class LogLevel : std::uint8_t; -void rust_log(LogLevel level, rust::Str msg) noexcept; +void rust_log(LogLevel level, rust::Str target, rust::Str msg) noexcept; // ==================== Logging ==================== /// LogTarget that routes all output to Rust's log crate. /// Accumulates writes until a newline or flush, then emits a single log call. +/// `target` is the tracing/log target string passed to Rust (e.g. "eckit", +/// "metkit") — global channels use "eckit", per-library debug channels use +/// the library name from `eckit::system::LibraryManager`. class RustLogTarget : public eckit::LogTarget { public: - explicit RustLogTarget(LogLevel level) : level_(level) {} + RustLogTarget(LogLevel level, std::string target) : level_(level), target_(std::move(target)) {} void write(const char* start, const char* end) override; void flush() override; @@ -56,6 +59,7 @@ class RustLogTarget : public eckit::LogTarget { private: LogLevel level_; + std::string target_; std::string buffer_; }; diff --git a/rust/crates/eckit-sys/src/lib.rs b/rust/crates/eckit-sys/src/lib.rs index 865a35757..c90c0b4a1 100644 --- a/rust/crates/eckit-sys/src/lib.rs +++ b/rust/crates/eckit-sys/src/lib.rs @@ -230,7 +230,9 @@ mod ffi { extern "Rust" { /// Called from C++ RustLogTarget to emit log messages via Rust's log crate. - fn rust_log(level: LogLevel, msg: &str); + /// `target` is the tracing/log target — "eckit" for global channels, + /// the library name (e.g. "metkit", "mir") for per-library debug channels. + fn rust_log(level: LogLevel, target: &str, msg: &str); /// Opaque Rust box holding a `dyn Library` trait object. type LibraryBox; @@ -257,14 +259,14 @@ pub use cxx::{Exception, UniquePtr}; pub use ffi::*; /// Called from C++ `RustLogTarget::write()` — routes to Rust `log` crate. -fn rust_log(level: ffi::LogLevel, msg: &str) { +fn rust_log(level: ffi::LogLevel, target: &str, msg: &str) { match level { - ffi::LogLevel::Error => log::error!(target: "eckit", "{msg}"), - ffi::LogLevel::Warn => log::warn!(target: "eckit", "{msg}"), - ffi::LogLevel::Info => log::info!(target: "eckit", "{msg}"), - ffi::LogLevel::Debug => log::debug!(target: "eckit", "{msg}"), + ffi::LogLevel::Error => log::error!(target: target, "{msg}"), + ffi::LogLevel::Warn => log::warn!(target: target, "{msg}"), + ffi::LogLevel::Info => log::info!(target: target, "{msg}"), + ffi::LogLevel::Debug => log::debug!(target: target, "{msg}"), // Trace + wildcard for cxx non-exhaustive enum - _ => log::trace!(target: "eckit", "{msg}"), + _ => log::trace!(target: target, "{msg}"), } } From bf67146693948d360dadb6034cedbe75ca864130 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 5 Jun 2026 18:16:25 +0200 Subject: [PATCH 06/22] Add RustReaderHandle to forward read/seek calls from Rust to C++ DataHandle --- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 61 ++++++++++++++++++ rust/crates/eckit-sys/cpp/eckit_bridge.h | 12 ++++ rust/crates/eckit-sys/src/lib.rs | 74 ++++++++++++++++++++++ 3 files changed, 147 insertions(+) diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index 4adf1477a..7042056d4 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -265,6 +265,67 @@ std::unique_ptr data_handle_tee(rust::Slice(new eckit::TeeHandle(handles)); } +namespace { + +// `eckit::DataHandle` subclass that forwards `read()` / `seek()` to a Rust +// `Read + Seek` source held in a `rust::Box`. `openForRead` +// rewinds via `seek(0)` to match `eckit::FileHandle`'s `fopen("r")` +// semantics — eckit's archive pipeline relies on re-opening the source for +// the analyser and per-database passes. +class RustReaderHandle : public eckit::DataHandle { +public: + + explicit RustReaderHandle(rust::Box reader) : reader_(std::move(reader)) {} + + void print(std::ostream& s) const override { s << "RustReaderHandle[]"; } + + eckit::Length openForRead() override { + if (invoke_reader_seek(*reader_, 0) < 0) { + throw eckit::ReadError("RustReaderHandle: rewind failed on openForRead"); + } + return eckit::Length(0); + } + + long read(void* buffer, long length) override { + if (length <= 0) { + return 0; + } + auto* bytes = static_cast(buffer); + rust::Slice slice{bytes, static_cast(length)}; + int64_t n = invoke_reader_read(*reader_, slice); + if (n < 0) { + throw eckit::ReadError("RustReaderHandle: error reading from Rust source"); + } + return static_cast(n); + } + + bool canSeek() const override { return true; } + + eckit::Offset seek(const eckit::Offset& offset) override { + int64_t pos = invoke_reader_seek(*reader_, static_cast(offset)); + if (pos < 0) { + throw eckit::ReadError("RustReaderHandle: seek failed"); + } + return eckit::Offset(pos); + } + + void close() override {} + + eckit::Length estimate() override { return eckit::Length(0); } + + eckit::Length size() override { return eckit::Length(0); } + +private: + + rust::Box reader_; +}; + +} // namespace + +std::unique_ptr data_handle_from_reader(rust::Box reader) { + return std::make_unique(new RustReaderHandle(std::move(reader))); +} + // ==================== Message + Reader ==================== bool MessageWrapper::is_valid() const { diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.h b/rust/crates/eckit-sys/cpp/eckit_bridge.h index dfd6dfa3f..f25e1cb28 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.h +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.h @@ -226,6 +226,18 @@ std::unique_ptr data_handle_from_multi(rust::Slice data_handle_tee(rust::Slice paths); +// Forward declaration — defined on the Rust side, cxx generates the type. +struct ReaderBox; + +/// Create a DataHandle that forwards `read()` calls to a Rust `std::io::Read` +/// source wrapped in a `ReaderBox`. Used to stream bytes from Rust into any +/// C++ API that consumes an `eckit::DataHandle&` (e.g. fdb5 archive, the +/// streaming retrieve API) without staging through a temp file or buffer. +/// +/// The returned handle is owned by the Rust side; on drop, the contained +/// `ReaderBox` is dropped, releasing the underlying `Read` source. +std::unique_ptr data_handle_from_reader(rust::Box reader); + // ==================== Library registration ==================== // Forward declaration — defined on the Rust side, cxx generates the type. diff --git a/rust/crates/eckit-sys/src/lib.rs b/rust/crates/eckit-sys/src/lib.rs index c90c0b4a1..ac0990640 100644 --- a/rust/crates/eckit-sys/src/lib.rs +++ b/rust/crates/eckit-sys/src/lib.rs @@ -141,6 +141,10 @@ mod ffi { /// Create a TeeHandle from multiple file paths — writes all targets in parallel. fn data_handle_tee(paths: &[String]) -> Result>; + /// Create a DataHandle that forwards `read()` calls to a Rust + /// `std::io::Read` source wrapped in a [`ReaderBox`]. + fn data_handle_from_reader(reader: Box) -> Result>; + // ==================== Message + Reader ==================== type MessageWrapper; @@ -234,6 +238,26 @@ mod ffi { /// the library name (e.g. "metkit", "mir") for per-library debug channels. fn rust_log(level: LogLevel, target: &str, msg: &str); + /// Opaque Rust box holding a `dyn ReadSeek + Send` source. + /// + /// Constructed via [`make_reader_box`]; the C++ `RustReaderHandle` + /// holds it by `rust::Box` and forwards `read()` / `seek()` + /// calls through [`invoke_reader_read`] / [`invoke_reader_seek`]. + type ReaderBox; + + /// Called by the C++ `RustReaderHandle::read` shim to fill the next + /// chunk from the wrapped Rust `Read` source. + /// + /// Returns the number of bytes written into `buf`, or `-1` on error. + /// A return of `0` signals EOF. + fn invoke_reader_read(reader: &mut ReaderBox, buf: &mut [u8]) -> i64; + + /// Called by the C++ `RustReaderHandle::seek` / `openForRead` shim to + /// reposition the wrapped Rust source. `offset` is from the start. + /// + /// Returns the new absolute position, or `-1` on error. + fn invoke_reader_seek(reader: &mut ReaderBox, offset: i64) -> i64; + /// Opaque Rust box holding a `dyn Library` trait object. type LibraryBox; @@ -258,6 +282,56 @@ mod ffi { pub use cxx::{Exception, UniquePtr}; pub use ffi::*; +// ==================== Read → DataHandle adapter ==================== + +/// Trait alias for `Read + Seek`. Required so the trait object inside +/// [`ReaderBox`] exposes both `read` (for streaming bytes into C++) and +/// `seek` (for rewind on `openForRead`, matching `eckit::FileHandle`'s +/// `fopen("r")` semantics). +pub trait ReadSeek: std::io::Read + std::io::Seek {} +impl ReadSeek for T {} + +/// Opaque wrapper holding a `Box`. +/// +/// The C++ `RustReaderHandle` (declared in `eckit_bridge.h` as `struct +/// ReaderBox`) carries this by `rust::Box` and forwards each C++ +/// `read(void*, long)` / `seek(Offset)` call via [`invoke_reader_read`] / +/// [`invoke_reader_seek`]. +pub struct ReaderBox(Box); + +/// Wrap a Rust `Read + Seek` source in a [`ReaderBox`] suitable for handing +/// to [`ffi::data_handle_from_reader`]. The `Seek` bound is load-bearing: +/// eckit's `DataHandle::openForRead` contract is "leave at offset 0", so the +/// C++ side rewinds the source on every re-open. +pub fn make_reader_box(reader: R) -> Box +where + R: std::io::Read + std::io::Seek + Send + 'static, +{ + Box::new(ReaderBox(Box::new(reader))) +} + +/// Called from C++ `RustReaderHandle::read` to fill the next chunk from the +/// wrapped Rust reader. Returns the byte count, `0` on EOF, or `-1` on error. +fn invoke_reader_read(reader: &mut ReaderBox, buf: &mut [u8]) -> i64 { + reader + .0 + .read(buf) + .map_or(-1, |n| i64::try_from(n).unwrap_or(i64::MAX)) +} + +/// Called from C++ `RustReaderHandle::seek` / `openForRead` to reposition +/// the inner Rust source from the start. Returns the new position, or `-1` +/// on error. +fn invoke_reader_seek(reader: &mut ReaderBox, offset: i64) -> i64 { + let Ok(off_u64) = u64::try_from(offset) else { + return -1; + }; + reader + .0 + .seek(std::io::SeekFrom::Start(off_u64)) + .map_or(-1, |n| i64::try_from(n).unwrap_or(i64::MAX)) +} + /// Called from C++ `RustLogTarget::write()` — routes to Rust `log` crate. fn rust_log(level: ffi::LogLevel, target: &str, msg: &str) { match level { From 18d3926626b976d34c2e5d1a71809d68a0ba6844 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 5 Jun 2026 18:44:59 +0200 Subject: [PATCH 07/22] Update documentation for `ReadSeek` trait and `make_reader_box` function --- rust/crates/eckit-sys/src/lib.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/rust/crates/eckit-sys/src/lib.rs b/rust/crates/eckit-sys/src/lib.rs index ac0990640..1a3927bfa 100644 --- a/rust/crates/eckit-sys/src/lib.rs +++ b/rust/crates/eckit-sys/src/lib.rs @@ -284,10 +284,11 @@ pub use ffi::*; // ==================== Read → DataHandle adapter ==================== -/// Trait alias for `Read + Seek`. Required so the trait object inside -/// [`ReaderBox`] exposes both `read` (for streaming bytes into C++) and -/// `seek` (for rewind on `openForRead`, matching `eckit::FileHandle`'s -/// `fopen("r")` semantics). +/// Trait alias for `Read + Seek`. +/// +/// Required so the trait object inside [`ReaderBox`] exposes both `read` +/// (for streaming bytes into C++) and `seek` (for rewind on `openForRead`, +/// matching `eckit::FileHandle`'s `fopen("r")` semantics). pub trait ReadSeek: std::io::Read + std::io::Seek {} impl ReadSeek for T {} @@ -299,10 +300,11 @@ impl ReadSeek for T {} /// [`invoke_reader_seek`]. pub struct ReaderBox(Box); -/// Wrap a Rust `Read + Seek` source in a [`ReaderBox`] suitable for handing -/// to [`ffi::data_handle_from_reader`]. The `Seek` bound is load-bearing: -/// eckit's `DataHandle::openForRead` contract is "leave at offset 0", so the -/// C++ side rewinds the source on every re-open. +/// Wrap a Rust `Read + Seek` source for [`ffi::data_handle_from_reader`]. +/// +/// The `Seek` bound is load-bearing: eckit's `DataHandle::openForRead` +/// contract is "leave at offset 0", so the C++ side rewinds the source on +/// every re-open. pub fn make_reader_box(reader: R) -> Box where R: std::io::Read + std::io::Seek + Send + 'static, From 35d7530999431207bdc47ce5e452204ee7957cf4 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Wed, 10 Jun 2026 02:19:44 +0200 Subject: [PATCH 08/22] Remove branch specification for bindman dependencies in Cargo.toml --- rust/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index a82848168..77d515889 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -16,9 +16,9 @@ categories = ["science"] eckit-sys = { path = "crates/eckit-sys" } # Build tools -bindman = { git = "ssh://git@github.com/ecmwf/bindman.git", branch = "generate_exception_bridge" } -bindman-build = { git = "ssh://git@github.com/ecmwf/bindman.git", branch = "generate_exception_bridge" } -bindman-utils = { git = "ssh://git@github.com/ecmwf/bindman.git", branch = "generate_exception_bridge" } +bindman = { git = "ssh://git@github.com/ecmwf/bindman.git" } +bindman-build = { git = "ssh://git@github.com/ecmwf/bindman.git" } +bindman-utils = { git = "ssh://git@github.com/ecmwf/bindman.git" } # External cxx = "1.0" From d65a1825bef776914903b6ab12e76f5cc5937d16 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Thu, 11 Jun 2026 02:46:56 +0200 Subject: [PATCH 09/22] Fix formatting issues in EasyCURL and test_easycurl --- src/eckit/io/EasyCURL.cc | 4 +++- tests/io/test_easycurl.cc | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/eckit/io/EasyCURL.cc b/src/eckit/io/EasyCURL.cc index 67bfeb196..66a624c9c 100644 --- a/src/eckit/io/EasyCURL.cc +++ b/src/eckit/io/EasyCURL.cc @@ -319,12 +319,14 @@ class CURLHandle : public eckit::Counted { try { lock_guard lock(options_mutex_); options_registry_.clear(); - } catch (...) { + } + catch (...) { throw; } } private: + void applyRegisteredOptions() { try { lock_guard lock(options_mutex_); diff --git a/tests/io/test_easycurl.cc b/tests/io/test_easycurl.cc index 3cf650050..69c82de3f 100644 --- a/tests/io/test_easycurl.cc +++ b/tests/io/test_easycurl.cc @@ -264,7 +264,7 @@ CASE("Follow redirects") { EXPECT(response.code() == 200); EXPECT(response.contentLength() == 3); - std::array buffer {' ', ' ', ' '}; + std::array buffer{' ', ' ', ' '}; EXPECT(response.read(buffer.data(), buffer.size()) == buffer.size()); EXPECT(std::string(buffer.data(), buffer.size()) == "Hi!"); } From cc845f0a253597613c86c576e0a12e449fe4d4df Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 01:41:25 +0200 Subject: [PATCH 10/22] Remove unused RustLibrary class and related library registration code --- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 78 -------------- rust/crates/eckit-sys/cpp/eckit_bridge.h | 42 -------- rust/crates/eckit-sys/src/lib.rs | 114 --------------------- 3 files changed, 234 deletions(-) diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index 7042056d4..d71f7ee49 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -377,73 +377,6 @@ std::unique_ptr new_reader(DataHandleWrapper& handle) { return std::make_unique(handle); } -// ==================== Library registration ==================== - -RustLibrary::RustLibrary(rust::Box lib) : - eckit::system::Library(std::string(library_name(*lib))), rust_(std::move(lib)) {} - -std::string RustLibrary::version() const { - return std::string(library_version(*rust_)); -} - -std::string RustLibrary::gitsha1(unsigned int count) const { - return std::string(library_git_sha1(*rust_, count)); -} - -std::string RustLibrary::home() const { - if (library_home_is_set(*rust_)) { - return std::string(library_home(*rust_)); - } - return Library::home(); -} - -std::string RustLibrary::libraryHome() const { - if (library_library_home_is_set(*rust_)) { - return std::string(library_library_home(*rust_)); - } - return Library::libraryHome(); -} - -std::string RustLibrary::prefixDirectory() const { - if (library_prefix_directory_is_set(*rust_)) { - return std::string(library_prefix_directory(*rust_)); - } - return Library::prefixDirectory(); -} - -std::string RustLibrary::expandPath(const std::string& path) const { - rust::Str rpath(path.data(), path.size()); - if (library_expand_path_is_set(*rust_, rpath)) { - return std::string(library_expand_path(*rust_, rpath)); - } - return Library::expandPath(path); -} - -bool RustLibrary::debug() const { - if (library_debug_is_set(*rust_)) { - return library_debug(*rust_); - } - return Library::debug(); -} - -const void* RustLibrary::addr() const { - // Return a pointer inside the code segment so dladdr can resolve the - // binary path. `this` is heap-allocated and dladdr cannot resolve it. - return reinterpret_cast(®ister_library); -} - -void register_library(rust::Box lib) { - // Heap-allocate and leak — Library registers itself with LibraryManager - // in its constructor and must live for the process lifetime. - new RustLibrary(std::move(lib)); -} - -std::unique_ptr library_configuration(rust::Str name) { - auto& lib = eckit::system::LibraryManager::lookup(std::string(name)); - const auto& cfg = lib.configuration(); - return std::make_unique(eckit::LocalConfiguration(cfg)); -} - // ==================== Stream (base class) ==================== void StreamWrapper::write_char(uint8_t c) { @@ -545,17 +478,6 @@ std::unique_ptr StreamWrapper::into_data_handle() { throw eckit::NotImplemented("StreamWrapper::into_data_handle is only supported on TCP streams", Here()); } -rust::Vec library_versions() { - rust::Vec out; - constexpr size_t sha1len = 8; - for (const auto& name : eckit::system::LibraryManager::list()) { - const auto& lib = eckit::system::LibraryManager::lookup(name); - out.push_back(LibraryVersion{rust::String(lib.name()), rust::String(lib.version()), - rust::String(lib.gitsha1(sha1len)), rust::String(lib.libraryHome())}); - } - return out; -} - // ==================== MemoryWriteStreamWrapper ==================== MemoryWriteStreamWrapper::MemoryWriteStreamWrapper() : buf_(4096), mem_(buf_) { diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.h b/rust/crates/eckit-sys/cpp/eckit_bridge.h index f25e1cb28..6a1f13975 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.h +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.h @@ -238,48 +238,6 @@ struct ReaderBox; /// `ReaderBox` is dropped, releasing the underlying `Read` source. std::unique_ptr data_handle_from_reader(rust::Box reader); -// ==================== Library registration ==================== - -// Forward declaration — defined on the Rust side, cxx generates the type. -struct LibraryBox; - -/// eckit::system::Library subclass that delegates to a Rust `dyn Library`. -class RustLibrary : public eckit::system::Library { - rust::Box rust_; - -public: - - explicit RustLibrary(rust::Box lib); - - std::string version() const override; - std::string gitsha1(unsigned int count) const override; - -protected: - - std::string home() const override; - std::string libraryHome() const override; - std::string prefixDirectory() const override; - std::string expandPath(const std::string& path) const override; - bool debug() const override; - const void* addr() const override; -}; - -/// Register a Rust library with eckit's LibraryManager. -void register_library(rust::Box lib); - -/// Get configuration for a registered library by name. -std::unique_ptr library_configuration(rust::Str name); - -// `LibraryVersion` is a shared struct defined by cxx in this same namespace -// (see the bridge in lib.rs). Forward-declare it here so the function -// signature below compiles when this header is included before the cxx -// definition. -struct LibraryVersion; - -/// Snapshot of every ECMWF library currently registered with -/// `eckit::system::LibraryManager`. Mirrors C++ `Environment::library_versions`. -rust::Vec library_versions(); - // ==================== Stream ==================== /// Base wrapper for `eckit::Stream`. Subclasses own the transport-specific diff --git a/rust/crates/eckit-sys/src/lib.rs b/rust/crates/eckit-sys/src/lib.rs index 1a3927bfa..937b801bd 100644 --- a/rust/crates/eckit-sys/src/lib.rs +++ b/rust/crates/eckit-sys/src/lib.rs @@ -29,15 +29,6 @@ mod ffi { Trace = 5, } - /// One ECMWF library reported by `eckit::system::LibraryManager`. - #[derive(Debug, Clone)] - struct LibraryVersion { - name: String, - version: String, - gitsha1: String, - home: String, - } - unsafe extern "C++" { include!("eckit_bridge.h"); @@ -215,21 +206,6 @@ mod ffi { /// Create a fixed memory stream for reading from existing data. #[must_use] fn stream_memory_read(data: &[u8]) -> UniquePtr; - - // ==================== Library registration ==================== - - /// Register a Rust-implemented library with eckit's LibraryManager. - /// Enables `~name` tilde expansion via `$NAME_HOME` env var. - fn register_library(lib: Box); - - /// Get configuration for a registered library by name. - fn library_configuration(name: &str) -> Result>; - - /// Snapshot of every ECMWF library registered with - /// `eckit::system::LibraryManager` (e.g. eckit, metkit, fdb5, mir). - /// Mirrors C++ `Environment::library_versions()` (mars-client). - #[must_use] - fn library_versions() -> Vec; } extern "Rust" { @@ -257,24 +233,6 @@ mod ffi { /// /// Returns the new absolute position, or `-1` on error. fn invoke_reader_seek(reader: &mut ReaderBox, offset: i64) -> i64; - - /// Opaque Rust box holding a `dyn Library` trait object. - type LibraryBox; - - // Callbacks from C++ RustLibrary into Rust trait methods - fn library_name(lib: &LibraryBox) -> &str; - fn library_version(lib: &LibraryBox) -> String; - fn library_git_sha1(lib: &LibraryBox, count: u32) -> String; - fn library_home(lib: &LibraryBox) -> String; - fn library_home_is_set(lib: &LibraryBox) -> bool; - fn library_library_home(lib: &LibraryBox) -> String; - fn library_library_home_is_set(lib: &LibraryBox) -> bool; - fn library_prefix_directory(lib: &LibraryBox) -> String; - fn library_prefix_directory_is_set(lib: &LibraryBox) -> bool; - fn library_expand_path(lib: &LibraryBox, path: &str) -> String; - fn library_expand_path_is_set(lib: &LibraryBox, path: &str) -> bool; - fn library_debug(lib: &LibraryBox) -> bool; - fn library_debug_is_set(lib: &LibraryBox) -> bool; } } @@ -346,78 +304,6 @@ fn rust_log(level: ffi::LogLevel, target: &str, msg: &str) { } } -// ==================== Library registration (internal plumbing) ==================== - -type OptStringFn = Box Option + Send + Sync>; -type OptStringArgFn = Box Option + Send + Sync>; -type OptBoolFn = Box Option + Send + Sync>; - -/// Opaque box holding library callbacks for FFI. Constructed by the `eckit` crate. -pub struct LibraryBox { - pub name: String, - pub version_fn: Box String + Send + Sync>, - pub git_sha1_fn: Box String + Send + Sync>, - pub home_fn: OptStringFn, - pub library_home_fn: OptStringFn, - pub prefix_directory_fn: OptStringFn, - pub expand_path_fn: OptStringArgFn, - pub debug_fn: OptBoolFn, -} - -// Callbacks from C++ RustLibrary into Rust closures - -fn library_name(lib: &LibraryBox) -> &str { - &lib.name -} - -fn library_version(lib: &LibraryBox) -> String { - (lib.version_fn)() -} - -fn library_git_sha1(lib: &LibraryBox, count: u32) -> String { - (lib.git_sha1_fn)(count) -} - -fn library_home(lib: &LibraryBox) -> String { - (lib.home_fn)().unwrap_or_default() -} - -fn library_home_is_set(lib: &LibraryBox) -> bool { - (lib.home_fn)().is_some() -} - -fn library_library_home(lib: &LibraryBox) -> String { - (lib.library_home_fn)().unwrap_or_default() -} - -fn library_library_home_is_set(lib: &LibraryBox) -> bool { - (lib.library_home_fn)().is_some() -} - -fn library_prefix_directory(lib: &LibraryBox) -> String { - (lib.prefix_directory_fn)().unwrap_or_default() -} - -fn library_prefix_directory_is_set(lib: &LibraryBox) -> bool { - (lib.prefix_directory_fn)().is_some() -} - -fn library_expand_path(lib: &LibraryBox, path: &str) -> String { - (lib.expand_path_fn)(path).unwrap_or_default() -} - -fn library_expand_path_is_set(lib: &LibraryBox, path: &str) -> bool { - (lib.expand_path_fn)(path).is_some() -} - -fn library_debug(lib: &LibraryBox) -> bool { - (lib.debug_fn)().unwrap_or(false) -} - -fn library_debug_is_set(lib: &LibraryBox) -> bool { - (lib.debug_fn)().is_some() -} - /// Initialize eckit runtime with Rust log bridge. /// /// Must be called before any eckit API usage. Safe to call multiple times. From de291accf75a7ef4084fd3465b981f4f8b62b642 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 01:42:29 +0200 Subject: [PATCH 11/22] Move cargo configuration to rust folder --- {.cargo => rust/.cargo}/config.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.cargo => rust/.cargo}/config.toml (100%) diff --git a/.cargo/config.toml b/rust/.cargo/config.toml similarity index 100% rename from .cargo/config.toml rename to rust/.cargo/config.toml From 78a5e9002cc2a3b07b842c312be49b45668cbb57 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 15:20:17 +0200 Subject: [PATCH 12/22] Move C++ classes to dedicated header files --- rust/crates/eckit-sys/build.rs | 5 + rust/crates/eckit-sys/cpp/config.h | 62 ++++ rust/crates/eckit-sys/cpp/datahandle.h | 71 +++++ rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 15 + rust/crates/eckit-sys/cpp/eckit_bridge.h | 341 +-------------------- rust/crates/eckit-sys/cpp/log.h | 55 ++++ rust/crates/eckit-sys/cpp/message.h | 62 ++++ rust/crates/eckit-sys/cpp/stream.h | 117 +++++++ 8 files changed, 396 insertions(+), 332 deletions(-) create mode 100644 rust/crates/eckit-sys/cpp/config.h create mode 100644 rust/crates/eckit-sys/cpp/datahandle.h create mode 100644 rust/crates/eckit-sys/cpp/log.h create mode 100644 rust/crates/eckit-sys/cpp/message.h create mode 100644 rust/crates/eckit-sys/cpp/stream.h diff --git a/rust/crates/eckit-sys/build.rs b/rust/crates/eckit-sys/build.rs index ba165780c..33216e5b7 100644 --- a/rust/crates/eckit-sys/build.rs +++ b/rust/crates/eckit-sys/build.rs @@ -42,6 +42,11 @@ fn build_cxx_bridge(include: &std::path::Path) { println!("cargo:rerun-if-changed=cpp/eckit_bridge.h"); println!("cargo:rerun-if-changed=cpp/eckit_bridge.cpp"); + println!("cargo:rerun-if-changed=cpp/log.h"); + println!("cargo:rerun-if-changed=cpp/config.h"); + println!("cargo:rerun-if-changed=cpp/datahandle.h"); + println!("cargo:rerun-if-changed=cpp/message.h"); + println!("cargo:rerun-if-changed=cpp/stream.h"); cxx_build::bridge("src/lib.rs") .file(crate_dir.join("cpp/eckit_bridge.cpp")) diff --git a/rust/crates/eckit-sys/cpp/config.h b/rust/crates/eckit-sys/cpp/config.h new file mode 100644 index 000000000..48c1968a8 --- /dev/null +++ b/rust/crates/eckit-sys/cpp/config.h @@ -0,0 +1,62 @@ +// eckit Configuration bridge — wraps `eckit::LocalConfiguration`. +#pragma once + +#include "eckit/config/LocalConfiguration.h" + +#include "rust/cxx.h" + +#include +#include +#include + +namespace eckit_bridge { + +/// Wraps `eckit::LocalConfiguration` for Rust FFI. +class ConfigWrapper { + eckit::LocalConfiguration config_; + +public: + + ConfigWrapper() = default; + explicit ConfigWrapper(const eckit::Configuration& other) : config_(other) {} + + // Read + bool has(rust::Str key) const; + bool is_list(rust::Str key) const; + bool is_empty() const; + rust::String get_string(rust::Str key, rust::Str default_val) const; + int64_t get_long(rust::Str key, int64_t default_val) const; + int32_t get_int(rust::Str key, int32_t default_val) const; + bool get_bool(rust::Str key, bool default_val) const; + double get_double(rust::Str key, double default_val) const; + rust::Vec get_string_vector(rust::Str key, const rust::Vec& default_val) const; + + // Sub-configurations (by key) + std::unique_ptr get_sub(rust::Str key) const; + size_t sub_count(rust::Str key) const; + std::unique_ptr sub_at(rust::Str key, size_t index) const; + + // Sub-configurations (root-level list) + size_t root_sub_count() const; + std::unique_ptr root_sub_at(size_t index) const; + + // Write + void set_string(rust::Str key, rust::Str value); + void set_long(rust::Str key, int64_t value); + void set_int(rust::Str key, int32_t value); + void set_bool(rust::Str key, bool value); + void set_double(rust::Str key, double value); + void remove(rust::Str key); + + // Access underlying for other C++ bridge code + const eckit::LocalConfiguration& inner() const { return config_; } + eckit::LocalConfiguration& inner() { return config_; } +}; + +// Factory functions — names match cxx bridge declarations +std::unique_ptr create(); +std::unique_ptr from_path(rust::Str path); +std::unique_ptr from_yaml(rust::Str yaml); +std::unique_ptr clone(const ConfigWrapper& src); + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/datahandle.h b/rust/crates/eckit-sys/cpp/datahandle.h new file mode 100644 index 000000000..802386369 --- /dev/null +++ b/rust/crates/eckit-sys/cpp/datahandle.h @@ -0,0 +1,71 @@ +// eckit DataHandle bridge — wraps `eckit::DataHandle` and its factories. +#pragma once + +#include "eckit/io/DataHandle.h" + +#include "rust/cxx.h" + +#include +#include + +namespace eckit_bridge { + +/// Wraps `eckit::DataHandle*` for Rust FFI. Takes ownership. +class DataHandleWrapper { + std::unique_ptr handle_; + +public: + + /// Takes ownership of a raw DataHandle pointer. + explicit DataHandleWrapper(eckit::DataHandle* h) : handle_(h) {} + + int64_t open_for_read(); + void open_for_write(int64_t estimated_length); + int64_t read(rust::Slice buf); + int64_t write(rust::Slice buf); + void close(); + int64_t position() const; + int64_t seek(int64_t offset); + bool can_seek() const; + int64_t estimate() const; + + /// Copy all data from this handle into target (both must be open). + int64_t save_into(DataHandleWrapper& target); + + /// Access underlying for other C++ bridge code. + eckit::DataHandle& inner() { return *handle_; } + const eckit::DataHandle& inner() const { return *handle_; } + + /// Release ownership — caller takes responsibility. + eckit::DataHandle* release() { return handle_.release(); } +}; + +/// Open a file as a DataHandle for reading. +std::unique_ptr data_handle_from_file(rust::Str path); + +/// Open a byte range of a file as a DataHandle. +std::unique_ptr data_handle_from_part(rust::Str path, int64_t offset, int64_t length); + +/// Create a DataHandle from an in-memory buffer (copies the data). +std::unique_ptr data_handle_from_buffer(rust::Slice data); + +/// Create a MultiHandle from multiple file paths. +std::unique_ptr data_handle_from_multi(rust::Slice paths); + +/// Create a TeeHandle from multiple file paths — writes all targets in +/// parallel. +std::unique_ptr data_handle_tee(rust::Slice paths); + +// Forward declaration — defined on the Rust side, cxx generates the type. +struct ReaderBox; + +/// Create a DataHandle that forwards `read()` calls to a Rust `std::io::Read` +/// source wrapped in a `ReaderBox`. Used to stream bytes from Rust into any +/// C++ API that consumes an `eckit::DataHandle&` (e.g. fdb5 archive, the +/// streaming retrieve API) without staging through a temp file or buffer. +/// +/// The returned handle is owned by the Rust side; on drop, the contained +/// `ReaderBox` is dropped, releasing the underlying `Read` source. +std::unique_ptr data_handle_from_reader(rust::Box reader); + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index d71f7ee49..77ef63a09 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -7,6 +7,21 @@ #include "eckit-sys/src/lib.rs.h" // cxx-generated — provides LogLevel values #include "eckit_bridge.h" +// Implementation-only eckit headers — used by the .cc bodies below but not +// part of any public bridge declaration, so they stay out of the per-topic +// sub-headers to keep those minimal. +#include "eckit/config/YAMLConfiguration.h" +#include "eckit/exception/Exceptions.h" +#include "eckit/filesystem/PathName.h" +#include "eckit/io/MemoryHandle.h" +#include "eckit/io/MultiHandle.h" +#include "eckit/io/PartFileHandle.h" +#include "eckit/io/TCPSocketHandle.h" +#include "eckit/io/TeeHandle.h" +#include "eckit/net/TCPClient.h" +#include "eckit/system/Library.h" +#include "eckit/system/LibraryManager.h" + namespace eckit_bridge { // ==================== Logging ==================== diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.h b/rust/crates/eckit-sys/cpp/eckit_bridge.h index 6a1f13975..be2419367 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.h +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.h @@ -1,4 +1,7 @@ -// eckit C++ bridge for Rust FFI +// eckit C++ bridge for Rust FFI — umbrella header. +// +// Pulled in by the cxx-generated bridge (`include!("eckit_bridge.h")` in +// lib.rs). Real declarations live in the per-topic headers below. #pragma once // Note: the auto-generated `rust::behavior::trycatch` lives in @@ -7,334 +10,8 @@ // `_exceptions.h` and must not see eckit's transitively through here, // or they would have two `trycatch` specializations in one translation unit. -#include "eckit/config/LocalConfiguration.h" -#include "eckit/config/YAMLConfiguration.h" -#include "eckit/filesystem/PathName.h" -#include "eckit/io/DataHandle.h" -#include "eckit/io/MemoryHandle.h" -#include "eckit/io/MultiHandle.h" -#include "eckit/io/PartFileHandle.h" -#include "eckit/io/TCPSocketHandle.h" -#include "eckit/io/TeeHandle.h" -#include "eckit/log/Log.h" -#include "eckit/log/LogTarget.h" -#include "eckit/message/Message.h" -#include "eckit/message/Reader.h" -#include "eckit/net/TCPClient.h" -#include "eckit/net/TCPStream.h" -#include "eckit/runtime/Main.h" -#include "eckit/serialisation/MemoryStream.h" -#include "eckit/serialisation/ResizableMemoryStream.h" -#include "eckit/serialisation/Stream.h" -#include "eckit/system/Library.h" -#include "eckit/system/LibraryManager.h" - -#include "rust/cxx.h" - -#include -#include -#include - -namespace eckit_bridge { - -// Forward declarations — full definitions in cxx-generated code -enum class LogLevel : std::uint8_t; -void rust_log(LogLevel level, rust::Str target, rust::Str msg) noexcept; - -// ==================== Logging ==================== - -/// LogTarget that routes all output to Rust's log crate. -/// Accumulates writes until a newline or flush, then emits a single log call. -/// `target` is the tracing/log target string passed to Rust (e.g. "eckit", -/// "metkit") — global channels use "eckit", per-library debug channels use -/// the library name from `eckit::system::LibraryManager`. -class RustLogTarget : public eckit::LogTarget { -public: - - RustLogTarget(LogLevel level, std::string target) : level_(level), target_(std::move(target)) {} - - void write(const char* start, const char* end) override; - void flush() override; - -private: - - LogLevel level_; - std::string target_; - std::string buffer_; -}; - -/// Main subclass that installs `RustLogTarget` on all channels. -/// Every new thread automatically gets `RustLogTarget` via the factory methods. -class RustMain : public eckit::Main { -public: - - RustMain(int argc, char** argv); - - eckit::LogTarget* createInfoLogTarget() const override; - eckit::LogTarget* createWarningLogTarget() const override; - eckit::LogTarget* createErrorLogTarget() const override; - eckit::LogTarget* createDebugLogTarget() const override; - eckit::LogTarget* createMetricsLogTarget() const override; -}; - -/// Initialize eckit runtime with Rust log bridge. -void init(); - -// ==================== Configuration ==================== - -/// Wraps `eckit::LocalConfiguration` for Rust FFI. -class ConfigWrapper { - eckit::LocalConfiguration config_; - -public: - - ConfigWrapper() = default; - explicit ConfigWrapper(const eckit::Configuration& other) : config_(other) {} - - // Read - bool has(rust::Str key) const; - bool is_list(rust::Str key) const; - bool is_empty() const; - rust::String get_string(rust::Str key, rust::Str default_val) const; - int64_t get_long(rust::Str key, int64_t default_val) const; - int32_t get_int(rust::Str key, int32_t default_val) const; - bool get_bool(rust::Str key, bool default_val) const; - double get_double(rust::Str key, double default_val) const; - rust::Vec get_string_vector(rust::Str key, const rust::Vec& default_val) const; - - // Sub-configurations (by key) - std::unique_ptr get_sub(rust::Str key) const; - size_t sub_count(rust::Str key) const; - std::unique_ptr sub_at(rust::Str key, size_t index) const; - - // Sub-configurations (root-level list) - size_t root_sub_count() const; - std::unique_ptr root_sub_at(size_t index) const; - - // Write - void set_string(rust::Str key, rust::Str value); - void set_long(rust::Str key, int64_t value); - void set_int(rust::Str key, int32_t value); - void set_bool(rust::Str key, bool value); - void set_double(rust::Str key, double value); - void remove(rust::Str key); - - // Access underlying for other C++ bridge code - const eckit::LocalConfiguration& inner() const { return config_; } - eckit::LocalConfiguration& inner() { return config_; } -}; - -// Factory functions — names match cxx bridge declarations -std::unique_ptr create(); -std::unique_ptr from_path(rust::Str path); -std::unique_ptr from_yaml(rust::Str yaml); -std::unique_ptr clone(const ConfigWrapper& src); - -// ==================== DataHandle ==================== - -/// Wraps `eckit::DataHandle*` for Rust FFI. Takes ownership. -class DataHandleWrapper { - std::unique_ptr handle_; - -public: - - /// Takes ownership of a raw DataHandle pointer. - explicit DataHandleWrapper(eckit::DataHandle* h) : handle_(h) {} - - int64_t open_for_read(); - void open_for_write(int64_t estimated_length); - int64_t read(rust::Slice buf); - int64_t write(rust::Slice buf); - void close(); - int64_t position() const; - int64_t seek(int64_t offset); - bool can_seek() const; - int64_t estimate() const; - - /// Copy all data from this handle into target (both must be open). - int64_t save_into(DataHandleWrapper& target); - - /// Access underlying for other C++ bridge code. - eckit::DataHandle& inner() { return *handle_; } - const eckit::DataHandle& inner() const { return *handle_; } - - /// Release ownership — caller takes responsibility. - eckit::DataHandle* release() { return handle_.release(); } -}; - -// ==================== Message + Reader ==================== - -/// Wraps `eckit::message::Message` for Rust FFI. -/// Message is a value type with internal reference counting. -class MessageWrapper { - eckit::message::Message msg_; - -public: - - MessageWrapper() = default; - explicit MessageWrapper(eckit::message::Message m) : msg_(std::move(m)) {} - - bool is_valid() const; - size_t length() const; - /// Byte offset of this message within the source data handle. Mirrors - /// `eckit::message::Message::offset()` — populated by the Reader when - /// scanning a file/stream. - int64_t offset() const; - rust::String get_string(rust::Str key) const; - int64_t get_long(rust::Str key) const; - double get_double(rust::Str key) const; - rust::Slice data() const; - void write_to(DataHandleWrapper& handle) const; - - /// Clone (Message is ref-counted internally). - std::unique_ptr clone() const; - - /// Access underlying for other C++ bridge code. - const eckit::message::Message& inner() const { return msg_; } - eckit::message::Message& inner() { return msg_; } -}; - -/// Wraps `eckit::message::Reader` for Rust FFI. -/// Reads messages from a `DataHandle`. -class ReaderWrapper { - std::unique_ptr reader_; - -public: - - explicit ReaderWrapper(DataHandleWrapper& handle); - - /// Returns next message, or an invalid message when exhausted. - std::unique_ptr next(); -}; - -// Factory -std::unique_ptr new_reader(DataHandleWrapper& handle); - -/// Open a file as a DataHandle for reading. -std::unique_ptr data_handle_from_file(rust::Str path); - -/// Open a byte range of a file as a DataHandle. -std::unique_ptr data_handle_from_part(rust::Str path, int64_t offset, int64_t length); - -/// Create a DataHandle from an in-memory buffer (copies the data). -std::unique_ptr data_handle_from_buffer(rust::Slice data); - -/// Create a MultiHandle from multiple file paths. -std::unique_ptr data_handle_from_multi(rust::Slice paths); - -/// Create a TeeHandle from multiple file paths — writes all targets in -/// parallel. -std::unique_ptr data_handle_tee(rust::Slice paths); - -// Forward declaration — defined on the Rust side, cxx generates the type. -struct ReaderBox; - -/// Create a DataHandle that forwards `read()` calls to a Rust `std::io::Read` -/// source wrapped in a `ReaderBox`. Used to stream bytes from Rust into any -/// C++ API that consumes an `eckit::DataHandle&` (e.g. fdb5 archive, the -/// streaming retrieve API) without staging through a temp file or buffer. -/// -/// The returned handle is owned by the Rust side; on drop, the contained -/// `ReaderBox` is dropped, releasing the underlying `Read` source. -std::unique_ptr data_handle_from_reader(rust::Box reader); - -// ==================== Stream ==================== - -/// Base wrapper for `eckit::Stream`. Subclasses own the transport-specific -/// resources (socket, buffer, etc.). All read/write methods delegate to the -/// `eckit::Stream*` set by the subclass. -class StreamWrapper { -protected: - - eckit::Stream* stream_ = nullptr; - -public: - - virtual ~StreamWrapper() = default; - - StreamWrapper(const StreamWrapper&) = delete; - StreamWrapper& operator=(const StreamWrapper&) = delete; - - // Write operations (eckit::Stream operator<<) - void write_char(uint8_t c); - void write_bool(bool v); - void write_int(int32_t v); - void write_long(int64_t v); - void write_unsigned_long(uint64_t v); - void write_double(double v); - void write_string(rust::Str v); - void write_blob(rust::Slice data); - - // Read operations (eckit::Stream operator>>) - uint8_t read_char(); - bool read_bool(); - int32_t read_int(); - int64_t read_long(); - uint64_t read_unsigned_long(); - double read_double(); - rust::String read_string(); - - // Raw byte read (for data transfer after protocol handshake) - virtual int64_t read_bytes(rust::Slice buf); - - /// Access underlying stream for other bridge code. - eckit::Stream& inner() { return *stream_; } - const eckit::Stream& inner() const { return *stream_; } - - /// Number of bytes written so far. - int64_t bytes_written() const; - - /// Get buffer contents (memory write streams only). - virtual rust::Slice buffer(); - - /// Hand off the underlying connection as a DataHandle for streaming reads. - /// - /// Only meaningful for TCP streams; the default throws `NotImplemented`. - /// After this call the stream is in an unspecified state and must not be - /// used; the returned `DataHandleWrapper` owns the connection. - virtual std::unique_ptr into_data_handle(); - -protected: - - StreamWrapper() = default; -}; - -/// TCP stream — connects to host:port via `eckit::net::TCPClient`. -class TcpStreamWrapper : public StreamWrapper { - eckit::net::TCPSocket socket_; - eckit::net::TCPStream tcp_; - -public: - - TcpStreamWrapper(const std::string& host, int port); - int64_t read_bytes(rust::Slice buf) override; - std::unique_ptr into_data_handle() override; -}; - -/// Resizable memory stream — for writing, buffer grows as needed. -class MemoryWriteStreamWrapper : public StreamWrapper { - eckit::Buffer buf_; - eckit::ResizableMemoryStream mem_; - -public: - - MemoryWriteStreamWrapper(); - rust::Slice buffer() override; -}; - -/// Fixed memory stream — for reading from existing data. -class MemoryReadStreamWrapper : public StreamWrapper { - eckit::Buffer buf_; - eckit::MemoryStream mem_; - -public: - - MemoryReadStreamWrapper(rust::Slice data); -}; - -// Factory functions -std::unique_ptr stream_connect(rust::Str host, int32_t port); -std::unique_ptr stream_memory_write(); -std::unique_ptr stream_memory_read(rust::Slice data); - -} // namespace eckit_bridge +#include "config.h" +#include "datahandle.h" +#include "log.h" +#include "message.h" +#include "stream.h" diff --git a/rust/crates/eckit-sys/cpp/log.h b/rust/crates/eckit-sys/cpp/log.h new file mode 100644 index 000000000..c0600a7fd --- /dev/null +++ b/rust/crates/eckit-sys/cpp/log.h @@ -0,0 +1,55 @@ +// eckit log bridge — routes `eckit::Log` output to Rust's `log` crate. +#pragma once + +#include "eckit/log/LogTarget.h" +#include "eckit/runtime/Main.h" + +#include "rust/cxx.h" + +#include +#include + +namespace eckit_bridge { + +// Forward declarations — full definitions in cxx-generated code +enum class LogLevel : std::uint8_t; +void rust_log(LogLevel level, rust::Str target, rust::Str msg) noexcept; + +/// LogTarget that routes all output to Rust's log crate. +/// Accumulates writes until a newline or flush, then emits a single log call. +/// `target` is the tracing/log target string passed to Rust (e.g. "eckit", +/// "metkit") — global channels use "eckit", per-library debug channels use +/// the library name from `eckit::system::LibraryManager`. +class RustLogTarget : public eckit::LogTarget { +public: + + RustLogTarget(LogLevel level, std::string target) : level_(level), target_(std::move(target)) {} + + void write(const char* start, const char* end) override; + void flush() override; + +private: + + LogLevel level_; + std::string target_; + std::string buffer_; +}; + +/// Main subclass that installs `RustLogTarget` on all channels. +/// Every new thread automatically gets `RustLogTarget` via the factory methods. +class RustMain : public eckit::Main { +public: + + RustMain(int argc, char** argv); + + eckit::LogTarget* createInfoLogTarget() const override; + eckit::LogTarget* createWarningLogTarget() const override; + eckit::LogTarget* createErrorLogTarget() const override; + eckit::LogTarget* createDebugLogTarget() const override; + eckit::LogTarget* createMetricsLogTarget() const override; +}; + +/// Initialize eckit runtime with Rust log bridge. +void init(); + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/message.h b/rust/crates/eckit-sys/cpp/message.h new file mode 100644 index 000000000..dcb4d365d --- /dev/null +++ b/rust/crates/eckit-sys/cpp/message.h @@ -0,0 +1,62 @@ +// eckit Message + Reader bridge — iterate GRIB messages over a DataHandle. +#pragma once + +#include "datahandle.h" +#include "eckit/message/Message.h" +#include "eckit/message/Reader.h" + +#include "rust/cxx.h" + +#include +#include +#include + +namespace eckit_bridge { + +/// Wraps `eckit::message::Message` for Rust FFI. +/// Message is a value type with internal reference counting. +class MessageWrapper { + eckit::message::Message msg_; + +public: + + MessageWrapper() = default; + explicit MessageWrapper(eckit::message::Message m) : msg_(std::move(m)) {} + + bool is_valid() const; + size_t length() const; + /// Byte offset of this message within the source data handle. Mirrors + /// `eckit::message::Message::offset()` — populated by the Reader when + /// scanning a file/stream. + int64_t offset() const; + rust::String get_string(rust::Str key) const; + int64_t get_long(rust::Str key) const; + double get_double(rust::Str key) const; + rust::Slice data() const; + void write_to(DataHandleWrapper& handle) const; + + /// Clone (Message is ref-counted internally). + std::unique_ptr clone() const; + + /// Access underlying for other C++ bridge code. + const eckit::message::Message& inner() const { return msg_; } + eckit::message::Message& inner() { return msg_; } +}; + +/// Wraps `eckit::message::Reader` for Rust FFI. +/// Reads messages from a `DataHandle`. +class ReaderWrapper { + std::unique_ptr reader_; + +public: + + explicit ReaderWrapper(DataHandleWrapper& handle); + + /// Returns next message, or an invalid message when exhausted. + std::unique_ptr next(); +}; + +// Factory +std::unique_ptr new_reader(DataHandleWrapper& handle); + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/stream.h b/rust/crates/eckit-sys/cpp/stream.h new file mode 100644 index 000000000..4e31c22b6 --- /dev/null +++ b/rust/crates/eckit-sys/cpp/stream.h @@ -0,0 +1,117 @@ +// eckit Stream bridge — TCP and in-memory `eckit::Stream` serialization. +#pragma once + +#include "datahandle.h" +#include "eckit/io/Buffer.h" +#include "eckit/net/TCPSocket.h" +#include "eckit/net/TCPStream.h" +#include "eckit/serialisation/MemoryStream.h" +#include "eckit/serialisation/ResizableMemoryStream.h" +#include "eckit/serialisation/Stream.h" + +#include "rust/cxx.h" + +#include +#include +#include + +namespace eckit_bridge { + +/// Base wrapper for `eckit::Stream`. Subclasses own the transport-specific +/// resources (socket, buffer, etc.). All read/write methods delegate to the +/// `eckit::Stream*` set by the subclass. +class StreamWrapper { +protected: + + eckit::Stream* stream_ = nullptr; + +public: + + virtual ~StreamWrapper() = default; + + StreamWrapper(const StreamWrapper&) = delete; + StreamWrapper& operator=(const StreamWrapper&) = delete; + + // Write operations (eckit::Stream operator<<) + void write_char(uint8_t c); + void write_bool(bool v); + void write_int(int32_t v); + void write_long(int64_t v); + void write_unsigned_long(uint64_t v); + void write_double(double v); + void write_string(rust::Str v); + void write_blob(rust::Slice data); + + // Read operations (eckit::Stream operator>>) + uint8_t read_char(); + bool read_bool(); + int32_t read_int(); + int64_t read_long(); + uint64_t read_unsigned_long(); + double read_double(); + rust::String read_string(); + + // Raw byte read (for data transfer after protocol handshake) + virtual int64_t read_bytes(rust::Slice buf); + + /// Access underlying stream for other bridge code. + eckit::Stream& inner() { return *stream_; } + const eckit::Stream& inner() const { return *stream_; } + + /// Number of bytes written so far. + int64_t bytes_written() const; + + /// Get buffer contents (memory write streams only). + virtual rust::Slice buffer(); + + /// Hand off the underlying connection as a DataHandle for streaming reads. + /// + /// Only meaningful for TCP streams; the default throws `NotImplemented`. + /// After this call the stream is in an unspecified state and must not be + /// used; the returned `DataHandleWrapper` owns the connection. + virtual std::unique_ptr into_data_handle(); + +protected: + + StreamWrapper() = default; +}; + +/// TCP stream — connects to host:port via `eckit::net::TCPClient`. +class TcpStreamWrapper : public StreamWrapper { + eckit::net::TCPSocket socket_; + eckit::net::TCPStream tcp_; + +public: + + TcpStreamWrapper(const std::string& host, int port); + int64_t read_bytes(rust::Slice buf) override; + std::unique_ptr into_data_handle() override; +}; + +/// Resizable memory stream — for writing, buffer grows as needed. +class MemoryWriteStreamWrapper : public StreamWrapper { + eckit::Buffer buf_; + eckit::ResizableMemoryStream mem_; + +public: + + MemoryWriteStreamWrapper(); + rust::Slice buffer() override; +}; + +/// Fixed memory stream — for reading from existing data. +class MemoryReadStreamWrapper : public StreamWrapper { + eckit::Buffer buf_; + eckit::MemoryStream mem_; + +public: + + MemoryReadStreamWrapper(rust::Slice data); +}; + +// Factory functions +std::unique_ptr stream_connect(rust::Str host, int32_t port); +std::unique_ptr stream_memory_write(); +std::unique_ptr stream_memory_read(rust::Slice data); + +} // namespace eckit_bridge From 0ed608ea38ee127135443ec28d886f538fe93aa5 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 15:45:38 +0200 Subject: [PATCH 13/22] Update README to clarify Cargo build features for eckit-sys crate --- rust/crates/eckit-sys/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/crates/eckit-sys/README.md b/rust/crates/eckit-sys/README.md index 7113623bc..b4666ba66 100644 --- a/rust/crates/eckit-sys/README.md +++ b/rust/crates/eckit-sys/README.md @@ -6,7 +6,9 @@ library. This crate provides raw FFI bindings using [cxx](https://cxx.rs/). For a safe, ergonomic API, use the higher-level `eckit` crate (forthcoming). -## Features +## Cargo build features + +These flags control what the underlying C++ eckit library is compiled with. ### Build strategy (mutually exclusive) From f1490c582af3859e42c92fbeb2dd941599693494 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 15:48:37 +0200 Subject: [PATCH 14/22] Refactor initialization of RustMain to use maybe_unused attribute --- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index 77ef63a09..799d2a97a 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -84,9 +84,8 @@ static void install_per_library_targets() { void init() { if (!eckit::Main::ready()) { - static const char* argv[] = {"eckit-rs", nullptr}; - static auto* main = new RustMain(1, const_cast(argv)); - (void)main; + static const char* argv[] = {"eckit-rs", nullptr}; + [[maybe_unused]] static auto* main_inst_ = new RustMain(1, const_cast(argv)); install_per_library_targets(); } } From f74decb8fa39e0fdde4743a856fd80e167723f86 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 16:24:36 +0200 Subject: [PATCH 15/22] Refactor StreamWrapper to use virtual stream() method for I/O operations --- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 44 ++++++++++------------ rust/crates/eckit-sys/cpp/stream.h | 28 +++++++++----- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index 799d2a97a..ec0d2ab23 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -394,63 +394,63 @@ std::unique_ptr new_reader(DataHandleWrapper& handle) { // ==================== Stream (base class) ==================== void StreamWrapper::write_char(uint8_t c) { - *stream_ << static_cast(c); + stream() << static_cast(c); } void StreamWrapper::write_bool(bool v) { - *stream_ << v; + stream() << v; } void StreamWrapper::write_int(int32_t v) { - *stream_ << v; + stream() << v; } void StreamWrapper::write_long(int64_t v) { - *stream_ << static_cast(v); + stream() << static_cast(v); } void StreamWrapper::write_unsigned_long(uint64_t v) { - *stream_ << static_cast(v); + stream() << static_cast(v); } void StreamWrapper::write_double(double v) { - *stream_ << v; + stream() << v; } void StreamWrapper::write_string(rust::Str v) { - *stream_ << std::string(v.data(), v.size()); + stream() << std::string(v.data(), v.size()); } void StreamWrapper::write_blob(rust::Slice data) { - stream_->writeBlob(data.data(), data.size()); + stream().writeBlob(data.data(), data.size()); } uint8_t StreamWrapper::read_char() { char c{}; - *stream_ >> c; + stream() >> c; return static_cast(c); } bool StreamWrapper::read_bool() { bool v{}; - *stream_ >> v; + stream() >> v; return v; } int32_t StreamWrapper::read_int() { int v{}; - *stream_ >> v; + stream() >> v; return v; } int64_t StreamWrapper::read_long() { long long v{}; - *stream_ >> v; + stream() >> v; return static_cast(v); } uint64_t StreamWrapper::read_unsigned_long() { unsigned long long v{}; - *stream_ >> v; + stream() >> v; return static_cast(v); } double StreamWrapper::read_double() { double v{}; - *stream_ >> v; + stream() >> v; return v; } rust::String StreamWrapper::read_string() { std::string v; - *stream_ >> v; + stream() >> v; return rust::String(v); } @@ -463,15 +463,13 @@ rust::Slice StreamWrapper::buffer() { } int64_t StreamWrapper::bytes_written() const { - return stream_->bytesWritten(); + return const_cast(this)->stream().bytesWritten(); } // ==================== TcpStreamWrapper ==================== TcpStreamWrapper::TcpStreamWrapper(const std::string& host, int port) : - socket_(eckit::net::TCPClient().connect(host, port)), tcp_(socket_) { - stream_ = &tcp_; -} + socket_(eckit::net::TCPClient().connect(host, port)), tcp_(socket_) {} int64_t TcpStreamWrapper::read_bytes(rust::Slice buf) { // The connection lives on `tcp_.socket()` — `socket_` was emptied by the @@ -494,9 +492,7 @@ std::unique_ptr StreamWrapper::into_data_handle() { // ==================== MemoryWriteStreamWrapper ==================== -MemoryWriteStreamWrapper::MemoryWriteStreamWrapper() : buf_(4096), mem_(buf_) { - stream_ = &mem_; -} +MemoryWriteStreamWrapper::MemoryWriteStreamWrapper() : buf_(4096), mem_(buf_) {} rust::Slice MemoryWriteStreamWrapper::buffer() { return {static_cast(buf_.data()), static_cast(mem_.bytesWritten())}; @@ -505,9 +501,7 @@ rust::Slice MemoryWriteStreamWrapper::buffer() { // ==================== MemoryReadStreamWrapper ==================== MemoryReadStreamWrapper::MemoryReadStreamWrapper(rust::Slice data) : - buf_(data.data(), data.size()), mem_(buf_) { - stream_ = &mem_; -} + buf_(data.data(), data.size()), mem_(buf_) {} // ==================== Factory functions ==================== diff --git a/rust/crates/eckit-sys/cpp/stream.h b/rust/crates/eckit-sys/cpp/stream.h index 4e31c22b6..cecff29b2 100644 --- a/rust/crates/eckit-sys/cpp/stream.h +++ b/rust/crates/eckit-sys/cpp/stream.h @@ -18,13 +18,9 @@ namespace eckit_bridge { /// Base wrapper for `eckit::Stream`. Subclasses own the transport-specific -/// resources (socket, buffer, etc.). All read/write methods delegate to the -/// `eckit::Stream*` set by the subclass. +/// resources (socket, buffer, etc.) and expose them via `stream()`; all +/// read/write methods in this base dispatch through that virtual accessor. class StreamWrapper { -protected: - - eckit::Stream* stream_ = nullptr; - public: virtual ~StreamWrapper() = default; @@ -54,10 +50,6 @@ class StreamWrapper { // Raw byte read (for data transfer after protocol handshake) virtual int64_t read_bytes(rust::Slice buf); - /// Access underlying stream for other bridge code. - eckit::Stream& inner() { return *stream_; } - const eckit::Stream& inner() const { return *stream_; } - /// Number of bytes written so far. int64_t bytes_written() const; @@ -74,6 +66,10 @@ class StreamWrapper { protected: StreamWrapper() = default; + + /// Subclass-supplied access to the owned `eckit::Stream`. Called by every + /// read/write method in this base. + virtual eckit::Stream& stream() = 0; }; /// TCP stream — connects to host:port via `eckit::net::TCPClient`. @@ -86,6 +82,10 @@ class TcpStreamWrapper : public StreamWrapper { TcpStreamWrapper(const std::string& host, int port); int64_t read_bytes(rust::Slice buf) override; std::unique_ptr into_data_handle() override; + +protected: + + eckit::Stream& stream() override { return tcp_; } }; /// Resizable memory stream — for writing, buffer grows as needed. @@ -97,6 +97,10 @@ class MemoryWriteStreamWrapper : public StreamWrapper { MemoryWriteStreamWrapper(); rust::Slice buffer() override; + +protected: + + eckit::Stream& stream() override { return mem_; } }; /// Fixed memory stream — for reading from existing data. @@ -107,6 +111,10 @@ class MemoryReadStreamWrapper : public StreamWrapper { public: MemoryReadStreamWrapper(rust::Slice data); + +protected: + + eckit::Stream& stream() override { return mem_; } }; // Factory functions From e3d7d9dc48170b68b3356a0cac640292fa5ead51 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 16:52:02 +0200 Subject: [PATCH 16/22] Refactor RustLogTarget to simplify log target creation and handling --- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 10 +++++----- rust/crates/eckit-sys/cpp/log.h | 5 +++++ rust/crates/eckit-sys/src/lib.rs | 23 +++++++++++++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index ec0d2ab23..054c590de 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -57,19 +57,19 @@ void RustLogTarget::flush() { RustMain::RustMain(int argc, char** argv) : Main(argc, argv) {} eckit::LogTarget* RustMain::createInfoLogTarget() const { - return new RustLogTarget(LogLevel::Info, "eckit"); + return new RustLogTarget(LogLevel::Info); } eckit::LogTarget* RustMain::createWarningLogTarget() const { - return new RustLogTarget(LogLevel::Warn, "eckit"); + return new RustLogTarget(LogLevel::Warn); } eckit::LogTarget* RustMain::createErrorLogTarget() const { - return new RustLogTarget(LogLevel::Error, "eckit"); + return new RustLogTarget(LogLevel::Error); } eckit::LogTarget* RustMain::createDebugLogTarget() const { - return new RustLogTarget(LogLevel::Debug, "eckit"); + return new RustLogTarget(LogLevel::Debug); } eckit::LogTarget* RustMain::createMetricsLogTarget() const { - return new RustLogTarget(LogLevel::Trace, "eckit"); + return new RustLogTarget(LogLevel::Trace); } /// Install a per-library `RustLogTarget` on every registered library's debug diff --git a/rust/crates/eckit-sys/cpp/log.h b/rust/crates/eckit-sys/cpp/log.h index c0600a7fd..056bfb70c 100644 --- a/rust/crates/eckit-sys/cpp/log.h +++ b/rust/crates/eckit-sys/cpp/log.h @@ -23,6 +23,11 @@ void rust_log(LogLevel level, rust::Str target, rust::Str msg) noexcept; class RustLogTarget : public eckit::LogTarget { public: + /// No target — the Rust side falls back to the `log` crate's default + /// (`module_path!()` of the bridge, i.e. `eckit_sys`). + explicit RustLogTarget(LogLevel level) : level_(level) {} + + /// Tagged target (e.g. a registered library name). RustLogTarget(LogLevel level, std::string target) : level_(level), target_(std::move(target)) {} void write(const char* start, const char* end) override; diff --git a/rust/crates/eckit-sys/src/lib.rs b/rust/crates/eckit-sys/src/lib.rs index 937b801bd..0d1add9a7 100644 --- a/rust/crates/eckit-sys/src/lib.rs +++ b/rust/crates/eckit-sys/src/lib.rs @@ -294,13 +294,22 @@ fn invoke_reader_seek(reader: &mut ReaderBox, offset: i64) -> i64 { /// Called from C++ `RustLogTarget::write()` — routes to Rust `log` crate. fn rust_log(level: ffi::LogLevel, target: &str, msg: &str) { - match level { - ffi::LogLevel::Error => log::error!(target: target, "{msg}"), - ffi::LogLevel::Warn => log::warn!(target: target, "{msg}"), - ffi::LogLevel::Info => log::info!(target: target, "{msg}"), - ffi::LogLevel::Debug => log::debug!(target: target, "{msg}"), - // Trace + wildcard for cxx non-exhaustive enum - _ => log::trace!(target: target, "{msg}"), + if target.is_empty() { + match level { + ffi::LogLevel::Error => log::error!("{msg}"), + ffi::LogLevel::Warn => log::warn!("{msg}"), + ffi::LogLevel::Info => log::info!("{msg}"), + ffi::LogLevel::Debug => log::debug!("{msg}"), + _ => log::trace!("{msg}"), + } + } else { + match level { + ffi::LogLevel::Error => log::error!(target: target, "{msg}"), + ffi::LogLevel::Warn => log::warn!(target: target, "{msg}"), + ffi::LogLevel::Info => log::info!(target: target, "{msg}"), + ffi::LogLevel::Debug => log::debug!(target: target, "{msg}"), + _ => log::trace!(target: target, "{msg}"), + } } } From cd4de7370fec3dd7ae02e9efd7832f182aab82f8 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 17:16:38 +0200 Subject: [PATCH 17/22] Refactor DataHandleWrapper methods to use static factory functions --- rust/crates/eckit-sys/cpp/datahandle.h | 47 ++++++++++++---------- rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 12 +++--- rust/crates/eckit-sys/src/lib.rs | 22 +++++----- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/rust/crates/eckit-sys/cpp/datahandle.h b/rust/crates/eckit-sys/cpp/datahandle.h index 802386369..e78b862a1 100644 --- a/rust/crates/eckit-sys/cpp/datahandle.h +++ b/rust/crates/eckit-sys/cpp/datahandle.h @@ -10,6 +10,9 @@ namespace eckit_bridge { +// Forward declaration — defined on the Rust side, cxx generates the type. +struct ReaderBox; + /// Wraps `eckit::DataHandle*` for Rust FFI. Takes ownership. class DataHandleWrapper { std::unique_ptr handle_; @@ -38,34 +41,34 @@ class DataHandleWrapper { /// Release ownership — caller takes responsibility. eckit::DataHandle* release() { return handle_.release(); } -}; -/// Open a file as a DataHandle for reading. -std::unique_ptr data_handle_from_file(rust::Str path); + // ============== Factories ============== -/// Open a byte range of a file as a DataHandle. -std::unique_ptr data_handle_from_part(rust::Str path, int64_t offset, int64_t length); + /// Open a file as a DataHandle for reading. + static std::unique_ptr from_file(rust::Str path); -/// Create a DataHandle from an in-memory buffer (copies the data). -std::unique_ptr data_handle_from_buffer(rust::Slice data); + /// Open a byte range of a file as a DataHandle. + static std::unique_ptr from_part(rust::Str path, int64_t offset, int64_t length); -/// Create a MultiHandle from multiple file paths. -std::unique_ptr data_handle_from_multi(rust::Slice paths); + /// Create a DataHandle from an in-memory buffer (copies the data). + static std::unique_ptr from_buffer(rust::Slice data); -/// Create a TeeHandle from multiple file paths — writes all targets in -/// parallel. -std::unique_ptr data_handle_tee(rust::Slice paths); + /// Create a MultiHandle from multiple file paths. + static std::unique_ptr from_multi(rust::Slice paths); -// Forward declaration — defined on the Rust side, cxx generates the type. -struct ReaderBox; + /// Create a TeeHandle from multiple file paths — writes all targets in + /// parallel. + static std::unique_ptr tee(rust::Slice paths); -/// Create a DataHandle that forwards `read()` calls to a Rust `std::io::Read` -/// source wrapped in a `ReaderBox`. Used to stream bytes from Rust into any -/// C++ API that consumes an `eckit::DataHandle&` (e.g. fdb5 archive, the -/// streaming retrieve API) without staging through a temp file or buffer. -/// -/// The returned handle is owned by the Rust side; on drop, the contained -/// `ReaderBox` is dropped, releasing the underlying `Read` source. -std::unique_ptr data_handle_from_reader(rust::Box reader); + /// Create a DataHandle that forwards `read()` calls to a Rust + /// `std::io::Read` source wrapped in a `ReaderBox`. Used to stream bytes + /// from Rust into any C++ API that consumes an `eckit::DataHandle&` + /// (e.g. fdb5 archive, the streaming retrieve API) without staging + /// through a temp file or buffer. + /// + /// The returned handle is owned by the Rust side; on drop, the contained + /// `ReaderBox` is dropped, releasing the underlying `Read` source. + static std::unique_ptr from_reader(rust::Box reader); +}; } // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp index 054c590de..c9d4ac513 100644 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp @@ -248,21 +248,21 @@ int64_t DataHandleWrapper::save_into(DataHandleWrapper& target) { return static_cast(handle_->saveInto(*target.handle_)); } -std::unique_ptr data_handle_from_file(rust::Str path) { +std::unique_ptr DataHandleWrapper::from_file(rust::Str path) { auto p = eckit::PathName{std::string(path)}; return std::make_unique(p.fileHandle()); } -std::unique_ptr data_handle_from_part(rust::Str path, int64_t offset, int64_t length) { +std::unique_ptr DataHandleWrapper::from_part(rust::Str path, int64_t offset, int64_t length) { return std::make_unique( new eckit::PartFileHandle(eckit::PathName{std::string(path)}, eckit::Offset(offset), eckit::Length(length))); } -std::unique_ptr data_handle_from_buffer(rust::Slice data) { +std::unique_ptr DataHandleWrapper::from_buffer(rust::Slice data) { return std::make_unique(new eckit::MemoryHandle(data.data(), data.size())); } -std::unique_ptr data_handle_from_multi(rust::Slice paths) { +std::unique_ptr DataHandleWrapper::from_multi(rust::Slice paths) { auto* mh = new eckit::MultiHandle(); for (const auto& p : paths) { (*mh) += eckit::PathName(std::string(p)).fileHandle(); @@ -270,7 +270,7 @@ std::unique_ptr data_handle_from_multi(rust::Slice(mh); } -std::unique_ptr data_handle_tee(rust::Slice paths) { +std::unique_ptr DataHandleWrapper::tee(rust::Slice paths) { std::vector handles; handles.reserve(paths.size()); for (const auto& p : paths) { @@ -336,7 +336,7 @@ class RustReaderHandle : public eckit::DataHandle { } // namespace -std::unique_ptr data_handle_from_reader(rust::Box reader) { +std::unique_ptr DataHandleWrapper::from_reader(rust::Box reader) { return std::make_unique(new RustReaderHandle(std::move(reader))); } diff --git a/rust/crates/eckit-sys/src/lib.rs b/rust/crates/eckit-sys/src/lib.rs index 0d1add9a7..07a0b7f9e 100644 --- a/rust/crates/eckit-sys/src/lib.rs +++ b/rust/crates/eckit-sys/src/lib.rs @@ -114,27 +114,29 @@ mod ffi { ) -> Result; /// Open a file as a DataHandle for reading. - fn data_handle_from_file(path: &str) -> Result>; + #[Self = "DataHandleWrapper"] + fn from_file(path: &str) -> Result>; /// Open a byte range of a file as a DataHandle. - fn data_handle_from_part( - path: &str, - offset: i64, - length: i64, - ) -> Result>; + #[Self = "DataHandleWrapper"] + fn from_part(path: &str, offset: i64, length: i64) -> Result>; /// Create a DataHandle from an in-memory buffer (copies the data). - fn data_handle_from_buffer(data: &[u8]) -> Result>; + #[Self = "DataHandleWrapper"] + fn from_buffer(data: &[u8]) -> Result>; /// Create a MultiHandle from multiple file paths. - fn data_handle_from_multi(paths: &[String]) -> Result>; + #[Self = "DataHandleWrapper"] + fn from_multi(paths: &[String]) -> Result>; /// Create a TeeHandle from multiple file paths — writes all targets in parallel. - fn data_handle_tee(paths: &[String]) -> Result>; + #[Self = "DataHandleWrapper"] + fn tee(paths: &[String]) -> Result>; /// Create a DataHandle that forwards `read()` calls to a Rust /// `std::io::Read` source wrapped in a [`ReaderBox`]. - fn data_handle_from_reader(reader: Box) -> Result>; + #[Self = "DataHandleWrapper"] + fn from_reader(reader: Box) -> Result>; // ==================== Message + Reader ==================== From d61d28984d974f13b4704ea3b46651742dd2dfb5 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 17:22:13 +0200 Subject: [PATCH 18/22] Split C++ implementation too --- rust/crates/eckit-sys/build.rs | 12 +- rust/crates/eckit-sys/cpp/config.cpp | 127 +++++ rust/crates/eckit-sys/cpp/datahandle.cpp | 149 ++++++ rust/crates/eckit-sys/cpp/eckit_bridge.cpp | 520 --------------------- rust/crates/eckit-sys/cpp/log.cpp | 79 ++++ rust/crates/eckit-sys/cpp/message.cpp | 59 +++ rust/crates/eckit-sys/cpp/stream.cpp | 140 ++++++ 7 files changed, 564 insertions(+), 522 deletions(-) create mode 100644 rust/crates/eckit-sys/cpp/config.cpp create mode 100644 rust/crates/eckit-sys/cpp/datahandle.cpp delete mode 100644 rust/crates/eckit-sys/cpp/eckit_bridge.cpp create mode 100644 rust/crates/eckit-sys/cpp/log.cpp create mode 100644 rust/crates/eckit-sys/cpp/message.cpp create mode 100644 rust/crates/eckit-sys/cpp/stream.cpp diff --git a/rust/crates/eckit-sys/build.rs b/rust/crates/eckit-sys/build.rs index 33216e5b7..23ea79bc1 100644 --- a/rust/crates/eckit-sys/build.rs +++ b/rust/crates/eckit-sys/build.rs @@ -41,15 +41,23 @@ fn build_cxx_bridge(include: &std::path::Path) { let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set")); println!("cargo:rerun-if-changed=cpp/eckit_bridge.h"); - println!("cargo:rerun-if-changed=cpp/eckit_bridge.cpp"); println!("cargo:rerun-if-changed=cpp/log.h"); + println!("cargo:rerun-if-changed=cpp/log.cpp"); println!("cargo:rerun-if-changed=cpp/config.h"); + println!("cargo:rerun-if-changed=cpp/config.cpp"); println!("cargo:rerun-if-changed=cpp/datahandle.h"); + println!("cargo:rerun-if-changed=cpp/datahandle.cpp"); println!("cargo:rerun-if-changed=cpp/message.h"); + println!("cargo:rerun-if-changed=cpp/message.cpp"); println!("cargo:rerun-if-changed=cpp/stream.h"); + println!("cargo:rerun-if-changed=cpp/stream.cpp"); cxx_build::bridge("src/lib.rs") - .file(crate_dir.join("cpp/eckit_bridge.cpp")) + .file(crate_dir.join("cpp/log.cpp")) + .file(crate_dir.join("cpp/config.cpp")) + .file(crate_dir.join("cpp/datahandle.cpp")) + .file(crate_dir.join("cpp/message.cpp")) + .file(crate_dir.join("cpp/stream.cpp")) .include(include) .include(crate_dir.join("cpp")) .include(&out_dir) // for eckit_exceptions.h diff --git a/rust/crates/eckit-sys/cpp/config.cpp b/rust/crates/eckit-sys/cpp/config.cpp new file mode 100644 index 000000000..91acd078a --- /dev/null +++ b/rust/crates/eckit-sys/cpp/config.cpp @@ -0,0 +1,127 @@ +// eckit Configuration bridge — implementation. + +#include "eckit_exceptions.h" + +#include "config.h" +#include "eckit-sys/src/lib.rs.h" + +#include "eckit/config/YAMLConfiguration.h" +#include "eckit/filesystem/PathName.h" + +namespace eckit_bridge { + +bool ConfigWrapper::has(rust::Str key) const { + return config_.has(std::string(key)); +} + +bool ConfigWrapper::is_list(rust::Str key) const { + return config_.isList(std::string(key)); +} + +bool ConfigWrapper::is_empty() const { + return config_.empty(); +} + +rust::String ConfigWrapper::get_string(rust::Str key, rust::Str default_val) const { + return rust::String(config_.getString(std::string(key), std::string(default_val))); +} + +int64_t ConfigWrapper::get_long(rust::Str key, int64_t default_val) const { + return config_.getLong(std::string(key), default_val); +} + +int32_t ConfigWrapper::get_int(rust::Str key, int32_t default_val) const { + return config_.getInt(std::string(key), default_val); +} + +bool ConfigWrapper::get_bool(rust::Str key, bool default_val) const { + return config_.getBool(std::string(key), default_val); +} + +double ConfigWrapper::get_double(rust::Str key, double default_val) const { + return config_.getDouble(std::string(key), default_val); +} + +rust::Vec ConfigWrapper::get_string_vector(rust::Str key, + const rust::Vec& default_val) const { + std::vector def; + def.reserve(default_val.size()); + for (const auto& s : default_val) { + def.emplace_back(std::string(s)); + } + auto vec = config_.getStringVector(std::string(key), def); + rust::Vec result; + result.reserve(vec.size()); + for (const auto& s : vec) { + result.push_back(rust::String(s)); + } + return result; +} + +std::unique_ptr ConfigWrapper::get_sub(rust::Str key) const { + return std::make_unique(config_.getSubConfiguration(std::string(key))); +} + +size_t ConfigWrapper::sub_count(rust::Str key) const { + return config_.getSubConfigurations(std::string(key)).size(); +} + +std::unique_ptr ConfigWrapper::sub_at(rust::Str key, size_t index) const { + auto subs = config_.getSubConfigurations(std::string(key)); + return std::make_unique(subs.at(index)); +} + +size_t ConfigWrapper::root_sub_count() const { + return config_.getSubConfigurations().size(); +} + +std::unique_ptr ConfigWrapper::root_sub_at(size_t index) const { + auto subs = config_.getSubConfigurations(); + return std::make_unique(subs.at(index)); +} + +void ConfigWrapper::set_string(rust::Str key, rust::Str value) { + config_.set(std::string(key), std::string(value)); +} + +void ConfigWrapper::set_long(rust::Str key, int64_t value) { + config_.set(std::string(key), static_cast(value)); +} + +void ConfigWrapper::set_int(rust::Str key, int32_t value) { + config_.set(std::string(key), value); +} + +void ConfigWrapper::set_bool(rust::Str key, bool value) { + config_.set(std::string(key), value); +} + +void ConfigWrapper::set_double(rust::Str key, double value) { + config_.set(std::string(key), value); +} + +void ConfigWrapper::remove(rust::Str key) { + config_.remove(std::string(key)); +} + +std::unique_ptr create() { + return std::make_unique(); +} + +std::unique_ptr from_path(rust::Str path) { + auto p = eckit::PathName{std::string(path)}; + auto yaml = eckit::YAMLConfiguration{p}; + return std::make_unique(yaml); +} + +std::unique_ptr from_yaml(rust::Str yaml) { + auto str = std::string(yaml); + auto parsed = eckit::YAMLConfiguration{str}; + return std::make_unique(parsed); +} + +std::unique_ptr clone(const ConfigWrapper& src) { + return std::make_unique(src.inner()); +} + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/datahandle.cpp b/rust/crates/eckit-sys/cpp/datahandle.cpp new file mode 100644 index 000000000..0d014dc33 --- /dev/null +++ b/rust/crates/eckit-sys/cpp/datahandle.cpp @@ -0,0 +1,149 @@ +// eckit DataHandle bridge — implementation. + +#include "eckit_exceptions.h" + +#include "datahandle.h" +#include "eckit-sys/src/lib.rs.h" + +#include "eckit/exception/Exceptions.h" +#include "eckit/filesystem/PathName.h" +#include "eckit/io/MemoryHandle.h" +#include "eckit/io/MultiHandle.h" +#include "eckit/io/PartFileHandle.h" +#include "eckit/io/TeeHandle.h" + +namespace eckit_bridge { + +int64_t DataHandleWrapper::open_for_read() { + return static_cast(handle_->openForRead()); +} + +void DataHandleWrapper::open_for_write(int64_t estimated_length) { + handle_->openForWrite(eckit::Length(estimated_length)); +} + +int64_t DataHandleWrapper::read(rust::Slice buf) { + return handle_->read(buf.data(), static_cast(buf.size())); +} + +int64_t DataHandleWrapper::write(rust::Slice buf) { + return handle_->write(buf.data(), static_cast(buf.size())); +} + +void DataHandleWrapper::close() { + handle_->close(); +} + +int64_t DataHandleWrapper::position() const { + return static_cast(handle_->position()); +} + +int64_t DataHandleWrapper::seek(int64_t offset) { + return static_cast(handle_->seek(eckit::Offset(offset))); +} + +bool DataHandleWrapper::can_seek() const { + return handle_->canSeek(); +} + +int64_t DataHandleWrapper::estimate() const { + return static_cast(handle_->estimate()); +} + +int64_t DataHandleWrapper::save_into(DataHandleWrapper& target) { + return static_cast(handle_->saveInto(*target.handle_)); +} + +std::unique_ptr DataHandleWrapper::from_file(rust::Str path) { + auto p = eckit::PathName{std::string(path)}; + return std::make_unique(p.fileHandle()); +} + +std::unique_ptr DataHandleWrapper::from_part(rust::Str path, int64_t offset, int64_t length) { + return std::make_unique( + new eckit::PartFileHandle(eckit::PathName{std::string(path)}, eckit::Offset(offset), eckit::Length(length))); +} + +std::unique_ptr DataHandleWrapper::from_buffer(rust::Slice data) { + return std::make_unique(new eckit::MemoryHandle(data.data(), data.size())); +} + +std::unique_ptr DataHandleWrapper::from_multi(rust::Slice paths) { + auto* mh = new eckit::MultiHandle(); + for (const auto& p : paths) { + (*mh) += eckit::PathName(std::string(p)).fileHandle(); + } + return std::make_unique(mh); +} + +std::unique_ptr DataHandleWrapper::tee(rust::Slice paths) { + std::vector handles; + handles.reserve(paths.size()); + for (const auto& p : paths) { + handles.push_back(eckit::PathName(std::string(p)).fileHandle()); + } + return std::make_unique(new eckit::TeeHandle(handles)); +} + +namespace { + +// `eckit::DataHandle` subclass that forwards `read()` / `seek()` to a Rust +// `Read + Seek` source held in a `rust::Box`. `openForRead` +// rewinds via `seek(0)` to match `eckit::FileHandle`'s `fopen("r")` +// semantics — eckit's archive pipeline relies on re-opening the source for +// the analyser and per-database passes. +class RustReaderHandle : public eckit::DataHandle { +public: + + explicit RustReaderHandle(rust::Box reader) : reader_(std::move(reader)) {} + + void print(std::ostream& s) const override { s << "RustReaderHandle[]"; } + + eckit::Length openForRead() override { + if (invoke_reader_seek(*reader_, 0) < 0) { + throw eckit::ReadError("RustReaderHandle: rewind failed on openForRead"); + } + return eckit::Length(0); + } + + long read(void* buffer, long length) override { + if (length <= 0) { + return 0; + } + auto* bytes = static_cast(buffer); + rust::Slice slice{bytes, static_cast(length)}; + int64_t n = invoke_reader_read(*reader_, slice); + if (n < 0) { + throw eckit::ReadError("RustReaderHandle: error reading from Rust source"); + } + return static_cast(n); + } + + bool canSeek() const override { return true; } + + eckit::Offset seek(const eckit::Offset& offset) override { + int64_t pos = invoke_reader_seek(*reader_, static_cast(offset)); + if (pos < 0) { + throw eckit::ReadError("RustReaderHandle: seek failed"); + } + return eckit::Offset(pos); + } + + void close() override {} + + eckit::Length estimate() override { return eckit::Length(0); } + + eckit::Length size() override { return eckit::Length(0); } + +private: + + rust::Box reader_; +}; + +} // namespace + +std::unique_ptr DataHandleWrapper::from_reader(rust::Box reader) { + return std::make_unique(new RustReaderHandle(std::move(reader))); +} + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp b/rust/crates/eckit-sys/cpp/eckit_bridge.cpp deleted file mode 100644 index c9d4ac513..000000000 --- a/rust/crates/eckit-sys/cpp/eckit_bridge.cpp +++ /dev/null @@ -1,520 +0,0 @@ -// eckit C++ bridge implementation - -// trycatch handler — must come before the cxx-generated header so the -// generated wrappers' Result handling picks up our specialization. -#include "eckit_exceptions.h" - -#include "eckit-sys/src/lib.rs.h" // cxx-generated — provides LogLevel values -#include "eckit_bridge.h" - -// Implementation-only eckit headers — used by the .cc bodies below but not -// part of any public bridge declaration, so they stay out of the per-topic -// sub-headers to keep those minimal. -#include "eckit/config/YAMLConfiguration.h" -#include "eckit/exception/Exceptions.h" -#include "eckit/filesystem/PathName.h" -#include "eckit/io/MemoryHandle.h" -#include "eckit/io/MultiHandle.h" -#include "eckit/io/PartFileHandle.h" -#include "eckit/io/TCPSocketHandle.h" -#include "eckit/io/TeeHandle.h" -#include "eckit/net/TCPClient.h" -#include "eckit/system/Library.h" -#include "eckit/system/LibraryManager.h" - -namespace eckit_bridge { - -// ==================== Logging ==================== - -void RustLogTarget::write(const char* start, const char* end) { - buffer_.append(start, end); - - std::string::size_type pos; - while ((pos = buffer_.find('\n')) != std::string::npos) { - std::string line = buffer_.substr(0, pos); - while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) { - line.pop_back(); - } - if (!line.empty()) { - rust_log(level_, rust::Str(target_.data(), target_.size()), rust::Str(line.data(), line.size())); - } - buffer_.erase(0, pos + 1); - } -} - -void RustLogTarget::flush() { - if (!buffer_.empty()) { - while (!buffer_.empty() && (buffer_.back() == '\r' || buffer_.back() == ' ')) { - buffer_.pop_back(); - } - if (!buffer_.empty()) { - rust_log(level_, rust::Str(target_.data(), target_.size()), rust::Str(buffer_.data(), buffer_.size())); - buffer_.clear(); - } - } -} - -RustMain::RustMain(int argc, char** argv) : Main(argc, argv) {} - -eckit::LogTarget* RustMain::createInfoLogTarget() const { - return new RustLogTarget(LogLevel::Info); -} -eckit::LogTarget* RustMain::createWarningLogTarget() const { - return new RustLogTarget(LogLevel::Warn); -} -eckit::LogTarget* RustMain::createErrorLogTarget() const { - return new RustLogTarget(LogLevel::Error); -} -eckit::LogTarget* RustMain::createDebugLogTarget() const { - return new RustLogTarget(LogLevel::Debug); -} -eckit::LogTarget* RustMain::createMetricsLogTarget() const { - return new RustLogTarget(LogLevel::Trace); -} - -/// Install a per-library `RustLogTarget` on every registered library's debug -/// channel. Each library's debug output is then tagged with its own name as -/// the tracing/log target. Idempotent — `Channel::setTarget` replaces. -static void install_per_library_targets() { - for (const auto& libname : eckit::system::LibraryManager::list()) { - const auto& lib = eckit::system::LibraryManager::lookup(libname); - lib.debugChannel().setTarget(new RustLogTarget(LogLevel::Debug, libname)); - } -} - -void init() { - if (!eckit::Main::ready()) { - static const char* argv[] = {"eckit-rs", nullptr}; - [[maybe_unused]] static auto* main_inst_ = new RustMain(1, const_cast(argv)); - install_per_library_targets(); - } -} - -// ==================== Configuration ==================== - -bool ConfigWrapper::has(rust::Str key) const { - return config_.has(std::string(key)); -} - -bool ConfigWrapper::is_list(rust::Str key) const { - return config_.isList(std::string(key)); -} - -bool ConfigWrapper::is_empty() const { - return config_.empty(); -} - -rust::String ConfigWrapper::get_string(rust::Str key, rust::Str default_val) const { - return rust::String(config_.getString(std::string(key), std::string(default_val))); -} - -int64_t ConfigWrapper::get_long(rust::Str key, int64_t default_val) const { - return config_.getLong(std::string(key), default_val); -} - -int32_t ConfigWrapper::get_int(rust::Str key, int32_t default_val) const { - return config_.getInt(std::string(key), default_val); -} - -bool ConfigWrapper::get_bool(rust::Str key, bool default_val) const { - return config_.getBool(std::string(key), default_val); -} - -double ConfigWrapper::get_double(rust::Str key, double default_val) const { - return config_.getDouble(std::string(key), default_val); -} - -rust::Vec ConfigWrapper::get_string_vector(rust::Str key, - const rust::Vec& default_val) const { - std::vector def; - def.reserve(default_val.size()); - for (const auto& s : default_val) { - def.emplace_back(std::string(s)); - } - auto vec = config_.getStringVector(std::string(key), def); - rust::Vec result; - result.reserve(vec.size()); - for (const auto& s : vec) { - result.push_back(rust::String(s)); - } - return result; -} - -std::unique_ptr ConfigWrapper::get_sub(rust::Str key) const { - return std::make_unique(config_.getSubConfiguration(std::string(key))); -} - -size_t ConfigWrapper::sub_count(rust::Str key) const { - return config_.getSubConfigurations(std::string(key)).size(); -} - -std::unique_ptr ConfigWrapper::sub_at(rust::Str key, size_t index) const { - auto subs = config_.getSubConfigurations(std::string(key)); - return std::make_unique(subs.at(index)); -} - -size_t ConfigWrapper::root_sub_count() const { - return config_.getSubConfigurations().size(); -} - -std::unique_ptr ConfigWrapper::root_sub_at(size_t index) const { - auto subs = config_.getSubConfigurations(); - return std::make_unique(subs.at(index)); -} - -void ConfigWrapper::set_string(rust::Str key, rust::Str value) { - config_.set(std::string(key), std::string(value)); -} - -void ConfigWrapper::set_long(rust::Str key, int64_t value) { - config_.set(std::string(key), static_cast(value)); -} - -void ConfigWrapper::set_int(rust::Str key, int32_t value) { - config_.set(std::string(key), value); -} - -void ConfigWrapper::set_bool(rust::Str key, bool value) { - config_.set(std::string(key), value); -} - -void ConfigWrapper::set_double(rust::Str key, double value) { - config_.set(std::string(key), value); -} - -void ConfigWrapper::remove(rust::Str key) { - config_.remove(std::string(key)); -} - -std::unique_ptr create() { - return std::make_unique(); -} - -std::unique_ptr from_path(rust::Str path) { - auto p = eckit::PathName{std::string(path)}; - auto yaml = eckit::YAMLConfiguration{p}; - return std::make_unique(yaml); -} - -std::unique_ptr from_yaml(rust::Str yaml) { - auto str = std::string(yaml); - auto parsed = eckit::YAMLConfiguration{str}; - return std::make_unique(parsed); -} - -std::unique_ptr clone(const ConfigWrapper& src) { - return std::make_unique(src.inner()); -} - -// ==================== DataHandle ==================== - -int64_t DataHandleWrapper::open_for_read() { - return static_cast(handle_->openForRead()); -} - -void DataHandleWrapper::open_for_write(int64_t estimated_length) { - handle_->openForWrite(eckit::Length(estimated_length)); -} - -int64_t DataHandleWrapper::read(rust::Slice buf) { - return handle_->read(buf.data(), static_cast(buf.size())); -} - -int64_t DataHandleWrapper::write(rust::Slice buf) { - return handle_->write(buf.data(), static_cast(buf.size())); -} - -void DataHandleWrapper::close() { - handle_->close(); -} - -int64_t DataHandleWrapper::position() const { - return static_cast(handle_->position()); -} - -int64_t DataHandleWrapper::seek(int64_t offset) { - return static_cast(handle_->seek(eckit::Offset(offset))); -} - -bool DataHandleWrapper::can_seek() const { - return handle_->canSeek(); -} - -int64_t DataHandleWrapper::estimate() const { - return static_cast(handle_->estimate()); -} - -int64_t DataHandleWrapper::save_into(DataHandleWrapper& target) { - return static_cast(handle_->saveInto(*target.handle_)); -} - -std::unique_ptr DataHandleWrapper::from_file(rust::Str path) { - auto p = eckit::PathName{std::string(path)}; - return std::make_unique(p.fileHandle()); -} - -std::unique_ptr DataHandleWrapper::from_part(rust::Str path, int64_t offset, int64_t length) { - return std::make_unique( - new eckit::PartFileHandle(eckit::PathName{std::string(path)}, eckit::Offset(offset), eckit::Length(length))); -} - -std::unique_ptr DataHandleWrapper::from_buffer(rust::Slice data) { - return std::make_unique(new eckit::MemoryHandle(data.data(), data.size())); -} - -std::unique_ptr DataHandleWrapper::from_multi(rust::Slice paths) { - auto* mh = new eckit::MultiHandle(); - for (const auto& p : paths) { - (*mh) += eckit::PathName(std::string(p)).fileHandle(); - } - return std::make_unique(mh); -} - -std::unique_ptr DataHandleWrapper::tee(rust::Slice paths) { - std::vector handles; - handles.reserve(paths.size()); - for (const auto& p : paths) { - handles.push_back(eckit::PathName(std::string(p)).fileHandle()); - } - return std::make_unique(new eckit::TeeHandle(handles)); -} - -namespace { - -// `eckit::DataHandle` subclass that forwards `read()` / `seek()` to a Rust -// `Read + Seek` source held in a `rust::Box`. `openForRead` -// rewinds via `seek(0)` to match `eckit::FileHandle`'s `fopen("r")` -// semantics — eckit's archive pipeline relies on re-opening the source for -// the analyser and per-database passes. -class RustReaderHandle : public eckit::DataHandle { -public: - - explicit RustReaderHandle(rust::Box reader) : reader_(std::move(reader)) {} - - void print(std::ostream& s) const override { s << "RustReaderHandle[]"; } - - eckit::Length openForRead() override { - if (invoke_reader_seek(*reader_, 0) < 0) { - throw eckit::ReadError("RustReaderHandle: rewind failed on openForRead"); - } - return eckit::Length(0); - } - - long read(void* buffer, long length) override { - if (length <= 0) { - return 0; - } - auto* bytes = static_cast(buffer); - rust::Slice slice{bytes, static_cast(length)}; - int64_t n = invoke_reader_read(*reader_, slice); - if (n < 0) { - throw eckit::ReadError("RustReaderHandle: error reading from Rust source"); - } - return static_cast(n); - } - - bool canSeek() const override { return true; } - - eckit::Offset seek(const eckit::Offset& offset) override { - int64_t pos = invoke_reader_seek(*reader_, static_cast(offset)); - if (pos < 0) { - throw eckit::ReadError("RustReaderHandle: seek failed"); - } - return eckit::Offset(pos); - } - - void close() override {} - - eckit::Length estimate() override { return eckit::Length(0); } - - eckit::Length size() override { return eckit::Length(0); } - -private: - - rust::Box reader_; -}; - -} // namespace - -std::unique_ptr DataHandleWrapper::from_reader(rust::Box reader) { - return std::make_unique(new RustReaderHandle(std::move(reader))); -} - -// ==================== Message + Reader ==================== - -bool MessageWrapper::is_valid() const { - return static_cast(msg_); -} - -size_t MessageWrapper::length() const { - return msg_.length(); -} - -int64_t MessageWrapper::offset() const { - // eckit::Offset has operator long long(), so cast through that. - return static_cast(static_cast(msg_.offset())); -} - -rust::String MessageWrapper::get_string(rust::Str key) const { - return rust::String(msg_.getString(std::string(key))); -} - -int64_t MessageWrapper::get_long(rust::Str key) const { - return msg_.getLong(std::string(key)); -} - -double MessageWrapper::get_double(rust::Str key) const { - return msg_.getDouble(std::string(key)); -} - -rust::Slice MessageWrapper::data() const { - return rust::Slice(static_cast(msg_.data()), msg_.length()); -} - -void MessageWrapper::write_to(DataHandleWrapper& handle) const { - msg_.write(handle.inner()); -} - -std::unique_ptr MessageWrapper::clone() const { - return std::make_unique(msg_); -} - -ReaderWrapper::ReaderWrapper(DataHandleWrapper& handle) : - reader_(std::make_unique(handle.inner(), true)) {} - -std::unique_ptr ReaderWrapper::next() { - auto msg = reader_->next(); - return std::make_unique(std::move(msg)); -} - -std::unique_ptr new_reader(DataHandleWrapper& handle) { - return std::make_unique(handle); -} - -// ==================== Stream (base class) ==================== - -void StreamWrapper::write_char(uint8_t c) { - stream() << static_cast(c); -} -void StreamWrapper::write_bool(bool v) { - stream() << v; -} -void StreamWrapper::write_int(int32_t v) { - stream() << v; -} -void StreamWrapper::write_long(int64_t v) { - stream() << static_cast(v); -} -void StreamWrapper::write_unsigned_long(uint64_t v) { - stream() << static_cast(v); -} -void StreamWrapper::write_double(double v) { - stream() << v; -} -void StreamWrapper::write_string(rust::Str v) { - stream() << std::string(v.data(), v.size()); -} -void StreamWrapper::write_blob(rust::Slice data) { - stream().writeBlob(data.data(), data.size()); -} - -uint8_t StreamWrapper::read_char() { - char c{}; - stream() >> c; - return static_cast(c); -} -bool StreamWrapper::read_bool() { - bool v{}; - stream() >> v; - return v; -} -int32_t StreamWrapper::read_int() { - int v{}; - stream() >> v; - return v; -} -int64_t StreamWrapper::read_long() { - long long v{}; - stream() >> v; - return static_cast(v); -} -uint64_t StreamWrapper::read_unsigned_long() { - unsigned long long v{}; - stream() >> v; - return static_cast(v); -} -double StreamWrapper::read_double() { - double v{}; - stream() >> v; - return v; -} -rust::String StreamWrapper::read_string() { - std::string v; - stream() >> v; - return rust::String(v); -} - -int64_t StreamWrapper::read_bytes(rust::Slice buf) { - throw eckit::SeriousBug("read_bytes not supported on this stream type"); -} - -rust::Slice StreamWrapper::buffer() { - throw eckit::SeriousBug("buffer() not supported on this stream type"); -} - -int64_t StreamWrapper::bytes_written() const { - return const_cast(this)->stream().bytesWritten(); -} - -// ==================== TcpStreamWrapper ==================== - -TcpStreamWrapper::TcpStreamWrapper(const std::string& host, int port) : - socket_(eckit::net::TCPClient().connect(host, port)), tcp_(socket_) {} - -int64_t TcpStreamWrapper::read_bytes(rust::Slice buf) { - // The connection lives on `tcp_.socket()` — `socket_` was emptied by the - // TCPSocket "copy" ctor in `tcp_(socket_)` (ownership transfer). - return tcp_.socket().read(buf.data(), static_cast(buf.size())); -} - -std::unique_ptr TcpStreamWrapper::into_data_handle() { - // Steal the live connection from `tcp_` and wrap it as an owning - // `eckit::TCPSocketHandle` DataHandle. The TCPSocketHandle ctor copies - // (ownership transfer) the socket into its own member, so after this call - // both `socket_` and `tcp_.socket()` are detached (fd = -1) and only the - // returned DataHandle holds the connection. - return std::make_unique(new eckit::TCPSocketHandle(tcp_.socket())); -} - -std::unique_ptr StreamWrapper::into_data_handle() { - throw eckit::NotImplemented("StreamWrapper::into_data_handle is only supported on TCP streams", Here()); -} - -// ==================== MemoryWriteStreamWrapper ==================== - -MemoryWriteStreamWrapper::MemoryWriteStreamWrapper() : buf_(4096), mem_(buf_) {} - -rust::Slice MemoryWriteStreamWrapper::buffer() { - return {static_cast(buf_.data()), static_cast(mem_.bytesWritten())}; -} - -// ==================== MemoryReadStreamWrapper ==================== - -MemoryReadStreamWrapper::MemoryReadStreamWrapper(rust::Slice data) : - buf_(data.data(), data.size()), mem_(buf_) {} - -// ==================== Factory functions ==================== - -std::unique_ptr stream_connect(rust::Str host, int32_t port) { - return std::make_unique(std::string(host), port); -} - -std::unique_ptr stream_memory_write() { - return std::make_unique(); -} - -std::unique_ptr stream_memory_read(rust::Slice data) { - return std::make_unique(data); -} - -} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/log.cpp b/rust/crates/eckit-sys/cpp/log.cpp new file mode 100644 index 000000000..8dc8ca9b1 --- /dev/null +++ b/rust/crates/eckit-sys/cpp/log.cpp @@ -0,0 +1,79 @@ +// eckit log bridge — implementation. + +// trycatch handler — must come before the cxx-generated header so the +// generated wrappers' Result handling picks up our specialization. +#include "eckit_exceptions.h" + +#include "eckit-sys/src/lib.rs.h" // cxx-generated — provides LogLevel values +#include "log.h" + +#include "eckit/system/Library.h" +#include "eckit/system/LibraryManager.h" + +namespace eckit_bridge { + +void RustLogTarget::write(const char* start, const char* end) { + buffer_.append(start, end); + + std::string::size_type pos; + while ((pos = buffer_.find('\n')) != std::string::npos) { + std::string line = buffer_.substr(0, pos); + while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) { + line.pop_back(); + } + if (!line.empty()) { + rust_log(level_, rust::Str(target_.data(), target_.size()), rust::Str(line.data(), line.size())); + } + buffer_.erase(0, pos + 1); + } +} + +void RustLogTarget::flush() { + if (!buffer_.empty()) { + while (!buffer_.empty() && (buffer_.back() == '\r' || buffer_.back() == ' ')) { + buffer_.pop_back(); + } + if (!buffer_.empty()) { + rust_log(level_, rust::Str(target_.data(), target_.size()), rust::Str(buffer_.data(), buffer_.size())); + buffer_.clear(); + } + } +} + +RustMain::RustMain(int argc, char** argv) : Main(argc, argv) {} + +eckit::LogTarget* RustMain::createInfoLogTarget() const { + return new RustLogTarget(LogLevel::Info); +} +eckit::LogTarget* RustMain::createWarningLogTarget() const { + return new RustLogTarget(LogLevel::Warn); +} +eckit::LogTarget* RustMain::createErrorLogTarget() const { + return new RustLogTarget(LogLevel::Error); +} +eckit::LogTarget* RustMain::createDebugLogTarget() const { + return new RustLogTarget(LogLevel::Debug); +} +eckit::LogTarget* RustMain::createMetricsLogTarget() const { + return new RustLogTarget(LogLevel::Trace); +} + +/// Install a per-library `RustLogTarget` on every registered library's debug +/// channel. Each library's debug output is then tagged with its own name as +/// the tracing/log target. Idempotent — `Channel::setTarget` replaces. +static void install_per_library_targets() { + for (const auto& libname : eckit::system::LibraryManager::list()) { + const auto& lib = eckit::system::LibraryManager::lookup(libname); + lib.debugChannel().setTarget(new RustLogTarget(LogLevel::Debug, libname)); + } +} + +void init() { + if (!eckit::Main::ready()) { + static const char* argv[] = {"eckit-rs", nullptr}; + [[maybe_unused]] static auto* main_inst_ = new RustMain(1, const_cast(argv)); + install_per_library_targets(); + } +} + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/message.cpp b/rust/crates/eckit-sys/cpp/message.cpp new file mode 100644 index 000000000..7fd997a61 --- /dev/null +++ b/rust/crates/eckit-sys/cpp/message.cpp @@ -0,0 +1,59 @@ +// eckit Message + Reader bridge — implementation. + +#include "eckit_exceptions.h" + +#include "eckit-sys/src/lib.rs.h" +#include "message.h" + +namespace eckit_bridge { + +bool MessageWrapper::is_valid() const { + return static_cast(msg_); +} + +size_t MessageWrapper::length() const { + return msg_.length(); +} + +int64_t MessageWrapper::offset() const { + // eckit::Offset has operator long long(), so cast through that. + return static_cast(static_cast(msg_.offset())); +} + +rust::String MessageWrapper::get_string(rust::Str key) const { + return rust::String(msg_.getString(std::string(key))); +} + +int64_t MessageWrapper::get_long(rust::Str key) const { + return msg_.getLong(std::string(key)); +} + +double MessageWrapper::get_double(rust::Str key) const { + return msg_.getDouble(std::string(key)); +} + +rust::Slice MessageWrapper::data() const { + return rust::Slice(static_cast(msg_.data()), msg_.length()); +} + +void MessageWrapper::write_to(DataHandleWrapper& handle) const { + msg_.write(handle.inner()); +} + +std::unique_ptr MessageWrapper::clone() const { + return std::make_unique(msg_); +} + +ReaderWrapper::ReaderWrapper(DataHandleWrapper& handle) : + reader_(std::make_unique(handle.inner(), true)) {} + +std::unique_ptr ReaderWrapper::next() { + auto msg = reader_->next(); + return std::make_unique(std::move(msg)); +} + +std::unique_ptr new_reader(DataHandleWrapper& handle) { + return std::make_unique(handle); +} + +} // namespace eckit_bridge diff --git a/rust/crates/eckit-sys/cpp/stream.cpp b/rust/crates/eckit-sys/cpp/stream.cpp new file mode 100644 index 000000000..03a1c1375 --- /dev/null +++ b/rust/crates/eckit-sys/cpp/stream.cpp @@ -0,0 +1,140 @@ +// eckit Stream bridge — implementation. + +#include "eckit_exceptions.h" + +#include "eckit-sys/src/lib.rs.h" +#include "stream.h" + +#include "eckit/exception/Exceptions.h" +#include "eckit/io/TCPSocketHandle.h" +#include "eckit/net/TCPClient.h" + +namespace eckit_bridge { + +// ==================== Stream (base class) ==================== + +void StreamWrapper::write_char(uint8_t c) { + stream() << static_cast(c); +} +void StreamWrapper::write_bool(bool v) { + stream() << v; +} +void StreamWrapper::write_int(int32_t v) { + stream() << v; +} +void StreamWrapper::write_long(int64_t v) { + stream() << static_cast(v); +} +void StreamWrapper::write_unsigned_long(uint64_t v) { + stream() << static_cast(v); +} +void StreamWrapper::write_double(double v) { + stream() << v; +} +void StreamWrapper::write_string(rust::Str v) { + stream() << std::string(v.data(), v.size()); +} +void StreamWrapper::write_blob(rust::Slice data) { + stream().writeBlob(data.data(), data.size()); +} + +uint8_t StreamWrapper::read_char() { + char c{}; + stream() >> c; + return static_cast(c); +} +bool StreamWrapper::read_bool() { + bool v{}; + stream() >> v; + return v; +} +int32_t StreamWrapper::read_int() { + int v{}; + stream() >> v; + return v; +} +int64_t StreamWrapper::read_long() { + long long v{}; + stream() >> v; + return static_cast(v); +} +uint64_t StreamWrapper::read_unsigned_long() { + unsigned long long v{}; + stream() >> v; + return static_cast(v); +} +double StreamWrapper::read_double() { + double v{}; + stream() >> v; + return v; +} +rust::String StreamWrapper::read_string() { + std::string v; + stream() >> v; + return rust::String(v); +} + +int64_t StreamWrapper::read_bytes(rust::Slice buf) { + throw eckit::SeriousBug("read_bytes not supported on this stream type"); +} + +rust::Slice StreamWrapper::buffer() { + throw eckit::SeriousBug("buffer() not supported on this stream type"); +} + +int64_t StreamWrapper::bytes_written() const { + return const_cast(this)->stream().bytesWritten(); +} + +std::unique_ptr StreamWrapper::into_data_handle() { + throw eckit::NotImplemented("StreamWrapper::into_data_handle is only supported on TCP streams", Here()); +} + +// ==================== TcpStreamWrapper ==================== + +TcpStreamWrapper::TcpStreamWrapper(const std::string& host, int port) : + socket_(eckit::net::TCPClient().connect(host, port)), tcp_(socket_) {} + +int64_t TcpStreamWrapper::read_bytes(rust::Slice buf) { + // The connection lives on `tcp_.socket()` — `socket_` was emptied by the + // TCPSocket "copy" ctor in `tcp_(socket_)` (ownership transfer). + return tcp_.socket().read(buf.data(), static_cast(buf.size())); +} + +std::unique_ptr TcpStreamWrapper::into_data_handle() { + // Steal the live connection from `tcp_` and wrap it as an owning + // `eckit::TCPSocketHandle` DataHandle. The TCPSocketHandle ctor copies + // (ownership transfer) the socket into its own member, so after this call + // both `socket_` and `tcp_.socket()` are detached (fd = -1) and only the + // returned DataHandle holds the connection. + return std::make_unique(new eckit::TCPSocketHandle(tcp_.socket())); +} + +// ==================== MemoryWriteStreamWrapper ==================== + +MemoryWriteStreamWrapper::MemoryWriteStreamWrapper() : buf_(4096), mem_(buf_) {} + +rust::Slice MemoryWriteStreamWrapper::buffer() { + return {static_cast(buf_.data()), static_cast(mem_.bytesWritten())}; +} + +// ==================== MemoryReadStreamWrapper ==================== + +MemoryReadStreamWrapper::MemoryReadStreamWrapper(rust::Slice data) : + buf_(data.data(), data.size()), mem_(buf_) {} + +// ==================== Factory functions ==================== + +std::unique_ptr stream_connect(rust::Str host, int32_t port) { + return std::make_unique(std::string(host), port); +} + +std::unique_ptr stream_memory_write() { + return std::make_unique(); +} + +std::unique_ptr stream_memory_read(rust::Slice data) { + return std::make_unique(data); +} + +} // namespace eckit_bridge From 51584a5abd39682eb849cc2747ead8690e7571d0 Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Fri, 12 Jun 2026 17:22:26 +0200 Subject: [PATCH 19/22] Pin bindman revision --- rust/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 77d515889..f1ef87ab2 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -16,9 +16,9 @@ categories = ["science"] eckit-sys = { path = "crates/eckit-sys" } # Build tools -bindman = { git = "ssh://git@github.com/ecmwf/bindman.git" } -bindman-build = { git = "ssh://git@github.com/ecmwf/bindman.git" } -bindman-utils = { git = "ssh://git@github.com/ecmwf/bindman.git" } +bindman = { git = "ssh://git@github.com/ecmwf/bindman.git", rev = "47edf68" } +bindman-build = { git = "ssh://git@github.com/ecmwf/bindman.git", rev = "47edf68" } +bindman-utils = { git = "ssh://git@github.com/ecmwf/bindman.git", rev = "47edf68" } # External cxx = "1.0" From fc545447f6d5e60f5a0cd18f390a804753e87dba Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Mon, 15 Jun 2026 15:50:55 +0200 Subject: [PATCH 20/22] Include application name in RustLogTarget constructors --- rust/crates/eckit-sys/cpp/log.cpp | 17 ++++++++++------- rust/crates/eckit-sys/cpp/log.h | 7 ++----- rust/crates/eckit-sys/src/lib.rs | 22 ++++++---------------- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/rust/crates/eckit-sys/cpp/log.cpp b/rust/crates/eckit-sys/cpp/log.cpp index 8dc8ca9b1..073f4223e 100644 --- a/rust/crates/eckit-sys/cpp/log.cpp +++ b/rust/crates/eckit-sys/cpp/log.cpp @@ -10,6 +10,8 @@ #include "eckit/system/Library.h" #include "eckit/system/LibraryManager.h" +#include // getprogname + namespace eckit_bridge { void RustLogTarget::write(const char* start, const char* end) { @@ -40,22 +42,22 @@ void RustLogTarget::flush() { } } -RustMain::RustMain(int argc, char** argv) : Main(argc, argv) {} +RustMain::RustMain(int argc, char** argv) : Main(argc, argv), app_name_(displayName()) {} eckit::LogTarget* RustMain::createInfoLogTarget() const { - return new RustLogTarget(LogLevel::Info); + return new RustLogTarget(LogLevel::Info, app_name_); } eckit::LogTarget* RustMain::createWarningLogTarget() const { - return new RustLogTarget(LogLevel::Warn); + return new RustLogTarget(LogLevel::Warn, app_name_); } eckit::LogTarget* RustMain::createErrorLogTarget() const { - return new RustLogTarget(LogLevel::Error); + return new RustLogTarget(LogLevel::Error, app_name_); } eckit::LogTarget* RustMain::createDebugLogTarget() const { - return new RustLogTarget(LogLevel::Debug); + return new RustLogTarget(LogLevel::Debug, app_name_); } eckit::LogTarget* RustMain::createMetricsLogTarget() const { - return new RustLogTarget(LogLevel::Trace); + return new RustLogTarget(LogLevel::Trace, app_name_); } /// Install a per-library `RustLogTarget` on every registered library's debug @@ -70,7 +72,8 @@ static void install_per_library_targets() { void init() { if (!eckit::Main::ready()) { - static const char* argv[] = {"eckit-rs", nullptr}; + const char* progname = getprogname(); + static const char* argv[] = {progname ? progname : "eckit-rs", nullptr}; [[maybe_unused]] static auto* main_inst_ = new RustMain(1, const_cast(argv)); install_per_library_targets(); } diff --git a/rust/crates/eckit-sys/cpp/log.h b/rust/crates/eckit-sys/cpp/log.h index 056bfb70c..704dfbc7b 100644 --- a/rust/crates/eckit-sys/cpp/log.h +++ b/rust/crates/eckit-sys/cpp/log.h @@ -23,11 +23,6 @@ void rust_log(LogLevel level, rust::Str target, rust::Str msg) noexcept; class RustLogTarget : public eckit::LogTarget { public: - /// No target — the Rust side falls back to the `log` crate's default - /// (`module_path!()` of the bridge, i.e. `eckit_sys`). - explicit RustLogTarget(LogLevel level) : level_(level) {} - - /// Tagged target (e.g. a registered library name). RustLogTarget(LogLevel level, std::string target) : level_(level), target_(std::move(target)) {} void write(const char* start, const char* end) override; @@ -43,6 +38,8 @@ class RustLogTarget : public eckit::LogTarget { /// Main subclass that installs `RustLogTarget` on all channels. /// Every new thread automatically gets `RustLogTarget` via the factory methods. class RustMain : public eckit::Main { + std::string app_name_; + public: RustMain(int argc, char** argv); diff --git a/rust/crates/eckit-sys/src/lib.rs b/rust/crates/eckit-sys/src/lib.rs index 07a0b7f9e..2b62efeb9 100644 --- a/rust/crates/eckit-sys/src/lib.rs +++ b/rust/crates/eckit-sys/src/lib.rs @@ -296,22 +296,12 @@ fn invoke_reader_seek(reader: &mut ReaderBox, offset: i64) -> i64 { /// Called from C++ `RustLogTarget::write()` — routes to Rust `log` crate. fn rust_log(level: ffi::LogLevel, target: &str, msg: &str) { - if target.is_empty() { - match level { - ffi::LogLevel::Error => log::error!("{msg}"), - ffi::LogLevel::Warn => log::warn!("{msg}"), - ffi::LogLevel::Info => log::info!("{msg}"), - ffi::LogLevel::Debug => log::debug!("{msg}"), - _ => log::trace!("{msg}"), - } - } else { - match level { - ffi::LogLevel::Error => log::error!(target: target, "{msg}"), - ffi::LogLevel::Warn => log::warn!(target: target, "{msg}"), - ffi::LogLevel::Info => log::info!(target: target, "{msg}"), - ffi::LogLevel::Debug => log::debug!(target: target, "{msg}"), - _ => log::trace!(target: target, "{msg}"), - } + match level { + ffi::LogLevel::Error => log::error!(target: target, "{msg}"), + ffi::LogLevel::Warn => log::warn!(target: target, "{msg}"), + ffi::LogLevel::Info => log::info!(target: target, "{msg}"), + ffi::LogLevel::Debug => log::debug!(target: target, "{msg}"), + _ => log::trace!(target: target, "{msg}"), } } From 4b7fd27b62a9497eff45f5dc88f59e28b560140b Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Mon, 15 Jun 2026 15:51:19 +0200 Subject: [PATCH 21/22] Fix formatting --- rust/crates/eckit-sys/cpp/log.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/crates/eckit-sys/cpp/log.cpp b/rust/crates/eckit-sys/cpp/log.cpp index 073f4223e..b81034239 100644 --- a/rust/crates/eckit-sys/cpp/log.cpp +++ b/rust/crates/eckit-sys/cpp/log.cpp @@ -10,7 +10,7 @@ #include "eckit/system/Library.h" #include "eckit/system/LibraryManager.h" -#include // getprogname +#include // getprogname namespace eckit_bridge { From 6428f2783298412233378975ad33209a86000c4c Mon Sep 17 00:00:00 2001 From: Vlad Pankratov Date: Mon, 15 Jun 2026 16:01:14 +0200 Subject: [PATCH 22/22] Add platform-specific program name retrieval in log.cpp --- rust/crates/eckit-sys/cpp/log.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rust/crates/eckit-sys/cpp/log.cpp b/rust/crates/eckit-sys/cpp/log.cpp index b81034239..2761c7fad 100644 --- a/rust/crates/eckit-sys/cpp/log.cpp +++ b/rust/crates/eckit-sys/cpp/log.cpp @@ -10,10 +10,24 @@ #include "eckit/system/Library.h" #include "eckit/system/LibraryManager.h" +#ifdef __APPLE__ #include // getprogname +#elif defined(__linux__) +extern "C" char* program_invocation_short_name; +#endif namespace eckit_bridge { +static const char* progname() { +#ifdef __APPLE__ + return getprogname(); +#elif defined(__linux__) + return program_invocation_short_name; +#else + return nullptr; +#endif +} + void RustLogTarget::write(const char* start, const char* end) { buffer_.append(start, end); @@ -72,8 +86,8 @@ static void install_per_library_targets() { void init() { if (!eckit::Main::ready()) { - const char* progname = getprogname(); - static const char* argv[] = {progname ? progname : "eckit-rs", nullptr}; + const char* name = progname(); + static const char* argv[] = {name ? name : "eckit-rs", nullptr}; [[maybe_unused]] static auto* main_inst_ = new RustMain(1, const_cast(argv)); install_per_library_targets(); }