Location>code7788 >text

An article explaining how Rust interoperates with Java

Popularity:525 ℃/2024-11-06 08:05:25

All posts on this blog, unless otherwise stated, use theCC BY-NC-SA 4.0License Agreement. Reprinted with permission fromonly you

Usage Scenarios

Java and Rust interoperate so that Rust can do more with the Java ecosystem on its back, and Java can enjoy the memory safety, ownership mechanisms, and fearless concurrency of the Rust language features.

Typical scenarios for interoperability include:

  • Performance Optimization: Improve the overall performance of your Java applications by leveraging Rust for computationally intensive tasks.
  • System-level programming: Combine Rust's underlying control capabilities with Java's high-level abstractions for more efficient system interactions.
  • Cross-platform development: Use Rust to write core logic and interact with Java on different platforms through JNI to realize efficient cross-platform development.
  • Safety-critical applications: In finance, healthcare and other fields, Rust is utilized to handle sensitive data and core functions to ensure a high degree of security.
  • Real-time systems: using Rust to handle time-critical parts in latency-sensitive applications such as game engines and audio processing.

background knowledge

JNI

  • The full name is Java Native Interface, which allows Java code to interoperate with applications written in other languages such as C or C++.
  • JNI Specification: This is the official specification of JNI, describing in detail how it is used, its interfaces and features.

Java Virtual Machine (JVM)

image

JNI is part of the Java Virtual Machine, and the JVM creates a JNI environment for each thread at startup.The JNI environment consists of pointers to the JVM's internal data structures that are used to store information about Java objects, methods, and fields.

JNIEnv (JNI Environment)

image

  • JNIEnvis a pointer to a structure representing the JNI environment of the current thread. It contains pointers to all JNI-related functions, allowing you to use them in your native code. Each thread has its own separateJNIEnv, so you can't pass this pointer between threads.
  • It is possible to combineJNIEnvThink of it as a "translator". When Rust code needs to interact with Java, it sends requests through this "translator" when calling Java methods or getting properties of Java objects. Each thread has its own "translator", which ensures that each thread is independent of the Java it interacts with.

additionally

  • When Java code calls a native method, the JVM loads the appropriate native library and creates aJNIEnvPointer.
  • Native code can use this pointer to access JNI-provided functions for Java object manipulation.
  • Each thread has a separateJNIEnv, which guarantees thread safety. New threads need to callAttachCurrentThreadGet the correspondingJNIEnv
  • JNI provides a data type conversion mechanism that enables data transfer between Java and C/C++.

Using the jni 0.21.1 library in the Rust ecosystem enables interaction with Java code.

Introduction to JNI 0.21.1

This project provides a full JNI binding for Rust that allows:

  • Use Rust code to interact with Java libraries, call Java methods, and access Java objects.

  • Use Java classes and interfaces from Rust code.

  • Realize efficient data exchange across languages.

  • Leverage the performance benefits of Rust and the mature ecosystem of Java

The MethodChannel in the source code of the cross-platform UI framework Flutter implements the communication between Dart and the Android layer, and its underlying C++ is also realized by calling onMethodCall in the plugin via JNI. This is the same idea as the above jni 0.21.1, but with the following differences:

  • Language features and type safety:
    Rustjnilibrary provides a safer way to work with Java objects and method calls. It utilizes Rust's ownership system to reduce potential memory errors, making it easier to manage resources and avoid common errors when using JNI in Rust.
  • Multi-platform support: jni 0.21.1 provides broader cross-platform support.

How to run the example

For sample source code, pleaseRead the original articleSee bottom of original articleSource Code Acquisition

When running the example in a Windows 11 environment, I encountered two problems:

  1. Windows PowerShell cannot execute Makefiles directly.
  2. Unexplained compilation error due to Rust being configured for a specific target platform

The following are solutions to these problems:

Executing the Makefile in MinGW-w64

  1. Ensure that it is installed in a MinGW-w64 environment.mingw32-make Tools (usually installed with MinGW-w64)
  2. Open the MinGW-w64 command line
  3. Navigate to the Makefile directory
  4. Execute the following command
