0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何用Rust通过JNI和Java进行交互

jf_wN0SrCdH 来源:Rust语言中文社区 2023-10-17 11:41 次阅读

近期工作中有Rust和Java互相调用需求,这篇文章主要介绍如何用Rust通过JNI和Java进行交互,还有记录一下开发过程中遇到的一些坑。


JNI简单来说是一套Java与其他语言互相调用的标准,主要是C语言,官方也提供了基于C的C++接口。 既然是C语言接口,那么理论上支持C ABI的语言都可以和Java语言互相调用,Rust就是其中之一。


关于JNI的历史背景以及更详细的介绍可以参考官方文档


在Rust中和Java互相调用,可以使用原始的JNI接口,也就是自己声明JNI的C函数原型,在Rust里按照C的方式去调用,但这样写起来会很繁琐,而且都是unsafe的操作; 不过Rust社区里已经有人基于原始的JNI接口,封装好了一套safe的接口,crate的名字就叫jni,用这个库来开发就方便多了


文中涉及的代码放在了这个github仓库https://github.com/metaworm/rust-java-demo


Rust JNI 工程配置


如果你熟悉Cargo和Maven,可以跳过这一节,直接看我提供的github源码即可

Rust工程配置


首先,通过cargo new java-rust-demo创建一个rust工程

然后切换到工程目录cd java-rust-demo,并编辑Cargo.toml:修改类型为动态库、加上对 jni crate 的依赖


	

[package] name = "rust-java-demo" version = "0.1.0" edition = "2021" [lib] crate-type = ['cdylib'] [dependencies] jni = {version = '0.19'}

重命名src目录下的main.rslib.rs,Rust库类型的工程编译入口为 lib.rs,然后添加以下代码


	

use jni::*; use jni::JNIEnv; #[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_init(env: JNIEnv, _class: JClass) { println!("rust-java-demo inited"); }



然后执行cargo build构建,生成的动态库默认会位于target/debug目录下,我这里用的linux系统,动态库文件名为librust_java_demo.so,如果是Windows系统,文件名为rust_java_demo.dll


这样,我们第一个JNI函数就创建成功了! 通过Java_pers_metaworm_RustJNI_init这个导出函数,给了Java的pers.metaworm.RustJNI这个类提供了一个native的静态方法init; 这里只是简单地打印了一句话,后面会通过这个初始化函数添加更多的功能


Java工程配置


还是在这个工程目录里,把Java部分的代码放在java这个目录下,在其中创建pers/metaworm/RustJNI.java文件


	

package pers.metaworm; public class RustJNI { static { System.loadLibrary("rust_java_demo"); } public static void main(String[] args) { init(); } static native void init(); }


我们使用流行的 maven 工具来构建Java工程,在项目根目录下创建 maven 的工程文件pom.xml


	

xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0modelVersion> <groupId>pers.metawormgroupId> <artifactId>RustJNIartifactId> <version>1.0-SNAPSHOTversion> <properties> <exec.mainClass>pers.metaworm.RustJNIexec.mainClass> <maven.compiler.source>1.8maven.compiler.source> <maven.compiler.target>1.8maven.compiler.target> <maven.compiler.encoding>UTF-8maven.compiler.encoding> properties> <dependencies> dependencies> <build> <sourceDirectory>javasourceDirectory> <plugins> <plugin> <groupId>org.apache.maven.pluginsgroupId> <artifactId>maven-compiler-pluginartifactId> <version>2.4version> <configuration> <encoding>UTF-8encoding> configuration> plugin> plugins> build> project>


运行 DMEO 工程


上面的工程配置弄好之后,就可以使用cargo build命令构建Rust提供的JNI动态库,mvn compile命令来编译Java代码


Rust和Java代码都编译好之后,执行java -Djava.library.path=target/debug -classpath target/classes pers.metaworm.RustJNI来运行


其中-Djava.library.path=target/debug指定了我们JNI动态库所在的路径,-classpath target/classes指定了Java代码的编译输出的类路径,pers.metaworm.RustJNI是Java main方法所在的类


不出意外的话,运行之后会在控制台输出init函数里打印的"rust-java-demo inited"


Java调用Rust


接口声明


前面的Java_pers_metaworm_RustJNI_init函数已经展示了如何给Java暴露一个native方法,即导出名称为Java_<类完整路径>_<方法名>的函数,然后在Java对应的类里声明对应的native方法


拓展:除了通过导出函数给Java提供native方法,还可以通过 RegisterNatives 函数动态注册native方法,对应的jni封装的函数为JNIEnv::register_native_methods,一般动态注册会在JNI_Onload这个导出函数里执行,jvm加载jni动态库时会执行这个函数(如果有的话)


