Skip to content

Commit ce25b6b

Browse files
committed
build: verify upstream jvm.h definitions at compile time
`scripts/jvm_h.py` downloads the appropriate jvm.h, extracts its function definitions, and generates a list that gets compared against in `runtime/build.rs`. Should make version bumps a lot easier in the future.
1 parent 16c4303 commit ce25b6b

File tree

7 files changed

+705
-4
lines changed

7 files changed

+705
-4
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ repository = "https://github.com/Serial-ATA/jvm"
2424
edition = "2024"
2525
license = "MIT OR APACHE-2.0"
2626

27+
[profile.dev]
28+
debug = "full"
29+
opt-level = 0
30+
2731
[workspace.lints.rust]
2832
rust_2018_idioms = { level = "deny", priority = -1 }
2933
rust_2021_compatibility = { level = "deny", priority = -1 }

justfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# -----------------------------------------------------------------------------
55

66
PROJECT_ROOT := justfile_directory()
7+
PYTHON_VENV := PROJECT_ROOT / ".venv"
78
BUILD_DIR := PROJECT_ROOT / "build"
89
DIST_DIR := BUILD_DIR / "dist"
910

@@ -28,6 +29,14 @@ native: native-debug
2829
dist *ARGS:
2930
PYTHONPATH={{ PROJECT_ROOT }} python3 {{ BUILD_DIR }}/entry.py {{ ARGS }}
3031

32+
venv:
33+
python -m venv {{ PYTHON_VENV }}
34+
{{ PYTHON_VENV }}/bin/python -m pip install --upgrade pip
35+
{{ PYTHON_VENV }}/bin/python -m pip install -r {{ PROJECT_ROOT }}/scripts/requirements.txt
36+
37+
script name *ARGS: venv
38+
{{ PYTHON_VENV }}/bin/python {{ PROJECT_ROOT }}/scripts/{{ name }}.py {{ ARGS }}
39+
3140
# Build and run the java binary with the provided arguments
3241
java +ARGS: debug
3342
just dist --profile debug

runtime/Cargo.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ bumpalo = "3.16.0"
3434
[build-dependencies]
3535
native_methods = { path = "../generators/native_methods" }
3636
build-deps.workspace = true
37+
quote.workspace = true
38+
syn.workspace = true
3739

3840
[features]
3941
default = ["libffi"]
@@ -45,7 +47,3 @@ workspace = true
4547
[lib]
4648
name = "jvm"
4749
crate-type = ["rlib", "dylib"]
48-
49-
[profile.dev]
50-
debuginfo = "full"
51-
opt-level = 0

