Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ on:
types: [labeled]

jobs:
# Run Rust CI (fmt + clippy + test) on the rust/ workspace
ci-rust:
name: ci-rust
if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }}
uses: ecmwf/reusable-workflows/.github/workflows/ci-rust.yml@main
with:
manifest-path: rust/Cargo.toml
features: --features vendored
run-doc: false
secrets:
private_repos_token: ${{ secrets.GH_REPO_READ_TOKEN }}

# Run CI including downstream packages on self-hosted runners
downstream-ci:
name: downstream-ci
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ Testing/*
tests/core/Testing/*
build
docs/_build

# Rust
rust/target/
rust/Cargo.lock
15 changes: 15 additions & 0 deletions rust/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[workspace]
resolver = "2"
members = ["crates/odc", "crates/odc-sys"]

[workspace.package]
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/ecmwf/odc"
rust-version = "1.90"
readme = "README.md"
keywords = ["ecmwf", "weather", "meteorology", "odb"]
categories = ["science"]

[workspace.dependencies]
# Internal
odc = { path = "crates/odc", default-features = false }
odc-sys = { path = "crates/odc-sys", default-features = false }

# Foundation crates
eckit = { git = "ssh://git@github.com/ecmwf/rust-wrappers-playground.git", default-features = false }
eckit-sys = { git = "ssh://git@github.com/ecmwf/eckit.git", branch = "rust-bindings", default-features = false }

# Build tools
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"
cxx-build = "1.0"
parking_lot = "0.12"
polars = { version = "0.54", default-features = false, features = ["fmt"] }
thiserror = "2"
32 changes: 32 additions & 0 deletions rust/crates/odc-sys/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "odc-sys"
version = "1.6.3"
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 odc (ODB-2 encoder/decoder) library using cxx"
links = "odc_sys"
build = "build.rs"

[features]
default = ["vendored"]

# Build strategy (mutually exclusive)
vendored = ["eckit-sys/vendored"]
system = ["eckit-sys/system"]

[dependencies]
cxx.workspace = true
eckit-sys = { workspace = true, default-features = false, features = ["eckit-sql"] }
bindman.workspace = true

[build-dependencies]
cxx-build.workspace = true
bindman-utils.workspace = true
bindman-build.workspace = true

[package.metadata.docs.rs]
18 changes: 18 additions & 0 deletions rust/crates/odc-sys/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# odc-sys

Low-level Rust bindings to ECMWF's [odc](https://github.com/ecmwf/odc) (ODB-2 encoder/decoder) C++ library.

This crate provides raw FFI bindings using [cxx](https://cxx.rs/). For a safe, ergonomic API, use the higher-level `odc` crate (planned).

## Features

### Build strategy (mutually exclusive)

- `vendored` - Build odc and its dependencies (eckit) from source.
- `system` - Link against system-installed odc.

`vendored` is enabled by default.

## License

Apache-2.0
203 changes: 203 additions & 0 deletions rust/crates/odc-sys/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use std::env;
use std::path::{Path, PathBuf};

const ODC_VERSION: &str = "1.6.3";

fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-changed=cpp/OdcBridge.h");
println!("cargo:rerun-if-changed=cpp/DecoderWrapper.h");
println!("cargo:rerun-if-changed=cpp/DecoderWrapper.cc");
println!("cargo:rerun-if-changed=cpp/EncoderWrapper.h");
println!("cargo:rerun-if-changed=cpp/EncoderWrapper.cc");
println!("cargo:rerun-if-changed=cpp/FrameWrapper.h");
println!("cargo:rerun-if-changed=cpp/FrameWrapper.cc");
println!("cargo:rerun-if-changed=cpp/ReaderWrapper.h");
println!("cargo:rerun-if-changed=cpp/ReaderWrapper.cc");
println!("cargo:rerun-if-changed=cpp/SettingsWrapper.h");
println!("cargo:rerun-if-changed=cpp/SettingsWrapper.cc");
println!("cargo:rerun-if-env-changed=ODC_DIR");
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"));

if cfg!(feature = "system") {
build_system();
} else {
build_vendored();
}
}

/// Generate `odc_exceptions.{h,rs}` covering odc's own subclasses
/// (`ODBDecodeError` + its subclasses, via recursive walk in `odc/core/Exceptions.h`)
/// plus eckit's exceptions inherited from eckit-sys.
fn generate_exceptions(include: &Path) {
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));

let own = vec![bindman_build::ExceptionSource {
header: include.join("odc/core/Exceptions.h"),
include_path: "odc/core/Exceptions.h".to_string(),
cpp_namespace: "odc::core".to_string(),
message_prefix: "odc".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: "odc",
out_dir: &out_dir,
own: &own,
inherited: &inherited,
});

bindman_build::publish_exception_sources(&own, &out_dir);
}

#[cfg(feature = "system")]
fn build_system() {
let crate_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));

let eckit_include = env::var("DEP_ECKIT_SYS_INCLUDE").expect("DEP_ECKIT_SYS_INCLUDE not set");
let eckit_cpp_dir = env::var("DEP_ECKIT_SYS_CPP_DIR").expect("DEP_ECKIT_SYS_CPP_DIR not set");

let (root, odc_include, lib_dir) = bindman_utils::cmake_find_package("odc", ODC_VERSION);

generate_exceptions(&odc_include);

println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=dylib=odccore");

cxx_build::bridge("src/lib.rs")
.file(crate_dir.join("cpp/DecoderWrapper.cc"))
.file(crate_dir.join("cpp/EncoderWrapper.cc"))
.file(crate_dir.join("cpp/FrameWrapper.cc"))
.file(crate_dir.join("cpp/ReaderWrapper.cc"))
.file(crate_dir.join("cpp/SettingsWrapper.cc"))
.include(&odc_include)
.include(&eckit_include)
.include(&eckit_cpp_dir)
.include(crate_dir.join("cpp"))
.include(&out_dir) // for odc_exceptions.h (generated)
.flag_if_supported("-std=c++17")
.compile("odc_sys_bridge");

bindman_utils::link_cpp_stdlib();

println!("cargo:root={}", root.display());
println!("cargo:include={}", odc_include.display());

bindman_build::check_cpp_api(&odc_include, &crate_dir.join("src/lib.rs"));
}

#[cfg(not(feature = "system"))]
fn build_system() {
unreachable!("build_system called without system feature");
}

#[cfg(feature = "vendored")]
fn build_vendored() {
use std::fs;
use std::process::Command;

const ECBUILD_REPO: &str = "https://github.com/ecmwf/ecbuild.git";
const ECBUILD_TAG: &str = "3.13.1";
const ODC_REPO: &str = "https://github.com/ecmwf/odc.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");

let eckit_root = env::var("DEP_ECKIT_SYS_ROOT").expect("DEP_ECKIT_SYS_ROOT not set");
let eckit_cpp_dir = env::var("DEP_ECKIT_SYS_CPP_DIR").expect("DEP_ECKIT_SYS_CPP_DIR not set");

let ecbuild_src = bindman_utils::git_clone(ECBUILD_REPO, ECBUILD_TAG, &src_dir.join("ecbuild"));
let odc_src = bindman_utils::git_clone(ODC_REPO, ODC_VERSION, &src_dir.join("odc"));

let ecbuild_bin = ecbuild_src.join("bin/ecbuild");
let num_jobs = bindman_utils::build_parallelism();

let cmake_prefix_path = eckit_root.clone();

let mut cmd = Command::new(&ecbuild_bin);
cmd.current_dir(&build_dir)
.arg(format!("--prefix={}", install_dir.display()))
.arg("--")
.arg(&odc_src)
.arg(format!("-DCMAKE_PREFIX_PATH={cmake_prefix_path}"))
.arg(format!(
"-DCMAKE_BUILD_TYPE={}",
bindman_utils::cmake_build_type()
))
.arg("-DENABLE_TESTS=OFF")
.arg("-DBUILD_TESTING=OFF")
.arg("-DENABLE_DOCS=OFF")
.arg("-DENABLE_FORTRAN=OFF")
.arg("-DENABLE_PYTHON=OFF");

#[cfg(target_os = "macos")]
cmd.arg("-DCMAKE_INSTALL_NAME_DIR=@rpath");

bindman_utils::run_command(&mut cmd, "ecbuild configure odc");

bindman_utils::run_command(
Command::new("cmake")
.args(["--build", ".", "--parallel", &num_jobs])
.current_dir(&build_dir),
"cmake build odc",
);

bindman_utils::run_command(
Command::new("cmake")
.args(["--install", "."])
.current_dir(&build_dir),
"cmake install odc",
);

let include_dir = install_dir.join("include");
let crate_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
let lib_dir = bindman_utils::resolve_lib_dir(&install_dir);

generate_exceptions(&include_dir);

cxx_build::bridge("src/lib.rs")
.file(crate_dir.join("cpp/DecoderWrapper.cc"))
.file(crate_dir.join("cpp/EncoderWrapper.cc"))
.file(crate_dir.join("cpp/FrameWrapper.cc"))
.file(crate_dir.join("cpp/ReaderWrapper.cc"))
.file(crate_dir.join("cpp/SettingsWrapper.cc"))
.include(&include_dir)
.include(format!("{eckit_root}/include"))
.include(&eckit_cpp_dir)
.include(crate_dir.join("cpp"))
.include(&out_dir) // for odc_exceptions.h (generated)
.flag_if_supported("-std=c++17")
.compile("odc_sys_bridge");

println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=dylib=odccore");
bindman_utils::link_cpp_stdlib();

println!("cargo:root={}", install_dir.display());
println!("cargo:include={}", include_dir.display());

bindman_build::check_cpp_api(&include_dir, &crate_dir.join("src/lib.rs"));
}

#[cfg(not(feature = "vendored"))]
fn build_vendored() {
unreachable!("build_vendored called without vendored feature");
}
25 changes: 25 additions & 0 deletions rust/crates/odc-sys/cpp/DecoderWrapper.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// odc Decoder bridge — implementation.

#include "DecoderWrapper.h"
#include "odc-sys/src/lib.rs.h"

#include <string>

namespace odc_bridge {

std::unique_ptr<DecoderWrapper> DecoderWrapper::create() {
return std::make_unique<DecoderWrapper>();
}

void DecoderWrapper::add_column(rust::Str name, uint8_t* data, size_t nrows, size_t elem_size, size_t stride) {
names_.emplace_back(std::string(name));
facades_.emplace_back(reinterpret_cast<char*>(data), nrows, elem_size, stride);
}

size_t DecoderWrapper::decode(const FrameWrapper& frame, size_t nthreads) {
odc::api::Decoder decoder(names_, facades_);
decoder.decode(frame.frame_, nthreads == 0 ? 1 : nthreads);
return frame.frame_.rowCount();
}

} // namespace odc_bridge
36 changes: 36 additions & 0 deletions rust/crates/odc-sys/cpp/DecoderWrapper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// odc Decoder bridge — wraps `odc::api::Decoder`.
#pragma once

#include "FrameWrapper.h"

#include "odc/api/Odb.h"

#include "rust/cxx.h"

#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>

namespace odc_bridge {

/// Accumulates per-column decode targets (StridedData facades over
/// Rust-owned buffers), then runs `odc::api::Decoder::decode`.
class DecoderWrapper {
std::vector<std::string> names_;
std::vector<odc::api::StridedData> facades_;

public:

static std::unique_ptr<DecoderWrapper> create();

/// `data` must point to caller-owned, 8-byte-aligned memory of at least
/// nrows * stride bytes, valid until decode() returns.
void add_column(rust::Str name, uint8_t* data, size_t nrows, size_t elem_size, size_t stride);

/// Decode the frame into the registered buffers; returns rows decoded.
size_t decode(const FrameWrapper& frame, size_t nthreads);
};

} // namespace odc_bridge
Loading
Loading