当在Java里首次调用native方法时,JVM就会寻找对应名称的导出的或者动态注册的native函数,并将Java的native方法和Rust的函数关联起来;如果JVM没找到对应的native函数,则会报java.lang.UnsatisfiedLinkError异常


为了演示,我们再添加一些代码来覆盖更多的交互场景


lib.rs


	

use jni::*; use jni::{jint, jobject, jstring}; use jni::JNIEnv; #[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_addInt( env: JNIEnv, _class: JClass, a: jint, b: jint, ) -> jint { a + b } #[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_getThisField( env: JNIEnv, this: JObject, name: JString, sig: JString, ) -> jobject { let result = env .get_field( this, &env.get_string(name).unwrap().to_string_lossy(), &env.get_string(sig).unwrap().to_string_lossy(), ) .unwrap(); result.l().unwrap().into_inner() }


RustJNI.java


	

package pers.metaworm; public class RustJNI { static { System.loadLibrary("rust_java_demo"); } public static void main(String[] args) { init(); System.out.println("test addInt: " + (addInt(1, 2) == 3)); RustJNI jni = new RustJNI(); System.out.println("test getThisField: " + (jni.getThisField("stringField", "Ljava/lang/String;") == jni.stringField)); System.out.println("test success"); } String stringField = "abc"; static native void init(); static native int addInt(int a, int b); native Object getThisField(String name, String sig); }


其中,addInt方法接收两个int参数,并返回相加的结果;getThisField是一个实例native方法,它获取this对象指定的字段并返回


参数传递


从上一节的例子里可以看到,jni函数的第一个参数总是JNIEnv,很多交互操作都需要通过这个对象来进行; 第二个参数是类对象(静态native方法)或this对象(实例native方法); 从第三个参数开始,每一个参数对应Java的native方法所声明的参数


对于基础的参数类型,可以直接用use jni::*提供的j开头的系列类型来声明,类型对照表:


Java 类型 Native 类型 类型描述
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void not applicable



对于引用类型(复合类型/对象类型),可以统一用jni::JObject声明;JObject是对jobject的rust封装,带有生命周期参数;对于String类型,也可以用 JString 来声明,JString是对JObject的一层简单封装


抛异常


前面的Java_pers_metaworm_RustJNI_getThisField函数里,用了很多unwrap,这在生产环境中是非常危险的,万一传了一个不存在的字段名,就直接crash了;所以我们改进一下这个函数,让他支持抛异常,出错的时候能让Java捕获到


	

#[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_getThisFieldSafely( env: JNIEnv, this: JObject, name: JString, sig: JString, ) -> jobject { let result = (|| { env.get_field( this, &env.get_string(name)?.to_string_lossy(), &env.get_string(sig)?.to_string_lossy(), )? .l() })(); match result { Ok(res) => res.into_inner(), Err(err) => { env.exception_clear().expect("clear"); env.throw_new("Ljava/lang/Exception;", format!("{err:?}")) .expect("throw"); std::null_mut() } } }


Java层的测试代码为

        

try { System.out.println("test getThisFieldSafely: " + (jni.getThisFieldSafely("stringField", "Ljava/lang/String;") == jni.stringField)); jni.getThisFieldSafely("fieldNotExists", "Ljava/lang/String;"); } catch (Exception e) { System.out.println("test getThisFieldSafely: catched exception: " + e.toString()); }


通过env.throw_new("Ljava/lang/Exception;", format!("{err:?}"))抛出了一个异常,从JNI函数返回后,Java就会捕获到这个异常; 代码里可以看到在抛异常之前,调用了env.exception_clear()来清除异常,这是因为前面的get_field已经抛出一个异常了,当env里已经有一个异常的时候,后续再调用env的函数都会失败,这个异常也会继续传递到上层的Java调用者,所以其实这里没有这两句,直接返回null的话,Java也可以捕获到异常;但我们通过throw_new可以自定义异常类型及异常消息

这其实不是一个典型的场景,典型的场景应该是Rust里的某个调用返回了Error,然后通过抛异常的形式传递到Java层,比如除0错误


	

#[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_divInt( env: JNIEnv, _class: JClass, a: jint, b: jint, ) -> jint { if b == 0 { env.throw_new("Ljava/lang/Exception;", "divide zero") .expect("throw"); 0 } else { a / b } }

Rust调用Java


创建对象、调用方法、访问字段...


