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