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)
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
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)]
#[no_mangle] pub extern "C" fn pqueue_u8_free(_pqueue: Option>) {} Use a Rust library from C – Free Option::None → Nothing is dropped void pqueue_u8_free(struct PQueueU8 *pqueue);
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
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
pub struct PQueueU8(std::collections::BinaryHeap); fn pqueue_u8_new(elements: &[u8]) -> Box { 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 { self.0.pop().ok_or("queue is empty") } } C++ bindings using cxx – Implementation The error type
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());
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)
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> vs. raw pointers – Nomicon on FFI ● Automate – cbindgen – cxx
Why Advantages of managed world + Performance/power of native world Access low-level drivers or API (e.g. OpenGL/Vulkan) Integrate with existing application
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); }
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
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
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!
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.
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
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
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
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
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
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)
Thank you Any questions https://github.com/fredmorcos https://twitter.com/Harald3DCV https://www.meetup.com/Mob-Programming-on-Open-Source-Software/ (Also home of the Rustic Mob)