diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..b4069aba --- /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 00000000..48efac09 --- /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 f92e3433..7425f3bd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,8 @@ doc/latex *.idx .mypy_cache build/ -Testing/ \ No newline at end of file +Testing/ + +# Rust +rust/target/ +rust/Cargo.lock \ No newline at end of file diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..8e0ab843 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,30 @@ +[workspace] +resolver = "2" +members = ["crates/metkit-sys"] + +[workspace.package] +edition = "2024" +license = "Apache-2.0" +repository = "https://github.com/ecmwf/metkit" +rust-version = "1.90" +readme = "README.md" +keywords = ["ecmwf", "weather", "meteorology", "grib"] +categories = ["science"] + +[workspace.dependencies] +# Internal +metkit-sys = { path = "crates/metkit-sys" } + +# Foundation crates +eckit-sys = { git = "ssh://git@github.com/ecmwf/eckit.git", branch = "rust-bindings", default-features = false } +eccodes-sys = { git = "ssh://git@github.com/ecmwf/eccodes.git", branch = "rust-bindings", default-features = false } + +# 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" +thiserror = "2" diff --git a/rust/crates/metkit-sys/Cargo.toml b/rust/crates/metkit-sys/Cargo.toml new file mode 100644 index 00000000..8fac5563 --- /dev/null +++ b/rust/crates/metkit-sys/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "metkit-sys" +version = "1.18.1" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +description = "C++ bindings to ECMWF metkit library using cxx" +links = "metkit_sys" +build = "build.rs" + +[features] +# Defaults: core features +default = ["vendored", "grib", "bufr", "mars2grib", "metkit-config"] + +# Build strategy (mutually exclusive) +vendored = ["eckit-sys/vendored", "eccodes-sys/vendored", "eccodes-sys/eccodes-threads"] +system = ["eckit-sys/system", "eccodes-sys/system"] + +# Format support (CMake default: ON) +grib = ["eccodes-sys/product-grib"] # GRIB format support (requires eccodes) +bufr = ["eccodes-sys/product-bufr"] # BUFR format support (requires eccodes) +netcdf = [] # NetCDF data support (requires NetCDF library) +odb = [] # ODB data support (requires odc) + +# Encoding (CMake default: ON) +mars2grib = ["eckit-sys/geo-codec-grids"] # MARS2GRIB encoder (requires eckit geo codec grids for ORCA/FESOM/ICON) + +# Configuration (CMake default: ON) +metkit-config = [] # Install metkit configuration files + +# Other (CMake default: OFF) +experimental = [] # Experimental features +fail-on-ccsds = [] # Fail on CCSDS + +[dependencies] +cxx.workspace = true +eckit-sys = { workspace = true, default-features = false } +eccodes-sys = { workspace = true, default-features = false } +bindman.workspace = true + +[build-dependencies] +cxx-build.workspace = true +bindman-utils.workspace = true +bindman-build.workspace = true + +[package.metadata.docs.rs] diff --git a/rust/crates/metkit-sys/README.md b/rust/crates/metkit-sys/README.md new file mode 100644 index 00000000..79e1ff24 --- /dev/null +++ b/rust/crates/metkit-sys/README.md @@ -0,0 +1,41 @@ +# metkit-sys + +Low-level Rust bindings to ECMWF's [metkit](https://github.com/ecmwf/metkit) C++ library. + +This crate provides raw FFI bindings using [cxx](https://cxx.rs/). For a safe, ergonomic API, use the higher-level `metkit` crate (planned). + +## Features + +### Build strategy (mutually exclusive) + +- `vendored` - Build metkit and its dependencies (eckit, ecCodes) from source. +- `system` - Link against system-installed metkit. + +`vendored` is enabled by default. + +### Format support (enabled by default) + +- `grib` - GRIB format support. Pulls in `eccodes-sys/product-grib`. +- `bufr` - BUFR format support. Pulls in `eccodes-sys/product-bufr`. + +### Format support (off by default; require external libraries) + +- `netcdf` - NetCDF data support (requires NetCDF library). +- `odb` - ODB data support (requires odc). + +### Encoding (enabled by default) + +- `mars2grib` - MARS2GRIB encoder. Pulls in `eckit-sys/geo-codec-grids` for ORCA/FESOM/ICON grid support. + +### Configuration (enabled by default) + +- `metkit-config` - Install metkit configuration files (e.g. `language.yaml`). + +### Other (off by default) + +- `experimental` - Experimental upstream features. +- `fail-on-ccsds` - Fail when encountering CCSDS-encoded messages. + +## License + +Apache-2.0 diff --git a/rust/crates/metkit-sys/build.rs b/rust/crates/metkit-sys/build.rs new file mode 100644 index 00000000..d2815eeb --- /dev/null +++ b/rust/crates/metkit-sys/build.rs @@ -0,0 +1,252 @@ +//! Build script for metkit-sys +//! +//! Supports two build modes: +//! - `vendored` (default): Clone and build metkit from source using ecbuild +//! - `system`: Use `CMake` `find_package` to find system-installed metkit + +const METKIT_VERSION: &str = "1.18.1"; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-env-changed=METKIT_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 cpp directory for downstream crates (metkit_bridge.h) + println!("cargo:cpp_dir={}", crate_dir.join("cpp").display()); +} + +/// Generate `metkit_exceptions.{h,rs}`. The bridge exposes `CodesHandleWrapper` +/// methods that can throw `metkit::codes::CodesException` / `CodesWrongLength`; +/// nothing in the bridge currently calls `mars2grib`, so its exception headers +/// aren't a source here. eckit's exceptions are inherited from eckit-sys. +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 own = vec![bindman_build::ExceptionSource { + header: include.join("metkit/codes/api/CodesTypes.h"), + include_path: "metkit/codes/api/CodesTypes.h".to_string(), + cpp_namespace: "metkit::codes".to_string(), + message_prefix: "metkit".to_string(), + base_class: "eckit::Exception".to_string(), + recursive: true, + }]; + + let inherited = bindman_build::collect_dep_exception_sources(); + + bindman_build::generate_exception_bridge(&bindman_build::ExceptionBridgeConfig { + primary_namespace: "metkit", + out_dir: &out_dir, + own: &own, + inherited: &inherited, + }); + + bindman_build::publish_exception_sources(&own, &out_dir); +} + +/// 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")); + let eckit_include = std::env::var("DEP_ECKIT_SYS_INCLUDE") + .expect("DEP_ECKIT_SYS_INCLUDE not set — eckit-sys must be a dependency"); + + println!("cargo:rerun-if-changed=cpp/metkit_bridge.h"); + println!("cargo:rerun-if-changed=cpp/metkit_bridge.cpp"); + + let mut build = cxx_build::bridge("src/lib.rs"); + build + .file(crate_dir.join("cpp/metkit_bridge.cpp")) + .include(include) + .include(crate_dir.join("cpp")) + .include(&eckit_include) + .include(&out_dir); // for metkit_exceptions.h (generated) + + // Include eckit's cpp dir for eckit_bridge.h (needed for StreamWrapper) + if let Ok(eckit_cpp_dir) = std::env::var("DEP_ECKIT_SYS_CPP_DIR") { + build.include(&eckit_cpp_dir); + } + + build + .flag_if_supported("-std=c++17") + .compile("metkit_sys_bridge"); + + bindman_utils::link_cpp_stdlib(); +} + +#[cfg(feature = "system")] +fn build_system() -> std::path::PathBuf { + let (root, include, lib_dir) = bindman_utils::cmake_find_package("metkit", METKIT_VERSION); + + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + println!("cargo:rustc-link-lib=dylib=metkit"); + 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 metkit 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 METKIT_REPO: &str = "https://github.com/ecmwf/metkit.git"; + + 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"); + + // Get dependency paths + let eckit_root = env::var("DEP_ECKIT_SYS_ROOT") + .expect("DEP_ECKIT_SYS_ROOT not set - eckit-sys must be a dependency"); + let eccodes_root = env::var("DEP_ECCODES_SYS_ROOT") + .expect("DEP_ECCODES_SYS_ROOT not set - eccodes-sys must be a dependency"); + + // Clone sources + let ecbuild_src = bindman_utils::git_clone(ECBUILD_REPO, ECBUILD_TAG, &src_dir.join("ecbuild")); + let metkit_src = bindman_utils::git_clone(METKIT_REPO, METKIT_VERSION, &src_dir.join("metkit")); + + let ecbuild_bin = ecbuild_src.join("bin/ecbuild"); + let num_jobs = bindman_utils::build_parallelism(); + + let cmake_prefix_path = format!("{eckit_root};{eccodes_root}"); + + // Build metkit + let mut cmd = Command::new(&ecbuild_bin); + cmd.current_dir(&build_dir) + .arg(format!("--prefix={}", install_dir.display())) + .arg("--") + .arg(&metkit_src) + .arg(format!("-DCMAKE_PREFIX_PATH={cmake_prefix_path}")) + .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("-DENABLE_MARS2GRIB_PYTHON=OFF"); + + // Feature-gated options + cmd.arg(format!( + "-DENABLE_GRIB={}", + bindman_utils::on_off(cfg!(feature = "grib")) + )); + cmd.arg(format!( + "-DENABLE_BUFR={}", + bindman_utils::on_off(cfg!(feature = "bufr")) + )); + cmd.arg(format!( + "-DENABLE_NETCDF={}", + bindman_utils::on_off(cfg!(feature = "netcdf")) + )); + cmd.arg(format!( + "-DENABLE_ODB={}", + bindman_utils::on_off(cfg!(feature = "odb")) + )); + cmd.arg(format!( + "-DENABLE_MARS2GRIB={}", + bindman_utils::on_off(cfg!(feature = "mars2grib")) + )); + cmd.arg(format!( + "-DENABLE_METKIT_CONFIG={}", + bindman_utils::on_off(cfg!(feature = "metkit-config")) + )); + cmd.arg(format!( + "-DENABLE_EXPERIMENTAL={}", + bindman_utils::on_off(cfg!(feature = "experimental")) + )); + cmd.arg(format!( + "-DENABLE_FAIL_ON_CCSDS={}", + bindman_utils::on_off(cfg!(feature = "fail-on-ccsds")) + )); + + // 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"); + + bindman_utils::run_command(&mut cmd, "ecbuild configure metkit"); + + bindman_utils::run_command( + Command::new("cmake") + .args(["--build", ".", "--parallel", &num_jobs]) + .current_dir(&build_dir), + "cmake build metkit", + ); + + bindman_utils::run_command( + Command::new("cmake") + .args(["--install", "."]) + .current_dir(&build_dir), + "cmake install metkit", + ); + + // Copy share directory (contains language.yaml needed at runtime) + let share_src = build_dir.join("share"); + let share_dst = install_dir.join("share"); + if share_src.exists() { + bindman_utils::copy_dir_all(&share_src, &share_dst) + .expect("Failed to copy share directory"); + } + + // 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=metkit"); + 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/metkit-sys/cpp/metkit_bridge.cpp b/rust/crates/metkit-sys/cpp/metkit_bridge.cpp new file mode 100644 index 00000000..a1c7e49a --- /dev/null +++ b/rust/crates/metkit-sys/cpp/metkit_bridge.cpp @@ -0,0 +1,351 @@ +// metkit C++ bridge implementation +#include "metkit_bridge.h" +#include "eckit/log/JSON.h" + +#include + +namespace metkit_bridge { + +// ==================== MarsRequest ==================== + +rust::String MarsRequestWrapper::verb() const { + return rust::String(request_.verb()); +} + +bool MarsRequestWrapper::has(rust::Str key) const { + return request_.has(std::string(key)); +} + +rust::Vec MarsRequestWrapper::values(rust::Str key) const { + const auto& vals = request_.values(std::string(key)); + rust::Vec result; + result.reserve(vals.size()); + for (const auto& v : vals) { + result.push_back(rust::String(v)); + } + return result; +} + +rust::String MarsRequestWrapper::get_first(rust::Str key) const { + return rust::String(request_[std::string(key)]); +} + +bool MarsRequestWrapper::empty() const { + return request_.empty(); +} + +size_t MarsRequestWrapper::count() const { + return request_.count(); +} + +bool MarsRequestWrapper::matches(const MarsRequestWrapper& filter) const { + return request_.matches(filter.request_); +} + +rust::Vec MarsRequestWrapper::params() const { + auto p = request_.params(); + rust::Vec result; + result.reserve(p.size()); + for (const auto& name : p) { + result.push_back(rust::String(name)); + } + return result; +} + +void MarsRequestWrapper::set_verb(rust::Str verb) { + request_.verb(std::string(verb)); +} + +void MarsRequestWrapper::set_value_string(rust::Str key, rust::Str value) { + request_.setValue(std::string(key), std::string(value)); +} + +void MarsRequestWrapper::set_values(rust::Str key, rust::Vec values) { + std::vector vec; + vec.reserve(values.size()); + for (const auto& v : values) { + vec.emplace_back(std::string(v)); + } + request_.values(std::string(key), vec); +} + +void MarsRequestWrapper::set_value_long(rust::Str key, int64_t value) { + request_.setValue(std::string(key), static_cast(value)); +} + +void MarsRequestWrapper::unset_values(rust::Str key) { + request_.unsetValues(std::string(key)); +} + +std::unique_ptr MarsRequestWrapper::extract(rust::Str category) const { + return std::make_unique(request_.extract(std::string(category))); +} + +std::unique_ptr MarsRequestWrapper::expand(bool inherit, bool strict) const { + metkit::mars::MarsLanguage lang(request_.verb()); + auto expanded = lang.expand(request_, inherit, strict); + return std::make_unique(std::move(expanded)); +} + +rust::String MarsRequestWrapper::to_json() const { + std::ostringstream oss; + eckit::JSON json(oss); + json << request_; + return rust::String(oss.str()); +} + +rust::String MarsRequestWrapper::dump() const { + std::ostringstream oss; + request_.dump(oss); + return rust::String(oss.str()); +} + +void MarsRequestWrapper::encode(eckit_bridge::StreamWrapper& stream) const { + stream.inner() << request_; +} + +// ==================== CodesHandle ==================== + +bool CodesHandleWrapper::is_defined(rust::Str key) const { + return handle_->isDefined(std::string(key)); +} + +bool CodesHandleWrapper::is_missing(rust::Str key) const { + return handle_->isMissing(std::string(key)); +} + +bool CodesHandleWrapper::has(rust::Str key) const { + return handle_->has(std::string(key)); +} + +rust::String CodesHandleWrapper::get_string(rust::Str key) const { + return rust::String(handle_->getString(std::string(key))); +} + +int64_t CodesHandleWrapper::get_long(rust::Str key) const { + return static_cast(handle_->getLong(std::string(key))); +} + +double CodesHandleWrapper::get_double(rust::Str key) const { + return handle_->getDouble(std::string(key)); +} + +rust::Vec CodesHandleWrapper::get_double_array(rust::Str key) const { + auto vec = handle_->getDoubleArray(std::string(key)); + rust::Vec result; + result.reserve(vec.size()); + for (double v : vec) { + result.push_back(v); + } + return result; +} + +rust::Vec CodesHandleWrapper::get_long_array(rust::Str key) const { + auto vec = handle_->getLongArray(std::string(key)); + rust::Vec result; + result.reserve(vec.size()); + for (long v : vec) { + result.push_back(static_cast(v)); + } + return result; +} + +void CodesHandleWrapper::set_string(rust::Str key, rust::Str value) { + handle_->set(std::string(key), std::string(value)); +} + +void CodesHandleWrapper::set_long(rust::Str key, int64_t value) { + handle_->set(std::string(key), static_cast(value)); +} + +void CodesHandleWrapper::set_double(rust::Str key, double value) { + handle_->set(std::string(key), value); +} + +void CodesHandleWrapper::set_double_array(rust::Str key, rust::Slice values) { + handle_->set(std::string(key), metkit::codes::Span(values.data(), values.size())); +} + +void CodesHandleWrapper::set_missing(rust::Str key) { + handle_->setMissing(std::string(key)); +} + +size_t CodesHandleWrapper::value_count(rust::Str key) const { + return handle_->size(std::string(key)); +} + +size_t CodesHandleWrapper::message_size() const { + return handle_->messageSize(); +} + +rust::Slice CodesHandleWrapper::message_data() const { + auto span = handle_->messageData(); + return {span.data(), span.size()}; +} + +std::unique_ptr CodesHandleWrapper::clone() const { + return std::make_unique(handle_->clone()); +} + +std::unique_ptr codes_handle_from_message(rust::Slice data) { + return std::make_unique( + metkit::codes::codesHandleFromMessageCopy(metkit::codes::Span(data.data(), data.size()))); +} + +std::unique_ptr codes_handle_from_file(rust::Str path) { + return std::make_unique( + metkit::codes::codesHandleFromFile(std::string(path), metkit::codes::Product::GRIB)); +} + +std::unique_ptr codes_handle_from_file_at_offset(rust::Str path, int64_t offset) { + return std::make_unique( + metkit::codes::codesHandleFromFile(std::string(path), metkit::codes::Product::GRIB, offset)); +} + +std::unique_ptr codes_handle_from_sample(rust::Str sample) { + return std::make_unique(metkit::codes::codesHandleFromSample(std::string(sample))); +} + +// ==================== HyperCube ==================== + +HyperCubeWrapper::HyperCubeWrapper(const MarsRequestWrapper& request) : + cube_(std::make_unique(request.inner())) {} + +size_t HyperCubeWrapper::size() const { + return cube_->size(); +} + +size_t HyperCubeWrapper::count() const { + return cube_->count(); +} + +size_t HyperCubeWrapper::count_vacant() const { + return cube_->countVacant(); +} + +bool HyperCubeWrapper::contains(const MarsRequestWrapper& request) const { + return cube_->contains(request.inner()); +} + +bool HyperCubeWrapper::clear(const MarsRequestWrapper& request) { + return cube_->clear(request.inner()); +} + +size_t HyperCubeWrapper::field_ordinal(const MarsRequestWrapper& request) const { + return cube_->fieldOrdinal(request.inner()); +} + +std::unique_ptr hypercube_create(const MarsRequestWrapper& request) { + return std::make_unique(request); +} + +// ==================== MarsRequest factory ==================== + +std::unique_ptr request_create(rust::Str verb) { + return std::make_unique(metkit::mars::MarsRequest(std::string(verb))); +} + +std::unique_ptr request_from_message(const eckit_bridge::MessageWrapper& msg) { + return std::make_unique(metkit::mars::MarsRequest(msg.inner())); +} + +std::unique_ptr request_decode(eckit_bridge::StreamWrapper& stream) { + return std::make_unique(metkit::mars::MarsRequest(stream.inner())); +} + +std::unique_ptr mars_request_handle(const MarsRequestWrapper& request, + const eckit_bridge::ConfigWrapper& config) { + return std::make_unique( + new metkit::mars::MarsRequestHandle(request.inner(), config.inner())); +} + +ParsedRequestsWrapper::ParsedRequestsWrapper(rust::Str input, bool strict) { + auto str = std::string(input); + std::istringstream iss(str); + requests_ = metkit::mars::MarsRequest::parse(iss, strict); +} + +size_t ParsedRequestsWrapper::count() const { + return requests_.size(); +} + +std::unique_ptr ParsedRequestsWrapper::at(size_t index) const { + return std::make_unique(requests_.at(index)); +} + +std::unique_ptr parse_requests(rust::Str input, bool strict) { + return std::make_unique(input, strict); +} + +std::unique_ptr parse_requests_raw(rust::Str input) { + auto str = std::string(input); + std::istringstream iss(str); + auto parsed = metkit::mars::MarsParser(iss).parse(); + auto wrapper = std::make_unique(); + for (auto& r : parsed) { + wrapper->push(r); + } + return wrapper; +} + +// ==================== RequestEnvironment ==================== + +void request_environment_init(rust::Vec keys, rust::Vec values) { + std::map env; + for (size_t i = 0; i < keys.size() && i < values.size(); ++i) { + env[std::string(keys[i])] = std::string(values[i]); + } + metkit::mars::RequestEnvironment::initialize(env); +} + +std::unique_ptr request_environment_request() { + const auto& env = metkit::mars::RequestEnvironment::instance(); + return std::make_unique(env.request()); +} + +// ==================== MarsLanguage ==================== + +MarsLanguageWrapper::MarsLanguageWrapper(rust::Str verb) : + lang_(std::make_unique(std::string(verb))) {} + +rust::Vec MarsLanguageWrapper::sink_keywords() const { + const auto& kw = lang_->sinkKeywords(); + rust::Vec result; + result.reserve(kw.size()); + for (const auto& k : kw) { + result.push_back(rust::String(k)); + } + return result; +} + +bool MarsLanguageWrapper::is_data(rust::Str keyword) const { + return lang_->isData(std::string(keyword)); +} + +std::unique_ptr language_create(rust::Str verb) { + return std::make_unique(verb); +} + +// ==================== ParamID / WindFamily ==================== + +size_t wind_family_count() { + return metkit::ParamID::getWindFamilies().size(); +} + +rust::String wind_family_u(size_t index) { + return rust::String(std::string(metkit::ParamID::getWindFamilies().at(index).u_)); +} + +rust::String wind_family_v(size_t index) { + return rust::String(std::string(metkit::ParamID::getWindFamilies().at(index).v_)); +} + +rust::String wind_family_vo(size_t index) { + return rust::String(std::string(metkit::ParamID::getWindFamilies().at(index).vo_)); +} + +rust::String wind_family_d(size_t index) { + return rust::String(std::string(metkit::ParamID::getWindFamilies().at(index).d_)); +} + +} // namespace metkit_bridge diff --git a/rust/crates/metkit-sys/cpp/metkit_bridge.h b/rust/crates/metkit-sys/cpp/metkit_bridge.h new file mode 100644 index 00000000..f6d63a8f --- /dev/null +++ b/rust/crates/metkit-sys/cpp/metkit_bridge.h @@ -0,0 +1,206 @@ +// metkit C++ bridge for Rust FFI +#pragma once + +// metkit's auto-generated exception handler (includes eckit's catches too) +#include "metkit_exceptions.h" + +// eckit-sys bridge — provides StreamWrapper +#include "eckit_bridge.h" + +#include "metkit/codes/api/CodesAPI.h" +#include "metkit/hypercube/HyperCube.h" +#include "metkit/mars/MarsLanguage.h" +#include "metkit/mars/MarsParser.h" +#include "metkit/mars/MarsRequest.h" +#include "metkit/mars/MarsRequestHandle.h" +#include "metkit/mars/ParamID.h" +#include "metkit/mars/RequestEnvironment.h" + +#include "rust/cxx.h" + +#include +#include +#include + +namespace metkit_bridge { + +// ==================== MarsRequest ==================== + +/// Wraps `metkit::mars::MarsRequest` for Rust FFI. +class MarsRequestWrapper { + metkit::mars::MarsRequest request_; + +public: + + MarsRequestWrapper() = default; + explicit MarsRequestWrapper(metkit::mars::MarsRequest r) : request_(std::move(r)) {} + + // Query + rust::String verb() const; + bool has(rust::Str key) const; + rust::Vec values(rust::Str key) const; + rust::String get_first(rust::Str key) const; + bool empty() const; + size_t count() const; + bool matches(const MarsRequestWrapper& filter) const; + rust::Vec params() const; + + // Mutation + void set_verb(rust::Str verb); + void set_value_string(rust::Str key, rust::Str value); + void set_values(rust::Str key, rust::Vec values); + void set_value_long(rust::Str key, int64_t value); + void unset_values(rust::Str key); + + // Extract parameters by category + std::unique_ptr extract(rust::Str category) const; + + // Expansion + std::unique_ptr expand(bool inherit, bool strict) const; + + // Stream serialization + void encode(eckit_bridge::StreamWrapper& stream) const; + + // Output + rust::String to_json() const; + rust::String dump() const; + + // Access underlying + const metkit::mars::MarsRequest& inner() const { return request_; } + metkit::mars::MarsRequest& inner() { return request_; } +}; + +// ==================== CodesHandle ==================== + +/// Wraps `metkit::codes::CodesHandle` for Rust FFI. +class CodesHandleWrapper { + std::unique_ptr handle_; + +public: + + explicit CodesHandleWrapper(std::unique_ptr h) : handle_(std::move(h)) {} + + // Query + bool is_defined(rust::Str key) const; + bool is_missing(rust::Str key) const; + bool has(rust::Str key) const; + + // Scalar get + rust::String get_string(rust::Str key) const; + int64_t get_long(rust::Str key) const; + double get_double(rust::Str key) const; + + // Array get + rust::Vec get_double_array(rust::Str key) const; + rust::Vec get_long_array(rust::Str key) const; + + // Scalar set + void set_string(rust::Str key, rust::Str value); + void set_long(rust::Str key, int64_t value); + void set_double(rust::Str key, double value); + + // Array set + void set_double_array(rust::Str key, rust::Slice values); + + // Missing + void set_missing(rust::Str key); + + // Size and data + size_t value_count(rust::Str key) const; + size_t message_size() const; + rust::Slice message_data() const; + + // Clone + std::unique_ptr clone() const; + + // Access underlying + const metkit::codes::CodesHandle& inner() const { return *handle_; } + metkit::codes::CodesHandle& inner() { return *handle_; } +}; + +// CodesHandle factory functions +std::unique_ptr codes_handle_from_message(rust::Slice data); +std::unique_ptr codes_handle_from_file(rust::Str path); +std::unique_ptr codes_handle_from_file_at_offset(rust::Str path, int64_t offset); +std::unique_ptr codes_handle_from_sample(rust::Str sample); + +// ==================== HyperCube ==================== + +/// Wraps `metkit::hypercube::HyperCube` for Rust FFI. +class HyperCubeWrapper { + std::unique_ptr cube_; + +public: + + explicit HyperCubeWrapper(const MarsRequestWrapper& request); + + size_t size() const; + size_t count() const; + size_t count_vacant() const; + bool contains(const MarsRequestWrapper& request) const; + bool clear(const MarsRequestWrapper& request); + size_t field_ordinal(const MarsRequestWrapper& request) const; + + // Access underlying + const metkit::hypercube::HyperCube& inner() const { return *cube_; } +}; + +std::unique_ptr hypercube_create(const MarsRequestWrapper& request); + +// ==================== MarsRequest factory ==================== + +std::unique_ptr request_create(rust::Str verb); +std::unique_ptr request_from_message(const eckit_bridge::MessageWrapper& msg); +std::unique_ptr request_decode(eckit_bridge::StreamWrapper& stream); + +/// Create a `MarsRequestHandle` — DataHandle for Hermes retrieve/list/get. +std::unique_ptr mars_request_handle(const MarsRequestWrapper& request, + const eckit_bridge::ConfigWrapper& config); + +/// Holds parsed requests — parse once, iterate by index. +class ParsedRequestsWrapper { + std::vector requests_; + +public: + + ParsedRequestsWrapper() = default; + ParsedRequestsWrapper(rust::Str input, bool strict); + void push(const metkit::mars::MarsRequest& r) { requests_.push_back(r); } + size_t count() const; + std::unique_ptr at(size_t index) const; +}; + +std::unique_ptr parse_requests(rust::Str input, bool strict); + +/// Raw parse without verb validation — uses MarsParser directly. +std::unique_ptr parse_requests_raw(rust::Str input); + +// ==================== RequestEnvironment ==================== + +void request_environment_init(rust::Vec keys, rust::Vec values); +std::unique_ptr request_environment_request(); + +// ==================== MarsLanguage ==================== + +class MarsLanguageWrapper { + std::unique_ptr lang_; + +public: + + explicit MarsLanguageWrapper(rust::Str verb); + + rust::Vec sink_keywords() const; + bool is_data(rust::Str keyword) const; +}; + +std::unique_ptr language_create(rust::Str verb); + +// ==================== ParamID / WindFamily ==================== + +size_t wind_family_count(); +rust::String wind_family_u(size_t index); +rust::String wind_family_v(size_t index); +rust::String wind_family_vo(size_t index); +rust::String wind_family_d(size_t index); + +} // namespace metkit_bridge diff --git a/rust/crates/metkit-sys/src/lib.rs b/rust/crates/metkit-sys/src/lib.rs new file mode 100644 index 00000000..4064224d --- /dev/null +++ b/rust/crates/metkit-sys/src/lib.rs @@ -0,0 +1,190 @@ +//! FFI bindings to ECMWF metkit C++ library. +//! +//! Provides cxx bridge to: +//! - `MarsRequest` — MARS request parsing, access, modification +//! - `RequestEnvironment` — environment metadata +//! - `MarsLanguage` — keyword categorization + +use bindman::track_cpp_api; + +// Auto-generated metkit Error enum + From impl +include!(concat!(env!("OUT_DIR"), "/metkit_exceptions.rs")); + +#[track_cpp_api( + ("metkit/mars/MarsRequest.h", class = "MarsRequest"), + ("metkit/mars/MarsLanguage.h", class = "MarsLanguage"), +)] +#[cxx::bridge(namespace = "metkit_bridge")] +pub mod ffi { + unsafe extern "C++" { + include!("metkit_bridge.h"); + + // ==================== MarsRequest ==================== + + type MarsRequestWrapper; + + fn verb(self: &MarsRequestWrapper) -> String; + fn has(self: &MarsRequestWrapper, key: &str) -> bool; + fn values(self: &MarsRequestWrapper, key: &str) -> Result>; + fn get_first(self: &MarsRequestWrapper, key: &str) -> Result; + fn empty(self: &MarsRequestWrapper) -> bool; + fn count(self: &MarsRequestWrapper) -> usize; + fn matches(self: &MarsRequestWrapper, filter: &MarsRequestWrapper) -> bool; + fn params(self: &MarsRequestWrapper) -> Vec; + + // Mutation + fn set_verb(self: Pin<&mut MarsRequestWrapper>, verb: &str); + fn set_value_string(self: Pin<&mut MarsRequestWrapper>, key: &str, value: &str); + fn set_values(self: Pin<&mut MarsRequestWrapper>, key: &str, values: Vec); + fn set_value_long(self: Pin<&mut MarsRequestWrapper>, key: &str, value: i64); + fn unset_values(self: Pin<&mut MarsRequestWrapper>, key: &str); + + // Extract parameters by category (e.g. "postproc") + fn extract( + self: &MarsRequestWrapper, + category: &str, + ) -> Result>; + + // Expansion + fn expand( + self: &MarsRequestWrapper, + inherit: bool, + strict: bool, + ) -> Result>; + + // Cross-crate ExternTypes from eckit-sys + #[namespace = "eckit_bridge"] + type StreamWrapper = eckit_sys::StreamWrapper; + + #[namespace = "eckit_bridge"] + type DataHandleWrapper = eckit_sys::DataHandleWrapper; + + #[namespace = "eckit_bridge"] + type ConfigWrapper = eckit_sys::ConfigWrapper; + + #[namespace = "eckit_bridge"] + type MessageWrapper = eckit_sys::MessageWrapper; + + fn encode(self: &MarsRequestWrapper, stream: Pin<&mut StreamWrapper>) -> Result<()>; + fn request_decode(stream: Pin<&mut StreamWrapper>) + -> Result>; + + /// Create a `MarsRequestHandle` — DataHandle for Hermes protocol. + fn mars_request_handle( + request: &MarsRequestWrapper, + config: &ConfigWrapper, + ) -> Result>; + + // Output + fn to_json(self: &MarsRequestWrapper) -> Result; + fn dump(self: &MarsRequestWrapper) -> String; + + // ==================== CodesHandle ==================== + + type CodesHandleWrapper; + + // Query + fn is_defined(self: &CodesHandleWrapper, key: &str) -> Result; + fn is_missing(self: &CodesHandleWrapper, key: &str) -> Result; + fn has(self: &CodesHandleWrapper, key: &str) -> Result; + + // Scalar get + fn get_string(self: &CodesHandleWrapper, key: &str) -> Result; + fn get_long(self: &CodesHandleWrapper, key: &str) -> Result; + fn get_double(self: &CodesHandleWrapper, key: &str) -> Result; + + // Array get + fn get_double_array(self: &CodesHandleWrapper, key: &str) -> Result>; + fn get_long_array(self: &CodesHandleWrapper, key: &str) -> Result>; + + // Scalar set + fn set_string(self: Pin<&mut CodesHandleWrapper>, key: &str, value: &str) -> Result<()>; + fn set_long(self: Pin<&mut CodesHandleWrapper>, key: &str, value: i64) -> Result<()>; + fn set_double(self: Pin<&mut CodesHandleWrapper>, key: &str, value: f64) -> Result<()>; + + // Array set + fn set_double_array( + self: Pin<&mut CodesHandleWrapper>, + key: &str, + values: &[f64], + ) -> Result<()>; + + // Missing + fn set_missing(self: Pin<&mut CodesHandleWrapper>, key: &str) -> Result<()>; + + // Size and data + fn value_count(self: &CodesHandleWrapper, key: &str) -> Result; + fn message_size(self: &CodesHandleWrapper) -> Result; + fn message_data(self: &CodesHandleWrapper) -> Result<&[u8]>; + + // Clone + #[rust_name = "clone_handle"] + fn clone(self: &CodesHandleWrapper) -> Result>; + + // Factory + fn codes_handle_from_message(data: &[u8]) -> Result>; + fn codes_handle_from_file(path: &str) -> Result>; + fn codes_handle_from_file_at_offset( + path: &str, + offset: i64, + ) -> Result>; + fn codes_handle_from_sample(sample: &str) -> Result>; + + // ==================== HyperCube ==================== + + type HyperCubeWrapper; + + fn size(self: &HyperCubeWrapper) -> usize; + fn count(self: &HyperCubeWrapper) -> usize; + fn count_vacant(self: &HyperCubeWrapper) -> usize; + fn contains(self: &HyperCubeWrapper, request: &MarsRequestWrapper) -> Result; + fn clear(self: Pin<&mut HyperCubeWrapper>, request: &MarsRequestWrapper) -> Result; + fn field_ordinal(self: &HyperCubeWrapper, request: &MarsRequestWrapper) -> Result; + + fn hypercube_create(request: &MarsRequestWrapper) -> Result>; + + // ==================== MarsRequest factory ==================== + + #[must_use] + fn request_create(verb: &str) -> UniquePtr; + + /// Create a `MarsRequest` from a GRIB message (reads metadata keys). + fn request_from_message(msg: &MessageWrapper) -> Result>; + + // Parsed requests — parse once, iterate by index + type ParsedRequestsWrapper; + fn count(self: &ParsedRequestsWrapper) -> usize; + fn at(self: &ParsedRequestsWrapper, index: usize) -> Result>; + fn parse_requests(input: &str, strict: bool) -> Result>; + + /// Raw parse without verb validation — uses `MarsParser` directly. + fn parse_requests_raw(input: &str) -> Result>; + + // ==================== RequestEnvironment ==================== + + fn request_environment_init(keys: Vec, values: Vec); + fn request_environment_request() -> Result>; + + // ==================== MarsLanguage ==================== + + type MarsLanguageWrapper; + + fn sink_keywords(self: &MarsLanguageWrapper) -> Vec; + fn is_data(self: &MarsLanguageWrapper, keyword: &str) -> bool; + + fn language_create(verb: &str) -> Result>; + + // ==================== ParamID / WindFamily ==================== + + #[must_use] + fn wind_family_count() -> usize; + fn wind_family_u(index: usize) -> Result; + fn wind_family_v(index: usize) -> Result; + fn wind_family_vo(index: usize) -> Result; + fn wind_family_d(index: usize) -> Result; + } +} + +// Public re-exports +pub use cxx::{Exception, UniquePtr}; +pub use ffi::*;