Java
— один из наиболее часто используемых языков программирования, который мы еще не обсуждали в нашем Rust Interop Guide. В этой статье мы рассмотрим три различных метода вызова кода Rust
из Java
: JNI
, JNR-FFI
и Project Panama
. Мы покажем различия между этими методами и проведем базовый бенчмаркинг для сравнения их производительности. Эти методы работают не только для Java
, но и для других языков JVM, таких как Kotlin
. Здесь мы в основном сосредоточимся на Java
, но примеры Kotlin
доступны в ветке Kotlin нашего репозитория GitHub.
Эта статья является частью нашего Rust Interop Guide.
JNI
Java Native Interface (JNI)
— это оригинальный встроенный метод взаимодействия Java
с нативными библиотеками. Нативные библиотеки — это библиотеки, которые не работают в JVM
, а вместо этого создаются для определенной операционной системы с использованием языка типа C
, C++
или Rust
. JNI
предоставляет для таких библиотек интерфейс взаимодействия со средой Java
и доступ к ее структурам данных. Крейт JNI
предоставляет типы Rust
для этого интерфейса, что делает работу с JNI
в Rust
очень удобной!

Преобразование Double в строку
В качестве примера мы используем функцию, которая преобразует число типа double
(также известное как f64
в Rust
) в строку. Такая функция должна, например, преобразовать значение 3.14
в строку "3.14". В Java мы можем сделать это, вызвав Double.toString
, но давайте посмотрим, сможем ли мы сделать это быстрее, интегрировав Rust
в наш Java
-проект.
Используя крейт JNI
, мы экспортируем функцию doubleToStringRust
из нашего кода Rust
, помечая ее как extern "C":
#[no_mangle]
pub extern "C" fn Java_golf_tweede_JniInterface_doubleToStringRust(
env: JNIEnv,
_class: JClass,
v: jdouble,
) -> jstring {
env.new_string(v.to_string()).unwrap().into_raw()
}
Примечание:
Имя функции имеет определенный формат, который сообщает
JNI
, в каком классе должна быть представлена функция. В нашем случае это классJniInterface
пакетаgolf.tweede
.В то время как тип
jdouble
— это просто псевдоним дляf64
, типjstring
— это объектJava
, для которогоJNI
-окружение предоставляет удобную функцию-конструктор. Мы компилируем код Rust в динамическую библиотеку, используя тип крейтаcdylib
вCargo.toml
:
[package]
name = "java-interop"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
После успешной сборки с помощью cargo build --release
вы должны увидеть файл libjava_interop.so
в папке target/release
, если вы работаете в Linux
. В Windows
библиотека Rust
будет скомпилирована в файл .dll
вместо файла .so
, а в MacOS
это будет файл .dylib
.
Теперь мы можем объявить нашу функцию doubleToStringRust
в классе JniInterface
пакета golf.tweede
как нативную и вызвать, загрузив динамическую библиотеку через System.load
:
package golf.tweede;
import java.nio.file.Path;
import java.nio.file.Paths;
public class JniInterface {
public static native String doubleToStringRust(double v);
static {
Path p = Paths.get("src/main/rust/target/release/libjava_interop.so");
System.load(p.toAbsolutePath().toString()); // load library
}
public static void main(String[] args) {
System.out.println(doubleToStringRust(0.123)); // this prints "0.123"!
}
}
Обратите внимание, ожидается что наша нативная библиотека будет находиться в указанной папке. В качестве альтернативы вы можете использовать вызов System.loadLibrary
(java_interop
) для загрузки собственной библиотеки из одной из папок в java.library.path
, которая в Linux
по умолчанию обращается к /usr/java/packages/lib
, /usr/lib64
, /lib64
, /lib
и /usr/lib
. Если вы хотите упаковать свой проект Java
в JAR
, вам все равно придется убедиться, что библиотека находится в правильном месте, либо включив ее в JAR
и извлекая библиотеку перед загрузкой, либо установив ее отдельно.
Измерение производительности
Чтобы сравнить нашу динамически связанную реализацию Rust
с Double.toString
вызовом Java
, мы запустим несколько тестов с использованием JMH
. Мы создадим функции, аннотированные @Benchmark
, для тестирования производительности:
@Benchmark
public String doubleToStringJavaBenchmark(BenchmarkState state) {
return Double.toString(state.value);
}
@Benchmark
public String doubleToStringRustBenchmark(BenchmarkState state) {
return doubleToStringRust(state.value);
}
Мы предоставляем входное значение через класс BenchmarkState
, чтобы гарантировать, что функции не оптимизируются компилятором Java
при использовании постоянного входного значения:
@State(Scope.Benchmark)
public static class BenchmarkState {
public double value = Math.PI;
}
Если мы соберем проект с помощью mvn clean verify
, то сможем запустить тесты производительности командой java -jar target/benchmarks.jar -f 1
. Вот результаты:
Benchmark Mode Cnt Score Error Units
Main.doubleToStringJavaBenchmark thrpt 5 29921713.259 ± 576120.424 ops/s
JniInterface.doubleToStringRustBenchmark thrpt 5 5401499.220 ± 23625.065 ops/s
Результаты показывают что Java
-функция почти в 6 раз быстрее функции JNI
Rust
. Эта разница в производительности вызвана дополнительными накладными расходами при взаимодействии с собственной библиотекой.
Ускорение
Давайте посмотрим, сможем ли мы добиться большего. Для начала, вместо использования стандартной реализации Rust
to_string
, воспользуемся крейтом Ryu
, который использует хитрый алгоритм для преобразования чисел с плавающей точкой в строки до 5 раз быстрее!
Вот новая функция Rust JNI
, которая использует Ryu
для преобразования чисел двойной точности в строки:
#[no_mangle]
pub extern "C" fn Java_golf_tweede_JniInterface_doubleToStringRyu(
env: JNIEnv,
_class: JClass,
value: jdouble,
) -> jstring {
let mut buffer = ryu::Buffer::new();
env.new_string(buffer.format(value)).unwrap().into_raw()
}
Теперь давайте посмотрим результаты бенчмарка. Мы превратили результаты в красивую столбчатую диаграмму для простого сравнения:

