Skip to main content

jvm_init/
lib.rs

1//! Create a Java Virtual Machine with classpath dependencies easily.
2//!
3//! This crate allows you to easily create a JVM with jarfiles added to the classpath to use with e.g. JNI.
4//!
5//! ## Example
6// Cannot run as there's no JVM available on rustdoc
7//! ```no_run
8//! # fn main() -> color_eyre::Result<()> {
9//! # let my_dependency = Vec::default();
10//! use jvm_init::JvmEngineBuilder;
11//! let engine = JvmEngineBuilder::default()
12//!     .add_dependency(&my_dependency, "example.jar")
13//!     .check_jni()
14//!     .try_init()?;
15//!
16//! // Use the thread handle with JNI
17//! let env = engine.attach_current_thread()?;
18//! # Ok(())
19//! # }
20//! ```
21
22pub mod error;
23
24use crate::error::EngineError;
25use jni::{InitArgsBuilder, JNIVersion, JavaVM};
26use std::fs;
27use std::io::Write;
28use std::ops::Deref;
29use std::path::Path;
30use std::pin::Pin;
31use tempfile::TempDir;
32
33pub struct JvmEngine {
34    jvm: Pin<Box<JavaVM>>,
35    // Need to keep it around to avoid deleting the JARfile dependencies
36    #[allow(unused)]
37    deps: TempDir,
38}
39
40struct JarDependency<'a> {
41    contents: &'a [u8],
42    name: &'a str,
43}
44
45pub struct JvmEngineBuilder<'a> {
46    deps: Vec<JarDependency<'a>>,
47    check_jni: bool,
48    verbose_jni: bool,
49}
50
51impl<'a> JvmEngineBuilder<'a> {
52    /// Create a new builder.
53    pub fn new() -> Self {
54        Self {
55            deps: Vec::default(),
56            verbose_jni: false,
57            check_jni: false,
58        }
59    }
60
61    /// Add a java classpath dependency. This dependency will be added to the classpath
62    /// when the VM is started. The contents should point to a valid jarfile. The name should end with .jar.
63    pub fn add_dependency(mut self, contents: &'a [u8], name: &'a str) -> Self {
64        self.deps.push(JarDependency { contents, name });
65        self
66    }
67
68    /// Enable the `-Xcheck:jni` flag.
69    pub fn check_jni(mut self) -> Self {
70        self.check_jni = true;
71        self
72    }
73
74    /// Enable the `-verbose:jni` flag.
75    pub fn verbose_jni(mut self) -> Self {
76        self.verbose_jni = true;
77        self
78    }
79
80    /// Try to initialize the virtual machine.
81    ///
82    /// # Errors
83    ///
84    /// - If an IO error occurs.
85    /// - If the JVM could not be initialized.
86    pub fn try_init(self) -> Result<JvmEngine, EngineError> {
87        JvmEngine::try_from_builder(self)
88    }
89}
90
91impl JvmEngine {
92    fn try_from_builder(builder: JvmEngineBuilder) -> Result<Self, EngineError> {
93        let tempdir = TempDir::new()?;
94        let classpath = builder
95            .deps
96            .into_iter()
97            .map(|dep| Self::write_jar(dep.contents, dep.name, tempdir.path()))
98            .collect::<Result<Vec<_>, EngineError>>()?;
99
100        let mut args = InitArgsBuilder::new().version(JNIVersion::V8);
101
102        if !classpath.is_empty() {
103            args = args.option(format!("-Djava.class.path={}", classpath.join(":")));
104        }
105
106        if builder.check_jni {
107            args = args.option("-Xcheck:jni");
108        }
109
110        if builder.verbose_jni {
111            args = args.option("-verbose:jni");
112        }
113
114        let jvm = JavaVM::new(args.build()?)?;
115        Ok(Self {
116            jvm: Box::pin(jvm),
117            deps: tempdir,
118        })
119    }
120
121    fn write_jar(bytes: &[u8], name: &str, dir: &Path) -> Result<String, EngineError> {
122        let path = dir.join(name);
123        let mut kernel = fs::File::create(&path)?;
124        kernel.write_all(bytes)?;
125
126        Ok(path.to_string_lossy().to_string())
127    }
128}
129
130impl<'a> Default for JvmEngineBuilder<'a> {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl Deref for JvmEngine {
137    type Target = JavaVM;
138
139    fn deref(&self) -> &Self::Target {
140        &self.jvm
141    }
142}