playdate_bindgen/
lib.rs

1#![cfg_attr(feature = "documentation", feature(get_mut_unchecked))]
2pub extern crate bindgen;
3
4use std::env;
5use std::path::{Path, PathBuf};
6use bindgen::callbacks::DeriveInfo;
7use bindgen::{EnumVariation, RustTarget, Builder, MacroTypeVariation};
8use utils::consts::*;
9use utils::toolchain::gcc::{ArmToolchain, Gcc};
10use utils::toolchain::sdk::Sdk;
11pub use bindgen_cfg as cfg;
12use rustify::rename::SharedRenamed;
13
14
15pub mod error;
16pub mod gen;
17pub mod rustify {
18	pub mod rename;
19}
20
21
22type Result<T, E = error::Error> = std::result::Result<T, E>;
23
24
25pub const SDK_VER_SUPPORTED: &str = ">=2.1.0, <3.0.0";
26
27
28/// Generated Rust bindings.
29pub enum Bindings {
30	Bindgen(Box<bindgen::Bindings>),
31	#[cfg(feature = "extra-codegen")]
32	Engaged(gen::Bindings),
33}
34
35
36impl Bindings {
37	#[inline(always)]
38	/// Write these bindings as source text to a file.
39	pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
40		match self {
41			Bindings::Bindgen(this) => this.write_to_file(path),
42			#[cfg(feature = "extra-codegen")]
43			Bindings::Engaged(this) => this.write_to_file(path),
44		}
45	}
46
47	#[inline(always)]
48	/// Write these bindings as source text to the given `Write`able.
49	pub fn write<'a>(&self, writer: Box<dyn std::io::Write + 'a>) -> std::io::Result<()> {
50		match self {
51			Bindings::Bindgen(this) => this.write(writer),
52			#[cfg(feature = "extra-codegen")]
53			Bindings::Engaged(this) => this.write(writer),
54		}
55	}
56}
57
58impl std::fmt::Display for Bindings {
59	#[inline(always)]
60	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61		match self {
62			Bindings::Bindgen(this) => std::fmt::Display::fmt(this, f),
63			#[cfg(feature = "extra-codegen")]
64			Bindings::Engaged(this) => std::fmt::Display::fmt(this, f),
65		}
66	}
67}
68
69
70pub struct Generator {
71	/// Playdate SDK.
72	pub sdk: Sdk,
73	/// Version of the Playdate SDK.
74	pub version: semver::Version,
75
76	/// ARM GCC.
77	pub gcc: ArmToolchain,
78
79	/// Suggested filename for export bindings.
80	pub filename: cfg::Filename,
81	/// Configured [`bindgen::Builder`].
82	pub builder: Builder,
83
84	/// Renamed symbols, if `rustify` is enabled.
85	renamed: SharedRenamed,
86
87	// configuration
88	pub derives: cfg::Derive,
89	pub features: cfg::Features,
90}
91
92
93impl Generator {
94	pub fn new(cfg: cfg::Cfg) -> Result<Self> { create_generator(cfg) }
95
96	pub fn generate(mut self) -> Result<Bindings> {
97		// disable formatting if we gonna extra work:
98		if cfg!(feature = "extra-codegen") {
99			self.builder = self.builder.formatter(bindgen::Formatter::None);
100		}
101
102		// generate:
103		let bindings = self.builder.generate()?;
104
105		#[cfg(not(feature = "extra-codegen"))]
106		return Ok(Bindings::Bindgen(bindings.into()));
107
108
109		#[cfg(feature = "extra-codegen")]
110		gen::engage(
111		            &bindings,
112		            self.renamed,
113		            &self.features,
114		            &self.filename.target,
115		            &self.sdk,
116		            None,
117		).map(Bindings::Engaged)
118	}
119}
120
121
122fn create_generator(cfg: cfg::Cfg) -> Result<Generator, error::Error> {
123	println!("cargo::rerun-if-env-changed=TARGET");
124	let cargo_target_triple = env::var("TARGET").expect("TARGET cargo env var");
125
126	println!("cargo::rerun-if-env-changed=PROFILE");
127	let cargo_profile = env::var("PROFILE").expect("PROFILE cargo env var");
128	let is_debug = cargo_profile == "debug" || env_cargo_feature("DEBUG");
129
130	let sdk = cfg.sdk
131	             .map(|p| Sdk::try_new_exact(p).or_else(|_| Sdk::try_new()))
132	             .unwrap_or_else(Sdk::try_new)?;
133	let version_path = sdk.version_file();
134	let version_raw = sdk.read_version()?;
135	let version = check_sdk_version(&version_raw)?;
136	println!("cargo::rerun-if-changed={}", version_path.display());
137	let sdk_c_api = sdk.c_api();
138
139	let main_header = sdk_c_api.join("pd_api.h");
140	println!("cargo::rerun-if-changed={}", main_header.display());
141	println!("cargo::rerun-if-env-changed={SDK_ENV_VAR}");
142	println!("cargo::metadata=include={}", sdk_c_api.display());
143
144
145	// builder:
146	let gcc = cfg.gcc
147	             .map(|p| {
148		             Gcc::try_from_path(p).and_then(ArmToolchain::try_new_with)
149		                                  .or_else(|_| ArmToolchain::try_new())
150	             })
151	             .unwrap_or_else(ArmToolchain::try_new)?;
152	let (mut builder, renamed) = create_builder(
153	                                            &cargo_target_triple,
154	                                            &sdk_c_api,
155	                                            &main_header,
156	                                            &cfg.derive,
157	                                            &cfg.features,
158	);
159	builder = apply_profile(builder, is_debug);
160	builder = apply_target(builder, &cargo_target_triple, &gcc);
161
162
163	let filename = cfg::Filename::new(version.to_owned(), cfg.derive)?;
164
165	Ok(Generator { sdk,
166	               gcc,
167	               version,
168	               filename,
169	               builder,
170	               renamed,
171	               derives: cfg.derive,
172	               features: cfg.features })
173}
174
175
176fn check_sdk_version(version: &str) -> Result<semver::Version, error::Error> {
177	is_version_matches(version)
178	.map(|(ver, res, req)| {
179		if res {
180			const PKG: &str = env!("CARGO_PKG_NAME");
181			const VER: &str = env!("CARGO_PKG_VERSION");
182			println!("cargo:warning=Playdate SDK v{ver} may not be compatible with {PKG} v{VER} which hasn't been tested with it. Supported '{req}' does not matches current '{ver}'.")
183		}
184		ver
185	})
186}
187
188fn is_version_matches(version: &str) -> Result<(semver::Version, bool, semver::VersionReq), error::Error> {
189	let requirement =
190		semver::VersionReq::parse(SDK_VER_SUPPORTED).expect("Builtin supported version requirement is invalid.");
191	let version = semver::Version::parse(version.trim())?;
192	let matches = requirement.matches(&version);
193	Ok((version, matches, requirement))
194}
195
196
197pub fn env_var(name: &'static str) -> Result<String> {
198	env::var(name).map_err(|err| error::Error::Env { err, ctx: name })
199}
200
201pub fn env_cargo_feature(feature: &str) -> bool { env::var(format!("CARGO_FEATURE_{feature}")).is_ok() }
202
203
204fn create_builder(_target: &str,
205                  capi: &Path,
206                  header: &Path,
207                  derive: &cfg::Derive,
208                  features: &cfg::Features)
209                  -> (Builder, SharedRenamed) {
210	let mut builder = bindgen::builder()
211	.header(format!("{}", header.display()))
212	.rust_target(RustTarget::nightly())
213
214	// allow types:
215	.allowlist_recursively(true)
216	.allowlist_type("PlaydateAPI")
217	.allowlist_type("PDSystemEvent")
218	.allowlist_type("LCDSolidColor")
219	.allowlist_type("LCDColor")
220	.allowlist_type("LCDPattern")
221	.allowlist_type("PDEventHandler")
222
223	.allowlist_var("LCD_COLUMNS")
224	.allowlist_var("LCD_ROWS")
225	.allowlist_var("LCD_ROWSIZE")
226	.allowlist_var("LCD_SCREEN_RECT")
227	.allowlist_var("SEEK_SET")
228	.allowlist_var("SEEK_CUR")
229	.allowlist_var("SEEK_END")
230	.allowlist_var("AUDIO_FRAMES_PER_CYCLE")
231	.allowlist_var("NOTE_C4")
232
233	// experimental:
234	.default_macro_constant_type(MacroTypeVariation::Unsigned)
235	.allowlist_var("LCDMakePattern")
236	.allowlist_type("LCDMakePattern")
237	.allowlist_var("LCDOpaquePattern")
238	.allowlist_type("LCDOpaquePattern")
239	.allowlist_type("LCDFontLanguage")
240
241	.bitfield_enum("FileOptions")
242	.bitfield_enum("PDButtons")
243
244	// types:
245	.use_core()
246	.ctypes_prefix("core::ffi")
247	.size_t_is_usize(true)
248	.no_convert_floats()
249	.translate_enum_integer_types(true)
250	.array_pointers_in_arguments(true)
251	.explicit_padding(false)
252
253	.default_enum_style(EnumVariation::Rust { non_exhaustive: false })
254
255	.layout_tests(true)
256	.enable_function_attribute_detection()
257	.detect_include_paths(true)
258
259	.clang_args(&["--include-directory", &capi.display().to_string()])
260	.clang_arg("-DTARGET_EXTENSION=1")
261
262	.dynamic_link_require_all(true)
263
264	// derives:
265	.derive_default(derive.default)
266	.derive_eq(derive.eq)
267	.derive_copy(derive.copy)
268	.derive_debug(derive.debug)
269	.derive_hash(derive.hash)
270	.derive_ord(derive.ord)
271	.derive_partialeq(derive.partialeq)
272	.derive_partialord(derive.partialord)
273
274	.must_use_type("playdate_*")
275	.must_use_type(".*")
276	.generate_comments(true);
277
278
279	builder = builder.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
280	if !derive.copy {
281		builder = builder.parse_callbacks(Box::new(DeriveCopyToPrimitives));
282	}
283	if derive.constparamty {
284		builder = builder.parse_callbacks(Box::new(DeriveConstParamTy));
285	}
286
287	let renamed = if features.rustify {
288		let hook = rustify::rename::RenameMap::new();
289		let renamed = hook.renamed.clone();
290		builder = builder.parse_callbacks(Box::new(hook));
291		renamed
292	} else {
293		Default::default()
294	};
295
296
297	// explicitly set "do not derive":
298	if !derive.default {
299		builder = builder.no_default(".*");
300	}
301	if !derive.copy {
302		builder = builder.no_copy(".*");
303	}
304	if !derive.debug {
305		builder = builder.no_debug(".*");
306	}
307	if !derive.hash {
308		builder = builder.no_hash(".*");
309	}
310	if !derive.partialeq {
311		builder = builder.no_partialeq(".*");
312	}
313
314	(builder, renamed)
315}
316
317
318fn apply_profile(mut builder: Builder, debug: bool) -> Builder {
319	// extra code-gen for `debug` feature:
320	if debug {
321		builder = builder.clang_arg("-D_DEBUG=1").derive_debug(true);
322	} else {
323		// should we set "-D_DEBUG=0"?
324		// builder = builder.derive_debug(false).no_debug(".*");
325	}
326	builder
327}
328
329
330// This is for build with ARM toolchain.
331// TODO: impl build with just LLVM.
332fn apply_target(mut builder: Builder, target: &str, gcc: &ArmToolchain) -> Builder {
333	builder = if DEVICE_TARGET == target {
334		let arm_eabi_include = gcc.include();
335		// println!("cargo::rustc-link-search={}", arm_eabi.join("lib").display()); // for executable
336		println!("cargo::metadata=include={}", arm_eabi_include.display());
337
338		// TODO: prevent build this for other targets:
339		// builder = builder.raw_line(format!("#![cfg(target = \"{DEVICE_TARGET}\")]\n\n"));
340
341		builder.clang_arg("-DTARGET_PLAYDATE=1")
342		       .blocklist_file("stdlib.h")
343		       .clang_args(&["-target", DEVICE_TARGET])
344		       .clang_arg("-fshort-enums")
345		       .clang_args(&["--include-directory", &arm_eabi_include.display().to_string()])
346		       .clang_arg(format!("-I{}", arm_eabi_include.display()))
347	} else {
348		builder.clang_arg("-DTARGET_SIMULATOR=1")
349	};
350	builder
351}
352
353
354/// Derives `Copy` to simple structs and enums.
355#[derive(Debug)]
356struct DeriveCopyToPrimitives;
357impl bindgen::callbacks::ParseCallbacks for DeriveCopyToPrimitives {
358	fn add_derives(&self, info: &DeriveInfo<'_>) -> Vec<String> {
359		const TYPES: &[&str] = &[
360		                         "PDButtons",
361		                         "FileOptions",
362		                         "LCDBitmapDrawMode",
363		                         "LCDBitmapFlip",
364		                         "LCDSolidColor",
365		                         "LCDLineCapStyle",
366		                         "PDStringEncoding",
367		                         "LCDPolygonFillRule",
368		                         "PDLanguage",
369		                         "PDPeripherals",
370		                         "l_valtype",
371		                         "LuaType",
372		                         "json_value_type",
373		                         "SpriteCollisionResponseType",
374		                         "SoundFormat",
375		                         "LFOType",
376		                         "SoundWaveform",
377		                         "TwoPoleFilterType",
378		                         "PDSystemEvent",
379		];
380
381		if TYPES.contains(&info.name) {
382			vec!["Copy".to_string()]
383		} else {
384			vec![]
385		}
386	}
387}
388
389
390#[derive(Debug)]
391/// Derives `Copy` to simple structs and enums.
392struct DeriveConstParamTy;
393
394impl bindgen::callbacks::ParseCallbacks for DeriveConstParamTy {
395	fn add_derives(&self, info: &DeriveInfo<'_>) -> Vec<String> {
396		const TYPES: &[&str] = &[
397		                         "PDButtons",
398		                         "FileOptions",
399		                         "LCDBitmapDrawMode",
400		                         "LCDBitmapFlip",
401		                         "LCDSolidColor",
402		                         "LCDLineCapStyle",
403		                         "PDStringEncoding",
404		                         "LCDPolygonFillRule",
405		                         "PDLanguage",
406		                         "PDPeripherals",
407		                         "l_valtype",
408		                         "LuaType",
409		                         "json_value_type",
410		                         "SpriteCollisionResponseType",
411		                         "SoundFormat",
412		                         "LFOType",
413		                         "SoundWaveform",
414		                         "TwoPoleFilterType",
415		                         "PDSystemEvent",
416		];
417
418		if TYPES.contains(&info.name) {
419			vec!["::core::marker::ConstParamTy".to_string()]
420		} else {
421			vec![]
422		}
423	}
424}
425
426
427pub fn rustfmt(mut rustfmt_path: Option<PathBuf>,
428               source: String,
429               config_path: Option<&Path>)
430               -> std::io::Result<String> {
431	use std::io::Write;
432	use std::process::{Command, Stdio};
433
434	rustfmt_path = rustfmt_path.or_else(|| std::env::var("RUSTFMT").map(PathBuf::from).ok());
435	#[cfg(feature = "which-rustfmt")]
436	{
437		rustfmt_path = rustfmt_path.or_else(|| which::which("rustfmt").ok());
438	}
439	let rustfmt = rustfmt_path.as_deref().unwrap_or(Path::new("rustfmt"));
440
441
442	let mut cmd = Command::new(rustfmt);
443
444	cmd.stdin(Stdio::piped()).stdout(Stdio::piped());
445
446	if let Some(path) = config_path {
447		cmd.arg("--config-path");
448		cmd.arg(path);
449	}
450
451	let mut child = cmd.spawn()?;
452	let mut child_stdin = child.stdin.take().unwrap();
453	let mut child_stdout = child.stdout.take().unwrap();
454
455	// Write to stdin in a new thread, so that we can read from stdout on this
456	// thread. This keeps the child from blocking on writing to its stdout which
457	// might block us from writing to its stdin.
458	let stdin_handle = std::thread::spawn(move || {
459		let _ = child_stdin.write_all(source.as_bytes());
460		source
461	});
462
463	let mut output = vec![];
464	std::io::copy(&mut child_stdout, &mut output)?;
465
466	let status = child.wait()?;
467	let source = stdin_handle.join()
468	                         .expect("The thread writing to rustfmt's stdin doesn't do anything that could panic");
469
470	match String::from_utf8(output) {
471		Ok(bindings) => {
472			match status.code() {
473				Some(0) => Ok(bindings),
474				Some(2) => Err(std::io::Error::other("Rustfmt parsing errors.".to_string())),
475				Some(3) => {
476					println!("cargo:warning=Rustfmt could not format some lines.");
477					Ok(bindings)
478				},
479				_ => Err(std::io::Error::other("Internal rustfmt error".to_string())),
480			}
481		},
482		_ => Ok(source),
483	}
484}
485
486
487#[cfg(test)]
488mod tests {
489	#[test]
490	fn same_env_var() {
491		assert_eq!(utils::consts::SDK_ENV_VAR, bindgen_cfg::Cfg::ENV_SDK_PATH);
492	}
493
494	#[test]
495	fn version_matches() {
496		use super::is_version_matches as check;
497
498		let map = |(_, res, _)| res;
499
500		assert!(check("0.0").map(map).is_err());
501		assert!(!check("0.0.0").map(map).unwrap());
502		assert!(check("2.1.0").map(map).unwrap());
503		assert!(check("2.7.0").map(map).unwrap());
504		assert!(!check("2.7.0-beta.3").map(map).unwrap());
505		assert!(!check("3.1.0").map(map).unwrap());
506	}
507}