Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Get the best of both worlds - Integrating Rust with other languages

Get the best of both worlds - Integrating Rust with other languages

Presented at We Are Developers - World Congress
June 15, 2022

E6d480740a408e723de6f097c3395de5?s=128

Harald Reingruber

June 15, 2022
Tweet

Other Decks in Technology

Transcript

  1. Get the best of both worlds Integrating Rust with other

    languages Harald Reingruber & Fred Morcos
  2. whoami Harald Reingruber – Software crafter – 3D graphics engineer

    @Dedalus Healthcare – ATM on parental leave – Hiking fan – Mob-Programming on Open Source Software Meetup organizer – https://twitter.com/Harald3DCV (DMs are open)
  3. whoami Fred Morcos – Software engineer – PowerDNS at Open-Xchange

    – “The Rustic Mob” programming meetup – Scuba diving – https://fredmorcos.com – https://github.com/fredmorcos
  4. Introduction • Use a Rust library from C and C++

    • Develop application logic in Rust – Develop e.g. UI in Gtk/C or Qt/C++ • Incrementally modernize a codebase • Priority queue (aka std::collections::BinaryHeap) – Put elements out of order – Pop elements in sorted order • Build a foreign function interface (FFI) module
  5. 01 Use a Rust library from C

  6. Use a Rust library from C – Usage const uint8_t

    elements[5] = {4, 2, 5, 1, 2}; struct PQueueU8 *pq = pqueue_u8_new(elements, 5); if (pq == NULL) { // Fail } for (int i = 0; i < 10; i++) { struct PQueueU8Value ret = pqueue_u8_pop(pq); switch (ret.status) { case PQueueU8Status_Success: printf("%d\n", ret.value); break; case PQueueU8Status_Empty: printf("Nothing\n"); break; case PQueueU8Status_InvalidArgument: // Fail } } pqueue_u8_free(pq);
  7. Use a Rust library from C – Header File struct

    PQueueU8; enum PQueueU8Status { PQueueU8Status_Success = 0, PQueueU8Status_Empty = 1, PQueueU8Status_InvalidArgument = -1, }; struct PQueueU8Value { enum PQueueU8Status status; uint8_t value; }; struct PQueueU8 *pqueue_u8_new(const uint8_t *const elements, const size_t len); struct PQueueU8Value pqueue_u8_pop(struct PQueueU8 *const pqueue); void pqueue_u8_free(struct PQueueU8 *pqueue);
  8. Use a Rust library from C – Interlude • Rust

    guarantees do not hold across FFI boundaries • Rust mangles function names – #[no_mangle] • Rust does not follow the C (or platform) calling convention – extern “C” or extern “system” • Some Rust types are not “ffi-safe” – Slices & tagged enums don’t have a C equivalent • Structs & enums with the Rust repr don’t have specified layouts – #[repr(C)]
  9. Use a Rust library from C – Constructor pub struct

    PQueueU8(std::collections::BinaryHeap<u8>); #[no_mangle] pub extern "C" fn pqueue_u8_new(elements: Option<&u8>, len: usize) -> Option<Box<PQueueU8>> { let mut pqueue = PQueueU8(std::collections::BinaryHeap::new()); let elements: &u8 = elements?; let elements: &[u8] = unsafe { std::slice::from_raw_parts(elements, len) }; for &element in elements { pqueue.0.push(element); } Some(Box::new(pqueue)) } struct PQueueU8 * pqueue_u8_new(const uint8_t *const elements, const size_t len);
  10. Use a Rust library from C – Pop #[repr(C)] pub

    struct PQueueU8Value { status: PQueueU8Status, value: u8, } #[repr(C)] pub enum PQueueU8Status { Success = 0, Empty = 1, InvalidArgument = -1, }
  11. #[no_mangle] pub extern "C" fn pqueue_u8_pop(pqueue: Option<&mut PQueueU8>) -> PQueueU8Value

    { if let Some(pqueue) = pqueue { if let Some(value) = pqueue.0.pop() { PQueueU8Value { status: PQueueU8Status::Success, value } } else { PQueueU8Value { status: PQueueU8Status::Empty, value: 0 } } } else { PQueueU8Value { status: PQueueU8Status::InvalidArgument, value: 0 } } } Use a Rust library from C – Pop struct PQueueU8Value pqueue_u8_pop(struct PQueueU8 *const pqueue);
  12. #[no_mangle] pub extern "C" fn pqueue_u8_free(_pqueue: Option<Box<PQueueU8>>) {} Use a

    Rust library from C – Free Option::None → Nothing is dropped void pqueue_u8_free(struct PQueueU8 *pqueue);
  13. Use a Rust library from C – Build & run

    $ cargo build $ cc -Wall -Wextra -Ltarget/debug -lpqueue c/main.c -o pqueue_test $ LD_LIBRARY_PATH=target/debug valgrind ./pqueue_test … 5 4 2 2 1 Nothing Nothing Nothing Nothing Nothing … ==15746== ==15746== HEAP SUMMARY: ==15746== in use at exit: 0 bytes in 0 blocks ==15746== total heap usage: 3 allocs, 3 frees, 1,056 bytes allocated ==15746== ==15746== All heap blocks were freed -- no leaks are possible ==15746== ==15746== For lists of detected and suppressed errors, rerun with: -s ==15746== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) # Cargo.toml … [lib] crate-type = ["staticlib", "cdylib"] …
  14. 02C++ bindings using cxx

  15. C++ bindings using cxx – Intro • Generate C++ bindings

    from Rust code using the cxx crate • Without maintaining C++ wrappers around a C API
  16. C++ bindings using cxx – Usage std::array<const uint8_t, 5> array{4,

    2, 5, 1, 2}; rust::Slice<const uint8_t> elements{array.data(), array.size()}; rust::Box<PQueueU8> pq = pqueue_u8_new(elements); for (int i = 0; i < 10; i++) { try { const uint8_t value = pq->pop(); std::cout << std::to_string(value) << std::endl; } catch (const rust::Error &e) { std::cerr << "pop error: " << e.what() << std::endl; } }
  17. #[cxx::bridge] mod ffi { extern "Rust" { type PQueueU8; //

    Freestanding function fn pqueue_u8_new(elements: &[u8]) -> Box<PQueueU8>; // Methods fn pop(self: &mut PQueueU8) -> Result<u8>; } } C++ bindings using cxx – Declarations Shared structures between Rust & C++
  18. pub struct PQueueU8(std::collections::BinaryHeap<u8>); fn pqueue_u8_new(elements: &[u8]) -> Box<PQueueU8> { let

    mut pqueue = PQueueU8(std::collections::BinaryHeap::new()); for &element in elements { pqueue.0.push(element); } Box::new(pqueue) } impl PQueueU8 { fn pop(&mut self) -> Result<u8, &'static str> { self.0.pop().ok_or("queue is empty") } } C++ bindings using cxx – Implementation The error type
  19. C++ bindings using cxx – Build & run $ cargo

    build $ ls target/cxxbridge/pqueue/src lib.rs.cc lib.rs.h $ c++ -Wall -Werror target/release/libpqueue.a -o pqueue_test $ valgrind ./pqueue_test … 5 4 2 2 1 pop error: queue is empty pop error: queue is empty pop error: queue is empty pop error: queue is empty pop error: queue is empty … ==16054== ==16054== HEAP SUMMARY: ==16054== in use at exit: 0 bytes in 0 blocks ==16054== total heap usage: 20 allocs, 20 frees, 74,596 bytes allocated ==16054== ==16054== All heap blocks were freed -- no leaks are possible ==16054== ==16054== For lists of detected and suppressed errors, rerun with: -s ==16054== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) # Cargo.toml … [lib] crate-type = ["staticlib", "cdylib"] … [dependencies] cxx = "1.0" … [build-dependencies] cxx-build = "1.0" … + build.rs
  20. Thank you (Part 1)

  21. References • pqueue code repository + presentation slides • cbindgen

    • cxx • Nomicon on FFI • Rust name mangling - RFC 2603 • Documentation on extern • Documentation on type layouts
  22. 03 Using a Rust library from Java

  23. Why • Advantages of managed world • Performance/power of native

    world • Access low-level drivers or API (e.g. OpenGL/Vulkan)
  24. Java Native Interface Let’s access the queue via JNI!

  25. How to start? Declare native “methods” in Java.

  26. Native method declaration package com.awesome_org.collections; public class RustyPriorityQueue { private

    long objPtr; public RustyPriorityQueue(byte[] elements) { this.objPtr = pqueue_i8_new(elements); } private static native long pqueue_i8_new(byte[] elements); }
  27. Native method declaration package com.awesome_org.collections; public class RustyPriorityQueue { ...

    public void push(byte element) { pqueue_i8_push(this.objPtr, element); } private native void pqueue_i8_push(long objPtr, byte element); }
  28. Native method declaration package com.awesome_org.collections; public class RustyPriorityQueue { ...

    public byte pop() { return pqueue_i8_pop(this.objPtr); } private native byte pqueue_i8_pop(long objPtr); } Straight forward --> let’s skip!
  29. Native method declaration package com.awesome_org.collections; public class RustyPriorityQueue { ...

    public void destroy() { pqueue_i8_free(this.objPtr); this.objPtr = 0; } private native void pqueue_i8_free(long objPtr); } Implementing a finalizer is not recommended.
  30. Prevent access of destroyed queue package com.awesome_org.collections; public class RustyPriorityQueue

    { ... public byte pop() { if (this.objPtr == 0) throw new IllegalStateException("Access of queue after destruction!"); return pqueue_i8_pop(this.objPtr); } private native byte pqueue_i8_pop(long objPtr); }
  31. Let’s get Rusty

  32. JNI function name convention Look up JNI name mangling specs:

    https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html or generate header file javac -h . RustyPriorityQueue.java
  33. C/C++ header file #include <jni.h> /* Header for class com_awesome_org_collections_RustyPriorityQueue

    */ ... JNIEXPORT jlong JNICALL Java_com_awesome_1org_collections_RustyPriorityQueue_pqueue_1i8_1new (JNIEnv *, jclass, jbyteArray); ...
  34. Implement the Rust side We don’t have jni.h in Rust.

    jni crate provides the necessary types and logic. [dependencies] jni = "0.19" Cargo.toml
  35. Implement the Rust side use jni::*; #[no_mangle] pub extern "system"

    fn Java_com_awesome_1org_collections_RustyPriorityQueue_pqueue_1i8_1new( env: JNIEnv, _class: JClass, j_items: jbyteArray, ) -> jlong { ... }
  36. Implement the Rust side What a function name!

  37. Implement the Rust side jni_fn to the rescue! [dependencies] jni

    = "0.19" jni_fn = "0.1" Cargo.toml
  38. Implement the Rust side use jni::*; use jni_fn::jni_fn; #[jni_fn("com.awesome_org.collections.RustyPriorityQueue")] pub

    fn pqueue_i8_new( env: JNIEnv, _class: JClass, j_items: jbyteArray, ) -> jlong { ... }
  39. Implement the Rust side #[jni_fn("com.awesome_org.collections.RustyPriorityQueue")] pub fn pqueue_i8_new(env: …, j_items:

    jbyteArray) -> jlong { let jarr: AutoArray<jbyte> = env.get_byte_array_elements(j_items, NoCopyBack).unwrap(); let items: &[i8] = slice::from_raw_parts(jarr.as_ptr(), jarr.size()); let mut pq = PQueueI8(BinaryHeap::new()); items.into_iter().for_each(|item| pq.0.push(*item)); Box::into_raw(Box::new(pq)) as jlong // return i64 ptr }
  40. Copying and pinning env.get_byte_array_elements() vs. env.get_primitive_array_critical() Should not copy the

    array memory! BUT might block Java GC until the AutoPrimitiveArray is destroyed (= end of critical section) https://www.ibm.com/docs/en/sdk-java-technology/8?topic=jni-copying-pinning
  41. Implement the Rust side #[jni_fn("com.awesome_org.collections.RustyPriorityQueue")] pub fn pqueue_i8_new(env: …, j_items:

    jbyteArray) -> jlong { let jarr: AutoPrimitiveArray = env.get_primitive_array_critical(j_items, NoCopyBack).unwrap(); let items: &[i8] = slice::from_raw_parts(jarr.as_ptr(), jarr.size()); let mut pq = PQueueI8(BinaryHeap::new()); items.into_iter().for_each(|item| pq.0.push(*item)); Box::into_raw(Box::new(pq)) as jlong // return i64 ptr }
  42. Convert jlong to &mut pub fn pqueue_i8_push(env: …, native_ptr: jlong,

    item: jbyte) { let pq = unsafe { &mut *(native_ptr as *mut PQueueI8) }; pq.push(item); } Also insert NULL check (native_ptr == 0) before!
  43. Convert jlong to &mut #[jni_fn("com.awesome_org.collections.RustyPriorityQueue")] pub fn pqueue_i8_pop(env: JNIEnv, _obj:

    JObject, native_object_ptr: jlong) -> jbyte { let pq = unsafe { &mut *(native_object_ptr as *mut PQueueI8) }; if let Some(val) = pq.0.pop() { return val; } else { // throw exception to Java! } } Check out pqueue_jni repo on my Github.
  44. Clean up native memory #[jni_fn("com.awesome_org.collections.RustyPriorityQueue")] pub fn pqueue_i8_free(env: …, native_ptr:

    jlong) { Unsafe { drop( Box::from_raw( native_ptr as *mut PQueueI8 ) ); } }
  45. Load native Library package com.awesome_org.collections; public class RustyPriorityQueue { static

    { System.loadLibrary("pqueue_jni"); } private long objPtr; public RustyPriorityQueue(byte[] elements) {} } Needs to be a shared (dynamic) library (.dll/.so)! crate-type = ["cdylib"]
  46. References https://docs.rs/jni https://docs.rs/jni_fn Includes links to official JNI specification

  47. Alternatives to JNI • Java Native Access (JNA) – Easier

    but slow (interesting if speed is not important) • Java Native Runtime (JNR-FFI) – Comparable performance to JNI without the hand-written glue code • Project Panama (still experimental) • Rule of thumb: Also consider the performance penalty of the interface when going native
  48. 04Demo

  49. Demo Let’s compare image filters in Java to Rust

  50. Links to our code Priority Queue Examples: https://github.com/fredmorcos/pqueue https://github.com/haraldreingruber/pqueue-jni Java

    Image Filter to Rust Comparison: https://github.com/haraldreingruber/rust-java-example
  51. Thank you Any questions https://github.com/fredmorcos https://twitter.com/Harald3DCV https://www.meetup.com/Mob-Programming-on-Open-Source-Software/ (Beginners friendly Rustic

    Mob)
  52. A Auto-generate the header file

  53. Auto-generate the header file • cbindgen can auto-generate the header

    file at build time • Based on the provided Rust FFI # Cargo.toml … [build-dependencies] cbindgen = "0.23" …
  54. Auto-generate the header file // Build.rs use std::env; use std::path::Path;

    fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let config = cbindgen::Config::default(); let config = cbindgen::Config { usize_is_size_t: true, ..config }; cbindgen::Builder::new() .with_config(config) .with_language(cbindgen::Language::C) .with_crate(&crate_dir) .generate() .unwrap() .write_to_file(Path::new(&crate_dir).join("c").join("pqueue.h")); let lib = Path::new(&crate_dir).join("src").join("lib.rs"); println!("cargo:rerun-if-changed={}", lib.display()); }
  55. Auto-generate the header file typedef struct PQueueU8 PQueueU8; typedef enum

    PQueueU8Status { Success = 0, Empty = 1, InvalidArgument = -1, } PQueueU8Status; typedef struct PQueueU8Value { enum PQueueU8Status status; uint8_t value; } PQueueU8Value; struct PQueueU8 *pqueue_u8_new(const uint8_t *elements, size_t len); enum PQueueU8Status pqueue_u8_push(struct PQueueU8 *pqueue, uint8_t element); struct PQueueU8Value pqueue_u8_pop(struct PQueueU8 *pqueue); void pqueue_u8_free(struct PQueueU8 *_pqueue);
  56. B C++ bindings using cxx

  57. C++ bindings using cxx • Generate C++ bindings from Rust

    code using the cxx crate • Without reviewing generated header files • And without maintaining C++ wrappers over a C API # Cargo.toml … [lib] crate-type = ["staticlib", "cdylib"] … [dependencies] cxx = "1.0" … [build-dependencies] cxx-build = "1.0" …
  58. C++ bindings using cxx // Build.rs use std::path::Path; fn main()

    { let lib = Path::new("src").join("lib.rs"); let test = Path::new("cxx").join("main.cc"); cxx_build::bridge(lib.to_str().unwrap()) .file(test.to_str().unwrap()) .flag_if_supported("-std=c++17") .compile("pqueue"); println!("cargo:rerun-if-changed={}", lib.display()); println!("cargo:rerun-if-changed={}", test.display()); }
  59. C++ bindings using cxx std::array<const uint8_t, 5> array{4, 2, 5,

    1, 2}; rust::Slice<const uint8_t> elements{array.data(), array.size()}; rust::Box<PQueueU8> pq = pqueue_u8_new(elements); pq->push(17); for (int i = 0; i < 10; i++) { PQueueU8Value ret = pq->pop(); switch (ret.status) { case PQueueU8Status::Success: std::cout << std::to_string(ret.value) << std::endl; break; case PQueueU8Status::Empty: std::cout << "Nothing" << std::endl; break; } }
  60. #[cxx::bridge] mod ffi { #[derive(Clone, Copy, PartialEq, Eq)] pub enum

    PQueueU8Status { Success = 0, Empty = 1 } #[derive(Clone, Copy, PartialEq, Eq)] pub struct PQueueU8Value { status: PQueueU8Status, value: u8 } extern "Rust" { type PQueueU8; // Freestanding function fn pqueue_u8_new(elements: &[u8]) -> Box<PQueueU8>; // Methods fn push(self: &mut PQueueU8, element: u8); fn pop(self: &mut PQueueU8) -> PQueueU8Value; } } C++ bindings using cxx
  61. pub struct PQueueU8(std::collections::BinaryHeap<u8>); fn pqueue_u8_new(elements: &[u8]) -> Box<PQueueU8> { let

    mut pqueue = PQueueU8(std::collections::BinaryHeap::new()); for &element in elements { pqueue.0.push(element); } Box::new(pqueue) } C++ bindings using cxx A slice!
  62. use ffi::{PQueueU8Value, PQueueU8Status}; impl PQueueU8 { fn push(&mut self, element:

    u8) { self.0.push(element); } fn pop(&mut self) -> PQueueU8Value { if let Some(value) = self.0.pop() { PQueueU8Value { status: PQueueU8Status::Success, value } } else { PQueueU8Value { status: PQueueU8Status::Empty, value: 0 } } } } C++ bindings using cxx Import the shared structures
  63. C++ bindings using cxx $ cargo build $ ll target/debug

    … libpqueue.a … libpqueue.so $ ll target/cxxbridge/pqueue/src lib.rs.cc lib.rs.h $ c++ -Wall -Werror target/release/libpqueue.a -o pqueue_test $ valgrind ./pqueue_test … 17 5 4 2 2 1 Nothing Nothing Nothing Nothing … ==59054== All heap blocks were freed -- no leaks are possible