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

Get the best of both worlds - Integrating Rust ...

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

Presented at RustLab
October 3, 2022

Shorter version was presented at
We Are Developers - World Congress
June 15, 2022

Avatar for Harald Reingruber

Harald Reingruber

October 03, 2022
Tweet

Other Decks in Technology

Transcript

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

    languages Harald Reingruber & Fred Morcos
  2. Agenda • Part 1: Use a Rust library from C

    and C++ • Part 2: Use a Rust library from Java • Part 3: Live demo – image processing
  3. whoami Harald Reingruber – Software crafter – 3D graphics engineer

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

    – “The Rustic Mob” programming meetup – Scuba diving https://fredmorcos.com https://github.com/fredmorcos
  5. 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
  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); 5 4 2 2 1 Nothing Nothing Nothing Nothing Nothing
  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); enum PQueueU8Status pqueue_u8_push(struct PQueueU8 *const pqueue, const uint8_t element); 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_push(pqueue: Option<&mut PQueueU8>, element: u8)

    -> PQueueU8Status { if let Some(pqueue) = pqueue { pqueue.0.push(element); PQueueU8Status::Success } else { PQueueU8Status::InvalidArgument } } Use a Rust library from C – Push enum PQueueU8Status pqueue_u8_push(struct PQueueU8 *const pqueue, const uint8_t element);
  13. #[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);
  14. 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"] …
  15. cbindgen – Auto-generate the header file • cbindgen can auto-generate

    the header file at build time • Based on the provided Rust FFI # Cargo.toml … [lib] crate-type = ["staticlib", "cdylib"] … [build-dependencies] cbindgen = "0.23" + build.rs
  16. cbindgen – Build script (build.rs) let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let

    enum_config = cbindgen::EnumConfig::default(); let enum_config = cbindgen::EnumConfig { prefix_with_name: true, ..enum_config }; let config = cbindgen::Config::default(); let config = cbindgen::Config { usize_is_size_t: true, enumeration: enum_config, ..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());
  17. cbindgen – Generated header file typedef struct PQueueU8 PQueueU8; typedef

    enum PQueueU8Status { PQueueU8Status_Success = 0, PQueueU8Status_Empty = 1, PQueueU8Status_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); struct PQueueU8Value pqueue_u8_pop(struct PQueueU8 *pqueue); enum PQueueU8Status pqueue_u8_push(struct PQueueU8 *pqueue, uint8_t element); void pqueue_u8_free(struct PQueueU8 *_pqueue);
  18. C++ bindings using cxx – Intro • Generate C++ bindings

    from Rust code using the cxx crate • Without maintaining C++ wrappers around a C API • Supports integration with build systems like CMake # Cargo.toml … [lib] crate-type = ["staticlib", "cdylib"] … [dependencies] cxx = "1.0" … [build-dependencies] cxx-build = "1.0" … + build.rs
  19. 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; } }
  20. #[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++
  21. 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
  22. C++ bindings using cxx – Build script (build.rs) 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_cxx_test"); println!("cargo:rerun-if-changed={}", lib.display()); println!("cargo:rerun-if-changed={}", test.display());
  23. 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)
  24. Conclusion • #[no_mangle] and extern “C” – Rust name mangling

    - RFC 2603 – Documentation on extern • Types that can and cannot be shared – Documentation on type layouts • Use high-level Rust constructs to represent lower-level C constructs – Option<&T> and Option<Box<T: Sized>> vs. raw pointers – Nomicon on FFI • Automate – cbindgen – cxx
  25. What I will talk about • Why access Rust from

    Java? • Priority Queue Example – Java part – Rust part • Caveats for Java+Native • Wrap-up
  26. Why Advantages of managed world + Performance/power of native world

    Access low-level drivers or API (e.g. OpenGL/Vulkan) Integrate with existing application
  27. Java Native Interface (JNI) Let’s access the queue via JNI!

    How to start? Declare native “methods” in Java.
  28. 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); }
  29. 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); }
  30. 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); }
  31. 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); } Note: Implementing a finalizer is not recommended.
  32. 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); }
  33. 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
  34. 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); ...
  35. 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
  36. 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 { ... }
  37. Pqueue new #[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 }
  38. Copying and pinning When use env.get_primitive_array_critical() instead of env.get_byte_array_elements()? Should

    not copy the array memory! BUT might block Java GC until the AutoPrimitiveArray is destroyed https://www.ibm.com/docs/en/sdk-java-technology/8?topic=jni-copying-pinning
  39. Pqueue new #[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 }
  40. Pqueue push #[jni_fn("com.awesome_org.collections.RustyPriorityQueue")] 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!
  41. Pqueue pop #[jni_fn("com.awesome_org.collections.RustyPriorityQueue")] pub fn pqueue_i8_pop(env: JNIEnv, _obj: JObject, native_ptr:

    jlong) -> jbyte { let pq = unsafe { &mut *(native_ptr as *mut PQueueI8) }; if let Some(val) = pq.0.pop() { return val; } else { let ex_class = env.find_class( "java/lang/ArrayIndexOutOfBoundsException"); env.throw_new(ex_class, "Queue is empty."); // Won't be consumed on the Java side because of the exception above. return i8::MIN; } } Again insert NULL check (native_ptr == 0) before!
  42. Pqueue free #[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 ) ); } } Again insert NULL check (native_ptr == 0) before!
  43. Load native Library package com.awesome_org.collections; public class RustyPriorityQueue { static

    { System.loadLibrary("pqueue_jni"); } private long objPtr; public RustyPriorityQueue(byte[] elements) {} } $ java -cp ./src/ -Djava.library.path=./target/release/ \ com.awesome_org.collections.RustyPriorityQueue Needs to be a shared (dynamic) library (.dll/.so)! crate-type = ["cdylib"]
  44. Rule of thumb Also consider the performance penalty of the

    JNI interface, when going native. Don’t process only a few elements. Process many elements per JNI call.
  45. Strict Provenance (Experimental feature) Pointer-integer casts will tell the compiler

    to opt-out of strict provenance. https://doc.rust-lang.org/nightly/core/ptr/index.html#provenance
  46. Strict Provenance (Experimental) Provenance is the permission to access an

    allocation’s sandbox. Spatial: A range of bytes that the pointer is allowed to access. Temporal: The lifetime (of the allocation) that access to these bytes is tied to. https://doc.rust-lang.org/nightly/core/ptr/index.html#provenance
  47. Strict Provenance (Experimental) Goal of the Strict Provenance feature: Adoption

    of tools like CHERI and Miri that help increasing the confidence in (unsafe) Rust. https://doc.rust-lang.org/nightly/core/ptr/index.html#provenance
  48. Strict Provenance (Experimental) Find out more: https://dub.sh/strict-provenance-twitter-thread by @Gankra_ -

    The unsafe Rust person. Pointers Are Complicated III, or: Pointer-integer casts exposed: https://www.ralfj.de/blog/2022/04/11/provenance-exposed.html by Ralf Jung
  49. Wrap-up: Java-Rust Interface • Java: native method prefix • Rust:

    provide matching signature with jni+jni_fn crates • Otherwise, check generated header file • If state is required, pass native ptr as jlong
  50. Wrap-up: Shared memory • Arrays can be passed with copying

    or pinning • Pinning is faster, but might block GC • Memory “leaked” to Java needs to be cleaned up • Freeing resources from finalize() is not guaranteed to work
  51. Wrap-up: Caveats • JNI interface carries a performance penalty •

    Pointer-integer-pointer round trips do not satisfy Strict Provenance
  52. 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)
  53. Links to our code Priority Queue Examples: https://github.com/fredmorcos/pqueue https://github.com/haraldreingruber/pqueue-jni Java

    to Rust Image Filter Comparison: https://github.com/haraldreingruber/rust-java-example