Этот код примерно на 50%
быстрее, чем наша исходная функция Rust
, но пока еще не приближается к производительности Java
-кода.
Преобразование множества чисел double с помощью массивов
Все еще слишком много накладных расходов при взаимодействии с нашей нативной библиотекой. Мы можем уменьшить эти накладные расходы, выполняя больше работы за вызов на стороне Rust
. Вместо того, чтобы отправлять только одно число double
каждый раз, когда мы вызываем функцию, мы можем отправить массив, содержащий много чисел double
одновременно, и заставить функцию объединить все числа double
в строку с пробелами между значениями. Хотя это немного синтетический пример, который не обязательно будет очень полезен на практике, он позволит нам продемонстрировать, как производительность улучшается при больших рабочих нагрузках.
На стороне Java
определяем функцию doubleArrayToStringRyu
:
public static native String doubleArrayToStringRyu(double[] v);
В нашей библиотеке Rust
мы реализуем ее следующим образом:
#[no_mangle]
pub extern "C" fn Java_golf_tweede_JniInterface_doubleArrayToStringRyu(
mut env: JNIEnv,
_class: JClass,
array: JDoubleArray,
) -> jstring {
let mut buffer = ryu::Buffer::new();
let len: usize = env.get_array_length(&array).unwrap().try_into().unwrap();
let mut output = String::with_capacity(10 * len);
{
let elements = unsafe {
env.get_array_elements_critical(&array, ReleaseMode::NoCopyBack)
.unwrap()
};
for v in elements.iter() {
output.push_str(buffer.format(*v)); // add number to output string
output.push(' ');
}
}
env.new_string(output).unwrap().into_raw()
}
Примечание:
Double[]
становитсяJDoubleArray
, для которого мы должны использоватьenv
для извлечения длины и его элементов. Поскольку этот массив используется только как входной, мы используемReleaseMode::NoCopyBack
, чтобы сообщитьJNI
, что ему не нужно копировать измененные значения обратно, на сторонуJava
.Мы создаем выходную строку с большой емкостью, чтобы убедиться, что ей не придется переаллоцировать так много при передаче в нее большого количества символов.
Чтобы протестировать эту новую функцию, мы добавляем массив из 1 миллиона double
в наше состояние тестирования:
@State(Scope.Benchmark)
public static class BenchmarkState {
public double value = Math.PI;
public double[] array = new double[1_000_000];
@Setup
public void setup() {
for (int i = 0; i < array.length; i++) {
array[i] = i / 12f;
}
}
}
А теперь мы определим бенчмарки для нашей функции Rust
JNI
и функции, написанной только на Java
, для сравнения:
@Benchmark
public String doubleArrayToStringRyuBenchmark(BenchmarkState state) {
return doubleArrayToStringRyu(state.array);
}
@Benchmark
public String doubleArrayToStringJavaBenchmark(BenchmarkState state) {
return String.join(" ", DoubleStream.of(state.array).mapToObj(Double::toString).toArray(String[]::new));
}
Давайте посмотрим, как они себя покажут:

Посмотрите на это! Теперь наша функция Java JNI Rust
почти в два раза быстрее функции, написанной только на Java
! ?
Конечно, можно было бы и дальше оптимизировать код, написанный только на Java
. Однако это показывает, что при определенных обстоятельствах может быть целесообразно использовать нативные библиотеки для повышения производительности, несмотря на дополнительные накладные расходы при вызове.
JNR-FFI
Теперь давайте рассмотрим другой метод вызова нативных библиотек из Java
: JNR-FFI
. В отличие от JNI
, JNR-FFI
использует общий интерфейс C
для взаимодействия с нативными библиотеками. Это означает, что нам не нужно писать специфичный для JNI
код на стороне Rust
, нам не нужно включать какие-либо конкретные имена классов и пакетов Java
в имена наших функций, а JNR-FFI
позаботится о преобразовании между типами C
и типами Java
.
JNR-FFI
похожа на JNA
, другую библиотеку Java
для взаимодействия с нативными библиотеками. Однако JNR-FFI
более современная и обеспечивает превосходную производительность, поэтому мы решили попробовать JNR-FFI
вместо JNA
.

Сначала добавим JNR-FFI
в наш проект, включив его как зависимость Maven
в pom.xml
(для пользователей Rust
: pom.xml
для Maven
похож на Cargo.toml
, но в формате XML
):
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-ffi</artifactId>
<version>2.2.17</version>
</dependency>
Теперь давайте реализуем функцию doubleToStringRust
в Rust
с универсальным интерфейсом C
вместо интерфейса JNI
, который мы использовали ранее:
use std::ffi::{c_char, c_double, CString};
#[no_mangle]
pub extern "C" fn doubleToStringRust(value: c_double) -> *mut c_char {
CString::new(value.to_string()).unwrap().into_raw()
}
Примечание:
Теперь мы используем типы
C
изstd::ffi
вместо типовJava JNI
.Чтобы вернуть строку через
C
-интерфейс, мы сначала создаемC
-строку, которая затем возвращается как указатель на символ с помощьюinto_raw
.
На стороне Java
мы сначала определяем интерфейс с функциями, которые реализует наша библиотека Rust
:
public interface RustLib {
String doubleToStringRust(double value);
}
И затем мы загружаем нашу библиотеку используя класс LibraryLoader
из пакета JNR
.
public static RustLib lib;
static {
System.setProperty("jnr.ffi.library.path", "src/main/rust/target/release");
lib = LibraryLoader.create(RustLib.class).load("java_interop"); // load library
}
Обратите внимание, здесь мы говорим ему загрузить только java_interop
вместо полного имени файла libjava_interop.so
. JNR-FFI
автоматически преобразует его в полное имя файла. Это позволяет выбирать между расширениями файлов .so
, .dll
или .dylib
в зависимости от платформы, на которой он работает, что упрощает добавление кросс-платформенной совместимости!
О нет! Утечки памяти!
Мы пошли дальше и также добавили Ryu
и функции массива из предыдущего в наш новый интерфейс. Функция массива теперь реализована как doubleArrayToStringRyu(array: *const c_double, len: usize)
в Rust
, и мы используем std::slice::from_raw_parts
для создания слайса &[f64]
:
#[no_mangle]
pub unsafe extern "C" fn doubleArrayToStringRyu(array: *const c_double, len: usize) -> *mut c_char {
let slice = std::slice::from_raw_parts(array, len);
...
CString::new(output).unwrap().into_raw()
}
После настройки и запуска бенчмарков для этих функций мы заметили нечто странное: бенчмарки замедляются после пары итераций. Если мы посмотрим на использование памяти во время теста, то увидим, почему:

