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 RustLab
October 3, 2022

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

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