下面用一段代码展示如何在Rust中创建Java对象、调用方法、获取字段、处理异常等常见用法


	

#[allow(non_snake_case)] fn call_java(env: &JNIEnv) { match (|| { let File = env.find_class("java/io/File")?; // 获取静态字段 let separator = env.get_static_field(File, "separator", "Ljava/lang/String;")?; let separator = env .get_string(separator.l()?.into())? .to_string_lossy() .to_string(); println!("File.separator: {}", separator); assert_eq!(separator, format!("{}", std::MAIN_SEPARATOR)); // env.get_static_field_unchecked(class, field, ty) // 创建实例对象 let file = env.new_object( "java/io/File", "(Ljava/lang/String;)V", &[JValue::Object(env.new_string("")?.into())], )?; // 调用实例方法 let abs = env.call_method(file, "getAbsolutePath", "()Ljava/lang/String;", &[])?; let abs_path = env .get_string(abs.l()?.into())? .to_string_lossy() .to_string(); println!("abs_path: {}", abs_path); jni::Result::Ok(()) })() { Ok(_) => {} // 捕获异常 Err(jni::JavaException) => { let except = env.exception_occurred().expect("exception_occurred"); let err = env .call_method(except, "toString", "()Ljava/lang/String;", &[]) .and_then(|e| Ok(env.get_string(e.l()?.into())?.to_string_lossy().to_string())) .unwrap_or_default(); env.exception_clear().expect("clear exception"); println!("call java exception occurred: {err}"); } Err(err) => { println!("call java error: {err:?}"); } } } #[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_callJava(env: JNIEnv) { println!("call java"); call_java(&env) }


总结一下常用的函数,具体用法可以参考JNIEnv的文档

  • 创建对象new_object

  • 创建字符串对象new_string

  • 调用方法call_methodcall_static_method

  • 获取字段get_fieldget_static_field

  • 修改字段set_fieldset_static_field


要注意的是调用方法、创建对象等需要传一个方法类型签名,这是因为Java支持方法重载,同一个类里一个名称的函数可能有多个,所以需要通过类型签名来区分,类型签名的规则可以参考官方文档


异常处理


call_java函数展示了如何在Rust中处理Java的异常: 通过JNIEnv对象动态获取字段或者调用方法,都会返回一个jni::Result类型,对应的Error类型为jni::Error;如果Error是jni::JavaException则表明在JVM执行过程中,某个地方抛出了异常,这种情况下就可以用exception_occurred函数来获取异常对象进行处理,然后调用exception_clear来清除异常,如果再返回到Java便可以继续执行


在非Java线程中调用Java


从Java中调用的Rust代码,本身就处于一个Java线程中,第一个参数为JNIEnv对象,Rust代码用这个对象和Java进行交互; 实际应用场景中,可能需要从一个非Java线程或者说我们自己的线程中去调用Java的方法,但我们的线程没有JNIEnv对象,这时就需要调用JavaVM::attach_current_thread函数将当前线程附加到JVM上,来获得一个JNIEnv


	

#[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_callJavaThread(env: JNIEnv) { let vm = env.get_java_vm().expect("get jvm"); std::spawn(move || { println!("call java in another thread"); let env = vm.attach_current_thread().expect("attach"); call_java(&env); }); }


attach_current_thread函数返回一个AttachGuard对象,可以解引用为JNIEnv,并且在作用域结束drop的时候自动调用detach_current_thread函数;原始的AttachCurrentThreadJNI函数,如果当前线程已经attach了,则会抛异常,jni crate里的JavaVM::attach_current_thread做了一层封装,如果当前已经attach了,则会返回之前attach的对象,保证不会重复attach


JavaVM对象通过JNIEnv::get_java_vm函数获取,可以在初始化的时候将这个变量存起来,给后续的其他线程使用


局部引用、全局引用与对象缓存


关于局部引用与全局引用的官方文档


Rust提供的native函数,传过来的对象引用都是局部引用,局部引用只在本次调用JNI调用范围内有效,而且不能跨线程使用;如果跨线程,必须使用全局引用


可以通过JNIEnv::new_global_ref来获取JClass、JObject的全局引用,这个函数返回一个GlobalRef对象,可以通过GlobalRef::as_object转成JObject或者JClass等对象;GlobalRef对象drop的时候,会调用DeleteGlobalRef将JVM内部的引用删除


前面的代码,从Rust调用Java方法都是通过名称加方法签名调用的,这种方式,写起来很舒服,但运行效率肯定是非常低的,因为每次都要通过名称去查找对应的方法


