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 1fed26d0..6701a760 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ Testing/* tests/core/Testing/* build docs/_build + +# Rust +rust/target/ +rust/Cargo.lock diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..e1f62f4c --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,29 @@ +[workspace] +resolver = "2" +members = ["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-sys = { path = "crates/odc-sys" } + +# Foundation crates +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", 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/odc-sys/Cargo.toml b/rust/crates/odc-sys/Cargo.toml new file mode 100644 index 00000000..ae57f622 --- /dev/null +++ b/rust/crates/odc-sys/Cargo.toml @@ -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] diff --git a/rust/crates/odc-sys/README.md b/rust/crates/odc-sys/README.md new file mode 100644 index 00000000..bd256af4 --- /dev/null +++ b/rust/crates/odc-sys/README.md @@ -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 diff --git a/rust/crates/odc-sys/build.rs b/rust/crates/odc-sys/build.rs new file mode 100644 index 00000000..50643f0e --- /dev/null +++ b/rust/crates/odc-sys/build.rs @@ -0,0 +1,186 @@ +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/odc_bridge.h"); + println!("cargo:rerun-if-changed=cpp/odc_bridge.cpp"); + 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/odc_bridge.cpp")) + .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/odc_bridge.cpp")) + .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"); +} diff --git a/rust/crates/odc-sys/cpp/odc_bridge.cpp b/rust/crates/odc-sys/cpp/odc_bridge.cpp new file mode 100644 index 00000000..80e22943 --- /dev/null +++ b/rust/crates/odc-sys/cpp/odc_bridge.cpp @@ -0,0 +1,144 @@ +// odc C++ bridge implementation +#include "odc_bridge.h" + +#include "eckit/io/MemoryHandle.h" + +#include + +namespace odc_bridge { + +// ==================== SelectIteratorWrapper ==================== + +SelectIteratorWrapper::SelectIteratorWrapper(odc::Select& select) : current_(select.begin()), end_(select.end()) {} + +SelectIteratorWrapper::SelectIteratorWrapper(odc::Select::iterator current, odc::Select::iterator end) : + current_(std::move(current)), end_(std::move(end)) {} + +bool SelectIteratorWrapper::valid() { + return current_ != end_; +} + +void SelectIteratorWrapper::advance() { + ++current_; +} + +size_t SelectIteratorWrapper::column_count() const { + return current_->columns().size(); +} + +rust::String SelectIteratorWrapper::column_name(size_t idx) const { + return rust::String(current_->columns()[idx]->name()); +} + +ColumnType SelectIteratorWrapper::column_type(size_t idx) const { + return current_->columns()[idx]->type(); +} + +double SelectIteratorWrapper::data(size_t idx) const { + return current_->data(idx); +} + +rust::String SelectIteratorWrapper::data_string(size_t idx) { + return rust::String(current_->string(idx)); +} + +int64_t SelectIteratorWrapper::data_integer(size_t idx) { + return current_->integer(idx); +} + +// ==================== SelectWrapper ==================== + +SelectWrapper::SelectWrapper(rust::Str sql, eckit_bridge::DataHandleWrapper& handle) : + select_(std::make_unique(std::string(sql), handle.inner())) {} + +std::unique_ptr SelectWrapper::begin() { + return std::make_unique(*select_); +} + +std::unique_ptr SelectWrapper::createSelectIterator(rust::Str sql) { + auto* it = select_->createSelectIterator(std::string(sql)); + it->next(); + return std::make_unique(odc::Select::iterator(it), select_->end()); +} + +rust::String SelectWrapper::database_name() { + return rust::String(select_->database().name()); +} + +std::unique_ptr select_create(rust::Str sql, eckit_bridge::DataHandleWrapper& handle) { + return std::make_unique(sql, handle); +} + +// ==================== WriteIteratorWrapper ==================== + +WriteIteratorWrapper::WriteIteratorWrapper(odc::Writer<>::iterator iter) : iter_(std::move(iter)) {} + +void WriteIteratorWrapper::set_column(size_t index, rust::Str name, ColumnType col_type) { + iter_->setColumn(index, std::string(name), col_type); +} + +void WriteIteratorWrapper::set_number_of_columns(size_t n) { + iter_->setNumberOfColumns(n); +} + +void WriteIteratorWrapper::set_data(size_t index, double value) { + iter_->data(index) = value; +} + +void WriteIteratorWrapper::set_data_string(size_t index, rust::Str value) { + size_t maxlen = sizeof(double) * iter_->columns()[index]->dataSizeDoubles(); + ::strncpy(reinterpret_cast(&iter_->data(index)), std::string(value).c_str(), maxlen); +} + +void WriteIteratorWrapper::set_data_integer(size_t index, int64_t value) { + iter_->data(index) = static_cast(value); +} + +void WriteIteratorWrapper::set_missing_value(size_t index, double value) { + iter_->missingValue(index, value); +} + +void WriteIteratorWrapper::write_row() { + ++iter_; +} + +void WriteIteratorWrapper::close() { + iter_->close(); +} + +// ==================== WriterWrapper ==================== + +WriterWrapper::WriterWrapper(eckit_bridge::DataHandleWrapper& handle) : + writer_(std::make_unique>(handle.inner())), outit_(writer_->begin()) {} + +std::unique_ptr WriterWrapper::begin() { + return std::make_unique(writer_->begin()); +} + +void WriterWrapper::pass1(SelectWrapper& select) { + auto it = select.select_->begin(); + auto end = select.select_->end(); + outit_->pass1(it, end); +} + +size_t WriterWrapper::rows_buffer_size() const { + return writer_->rowsBufferSize(); +} + +void WriterWrapper::set_rows_buffer_size(size_t n) { + writer_->rowsBufferSize(n); +} + +// Note: data_handle() not exposed — Writer owns the DataHandle internally +// and it's the same one passed to the constructor. Access it via the +// original DataHandleWrapper on the Rust side. + +rust::String WriterWrapper::path() const { + return rust::String(writer_->path()); +} + +std::unique_ptr writer_create(eckit_bridge::DataHandleWrapper& handle) { + return std::make_unique(handle); +} + +} // namespace odc_bridge diff --git a/rust/crates/odc-sys/cpp/odc_bridge.h b/rust/crates/odc-sys/cpp/odc_bridge.h new file mode 100644 index 00000000..56bf45a7 --- /dev/null +++ b/rust/crates/odc-sys/cpp/odc_bridge.h @@ -0,0 +1,122 @@ +// odc C++ bridge for Rust FFI +#pragma once + +#include "eckit_bridge.h" +#include "odc_exceptions.h" + +#include "odc/Select.h" +#include "odc/Writer.h" +#include "odc/api/ColumnType.h" + +#include "rust/cxx.h" + +#include +#include +#include + +namespace odc_bridge { + +// ColumnType is odc::api::ColumnType — cxx static_asserts values match. +using odc::api::ColumnType; + +// ==================== SelectIteratorWrapper ==================== + +/// Wraps `odc::Select::iterator` pair for row-by-row iteration. +class SelectIteratorWrapper { + odc::Select::iterator current_; + odc::Select::iterator end_; + +public: + + SelectIteratorWrapper(odc::Select& select); + SelectIteratorWrapper(odc::Select::iterator current, odc::Select::iterator end); + + bool valid(); + void advance(); + + size_t column_count() const; + rust::String column_name(size_t idx) const; + ColumnType column_type(size_t idx) const; + double data(size_t idx) const; + rust::String data_string(size_t idx); + int64_t data_integer(size_t idx); +}; + +// ==================== SelectWrapper ==================== + +/// Wraps `odc::Select` — owns the query, produces iterators. +class SelectWrapper { + std::unique_ptr select_; + + friend class WriterWrapper; + +public: + + SelectWrapper(rust::Str sql, eckit_bridge::DataHandleWrapper& handle); + + std::unique_ptr begin(); + std::unique_ptr createSelectIterator(rust::Str sql); + rust::String database_name(); +}; + +std::unique_ptr select_create(rust::Str sql, eckit_bridge::DataHandleWrapper& handle); + +// ==================== WriteIteratorWrapper ==================== + +/// Wraps `odc::Writer<>::iterator` for row-by-row writing. +class WriteIteratorWrapper { + odc::Writer<>::iterator iter_; + +public: + + explicit WriteIteratorWrapper(odc::Writer<>::iterator iter); + + /// Define a column. Must be called before writing any rows. + void set_column(size_t index, rust::Str name, ColumnType col_type); + + /// Set number of columns. + void set_number_of_columns(size_t n); + + /// Set a double value at column index for the current row. + void set_data(size_t index, double value); + + /// Set a string value at column index for the current row. + void set_data_string(size_t index, rust::Str value); + + /// Set an integer value at column index for the current row. + void set_data_integer(size_t index, int64_t value); + + /// Set the missing value for a column. + void set_missing_value(size_t index, double value); + + /// Write the current row (advances the iterator). + void write_row(); + + /// Close the writer. + void close(); +}; + +// ==================== WriterWrapper ==================== + +/// Wraps `odc::Writer<>` — writes filtered ODB data via pass1. +class WriterWrapper { + std::unique_ptr> writer_; + odc::Writer<>::iterator outit_; + +public: + + explicit WriterWrapper(eckit_bridge::DataHandleWrapper& handle); + + void pass1(SelectWrapper& select); + + /// Get a write iterator for row-by-row writing. + std::unique_ptr begin(); + + size_t rows_buffer_size() const; + void set_rows_buffer_size(size_t n); + rust::String path() const; +}; + +std::unique_ptr writer_create(eckit_bridge::DataHandleWrapper& handle); + +} // namespace odc_bridge diff --git a/rust/crates/odc-sys/src/lib.rs b/rust/crates/odc-sys/src/lib.rs new file mode 100644 index 00000000..54bb116a --- /dev/null +++ b/rust/crates/odc-sys/src/lib.rs @@ -0,0 +1,133 @@ +//! FFI bindings to ECMWF odc (ODB-2 encoder/decoder) library. + +use bindman::track_cpp_api; + +// Auto-generated odc Error enum + From impl +include!(concat!(env!("OUT_DIR"), "/odc_exceptions.rs")); + +#[track_cpp_api( + ("odc/Select.h", class = "Select"), + ("odc/Writer.h", class = "Writer"), + ignore = ["end", "dataHandle"] +)] +#[cxx::bridge(namespace = "odc_bridge")] +pub mod ffi { + /// ODB column data types — compile-time verified against C++ `odc::api::ColumnType`. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[repr(i32)] + enum ColumnType { + #[cxx_name = "IGNORE"] + Ignore = 0, + #[cxx_name = "INTEGER"] + Integer = 1, + #[cxx_name = "REAL"] + Real = 2, + #[cxx_name = "STRING"] + String = 3, + #[cxx_name = "BITFIELD"] + Bitfield = 4, + #[cxx_name = "DOUBLE"] + Double = 5, + } + + unsafe extern "C++" { + include!("odc_bridge.h"); + + // Verify ColumnType matches C++ odc::api::ColumnType at compile time + #[namespace = "odc::api"] + type ColumnType; + + // Cross-crate ExternType from eckit-sys + #[namespace = "eckit_bridge"] + type DataHandleWrapper = eckit_sys::DataHandleWrapper; + + // ==================== SelectIteratorWrapper ==================== + + type SelectIteratorWrapper; + + #[must_use] + fn valid(self: Pin<&mut SelectIteratorWrapper>) -> bool; + fn advance(self: Pin<&mut SelectIteratorWrapper>) -> Result<()>; + fn column_count(self: &SelectIteratorWrapper) -> usize; + fn column_name(self: &SelectIteratorWrapper, idx: usize) -> Result; + fn column_type(self: &SelectIteratorWrapper, idx: usize) -> Result; + fn data(self: &SelectIteratorWrapper, idx: usize) -> Result; + fn data_string(self: Pin<&mut SelectIteratorWrapper>, idx: usize) -> Result; + fn data_integer(self: Pin<&mut SelectIteratorWrapper>, idx: usize) -> Result; + + // ==================== SelectWrapper ==================== + + type SelectWrapper; + + fn begin(self: Pin<&mut SelectWrapper>) -> Result>; + #[cxx_name = "createSelectIterator"] + fn create_select_iterator( + self: Pin<&mut SelectWrapper>, + sql: &str, + ) -> Result>; + fn database_name(self: Pin<&mut SelectWrapper>) -> Result; + + fn select_create( + sql: &str, + handle: Pin<&mut DataHandleWrapper>, + ) -> Result>; + + // ==================== WriteIteratorWrapper ==================== + + type WriteIteratorWrapper; + + fn set_column( + self: Pin<&mut WriteIteratorWrapper>, + index: usize, + name: &str, + col_type: ColumnType, + ) -> Result<()>; + fn set_number_of_columns(self: Pin<&mut WriteIteratorWrapper>, n: usize) -> Result<()>; + fn set_data(self: Pin<&mut WriteIteratorWrapper>, index: usize, value: f64) -> Result<()>; + fn set_data_string( + self: Pin<&mut WriteIteratorWrapper>, + index: usize, + value: &str, + ) -> Result<()>; + fn set_data_integer( + self: Pin<&mut WriteIteratorWrapper>, + index: usize, + value: i64, + ) -> Result<()>; + fn set_missing_value( + self: Pin<&mut WriteIteratorWrapper>, + index: usize, + value: f64, + ) -> Result<()>; + fn write_row(self: Pin<&mut WriteIteratorWrapper>) -> Result<()>; + fn close(self: Pin<&mut WriteIteratorWrapper>) -> Result<()>; + + // ==================== WriterWrapper ==================== + + type WriterWrapper; + + fn pass1(self: Pin<&mut WriterWrapper>, select: Pin<&mut SelectWrapper>) -> Result<()>; + #[cxx_name = "begin"] + fn create_write_iterator( + self: Pin<&mut WriterWrapper>, + ) -> Result>; + fn rows_buffer_size(self: &WriterWrapper) -> usize; + fn set_rows_buffer_size(self: Pin<&mut WriterWrapper>, n: usize); + fn path(self: &WriterWrapper) -> Result; + + fn writer_create(handle: Pin<&mut DataHandleWrapper>) -> Result>; + } +} + +pub use cxx::{Exception, UniquePtr}; +pub use ffi::*; + +// SAFETY: All odc wrapper types own their data with no thread-local or global mutable state. +#[allow(clippy::non_send_fields_in_send_ty)] +mod send_impls { + use super::ffi::{SelectIteratorWrapper, SelectWrapper, WriteIteratorWrapper, WriterWrapper}; + unsafe impl Send for SelectIteratorWrapper {} + unsafe impl Send for SelectWrapper {} + unsafe impl Send for WriteIteratorWrapper {} + unsafe impl Send for WriterWrapper {} +}