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

    View Slide

  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

    View Slide

  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)

    View Slide

  4. whoami
    Fred Morcos
    – Software engineer
    – PowerDNS at Open-Xchange
    – “The Rustic Mob” programming meetup
    – Scuba diving
    https://fredmorcos.com https://github.com/fredmorcos

    View Slide

  5. Part 1: Use a Rust library from C & C++

    View Slide

  6. 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

    View Slide

  7. 01 Use a Rust library from C

    View Slide

  8. 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

    View Slide

  9. 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);

    View Slide

  10. 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)]

    View Slide

  11. Use a Rust library from C – Constructor
    pub struct PQueueU8(std::collections::BinaryHeap);
    #[no_mangle]
    pub extern "C"
    fn pqueue_u8_new(elements: Option<&u8>, len: usize) -> Option> {
    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);

    View Slide

  12. 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,
    }

    View Slide

  13. #[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);

    View Slide

  14. #[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);

    View Slide

  15. #[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);

    View Slide

  16. 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"]

    View Slide

  17. 02cbindgen

    View Slide

  18. 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

    View Slide

  19. 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());

    View Slide

  20. 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);

    View Slide

  21. 03C++ bindings using cxx

    View Slide

  22. 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

    View Slide

  23. C++ bindings using cxx – Usage
    std::array array{4, 2, 5, 1, 2};
    rust::Slice elements{array.data(), array.size()};
    rust::Box 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;
    }
    }

    View Slide

  24. #[cxx::bridge]
    mod ffi {
    extern "Rust" {
    type PQueueU8;
    // Freestanding function
    fn pqueue_u8_new(elements: &[u8]) -> Box;
    // Methods
    fn pop(self: &mut PQueueU8) -> Result;
    }
    }
    C++ bindings using cxx – Declarations
    Shared structures between Rust & C++

    View Slide

  25. 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

    View Slide

  26. 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());

    View Slide

  27. 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)

    View Slide

  28. 04Conclusion

    View Slide

  29. 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

    View Slide

  30. Thank you
    (Part 1)

    View Slide

  31. Code repo & presentation slides
    https://github.com/fredmorcos/pqueue

    View Slide

  32. Part 2: Using a Rust library from Java

    View Slide

  33. What I will talk about
    ● Why access Rust from Java?
    ● Priority Queue Example
    – Java part
    – Rust part
    ● Caveats for Java+Native
    ● Wrap-up

    View Slide

  34. Why
    Advantages of managed world
    +
    Performance/power of native world
    Access low-level drivers or API
    (e.g. OpenGL/Vulkan)
    Integrate with existing application

    View Slide

  35. Java Native Interface (JNI)
    Let’s access the queue via JNI!
    How to start?
    Declare native “methods” in Java.

    View Slide

  36. 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);
    }

    View Slide

  37. 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);
    }

    View Slide

  38. 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);
    }

    View Slide

  39. 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.

    View Slide

  40. 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);
    }

    View Slide

  41. Let’s get Rusty

    View Slide

  42. 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

    View Slide

  43. C/C++ header file
    #include
    /* Header for class com_awesome_org_collections_RustyPriorityQueue */
    ...
    JNIEXPORT jlong JNICALL
    Java_com_awesome_1org_collections_RustyPriorityQueue_pqueue_1i8_1new
    (JNIEnv *, jclass, jbyteArray);
    ...

    View Slide

  44. 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

    View Slide

  45. 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 {
    ...
    }

    View Slide

  46. Implement the Rust side
    What a function name!

    View Slide

  47. Implement the Rust side
    jni_fn to the rescue!
    [dependencies]
    jni = "0.19"
    jni_fn = "0.1"
    Cargo.toml

    View Slide

  48. Pqueue new
    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 {
    ...
    }

    View Slide

  49. Pqueue new
    #[jni_fn("com.awesome_org.collections.RustyPriorityQueue")]
    pub fn pqueue_i8_new(env: …, j_items: jbyteArray) -> jlong {
    let jarr: AutoArray =
    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
    }

    View Slide

  50. 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

    View Slide

  51. 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
    }

    View Slide

  52. 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!

    View Slide

  53. 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!

    View Slide

  54. 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!

    View Slide

  55. 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"]

    View Slide

  56. Caveats

    View Slide

  57. 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.

    View Slide

  58. 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

    View Slide

  59. 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

    View Slide

  60. 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

    View Slide

  61. 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

    View Slide

  62. Wrap-up

    View Slide

  63. Wrap-up
    What you should remember:

    View Slide

  64. 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

    View Slide

  65. 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

    View Slide

  66. Wrap-up: Caveats
    ● JNI interface carries a performance penalty
    ● Pointer-integer-pointer round trips do not
    satisfy Strict Provenance

    View Slide

  67. References
    https://docs.rs/jni
    https://docs.rs/jni_fn
    Includes links to official
    JNI specification.

    View Slide

  68. 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)

    View Slide

  69. Part 3: Demo

    View Slide

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

    View Slide

  71. Mixing color channels
    RGB 2 Y(UV)
    Y = 0.299R + 0.587G + 0.144B

    View Slide

  72. Filter Kernel
    Box Blur Filter (Average)
    Image credits: https://www.opencv-srf.com/2018/01/filter-images-and-videos.html

    View Slide

  73. Filter Kernel
    Gaussian Blur Filter
    Image credits: https://www.opencv-srf.com/2018/01/filter-images-and-videos.html

    View Slide

  74. 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

    View Slide

  75. 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)

    View Slide