其实JNI原始的C接口,是通过jobjectID、jclassID、jmethodID、jfieldID来和Java交互的,只不过是jni crate给封装了一层比较友好的接口


如果我们对性能要求比较高,则可以在初始化的时候获取一些JClass、JObject的全局引用,缓存起来,后面再转成JClass、JObject来使用,千万不要对jmethodID、jfieldID获取全局引用,因为这俩都是通过jclassID生成的,其声明周期和jclassID对应的对象相同,不是需要GC的对象,如果对jmethodID获取全局引用然后调用,会导致某些JVM Crash;对于jmethodID、jfieldID,则可以基于JClass、JObject的全局引用获取,后面直接使用即可


获取到这些全局的ID之后,就可以通过JNIEnv::call_method_unchecked系列函数,来更高效地调用Java


我用Rust强大的宏,实现了这个过程,可以让我们直接在Rust中以声明的方式缓存的所需类及其方法ID


	

#[allow(non_snake_case)] pub mod cache { use anyhow::Context; use jni::Result as JniResult; use jni::*; use jni::JNIEnv; pub fn method_global_ref<'a>( env: JNIEnv<'a>, class: JClass, name: &str, sig: &str, ) -> JniResult'a>> { let method = env.get_method_id(class, name, sig)?.into_inner(); Ok(JMethodID::from(method.cast())) } pub fn static_method_global_ref<'a>( env: JNIEnv<'a>, class: JClass, name: &str, sig: &str, ) -> ::Result'a>> { let method = env.get_static_method_id(class, name, sig)?.into_inner(); Ok(JStaticMethodID::from(method.cast())) } macro_rules! gen_global_ref { (@method_type) => { JMethodID<'static> }; (@method_type static) => { JStaticMethodID<'static> }; (@method_ref) => { method_global_ref }; (@method_ref static) => { static_method_global_ref }; ( $( #[name = $classname:literal] class $name:ident { $($method:ident : $($modify:ident)* $sig:literal,)* } )* ) => { $( #[allow(non_snake_case)] pub struct $name { pub class: JClass<'static>, $(pub $method: gen_global_ref!(@method_type $($modify)*),)* } impl $name { pub fn from_env(env: JNIEnv<'static>) -> anyhow::Result<Self> { Self::from_class(env, env.find_class($classname)?) } pub fn from_class(env: JNIEnv<'static>, class: JClass) -> anyhow::Result<Self> { let cls = env.new_global_ref(class)?; let class = JClass::from(*cls.as_obj()); core::forget(cls); Ok(Self { class, $( $method: gen_global_ref!(@method_ref $($modify)*)( env, class, stringify!($method), $sig).context(stringify!($method) )?, )* }) } } // TODO: impl Drop )* pub struct CachedClasses { $(pub $name: $name,)* } impl CachedClasses { pub fn from_env(env: JNIEnv<'static>) -> anyhow::Result<Self> { Ok(Self { $($name: $name::from_env(env).context(stringify!($name))?,)* }) } } unsafe impl Sync for CachedClasses {} unsafe impl Send for CachedClasses {} } } gen_global_ref! { #[name = "java/lang/Thread"] class Thread { currentThread: static "()Ljava/lang/Thread;", getStackTrace: "()[Ljava/lang/StackTraceElement;", } #[name = "java/lang/StackTraceElement"] class StackTraceElement { getLineNumber: "()I", toString: "()Ljava/lang/String;", } #[name = "java/io/File"] class File { getAbsolutePath: "()Ljava/lang/String;", } } static mut CLASSES: Option<Box> = None; pub unsafe fn init(env: JNIEnv<'static>) -> anyhow::Result<Option<Box>> { Ok(CLASSES.replace(CachedClasses::from_env(env)?.into())) } pub fn get() -> &'static CachedClasses { unsafe { CLASSES.as_ref().expect("Cached Java Classed not inited") } } }



审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 接口
    +关注

    关注

    33

    文章

    8580

    浏览量

    151031
  • JAVA
    +关注

    关注

    19

    文章

    2966

    浏览量

    104704
  • C语言
    +关注

    关注

    180

    文章

    7604

    浏览量

    136711
  • C++
    C++
    +关注

    关注

    22

    文章

    2108

    浏览量

    73624
  • Rust
    +关注

    关注

    1

    文章

    228

    浏览量

    6601

原文标题:【Rust笔记】Rust与Java交互-JNI模块编写-实践总结

