Skip to main content

opencv_binding_generator/writer/rust_native/
mod.rs

1use std::collections::HashMap;
2use std::fmt::Debug;
3use std::fs::{File, OpenOptions};
4use std::io::{BufWriter, ErrorKind, Write};
5use std::path::{Path, PathBuf};
6use std::sync::LazyLock;
7use std::{fs, io, iter};
8
9use class::ClassExt;
10use comment::RenderComment;
11use dunce::canonicalize;
12use element::{RustElement, RustNativeGeneratedElement};
13use func::FuncExt;
14use semver::Version;
15pub use string_ext::RustStringExt;
16
17use crate::comment::strip_doxygen_comment_markers;
18use crate::field::Field;
19use crate::name_pool::NamePool;
20use crate::type_ref::{Constness, CppNameStyle, FishStyle, NameStyle};
21use crate::{
22	Class, CompiledInterpolation, Const, Element, Enum, Func, GeneratedType, GeneratorVisitor, IteratorExt, StrExt,
23	SupportedModule, Typedef, opencv_module_from_path, settings,
24};
25
26mod abstract_ref_wrapper;
27mod class;
28mod comment;
29mod constant;
30pub mod element;
31mod enumeration;
32mod field;
33mod func;
34mod function;
35pub mod renderer;
36mod smart_ptr;
37mod string_ext;
38mod tuple;
39pub mod type_ref;
40mod typedef;
41mod vector;
42
43type Entries = Vec<(String, String)>;
44type UniqueEntries = HashMap<String, String>;
45
46/// Writer of the source files used when building OpenCV to run on a native platform (as opposed to for example WASM)
47#[derive(Clone, Debug)]
48pub struct RustNativeBindingWriter {
49	debug: bool,
50	src_cpp_dir: PathBuf,
51	module: SupportedModule,
52	opencv_version: Version,
53	debug_path: PathBuf,
54	out_dir: PathBuf,
55	comment: String,
56	prelude_traits: Vec<String>,
57	consts: Entries,
58	enums: Entries,
59	rust_funcs: Entries,
60	rust_typedefs: UniqueEntries,
61	rust_classes: Entries,
62	extern_funcs: Entries,
63	extern_classes: Entries,
64	cpp_funcs: Entries,
65	cpp_classes: Entries,
66}
67
68impl RustNativeBindingWriter {
69	pub fn new(
70		src_cpp_dir: &Path,
71		out_dir: impl Into<PathBuf>,
72		module: SupportedModule,
73		opencv_version: Version,
74		debug: bool,
75	) -> Self {
76		let out_dir = out_dir.into();
77		let debug_path = out_dir.join(format!("{}.log", module.opencv_name()));
78		#[expect(clippy::collapsible_if)]
79		if false {
80			if debug {
81				File::create(&debug_path).expect("Can't create debug log");
82			}
83		}
84		Self {
85			debug,
86			src_cpp_dir: canonicalize(src_cpp_dir).expect("Can't canonicalize src_cpp_dir"),
87			module,
88			opencv_version,
89			debug_path,
90			out_dir,
91			comment: String::new(),
92			prelude_traits: vec![],
93			consts: vec![],
94			enums: vec![],
95			rust_funcs: vec![],
96			rust_typedefs: UniqueEntries::new(),
97			rust_classes: vec![],
98			extern_funcs: vec![],
99			extern_classes: vec![],
100			cpp_funcs: vec![],
101			cpp_classes: vec![],
102		}
103	}
104
105	fn emit_debug_log(&mut self, obj: &impl Debug) {
106		#[expect(clippy::collapsible_if)]
107		if false {
108			if self.debug {
109				let mut f = OpenOptions::new()
110					.append(true)
111					.open(&self.debug_path)
112					.expect("Can't open debug file");
113				writeln!(f, "{obj:#?}").expect("Can't write debug info");
114			}
115		}
116	}
117}
118
119impl GeneratorVisitor<'_> for RustNativeBindingWriter {
120	fn wants_file(&mut self, path: &Path) -> bool {
121		match self.module {
122			// tracking_legacy.hpp includes header from the video module, but we need those definitions in the tracking module
123			SupportedModule::Tracking if path.ends_with("video/detail/tracking.detail.hpp") => true,
124			_ => opencv_module_from_path(path) == Some(self.module),
125		}
126	}
127
128	fn visit_module_comment(&mut self, comment: String) {
129		self.comment = strip_doxygen_comment_markers(&comment);
130	}
131
132	fn visit_const(&mut self, cnst: Const) {
133		self.emit_debug_log(&cnst);
134		self.consts.push((
135			cnst.rust_name(NameStyle::decl()).into_owned(),
136			cnst.gen_rust(&self.opencv_version),
137		));
138	}
139
140	fn visit_enum(&mut self, enm: Enum) {
141		self.emit_debug_log(&enm);
142		self.enums.push((
143			enm.rust_name(NameStyle::decl()).into_owned(),
144			enm.gen_rust(&self.opencv_version),
145		));
146	}
147
148	fn visit_func(&mut self, func: Func) {
149		self.emit_debug_log(&func);
150		for func in func.with_companion_functions() {
151			let name = func.identifier();
152			self.rust_funcs.push((name.clone(), func.gen_rust(&self.opencv_version)));
153			self.extern_funcs.push((name.clone(), func.gen_rust_externs()));
154			self.cpp_funcs.push((name, func.gen_cpp()));
155		}
156	}
157
158	fn visit_typedef(&mut self, typedef: Typedef) {
159		self.emit_debug_log(&typedef);
160		let cpp_refname = typedef.cpp_name(CppNameStyle::Reference);
161		if !self.rust_typedefs.contains_key(cpp_refname.as_ref()) {
162			self
163				.rust_typedefs
164				.insert(cpp_refname.into_owned(), typedef.gen_rust(&self.opencv_version));
165		}
166	}
167
168	fn visit_class(&mut self, class: Class) {
169		self.emit_debug_log(&class);
170		if class.kind().is_trait() {
171			self
172				.prelude_traits
173				.push(class.rust_trait_name(NameStyle::decl(), Constness::Const).into_owned());
174			self
175				.prelude_traits
176				.push(class.rust_trait_name(NameStyle::decl(), Constness::Mut).into_owned());
177		}
178		let name = class.cpp_name(CppNameStyle::Reference).into_owned();
179		self.rust_classes.push((name.clone(), class.gen_rust(&self.opencv_version)));
180		self.extern_classes.push((name.clone(), class.gen_rust_externs()));
181		self.cpp_classes.push((name, class.gen_cpp()));
182	}
183
184	fn visit_generated_type(&mut self, typ: GeneratedType) {
185		let typ = typ.as_ref();
186		let safe_id = typ.element_safe_id();
187
188		fn write_generated_type(types_dir: &Path, typ: &str, safe_id: &str, generator: impl FnOnce() -> String) {
189			let suffix = format!(".type.{typ}");
190			let mut file_name = format!("050-{safe_id}");
191			ensure_filename_length(&mut file_name, suffix.len());
192			file_name.push_str(&suffix);
193			let path = types_dir.join(file_name);
194			let file = OpenOptions::new().create_new(true).write(true).open(&path);
195			match file {
196				Ok(mut file) => {
197					let gener = generator();
198					if !gener.is_empty() {
199						file
200							.write_all(gener.as_bytes())
201							.unwrap_or_else(|e| panic!("Can't write to {typ} file: {e}"));
202					} else {
203						drop(file);
204						fs::remove_file(&path).expect("Can't remove empty file");
205					}
206				}
207				Err(e) if e.kind() == ErrorKind::AlreadyExists => { /* expected, we need to exclusively create file */ }
208				Err(e) if e.kind() == ErrorKind::PermissionDenied => { /* happens sporadically on Windows */ }
209				Err(e) => panic!("Error while creating file: {} for {typ} generated type: {e}", path.display()),
210			}
211		}
212
213		write_generated_type(&self.out_dir, "rs", &safe_id, || typ.gen_rust(&self.opencv_version));
214		write_generated_type(&self.out_dir, "externs.rs", &safe_id, || typ.gen_rust_externs());
215		write_generated_type(&self.out_dir, "cpp", &safe_id, || typ.gen_cpp());
216	}
217
218	fn goodbye(mut self) {
219		static RUST_HDR: LazyLock<CompiledInterpolation> =
220			LazyLock::new(|| include_str!("tpl/module/rust_hdr.tpl").compile_interpolation());
221
222		static RUST_PRELUDE: LazyLock<CompiledInterpolation> =
223			LazyLock::new(|| include_str!("tpl/module/prelude.tpl.rs").compile_interpolation());
224
225		static CPP_HDR: LazyLock<CompiledInterpolation> =
226			LazyLock::new(|| include_str!("tpl/module/cpp_hdr.tpl.cpp").compile_interpolation());
227
228		let pub_use_traits = if self.prelude_traits.is_empty() {
229			"".to_string()
230		} else {
231			self.prelude_traits.sort_unstable();
232			format!("pub use super::{{{}}};", self.prelude_traits.join(", "))
233		};
234		let prelude = RUST_PRELUDE.interpolate(&HashMap::from([("pub_use_traits", pub_use_traits)]));
235		let comment = RenderComment::new(self.comment, &self.opencv_version);
236		let comment = comment.render_with_comment_marker("//!");
237		let module_opencv_name = self.module.opencv_name();
238		let rust_path = self.out_dir.join(format!("{module_opencv_name}.rs"));
239		{
240			let mut rust = BufWriter::new(File::create(rust_path).expect("Can't create rust file"));
241			rust
242				.write_all(
243					RUST_HDR
244						.interpolate(&HashMap::from([
245							("static_modules", settings::STATIC_RUST_MODULES.iter().join(", ").as_str()),
246							("comment", comment.as_ref()),
247							("prelude", &prelude),
248						]))
249						.as_bytes(),
250				)
251				.expect("Can't write rust file");
252			write_lines(&mut rust, self.consts).expect("Can't write consts to rust file");
253			write_lines(&mut rust, self.enums).expect("Can't write enums to rust file");
254			write_lines(&mut rust, self.rust_typedefs.into_iter().collect()).expect("Can't write typedefs to rust file");
255			write_lines(&mut rust, self.rust_funcs).expect("Can't write funcs to rust file");
256			write_lines(&mut rust, self.rust_classes).expect("Can't write classes to rust file");
257		}
258
259		let includes = if self.src_cpp_dir.join(format!("{module_opencv_name}.hpp")).exists() {
260			format!("#include \"{module_opencv_name}.hpp\"")
261		} else {
262			format!("#include \"ocvrs_common.hpp\"\n#include <opencv2/{module_opencv_name}.hpp>")
263		};
264		{
265			let cpp_path = self.out_dir.join(format!("{module_opencv_name}.cpp"));
266			let mut cpp = BufWriter::new(File::create(cpp_path).expect("Can't create cpp file"));
267			cpp.write_all(
268				CPP_HDR
269					.interpolate(&HashMap::from([("module", module_opencv_name), ("includes", &includes)]))
270					.as_bytes(),
271			)
272			.expect("Can't write cpp file");
273			cpp.write_all(b"extern \"C\" {\n")
274				.expect("Can't write code wrapper begin to cpp file");
275			write_lines(&mut cpp, self.cpp_funcs).expect("Can't write cpp funcs to file");
276			write_lines(&mut cpp, self.cpp_classes).expect("Can't write cpp classes to file");
277			cpp.write_all(b"}\n").expect("Can't write code wrapper end to cpp file");
278		}
279
280		let externs_path = self.out_dir.join(format!("{module_opencv_name}.externs.rs"));
281		let mut externs_rs = BufWriter::new(File::create(externs_path).expect("Can't create rust exports file"));
282		write_lines(&mut externs_rs, self.extern_funcs).expect("Can't write extern funcs to file");
283		write_lines(&mut externs_rs, self.extern_classes).expect("Can't write extern classes to file");
284	}
285}
286
287fn write_lines<T: AsRef<[u8]>>(mut out: impl Write, mut v: Vec<(String, T)>) -> io::Result<()> {
288	v.sort_unstable_by(|(name_left, _), (name_right, _)| name_left.cmp(name_right));
289	for (_, code) in v {
290		out.write_all(code.as_ref())?;
291	}
292	Ok(())
293}
294
295fn ensure_filename_length(file_name: &mut String, reserve: usize) {
296	const MAX_FILENAME_LEN: usize = 255;
297
298	let max_length = MAX_FILENAME_LEN - reserve;
299
300	if file_name.len() > max_length {
301		*file_name = file_name[..max_length].to_string();
302	}
303}
304
305fn rust_disambiguate_names<'tu, 'ge>(
306	args: impl IntoIterator<Item = Field<'tu, 'ge>>,
307) -> impl Iterator<Item = (String, Field<'tu, 'ge>)>
308where
309	'tu: 'ge,
310{
311	let args = args.into_iter();
312	let size_hint = args.size_hint();
313	NamePool::with_capacity(size_hint.1.unwrap_or(size_hint.0)).into_disambiguator(args, |f| f.rust_leafname(FishStyle::No))
314}
315
316fn rust_disambiguate_names_ref<'f, 'tu, 'ge>(
317	args: impl IntoIterator<Item = &'f Field<'tu, 'ge>>,
318) -> impl Iterator<Item = (String, &'f Field<'tu, 'ge>)>
319where
320	'tu: 'ge,
321	'tu: 'f,
322	'ge: 'f,
323{
324	let args = args.into_iter();
325	let size_hint = args.size_hint();
326	NamePool::with_capacity(size_hint.1.unwrap_or(size_hint.0)).into_disambiguator(args, |f| f.rust_leafname(FishStyle::No))
327}
328
329pub fn disambiguate_single_name(name: &str) -> impl Iterator<Item = String> + '_ {
330	let mut i = 0;
331	iter::from_fn(move || {
332		let out = format!("{}{}", name, disambiguate_num(i));
333		i += 1;
334		Some(out)
335	})
336}
337
338fn disambiguate_num(counter: usize) -> String {
339	match counter {
340		0 => "".to_string(),
341		n => format!("_{n}"),
342	}
343}