runtime/build.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,191 @@
1+
#![feature(result_option_map_or_default)]
2+
3+
use std::collections::HashMap;
4+
use std::ffi::OsStr;
5+
use std::fmt::Display;
6+
use std::path::{Path, PathBuf};
7+
use syn::Type;
8+
19
fn main() {
210
println!("cargo::rerun-if-changed=../generators/native_methods/");
11+
println!("cargo::rerun-if-changed=../generated");
312
build_deps::rerun_if_changed_paths("src/native/**/*.def").unwrap();
413

514
if let Err(e) = native_methods::generate() {
615
println!("cargo::error=Failed to generate native methods: {e}");
716
std::process::exit(1);
817
}
18+
19+
collect_jvm_h()
20+
}
21+
22+
/// Compares the function definitions in `./vm_functions.txt` to the ones defined in `./src/native/jvm`.
23+
///
24+
/// The actual generation is handled by `../scripts/jvm_h.py`.
25+
fn collect_jvm_h() {
26+
fn collect_files(src_dir: &Path) -> std::io::Result<Vec<PathBuf>> {
27+
let mut ret = Vec::new();
28+
for entry in std::fs::read_dir(src_dir)? {
29+
let entry = entry?;
30+
31+
let path = entry.path();
32+
if path.is_dir() {
33+
ret.append(&mut collect_files(&path)?);
34+
return Ok(ret);
35+
}
36+
37+
if path.extension().and_then(OsStr::to_str) != Some("rs") {
38+
continue;
39+
}
40+
41+
ret.push(path);
42+
}
43+
44+
Ok(ret)
45+
}
46+
47+
#[derive(Debug, PartialEq)]
48+
struct JniFunction {
49+
name: String,
50+
parameters: Vec<String>,
51+
return_type: Option<String>,
52+
}
53+
54+
impl Display for JniFunction {
55+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56+
let ret_ty = self
57+
.return_type
58+
.as_ref()
59+
.map_or_default(|ret| format!(" -> {ret}"));
60+
write!(f, "{}({}){ret_ty}", self.name, self.parameters.join(", "))
61+
}
62+
}
63+
64+
let vm_functions = Path::new(env!("CARGO_MANIFEST_DIR")).join("vm_functions.txt");
65+
let jvm_h_py = Path::new(env!("CARGO_MANIFEST_DIR"))
66+
.join("..")
67+
.join("scripts")
68+
.join("jvm_h.py");
69+
70+
println!("cargo:rerun-if-changed={}", vm_functions.display());
71+
println!("cargo:rerun-if-changed={}", jvm_h_py.display());
72+
if !vm_functions.exists() {
73+
panic!(
74+
"Expected VM function list at `{}`. Regenerate it with `{}`",
75+
vm_functions.display(),
76+
jvm_h_py.canonicalize().unwrap().display()
77+
);
78+
}
79+
80+
let expected_vm_functions =
81+
std::fs::read_to_string(&vm_functions).expect("failed to read VM functions");
82+
83+
let src_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
84+
.join("src")
85+
.join("native")
86+
.join("jvm");
87+
println!("cargo:rerun-if-changed={}", src_dir.display());
88+
89+
let mut defined_jvm_functions = HashMap::new();
90+
for file in collect_files(&src_dir).unwrap() {
91+
let content = std::fs::read_to_string(&file).expect("Failed to read file");
92+
93+
let file = match syn::parse_file(&content) {
94+
Ok(file) => file,
95+
Err(e) => {
96+
panic!("Failed to parse file at '{}': {e}", file.display());
97+
},
98+
};
99+
100+
for item in file.items {
101+
fn encode_type(ty: &Type) -> String {
102+
match ty {
103+
Type::Ptr(ty) => {
104+
let mutability = if ty.const_token.is_some() {
105+
"const"
106+
} else {
107+
"mut"
108+
};
109+
let elem = &ty.elem;
110+
format!("*{mutability} {}", quote::quote!(#elem))
111+
},
112+
_ => quote::quote!(#ty).to_string(),
113+
}
114+
}
115+
116+
let syn::Item::Fn(item_fn) = item else {
117+
continue;
118+
};
119+
120+
let is_jni_call = item_fn
121+
.attrs
122+
.iter()
123+
.any(|attr| attr.meta.path().is_ident("jni_call"));
124+
125+
if !is_jni_call {
126+
continue;
127+
}
128+
129+
let name = item_fn.sig.ident.to_string();
130+
let mut parameters = Vec::new();
131+
132+
for input in item_fn.sig.inputs.iter() {
133+
if let syn::FnArg::Typed(pat_type) = input {
134+
parameters.push(encode_type(&pat_type.ty));
135+
}
136+
}
137+
138+
let return_type = match &item_fn.sig.output {
139+
syn::ReturnType::Default => None,
140+
syn::ReturnType::Type(_, ty) => Some(encode_type(ty)),
141+
};
142+
143+
let existing = defined_jvm_functions.insert(
144+
name.clone(),
145+
JniFunction {
146+
name,
147+
parameters,
148+
return_type,
149+
},
150+
);
151+
152+
assert!(existing.is_none());
153+
}
154+
}
155+
156+
for line in expected_vm_functions.lines() {
157+
if line.starts_with('#') {
158+
continue;
159+
}
160+
161+
let mut parts = line.split('\t');
162+
let name = parts.next().unwrap();
163+
let params = parts.next().unwrap();
164+
let return_type = parts
165+
.next()
166+
.map(|ret| ret.strip_prefix("-> ").unwrap().to_string());
167+
168+
let expected = JniFunction {
169+
name: name.to_string(),
170+
parameters: params
171+
.split(',')
172+
.filter(|p| !p.is_empty())
173+
.map(ToString::to_string)
174+
.collect(),
175+
return_type,
176+
};
177+
178+
let Some(defined) = defined_jvm_functions.get(&*name) else {
179+
println!("cargo::warning=JVM function {name} not found",);
180+
continue;
181+
};
182+
183+
if &expected != defined {
184+
println!(
185+
"cargo::warning=JVM function {} has the wrong signature. (found: {defined}, \
186+
expected: {expected})",
187+
defined.name
188+
);
189+
}
190+
}
9191
}

0 commit comments

Comments
 (0)