Rustize HPX

Google Summer of Code 2024 @ HPX, STEllAR Group

Motivation:

HPX, as a framework designed for high performance computing. Which focuses on parallelism while hiding concurrency from the user (unlike openmp). Since it’s written in C++ so, it causes concerns of Memory Safety (According to White House press release) Thus, interoping it to a memory safe language that turns out to be Rust.

Implementation:

  • There weren’t any libraries that interoperate a Cpp library to rust. So I’ve followed the convention that was followed with C libraries interoperability to rust.
  • I’ve taken libgit2-rs & ssh2-rs as a reference for setting up the crate.
  • The hpx-sys crate contains bindings for hpx. In this implementation of hpx-rs the user is supposed to have hpx installed on his machine.

    1. Build-system

  • We decided to use pkg-config as it supports hpx and it also has a wrapper in rust. It simplifies the work of the user to export each and every dependency of rust by just pointing the pkg-config path to hpx pkg-config.
    fn try_hpx_application() -> Result<pkg_config::Library, pkg_config::Error> {
      let mut cfg = pkg_config::Config::new();
      match cfg
          .range_version("1.10.0".."1.12.0")
          .probe("hpx_application")
      {
          Ok(lib) => {
              for include in &lib.include_paths {
                  println!("cargo:root={}", include.display());
              }
              Ok(lib)
          }
          Err(e) => {
              println!("cargo:warning=failed to probe hpx_application: {e}");
              Err(e)
          }
      }
    }
    fn main() {
      let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
      let lib_rs_path = Path::new(&manifest_dir).join("src").join("lib.rs");
      println!(
          "cargo:warning=Looking for lib.rs at: {}",
          lib_rs_path.display()
      );
      let mut build = cxx_build::bridge(lib_rs_path);
      let hpx_application = match try_hpx_application() {
          Ok(lib) => lib,
          Err(_) => panic!("Failed to find hpx_application using pkg-config"),
      };
    
      for path in &hpx_application.include_paths {
          build.include(path);
      }
    
      build.std("c++17").compile("hpx-sys");
    
      println!("cargo:rerun-if-changed=src/lib.rs");
      println!("cargo:rerun-if-changed=include/wrapper.h");
    }
    
  • To avoid user to have hpx installed on his machine we could have VENDORED it via build.rs(followed in several C interoperate libs) but the issue with that approach is that if suppose user already have HPX installed on his machine you don’t want to vendor hpx but down the dependency graph of the crate if something else set VENDORED = 1 flag then all the dependencies have to be build again even hpx. It’s just extra overhead. (git2-rs and ssh2-rs follows vendoring refer their build.rs for more details)

2. Interface between the C++ and Rust

  • For this we decided to go with CXX as it provides support for std lib types and also Rust code feels like Rust and the C++ code feels like C++ with CXX which is not possible at the time with some other ffi crates (as they expect C++ ->C ->Rust)
  • Currently the focus is only on i32 vector datatype as hpx uses metaprogramming where as cxx doesn’t support rust generics. So this ensure consistency between Rust and C++ sides.
  • Also the functions are declared as inline so that llvm could inline these functions at call stack.
  • Type usage:
    • rust::Slice -> mostly for read-only data
    • rust::Vec -> mutable data ``` inline std::int32_t init(rust::Fn<int(int, char **)> rust_fn, int argc, char **argv) { return hpx::init( & { return rust_fn(argc, argv); }, argc, argv); }

inline std::int32_t finalize_with_timeout(double shutdown_timeout, double localwait) { return hpx::finalize(shutdown_timeout, localwait); }

inline void terminate() { return hpx::terminate(); }

inline std::int32_t disconnect() { return hpx::disconnect(); }

inline std::int32_t disconnect_with_timeout(double shutdown_timeout, double localwait) { return hpx::disconnect(shutdown_timeout, localwait); }

inline std::int32_t finalize() { return hpx::finalize(); }

inline void hpx_copy(rust::Slice src, rust::Slice dest) { hpx::copy(hpx::execution::par, src.begin(), src.end(), dest.begin()); }

inline void hpx_copy_n(rust::Slice src, size_t count, rust::Slice dest) { hpx::copy_n(hpx::execution::par, src.begin(), count, dest.begin()); } inline void hpx_partial_sort_comp(rust::Vec& src, size_t last, rust::Fn<bool(int32_t, int32_t)> comp) { if (last > src.size()) { last = src.size(); }

hpx::partial_sort(hpx::execution::par, 
                  src.begin(), 
                  src.begin() + last, 
                  src.end(),
                  [&](int32_t a, int32_t b) { return comp(a, b); }); } ``` #### 3. Rust Bindings - Along with ffi function declaration we have defined tests and wrappers   for hpx-rs. - Wrappers handle type conversions, error checking, and provide a idiomatic    Rust interface. - Also defined functions like `create_c_args` to reduce the user   headache of creating c style arguments to pass as arguments while   initializing hpx-runtime. ``` #[cxx::bridge] pub mod ffi {