// The current example is a makefile, just execute mingw32-make -f makefile directly.
mingw32-make -f YourMakefileName

Verify the current Rust environment

For example, the author has configured files in the C:\Users\ directory:

[build]
target = "aarch64-linux-android"

This led to confusion and inexplicable compilation errors when using mingw32-make on Windows to compile Rust .so files for the Android platform.The solution is to delete unnecessary files and ensure that the current operating environment is consistent with the target platform (e.g. Windows).

Output results:

Hello, josh!
[B@2f92e0f4
factCallback: res = 720
counterCallback: count = 1
counterCallback: count = 2
counterCallback: count = 3
counterCallback: count = 4
counterCallback: count = 5
Invoking asyncComputation (thread id = 1)
asyncCallback: thread id = 23, progress = 0%
asyncCallback: thread id = 23, progress = 10%
asyncCallback: thread id = 23, progress = 20%
asyncCallback: thread id = 23, progress = 30%
asyncCallback: thread id = 23, progress = 40%
asyncCallback: thread id = 23, progress = 50%
asyncCallback: thread id = 23, progress = 60%
asyncCallback: thread id = 23, progress = 70%
asyncCallback: thread id = 23, progress = 80%
asyncCallback: thread id = 23, progress = 90%
asyncCallback: thread id = 23, progress = 100%

simple analysis of the example

Let's dive into the flow of the asyncComputation in the example. The core purpose is to perform an asynchronous computation on the Rust side, while the Rust side calls the Java side to report on the progress of the computation.

Overall Flowchart Description:

  1. Java's main() method calls asyncComputation().
  2. asyncComputation() calls Rust's Java_HelloWorld_asyncComputation() function via JNI.
  3. The Rust function creates a new thread to perform the asynchronous computation.
  4. In the new thread, Rust performs calculations and periodically calls Java's asyncCallback() method to report progress.
  5. When Rust completes its computation, control returns to Java's main thread.

This procedure shows the two-way interaction between Java calling Rust (step 1) and Rust calling back Java (step 4).

The source code is as follows

image

1 Pass an instance of the HelloWorld class to the Rust side, which corresponds to the callback object at #3 in the following Rust-side implementation

image

Rust Implementation Notes

  1. JNIEnv Parameters: See the explanations in the related concepts above for more details.
  2. JClass: Represents a reference to the Java class from which this local method is called, and is primarily used to access class-level static methods and fields.
  3. callback: A newly created instance of the HelloWorld object in Java.
  4. Getting JVM objects: becauseenv Objects do not support passing and sharing between threads (only Send is implemented), whereas the JVM does. With JVM objects you can pass between threads and eventually get theenv
  5. Create global reference: Get a global reference to the HelloWorld() instance object to prevent it from being garbage collected.
  6. Thread-safe: each thread has its ownJNIEnv, to ensure thread safety. In the new thread you need to callAttachCurrentThread Get the correspondingJNIEnv
  7. Reverse calls to Java: viaenv The callback method is the asyncCallback in the newly created instance of HelloWorld. The object of the call is the newly created instance of HelloWorld, and the callback method is the asyncCallback. in JNI, the"(I)V" is a method signature that describes the parameters and return type of a Java method."(I)V" Indicates that it accepts an integer argument and returnsvoid method. Here, the asyncCallback method receives an integer (progress) as a parameter with no return value.

summarize

  1. Java and Rust interoperability allows the two languages to complement each other's strengths, improving performance and security for a variety of scenarios such as performance optimization, system-level programming, and cross-platform development.
  2. JNI (Java Native Interface) is a key technology that enables Java to interoperate with Rust, allowing Java code to interact with applications written in other languages.
  3. Through sample analysis, we understand the two-way interaction between Java calls to Rust functions and Rust callbacks to Java methods, demonstrating the seamless collaboration between the two languages.

Reference Links

/jni-rs/jni-rs

JNI APIs and Developer Guides

Leveraging Rust in our high-performance Java database