Красная линия показывает общее использование памяти, так что, похоже, у нас заканчивается память!
Если мы посмотрим документацию на CString::into_raw
, то увидим, что нам нужно вручную вызвать CString::from_raw
, чтобы освободить память после использования into_raw
. В настоящее время мы этого не делаем. Память никогда не освобождается что и объясняет нашу утечку памяти.
Чтобы исправить это, мы добавляем функцию в нашу собственную библиотеку Rust
, которая освобождает CString
с помощью from_raw
:
#[no_mangle]
pub unsafe extern "C" fn freeString(string: *mut c_char) {
let _ = CString::from_raw(string);
}
Чтобы вызвать эту функцию, нам нужно обновить наш интерфейс на стороне Java
. Раньше наши функции просто возвращали строки Java
. Это означало, что JNR-FFI
автоматически преобразует char
-указатели C
в строки Java
. Однако для правильного освобождения строк C
нам нужен доступ к исходным C
-указателям. Поэтому мы заставляем функции в интерфейсе Java
возвращать указатели вместо строк:
public interface RustLib {
Pointer doubleToStringRust(double value);
Pointer doubleToStringRyu(double value);
Pointer doubleArrayToStringRyu(double[] array, int len);
void freeString(Pointer string);
}
Чтобы получить строки из этих указателей, мы можем написать удобную вспомогательную функцию, которая также будет вызывать freeString
для нас:
public static String pointerToString(Pointer pointer) {
String string = pointer.getString(0);
lib.freeString(pointer); // frees the original C string
return string;
}
Используя эту вспомогательную функцию, мы больше не теряем память!
Сравнение производительности
Теперь, когда мы исправили проблемы с памятью, давайте сравним производительность между JNR-FFI
и JNI
.
Вот результаты, которые мы получили для doubleToString
:

А вот результаты для doubleArrayToString
:

Как видете, JNR-FFI
, похоже, немного медленнее JNI
. Хотя приятно, что JNR-FFI
может взаимодействовать с интерфейсом C
без необходимости писать специфичный для JNI
код, это влечет за собой некоторые дополнительные накладные расходы.
Project Panama
Последний метод, который мы обсудим, — Project Panama
. Project Panama
— это новейший способ взаимодействия с нативными библиотеками Java
, который разрабатывается в OpenJDK
. Поскольку он все еще находится в разработке, для него требуется последняя версия JDK
; мы используем OpenJDK 23
. Подобно JNR-FFI
, он использует универсальный интерфейс C
. Однако, в отличие от JNR-FFI
, Project Panama
может автоматически генерировать интерфейс на стороне Java
.

Чтобы сгенерировать биндинги Java
для нашего кода Rust
, мы сначала генерируем файл заголовка C
с помощью cbindgen
. Затем мы можем использовать jextract
для генерации интерфейса Java
на основе заголовков C
.
Вы можете установить cbindgen
с помощью cargo
:
cargo install --force cbindgen
Самый простой способ установить jextract
— с помощью SDKMAN
:
sdk use java 23-open
sdk install jextract
Теперь мы можем запустить cbindgen
в нашем проекте Rust
для генерации заголовочного файла java_interop.h
:
cbindgen --lang c --output bindings/java_interop.h
Теперь мы можем запустить jextract
в нашем проекте Java
с помощью следующей команды для генерации Java
-биндингов:
jextract \
--include-dir src/main/rust/bindings/ \
--output src/main/java \
--target-package golf.tweede.gen \
--library :src/main/rust/target/release/libjava_interop.so \
src/main/rust/bindings/java_interop.h
В качестве альтернативы мы можем автоматически генерировать наши биндинги, добавив скрипт build.rs
в наш Rust
-проект. Скрипт будет вызывать cbindgen
и jextract
:
fn main() {
// Create C headers with cbindgen
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::Builder::new()
.with_crate(crate_dir.clone())
.with_language(cbindgen::Language::C)
.generate()
.unwrap()
.write_to_file("bindings/java_interop.h");
// Create Java interface with JExtract
let java_project_dir = std::path::Path::new(&crate_dir).ancestors().nth(3).unwrap();
std::process::Command::new("jextract")
.current_dir(java_project_dir)
.arg("--include-dir")
.arg("src/main/rust/bindings/")
.arg("--output")
.arg("src/main/java")
.arg("--target-package")
.arg("golf.tweede.gen")
.arg("--library")
.arg(":src/main/rust/target/release/libjava_interop.so")
.arg("src/main/rust/bindings/java_interop.h")
.spawn()
.unwrap();
}
Если теперь мы соберем наш Rust
-проект, то увидим, что в папке bindings
появился файл java_interop.h
, содержащий определения заголовков C
для наших функций Rust
. Мы также увидим, как в src/main/java/golf/tweede/gen
генерируется множество файлов Java
. Файл, который нас здесь интересует, — это java_interop_h.java
, который содержит Java
-биндинги для наших функций Rust
в дополнение к множеству других биндингов нативных библиотек.
Поскольку Project Panama
использует интерфейс C
, мы должны убедиться, что освободили наши строки, иначе произойдет утечка памяти, с которой мы уже столкнулись в разделе JNR-FFI
. По этой причине биндинги из Project Panama
возвращают сегмент памяти вместо строки. Мы снова напишем вспомогательную функцию, чтобы извлечь строку из этого сегмента памяти и освободить исходную C
-строку, вызвав наш метод freeString
:
private static String segmentToString(MemorySegment segment) {
String string = segment.getString(0);
java_interop_h.freeString(segment);
return string;
}
public static String doubleToStringRust(double value) {
return segmentToString(java_interop_h.doubleToStringRust(value));
}
public static String doubleToStringRyu(double value) {
return segmentToString(java_interop_h.doubleToStringRyu(value));
}
Давайте посмотрим, как Project Panama
выглядит в бенчмарках:

