midi-harbor/scripts/midi2-bindings/src/main.rs
2026-09-28 13:59:10 -05:00

200 lines
8.5 KiB
Rust

//! Generates the Windows MIDI Services bindings in `crates/platform/src/midi/wms/`.
//!
//! Windows MIDI Services has two forms of its API: the one Windows itself carries from late 2026,
//! `Windows.Devices.Midi2`, and the App SDK, `Microsoft.Windows.Devices.Midi2`, which works with
//! the service Windows ships before then once the user installs its runtime. Midi Harbor binds
//! both. From the repository root:
//!
//! ```text
//! cargo run --manifest-path scripts/midi2-bindings/Cargo.toml -- inbox <Windows.Devices.Midi2.winmd>
//! cargo run --manifest-path scripts/midi2-bindings/Cargo.toml -- appsdk <Microsoft.Windows.Devices.Midi2.winmd>
//! ```
//!
//! The metadata is in the `Windows.Devices.Midi2` and `Microsoft.Windows.Devices.Midi2` NuGet
//! packages, under `ref/native`.
use std::path::Path;
/// The in-box API's types the backend uses. Their dependencies are generated with them.
const INBOX: &[&str] = &[
"Windows.Devices.Midi2.MidiApi",
"Windows.Devices.Midi2.MidiSession",
"Windows.Devices.Midi2.MidiEndpointConnection",
"Windows.Devices.Midi2.MidiEndpointConnectionSettings",
"Windows.Devices.Midi2.MidiMessageReceivedEventArgs",
"Windows.Devices.Midi2.IMidiMessageReceivedEventSource",
"Windows.Devices.Midi2.MidiGroup",
"Windows.Devices.Midi2.MidiSendMessageResults",
"Windows.Devices.Midi2.MidiMessageProcessingPluginAddResult",
"Windows.Devices.Midi2.Enumeration.MidiFunctionBlock",
"Windows.Devices.Midi2.Enumeration.MidiFunctionBlockDirection",
"Windows.Devices.Midi2.Enumeration.MidiFunctionBlockRepresentsMidi10Connection",
"Windows.Devices.Midi2.Enumeration.MidiDeclaredEndpointInfo",
"Windows.Devices.Midi2.ServiceConfig.MidiServiceTransportPluginConfigManager",
"Windows.Devices.Midi2.ServiceConfig.MidiServiceConfigResponse",
"Windows.Devices.Midi2.ServiceConfig.MidiServiceConfigResponseStatus",
"Windows.Devices.Midi2.Transports.Virtual.MidiVirtualDeviceManager",
"Windows.Devices.Midi2.Transports.Virtual.MidiVirtualDevice",
"Windows.Devices.Midi2.Transports.Virtual.MidiVirtualDeviceCreationConfig",
];
/// The App SDK's types the backend uses, which sit in slightly different places.
const APPSDK: &[&str] = &[
"Microsoft.Windows.Devices.Midi2.MidiSession",
"Microsoft.Windows.Devices.Midi2.MidiEndpointConnection",
"Microsoft.Windows.Devices.Midi2.IMidiEndpointConnectionSettings",
"Microsoft.Windows.Devices.Midi2.MidiMessageReceivedEventArgs",
"Microsoft.Windows.Devices.Midi2.IMidiMessageReceivedEventSource",
"Microsoft.Windows.Devices.Midi2.MidiGroup",
"Microsoft.Windows.Devices.Midi2.MidiSendMessageResults",
"Microsoft.Windows.Devices.Midi2.MidiFunctionBlock",
"Microsoft.Windows.Devices.Midi2.MidiFunctionBlockDirection",
"Microsoft.Windows.Devices.Midi2.MidiFunctionBlockRepresentsMidi10Connection",
"Microsoft.Windows.Devices.Midi2.MidiDeclaredEndpointInfo",
"Microsoft.Windows.Devices.Midi2.Endpoints.Virtual.MidiVirtualDeviceManager",
"Microsoft.Windows.Devices.Midi2.Endpoints.Virtual.MidiVirtualDevice",
"Microsoft.Windows.Devices.Midi2.Endpoints.Virtual.MidiVirtualDeviceCreationConfig",
];
/// What goes above the generated code.
const HEADER: &str = "// Generated by scripts/midi2-bindings with `windows-bindgen` 0.64.0. Not edited by hand:
// run that tool to regenerate.
#![allow(
non_snake_case,
non_upper_case_globals,
non_camel_case_types,
dead_code,
unsafe_op_in_unsafe_fn,
clippy::all,
clippy::pedantic,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::integer_division
)]
";
fn main() {
let arguments: Vec<String> = std::env::args().skip(1).collect();
let (types, output) = match arguments.first().map(String::as_str) {
Some("inbox") => (INBOX, "crates/platform/src/midi/wms/inbox_bindings.rs"),
Some("appsdk") => (APPSDK, "crates/platform/src/midi/wms/appsdk_bindings.rs"),
_ => {
eprintln!("usage: midi2-bindings <inbox|appsdk> <path to the .winmd>");
std::process::exit(2);
}
};
let Some(metadata) = arguments.get(1).cloned() else {
eprintln!("usage: midi2-bindings <inbox|appsdk> <path to the .winmd>");
std::process::exit(2);
};
// Generate.
let mut bindgen = vec![
"--in".to_owned(),
metadata,
"default".to_owned(),
"--out".to_owned(),
output.to_owned(),
"--reference".to_owned(),
"windows,skip-root,Windows.Foundation".to_owned(),
"--reference".to_owned(),
"windows,skip-root,Windows.Data".to_owned(),
"--filter".to_owned(),
];
bindgen.extend(types.iter().map(|name| (*name).to_owned()));
// Methods whose types are not asked for are skipped, which is expected, so the warnings are
// shown and not treated as failure.
let warnings = windows_bindgen::bindgen(bindgen);
eprintln!("{warnings}");
// Correct what the generator gets wrong, and add the header.
let generated = std::fs::read_to_string(Path::new(output)).unwrap_or_default();
let body = generated
.find("pub mod ")
.and_then(|start| generated.get(start..))
.unwrap_or_default();
let fixed = fallible_statics(body);
if let Err(error) = std::fs::write(output, format!("{HEADER}{fixed}")) {
eprintln!("could not write {output}: {error}");
std::process::exit(1);
}
}
/// Makes static methods marked as never failing return a `Result` after all.
///
/// The metadata marks some static methods `[noexcept]`, and bindgen 0.64 then returns the bare
/// value from inside the closure that fetches the class's factory, which must return a `Result`:
/// fetching the factory can fail, since the class may not be installed. Such a method returns a
/// `windows_core::Result` of its value here instead.
fn fallible_statics(source: &str) -> String {
const OPENING: &str = "(|this| unsafe {";
const NOEXCEPT: &str = "debug_assert!(hresult__.0 == 0);";
let mut edits: Vec<(usize, usize, String)> = Vec::new();
let mut from = 0;
while let Some(found) = source.get(from..).and_then(|rest| rest.find(OPENING)) {
let opening = from + found;
from = opening + OPENING.len();
// Only a static helper call, `Self::I...(`, whose closure is noexcept.
let before = source.get(..opening).unwrap_or_default();
let is_static = before
.rsplit_once("Self::I")
.is_some_and(|(_, name)| name.chars().all(|c| c.is_alphanumeric() || c == '_'));
if !is_static {
continue;
}
let Some(close) = source.get(from..).and_then(|rest| rest.find("})")) else {
continue;
};
let closure = source.get(from..from + close).unwrap_or_default();
let Some(check) = closure.find(NOEXCEPT) else {
continue;
};
// Wrap the value the closure ends with.
let tail_start = from + check + NOEXCEPT.len();
let tail = source.get(tail_start..from + close).unwrap_or_default();
let expression = tail.trim();
let offset = tail_start + tail.find(expression).unwrap_or(0);
edits.push((offset, offset + expression.len(), format!("Ok({expression})")));
// Make the function return a Result.
let Some(signature) = before.rfind("pub fn ") else {
continue;
};
let Some(body) = before.get(signature..).and_then(|rest| rest.find(" {\n")) else {
continue;
};
let header = before.get(signature..signature + body).unwrap_or_default();
if let Some(arrow) = header.rfind(") -> ") {
let returned_start = signature + arrow + ") -> ".len();
let mut returned_end = signature + body;
// A generic method's `where` clause follows its return type.
if let Some(clause) = source
.get(returned_start..returned_end)
.and_then(|returned| returned.find("\n"))
{
returned_end = returned_start + clause;
}
let returned = source.get(returned_start..returned_end).unwrap_or_default();
edits.push((
returned_start,
returned_end,
format!("windows_core::Result<{returned}>"),
));
}
}
// Applied from the end, so earlier offsets stay valid.
edits.sort_by_key(|edit| std::cmp::Reverse(edit.0));
let mut fixed = source.to_owned();
for (start, end, replacement) in edits {
fixed.replace_range(start..end, &replacement);
}
fixed
}