文章出处:【微信号:Rust语言中文社区,微信公众号:Rust语言中文社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    聊聊Rust与C语言交互的具体步骤

    rust FFI 是rust与其他语言互调的桥梁,通过FFI rust 可以有效继承 C 语言的历史资产。本期通过几个例子来聊聊
    发表于 07-06 11:15 1705次阅读

    【NanoPi Duo开发板试用申请】智能家居,Java高级语言控制

    服务器进行交互,并通过Android端进行控制。 实现轻松的接入,为智能硬件助力,将底层硬件的控制通过j
    发表于 09-21 15:35

    请问Labview与Java交互是否可以实现?

    各位大佬,小弟目前在做毕业设计是关于人工智能的,本人想利用laview myrio 机器人的sensor 传送数据到 java, 然后java进行信息处理然后输出一个相应的信息,然后传递到labview 使得机器人
    发表于 08-13 17:09

    芯灵思SinlinxA33开发板的安卓控制LED-2-JNI基础

    ,对应于在java层使用native关键字声明的方法的。直白的说,就是在Java层声明,C/C++语言实现的。当然,这个函数并不一般,它会通过JNI某种机制与
    发表于 02-22 16:55

    基于JNI的嵌入式手机软件该如何去设计?

    Java的性能问题及几种解决方案什么是JNI技术基于JNI的嵌入式手机软件开发实例
    发表于 04-23 07:17

    何用 rust 语言开发 stm32

    本文介绍如何用 rust 语言开发 stm32。开发平台为 linux(gentoo)。硬件准备本文使用的芯片为 STM32F103C8T6。该芯片性价比较高,价格低廉,适合入门学习。需要
    发表于 11-26 06:20

    JNI如何实现Android stdio IIC与从机通信的呢

    RK3288如何实现JNI对接上层Java和下层的C++呢?JNI如何实现Android stdio IIC与从机通信的呢?
    发表于 03-04 06:04

    何用java映射创建java对象和调用java对象呢

    java是一种解析语言,java程序是通过java虚拟机解析.class的方式运行起来。因此,java中就存在
    发表于 04-11 14:43

    java与c之间的数据交互

    最近作一个tiemsten数据库的项目,用到了jni技术。在这个项目中,我们用java来写界面和业务逻辑,用c语言写数据库odbc访问。单纯的odbc其实没有什么难的,但是在java和c之间
    发表于 11-27 10:22 1789次阅读

    JNI java调用so动态库方法

    JNI Java调用so包相关问题总结,出现了很多问题,按照操作应该不会发生不到so包的错误,其实最后出现的也是说加载不到libpython2.7.x.so.1,我就纳闷了,怎么和python扯上
    发表于 11-28 13:13 3517次阅读

    RSA算法的JNI封装步骤

    要求较高的算法往往是基于C/C++语言(与硬件关联性更强)实现的。如果应用程序需要基于JAVA编程实现时,这就会有一些矛盾。此时,通过JNI技术,Java开发者可以在不了解算法内容的情
    的头像 发表于 06-04 17:45 2195次阅读
    RSA算法的<b class='flag-5'>JNI</b>封装步骤

    Go/Rust挑战Java/Python地位

    编程语言方面,Java 和 Python 仍然遥遥领先,并且分别微小增长了 1.7% 和 3.4%;围绕 Go (增长 20%) 和 Rust (增长 22%) 的兴趣则大幅增加。报告称,如果这种
    的头像 发表于 03-06 10:19 699次阅读

    何用Java播放声音

    在本篇文章中,我们将学习如何用Java播放音乐,Java 声音 API 的设计是为了流畅和连续地播放声音,甚至是很长的声音。我们将使用 Java 提供的 Clip 和 SourceDa
    的头像 发表于 10-09 10:56 3235次阅读

    何用Java代码调用

    CloneNotSupportedException ; 你敢说你没用过这些方法?如果你用过,那你就是一定用过不是Java语言编写的方法。 答案就是【native】关键词,用此关键词修饰的方法,多数情况就不是用Java实现的。 那么为什么要用 native 来修饰方法,
    的头像 发表于 10-11 15:29 499次阅读
    如<b class='flag-5'>何用</b><b class='flag-5'>Java</b>代码调用

    何用Rust编写一个ChatGPT桌面应用(保姆级教程)

    用IDEA开发的java仔) 安装 Rust 语言工具链:首先,请确保你已安装了 Rust 编程语言工具链,包括 Rust 编译器 (rustc) 和包管理工具 (cargo)。可以
    的头像 发表于 09-25 11:19 345次阅读
    如<b class='flag-5'>何用</b><b class='flag-5'>Rust</b>编写一个ChatGPT桌面应用(保姆级教程)