Практическое руководство по интеграции JNI + полный пример

Введение
Сегодня и Rust, и Java широко используются, но каждый из них по-своему хорош в своей области. В реалистичных сценариях часто бывает полезно комбинировать Rust и Java, чтобы одновременно повысить эффективность и системного кода, и кода приложений:
В приложении на Java может понадобиться обойтись без помощи сборщика мусора (GC) и вручную управлять памятью в тех областях программы, где всё зависит от высокой производительности.
Можно попробовать портировать на Rust алгоритм, требующий высокой производительности. Также это поможет, если нужно скрыть реализацию.
С другой стороны, в приложении, написанном на Rust, может потребоваться просто предоставить определённую возможность из Rust экосистеме Java, просто упаковав эту фичу в виде JAR.
В этом посте будет подробно рассказано, как организовать и интегрировать Rust и Java в рамках одного и того же проекта. Здесь мы делаем акцент на практике, разберём конкретные примеры кода и приведём пошаговые объяснения. Дочитав пост, вы узнаете, как написать межъязыковое приложение, в котором бесшовно взаимодействуют Rust и Java.
Контекст: понятие о JNI и об управлении памятью в Java
Нативный интерфейс Java (JNI) служит мостом, соединяющим Java и нативный код, написанный на C/C++ или Rust. Притом, что в синтаксическом отношении JNI относительно прост, известно, насколько он нетривиален на практике. Всё дело в заложенных в нём неявных правилах управления памятью и потоками.
Сегменты памяти в среде выполнения Java
Куча Java: Здесь располагаются объекты Java. Кучей автоматически управляет сборщик мусора.
Нативная память: это память, выделяемая нативным кодом (например, на Rust). Сборщик мусора ею непосредственно не управляет, поэтому при работе с ней требуется специально не допускать возникновения утечек.
Прочие: различные сегменты, например, кэши для кода и метаданные для скомпилированных классов. Понимать эти границы принципиально важно, чтобы писать высокопроизводительный межъязыковой код, безопасно работающий с памятью.
Практическая интеграция: проект rust-java-demo
Разберём реальный пример. В нашем открытом репозитории rust-java-demo продемонстрировано, как бесшовно интегрировать код Rust в приложения, написанные на Java.
Упаковываем платформо-специфичные библиотеки Rust в отдельный архив JAR
Байт-код Java не зависит от конкретной платформы, а двоичные файлы Rust — зависят. Если внедрить динамическую библиотеку Rust в архив JAR, возникает зависимость от платформы. Притом, что можно собрать отдельный файл JAR для каждой архитектуры, это осложнит распространение и развёртывание программы.
Есть лучший выход: упаковать платформо-специфичные библиотеки Rust в разные каталоги в пределах одного архива JAR, а затем динамически загружать нужную библиотеку во время выполнения.
Распаковав мультиплатформенный архив JAR (jar xf rust-java-demo-2c59460-multi-platform.jar), найдёте в нём такую структуру каталогов:

При помощи простой утилиты мы, ориентируясь на платформу хоста, загружаем нужную библиотеку:
static {
JarJniLoader.loadLib(
RustJavaDemo.class,
"/io/greptime/demo/rust/libs",
"demo"
);
}
При таком подходе можно гибко работать с платформами, не мешая разработчику и не жертвуя удобством эксплуатации.
Унификация логов в пределах Rust и Java
Отладка межъязыковых проектов может быстро превратиться в кошмар, если не позаботиться об унификации логирования. Решая эту проблему, мы как через воронку пропустили все логи — на Rust и на Java — через один и тот же бэкенд SLF4J.
На стороне Java определим простую обёртку Logger:
public class Logger {
private final org.slf4j.Logger inner;
public Logger(org.slf4j.Logger inner) {
this.inner = inner;
}
public void error(String msg) { inner.error(msg); }
public void info(String msg) { inner.info(msg); }
public void debug(String msg) { inner.debug(msg); }
// ...
}
После этого Rust будет вызывать данный логгер через интерфейс JNI. Вот упрощённая реализация на Rust:
impl log::Log for Logger {
fn log(&self, record: &log::Record) {
let env = ...; // получаем среду JNI
let java_logger = find_java_side_logger();
let logger_method = java_logger.methods.find_method(record.level());
unsafe {
env.call_method_unchecked(
java_logger,
logger_method,
ReturnType::Primitive(Primitive::Void),
&[JValue::from(format_msg(record)).as_jni()]
);
}
}
}
Затем регистрируем его как глобальный логгер:
log::set_logger(&LOGGER).expect("Failed to set global logger");
Теперь логи из обоих языков выводятся в рамках одного и того же потока. Это упрощает диагностику и мониторинг.
Вызов асинхронных функций Rust из Java
Одна из выдающихся черт Rust — его мощная асинхронная среда выполнения. К сожалению, методы JNI нельзя объявлять async, поэтому вызывать асинхронный код Rust из Java не так просто:
#[no_mangle]
pub extern "system" fn Java_io_greptime_demo_RustJavaDemo_hello(...) {
// ❌ Это не будет компилироваться
foo().await;
}
async fn foo() { ... }
Но block_on() блокирует актуальный поток, даже, если это поток Java. Вместо этого воспользуемся более идиоматичным подходом: скомбинируем асинхронное порождение задач с CompletableFuture на стороне Java, обеспечив таким образом неблокирующую интеграцию.
На стороне Java:
public class AsyncRegistry {
private static final AtomicLong FUTURE_ID = new AtomicLong();
private static final Map<Long, CompletableFuture<?>> FUTURE_REGISTRY = new ConcurrentHashMap<>();
}
public CompletableFuture<Integer> add_one(int x) {
long futureId = native_add_one(x); // Call Rust
return AsyncRegistry.take(futureId); // Get CompletableFuture
}
Этот паттерн, используемый в Apache OpenDAL , позволяет Java-разработчикам решать, когда блокировать код и блокировать ли вообще. Благодаря этому интеграция получается более гибкой.
Отображение ошибок Rust на исключения Java
Чтобы унифицировать обработку исключений в обоих языках, преобразуем Result::Err Rust в RuntimeException Java :
fn throw_runtime_exception(env: &mut JNIEnv, msg: String) {
let msg = if let Some(ex) = env.exception_occurred() {
env.exception_clear();
let exception_info = ...; // Извлекаем класс исключения + сообщение
format!("{}. Java exception occurred: {}", msg, exception_info)
} else {
msg
};
env.throw_new("java/lang/RuntimeException", &msg);
}
Так мы гарантируем, что код Java сможет единообразно обрабатывать все исключения, независимо от того, откуда они поступают — из Rust или из Java.
Заключение
В этой статье мы исследовали ключевые аспекты взаимодействия между Rust и Java:
Упаковку платформо-специфичных нативных библиотек в единый файл JAR.
Унификацию логов между Rust и Java.
Наладку взаимодействия между асинхронными функциями Rust и CompletableFuture из Java.
Отображение ошибок Rust на исключения Java.
Более подробные объяснения и полнофункциональный пример выложены в нашем открытом репозитории.
Gabenskiy
что-то введение не заметил, а где такая комбинация может понадобиться?
ph_piter Автор
В первую очередь о Rust под Android, о чем в блоге Google была статья в середине ноября https://security.googleblog.com/2025/11/rust-in-android-move-fast-fix-things.html и существует вот такой курс https://google.github.io/comprehensive-rust/android.html