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