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