unsafe extern "C++" {
    include!("hpx-sys/include/wrapper.h");

    unsafe fn init(
        func: unsafe fn(i32, *mut *mut c_char) -> i32,
        argc: i32,
        argv: *mut *mut c_char,
    ) -> i32;

    fn finalize() -> i32;
    fn finalize_with_timeout(shutdown_timeout: f64, localwait: f64) -> i32;
    fn terminate();
    fn disconnect() -> i32;
    fn disconnect_with_timeout(shutdown_timeout: f64, localwait: f64) -> i32;
    fn hpx_copy(src: &[i32], dest: &mut [i32]);
    fn hpx_copy_n(src: &[i32], count: usize, dest: &mut [i32]);
    fn hpx_copy_if(src: &Vec<i32>, dest: &mut Vec<i32>, pred: fn(i32) -> bool);
    fn hpx_count(src: &Vec<i32>, value: i32) -> i64;
    fn hpx_count_if(src: &Vec<i32>, pred: fn(i32) -> bool) -> i64;
    fn hpx_ends_with(src: &[i32], dest: &[i32]) -> bool;
    fn hpx_equal(slice1: &[i32], slice2: &[i32]) -> bool;
    fn hpx_fill(src: &mut [i32], value: i32); // will only work for linear vectors
    fn hpx_find(src: &[i32], value: i32) -> i64;
    fn hpx_sort(src: &mut [i32]);
    fn hpx_sort_comp(src: &mut Vec<i32>, comp: fn(i32, i32) -> bool);
    fn hpx_merge(src1: &[i32], src2: &[i32], dest: &mut Vec<i32>);
    fn hpx_partial_sort(src: &mut Vec<i32>, last: usize);
    fn hpx_partial_sort_comp(src: &mut Vec<i32>, last: usize, comp: fn(i32, i32) -> bool);
} } pub fn create_c_args(args: &[&str]) -> (i32, Vec<*mut c_char>) {
let c_args: Vec<CString> = args.iter().map(|s| CString::new(*s).unwrap()).collect();
let ptrs: Vec<*mut c_char> = c_args.iter().map(|s| s.as_ptr() as *mut c_char).collect();
(ptrs.len() as i32, ptrs) }

pub fn copy_vector(src: &[i32]) -> Vec { let mut dest = vec![0; src.len()]; ffi::hpx_copy(src, &mut dest); dest } ```

4. Code Coverage Report

Pull Requests:

  • Crate structure
  • CI workflow for tests, checks and formatter: 1 & 2
  • HPX Algos
  • Performance Benchmarks
  • Demo pr’s for familiarizing with the project 1 & 2

    Future Plans:

  • Complete the interoperability of hpx::algorithims [Highest Priority]
  • Enhancing hpx-sys to be generic (Demo) [NOTE: It'll be better to use traits]
  • Develop a method to interoperate hpx::future [experimental]

PS: Want to write bindings for my favorite language, where should I start?

  • Start with hpx-runtime APIs like hpx::init, hpx::finalize as they are most important for you to ensure if your bindings are working as expected.
  • Then proceed with hpx::copy_if or any other hpx/alogrithm.hpp API.
  • The Future API’s of hpx are the trikiest one’s to implement.
  • This will provide good insights about how to design your bindings.

Acknowledgments:

I want to thank my mentor Shreyas Atre and Dr. Hartmut Kaiser for their mentorship and valuable feedback through out the course of this project. Special thanks to zao for providing his inputs when facing errors.