Хотя Project Panama
все еще намного медленнее, чем без использования нативной библиотеки, он обеспечивает значительно лучшую производительность по сравнению с JNI
и JNR-FFI
!
Использование массивов: совместное использование памяти
Для следущего бенчмарка мы предоставим массив double
для нашего Rust
-кода. Для этого нам нужно выделить сегмент off-heap
-памяти с помощью API
Foreign Function & Memory
(FFM
). Сначала мы определяем ограниченную арену с помощью Arena.ofConfined()
. Эта арена будет определять время жизни выделяемой нами памяти. Затем мы выделяем сегмент памяти для нашего массива double
с помощью allocateFrom
и передаем его в наш интерфейс Rust
:
public static String doubleArrayToStringRyu(double[] array) {
String output;
try (Arena offHeap = Arena.ofConfined()) {
// allocate off-heap memory for input array
MemorySegment segment = offHeap.allocateFrom(ValueLayout.JAVA_DOUBLE, array);
output = segmentToString(java_interop_h.doubleArrayToStringRyu(segment, array.length));
} // release memory for input array
return output;
}
Выделенный сегмент памяти автоматически освобождается ареной, как только мы достигаем конца блока try
.
Давайте посмотрим, как функция обработки массива в Project Panama
выглядит по сравнению с другими методами:

Заключение
Вызов функции из нативной библиотеки добавляет некоторые накладные расходы на производительность. Это означает, что если вы не обрабатываете много данных или не выполняете много дорогостоящих вычислений, использование
FFI
обычно не оправдывает себя с точки зрения производительности. ОднакоFFI
все равно может быть полезным инструментом для получения доступа к библиотекам из других языков программирования или для постепенной миграции большой кодовой базы на другой язык, напримерRust
.JNI
требует от нас использования типовJava
на сторонеRust
и позволяет взаимодействовать со средойJava
через его интерфейс. Хотя это требует от нас написания специфичного дляJNI
кода на сторонеRust
, это делает удобной работу с объектамиJava
, такими как массивы, и дает полный контроль над тем, как мы взаимодействуем со средойJava
.С другой стороны,
JNR-FFI
иProject Panama
используют универсальный интерфейсC
на сторонеRust
. Это означает, что нам не придется писать специфичный дляJava
интерфейс, что упрощает работу с существующими библиотеками, которые уже предоставляют интерфейсC
. Однако это также означает, что нам следует быть осторожными с тем, как мы управляем памятью на сторонеJava
, чтобы предотвратить утечки памяти.Из трех опробованных нами методов
Project Panama
обеспечивает наилучшую производительность. Он также удобен в использовании с его автоматически сгенерированными биндингами с использованиемcbindgen
иjextract
.Project Panama
все еще находится в разработке и требует последней версииJDK
, что может быть недостатком для проектовJava
, которые застряли на старых версияхJDK
.Мы предполагаем, что
Project Panama
станет основным предпочтительным методомFFI
дляJava
. Хотя работа с типамиC
может быть незначительным неудобством, проблемы с памятью, которые они приносят, можно решить с помощью небольших функций-оболочек. В идеале в будущем можно было бы разработать специальный инструмент, такой какCXX
(как показано в нашем блоге по взаимодействию сC++
), который объединитcbindgen
иjextract
для создания безопасного интерфейса междуRust
иJava
. Но на данный момент иJNI
, иProject Panama
уже предлагают отличные возможности для интеграцииRust
в ваш проектJava
илиKotlin
.Код, используемый для различных бенчмарков, обсуждаемых в этом блоге, доступен на
GitHub
. В будущем мы, возможно, рассмотримUniFFI
, еще один инструмент для генерации биндинговFFI
изRust
дляKotlin
и других языков программирования.