From ea73321e6d5261c68f017b0eaa070c54b2fe7ad7 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 10 Nov 2026 16:19:50 -0800 Subject: [PATCH 1/8] chore(quest): claim quest/m1/ffi-export/target.md Quest-Claim: cda761e93f651ae6 From e529595a9ddab4a8cdbeb73b16210a6756156bdf Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 10 Nov 2026 18:20:30 -0800 Subject: [PATCH 2/8] feat(ffi)!: share one binding across targets `--target` may repeat: each target's API is checked in turn and merged into one binding, where an item only some targets have works on those and raises the language's own error elsewhere, and an item whose definition differs between two targets is refused at its span, naming both. `--api` stages one target's API for an export on another host to add with `--with`. Co-Authored-By: Claude Opus 5.5 --- ffi/README.md | 57 +++- ffi/fixtures/api/src/lib.rs | 70 ++++ ffi/runtime/python.rs | 10 + ffi/src/abi.rs | 32 +- ffi/src/c.rs | 13 + ffi/src/cli.rs | 171 +++++++--- ffi/src/collect.rs | 16 +- ffi/src/config.rs | 8 + ffi/src/dart.rs | 87 ++++- ffi/src/driver.rs | 211 +++++++++--- ffi/src/glue.rs | 189 ++++++++--- ffi/src/go.rs | 50 ++- ffi/src/ir.rs | 85 ++++- ffi/src/kotlin.rs | 77 ++++- ffi/src/main.rs | 1 + ffi/src/merge.rs | 523 +++++++++++++++++++++++++++++ ffi/src/python.rs | 71 +++- ffi/src/swift.rs | 43 ++- ffi/tests/api.c | 7 + ffi/tests/api.dart | 21 ++ ffi/tests/api.go | 7 + ffi/tests/api.kt | 12 + ffi/tests/api.py | 14 + ffi/tests/api.swift | 7 + ffi/tests/fixtures.rs | 175 ++++++++-- quest/m1/ffi-export/moq-targets.md | 41 +++ quest/m1/ffi-export/target.md | 42 --- 27 files changed, 1764 insertions(+), 276 deletions(-) create mode 100644 ffi/src/merge.rs create mode 100644 quest/m1/ffi-export/moq-targets.md delete mode 100644 quest/m1/ffi-export/target.md diff --git a/ffi/README.md b/ffi/README.md index bb0ae84b4..7cf4ed02f 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -5,11 +5,11 @@ attributes in the Rust source. Every language's generator but Python's builds on the C ABI. ```bash -rust2-ffi --config ffi.toml (--out dir | --census dir) [--manifest-path Cargo.toml] [--target triple] [-- cargo args] +rust2-ffi --config ffi.toml (--out dir | --census dir | --api file) [--manifest-path Cargo.toml] [--target triple]... [--with file]... [-- cargo args] ``` -It runs `cargo check` on the package with itself as `RUSTC_WRAPPER`, for `--target` or -else the host, whatever cargo's config names, through +It runs `cargo check` on the package with itself as `RUSTC_WRAPPER`, for each `--target` +or else the host, whatever cargo's config names, through the front end it shares with [rust2-ts](../ts/README.md) in [`driver/`](../driver). The package is the facade: once rustc has checked its library, it walks every `pub` item reachable from the crate root, following `pub use` re-exports of other crates' items and @@ -19,10 +19,10 @@ it ships, and a type no path from its root reaches is refused. The prefix of eve is the library's crate name, Cargo's `[lib] name`, which defaults to the package's name in snake case. `--out` writes a crate of `extern "C"` functions over the facade, its runtime beside it, and the header in `include/.h`; `cargo build` on that crate makes a -static and a shared library for that target alone: its build script refuses another, on +static and a shared library for those targets alone: its build script refuses another, on which an item the target compiles would be missing, or one it leaves out left over. So a -`cfg`-gated item is in its target's export and in no other's, and the layouts, `usize` -included, are that target's. That crate enables the facade's features as the check did; +`cfg`-gated item works on its targets and on no other ([Targets](#targets)), and the +layouts, `usize` included, are the target's the crate builds for. That crate enables the facade's features as the check did; a feature named in a dependency, `--features /`, is an error, since the facade enables what its dependencies build with. It resolves them as the check did, wherever it is built: its manifest carries, as a `[patch]`, each patch or `[replace]` cargo applied @@ -135,6 +135,42 @@ object no export C can call returns or lends to a closure, a generic type no exp instantiates, and whatever takes one, is left out and listed in the census. Everything else is refused, and the census counts why. +## Targets + +One binding serves every target its package ships to, such as one Kotlin file for each +Android ABI and the desktops, though their APIs differ by `cfg`. Each `--target` is +checked in turn, and the binding holds every item any of them has. On each target, every +item that target compiles works exactly, and no other does: + +- A function, method, or constructor only some targets have is declared everywhere. On + another target, the library defines it to fail, and the binding checks the library's + target before calling it, raising the language's own error, as for a closed object: + `UnsupportedOperationException` in Kotlin, `NotImplementedError` in Python, a trap in + Swift, a panic in Go, and `UnsupportedError` in Dart. C and C++ abort with the message. +- So does a variant only some targets have, when a value holding it crosses into a target + without it. A record's fields cross whole, so one whose fields differ between targets + is refused, as below. +- A type only some targets have is declared everywhere, and only those targets' items + make or take it. +- A constant only some targets have keeps its value everywhere, since reading it calls + nothing. +- An item whose definition differs between two targets, such as a function whose + signature does, a record with a field only one has, or a constant of another value, is + refused at its span, with a note at the other's, naming both targets. + +The crate's build script sets `cfg(rust2_ffi_target = "")` for the target it +builds for, which gates each item, and refuses a target it was not exported for. +`_target()` returns the library's target as its index among the header's, +listed in its comment, which each binding reads once when it loads. + +A target's check may need its own C toolchain, as moq's `aws-lc-sys` and `ring` do even +for a check, which only its own host may have. `--api file` writes one target's API, with +the spans of its items, instead of exporting; an export elsewhere adds it with +`--with file`, as though it had checked that target too. Both must check the same +package, features, and `exclude`, from the same sources: a staged span's file is relative +to the workspace's root or cargo's home, so a checkout at another path keeps it, and is +dropped for the other target's span where the file differs. + ## The C ABI - **Handles** are `Arc`s, and a call borrows the value as Rust's receiver or parameter @@ -688,7 +724,9 @@ nix develop .#bench --command just ffi bench # frame writes and reads, through ``` The `api` fixture covers each rule once, and re-exports the `dep` fixture's types, which -export into the same library. `tests/api.c` runs the same script as the +export into the same library. The fixture's binding serves the host and wasm32, whose +items the host's library lacks, and each language's test checks that the host's items work +and that wasm32's fail. `tests/api.c` runs the same script as the fixture's `examples/transcript.rs`, and the two transcripts must match line for line; then it breaks each runtime check, which must abort with its message. `tests/api.cpp` runs the script through the C++ header at C++17, 20, and 23, with exceptions and RTTI off @@ -721,7 +759,10 @@ or a reader reaches it and that the GC frees it once none does, that the GC free never closed, and that each kind of call meeting a closed object, including one racing `Close`, returns `rt.ErrClosed`, and breaks each runtime check, which must panic with its message; a runtime package from another -build must fail its contract check. The `reject` fixture's expected errors, each with its notes, are `fixtures/reject/expected.txt`. +build must fail its contract check. The `target` test exports a package for the host, for wasm32, and for both into one +binding, which builds for each, and staged with `--api` and `--with`; a build for a target +not exported fails, and items that differ between the two are refused, naming both. +The `reject` fixture's expected errors, each with its notes, are `fixtures/reject/expected.txt`. moq's crates export through `target/moq-ffi`, an empty library on each that `just ffi moq` writes in place of moq-ffi's remainder. `moq/moq.c` publishes and subscribes to a moq-net track in one process, polling each read, scopes an origin to moq-pattern's patterns and diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 1a9201ec2..2d0c774f7 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -2180,3 +2180,73 @@ pub fn refuse(reason: &str) -> Result> { reason: reason.to_string(), })) } + +/// The kind of target the library was built for. One binding serves the host and wasm32, +/// so it has every target's variants, and a target refuses one it lacks. +pub enum Platform { + /// A native target, with its pointers' width in bits. + Native(u32), + /// wasm32, which only that target has. + #[cfg(target_arch = "wasm32")] + Wasm { bits: u32 }, +} + +/// The library's target's platform. +pub fn platform() -> Platform { + #[cfg(target_arch = "wasm32")] + return Platform::Wasm { bits: usize::BITS }; + #[cfg(not(target_arch = "wasm32"))] + Platform::Native(usize::BITS) +} + +/// The width of `platform`'s pointers, in bits. +pub fn pointer_bits(platform: Platform) -> u32 { + match platform { + Platform::Native(bits) => bits, + #[cfg(target_arch = "wasm32")] + Platform::Wasm { bits } => bits, + } +} + +/// The family of the library's target, a plain enum with a variant only wasm32 has. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Family { + Native, + #[cfg(target_arch = "wasm32")] + Wasm, +} + +/// Whether `family` is the library's target's. +pub fn is_family(family: Family) -> bool { + #[cfg(target_arch = "wasm32")] + return family == Family::Wasm; + #[cfg(not(target_arch = "wasm32"))] + { + family == Family::Native + } +} + +/// Only wasm32 has it: a call on another target fails. +#[cfg(target_arch = "wasm32")] +pub fn wasm_only() -> u32 { + 32 +} + +/// A type only wasm32 has. +#[cfg(target_arch = "wasm32")] +pub struct Worker { + id: u32, +} + +#[cfg(target_arch = "wasm32")] +impl Worker { + /// A worker numbered `id`. + pub fn new(id: u32) -> Self { + Self { id } + } + + /// Its number. + pub fn id(&self) -> u32 { + self.id + } +} diff --git a/ffi/runtime/python.rs b/ffi/runtime/python.rs index fa1f73080..fc475702b 100644 --- a/ffi/runtime/python.rs +++ b/ffi/runtime/python.rs @@ -101,6 +101,16 @@ pub fn raise(exc: *mut PyObject, msg: &str) -> R { Err(Raised) } +/// Raise for a call of `what`, or a value of it crossing in, which the library's target +/// lacks: the binding serves other targets, which have it. +pub fn missing(what: &str) -> R { + // SAFETY: a static exception type. + raise( + unsafe { PyExc_NotImplementedError }, + &format!("{what}: it is not on this library's target"), + ) +} + pub fn type_error(msg: &str) -> R { // SAFETY: a static exception type. raise(unsafe { PyExc_TypeError }, msg) diff --git a/ffi/src/abi.rs b/ffi/src/abi.rs index f2b5715ae..d35a0f848 100644 --- a/ffi/src/abi.rs +++ b/ffi/src/abi.rs @@ -14,7 +14,7 @@ use std::collections::{BTreeMap, HashMap}; use rustc_span::Span; -use crate::ir::{Access, Api, Dyn, Function, Home, Int, Literal, Receiver, Type}; +use crate::ir::{Access, Api, Dyn, Function, Home, Int, Literal, On, Receiver, Type}; /// A C name claimed twice: the name, then what claimed it first and second. pub type Clash = (String, (String, Span), (String, Span)); @@ -92,6 +92,9 @@ fn owned(ty: &Type) -> Type { /// The suffix naming each instance of a generic item, by its arguments. type Suffixes = HashMap, String>; +/// The `cfg` the crate's build script sets to the target it builds for. +pub const CFG: &str = "rust2_ffi_target"; + pub struct Abi<'a> { pub api: &'a Api, pub prefix: String, @@ -129,6 +132,33 @@ impl<'a> Abi<'a> { abi } + /// The `cfg` predicate a crate's Rust compiles what is on `on` under, or none if it is + /// on every target: its build script sets [`CFG`] to the target it builds for. + pub fn cfg(&self, on: On) -> Option { + if self.api.everywhere(on) { + return None; + } + let targets: Vec = self.api.named(on).iter().map(|t| format!("{CFG} = {t:?}")).collect(); + Some(format!("any({})", targets.join(", "))) + } + + /// The targets every type `ty` names is on, which a value of it is on. + pub fn on(&self, ty: &Type) -> On { + let api = self.api; + let mut on = On::ALL; + crate::ir::each(ty, &mut |ty| { + let of = match ty { + Type::Record(i) => api.records[*i].on, + Type::Enum(i) => api.enums[*i].on, + Type::Object(i) => api.objects[*i].on, + Type::Callback(i, _) => api.callbacks[*i].on, + _ => On::ALL, + }; + on.0 &= of.0; + }); + on + } + /// The C name for a Rust path, which starts with the facade's root. pub fn name(&self, path: &[String]) -> String { std::iter::once(self.prefix.clone()) diff --git a/ffi/src/c.rs b/ffi/src/c.rs index ba5f00197..594f20d76 100644 --- a/ffi/src/c.rs +++ b/ffi/src/c.rs @@ -72,6 +72,12 @@ impl Header<'_> { let api = abi.api; let p = abi.prefix.clone(); let upper = p.to_uppercase(); + let targets: String = (api.targets.iter().enumerate()) + .map(|(i, t)| format!("\n * {i}: {t}")) + .collect(); + self.line(format!( + "/* The index of the library's target among those this header serves:{targets} */\nuint32_t {p}_target(void);\n" + )); self.line(format!( "/* Bytes. Passed in, a view Rust reads during the call and never frees. Returned,\n * an owned buffer to free once with {p}_buf_free. A null ptr is None. */\ntypedef struct {p}_buf {{\n\tconst uint8_t *ptr;\n\tsize_t len;\n\tvoid *owner;\n}} {p}_buf;\n\nvoid {p}_buf_free({p}_buf buf);\n" )); @@ -437,6 +443,13 @@ impl Header<'_> { )); } } + let api = self.abi.api; + if !api.everywhere(function.on) { + docs.push_str(&format!( + "\n\nOnly on {}: elsewhere the library aborts if it is called.", + api.named(function.on).join(", ") + )); + } self.out.push_str(&comment(docs.trim(), "")); if let Some((handle, poll, free)) = &export.future { self.line(format!("typedef struct {handle} {handle};")); diff --git a/ffi/src/cli.rs b/ffi/src/cli.rs index 0d0d70856..6c4985778 100644 --- a/ffi/src/cli.rs +++ b/ffi/src/cli.rs @@ -8,7 +8,7 @@ use rust2_driver::cargo::{Args, Check, Output}; use crate::config::{self, Config}; -const USAGE: &str = "usage: rust2-ffi --config (--out | --census ) [--manifest-path ] [--target ] [-- ] +const USAGE: &str = "usage: rust2-ffi --config (--out | --census | --api ) [--manifest-path ] [--target ]... [--with ]... [-- ] Exports the pub API of the package's library, following its `pub use` re-exports into other crates, as one library: a crate in that builds the C ABI, with its C header @@ -17,7 +17,13 @@ to leave out. The package's Cargo.toml is beside ffi.toml unless --manifest-path where. Cargo args, such as --features, are passed to `cargo check`. --target exports the API the package has on that target, the host by default, whatever -cargo's config names: its `cfg`s and its layouts. The crate builds for that target alone. +cargo's config names: its `cfg`s and its layouts. Given several times, it exports each +target's API into one binding, checking each in turn: an item only some have is on those +alone, and one that differs between two is an error. The crate builds for those targets +alone. + +--api writes one target's API to instead of exporting, for an export on another +host, whose C toolchain the check of a target may need, to add with --with . --census writes a Markdown report, /.md, of what exports and of every construct the export refuses, instead of exporting."; @@ -33,63 +39,140 @@ pub fn run(args: &[String]) -> ExitCode { } fn try_run(args: &[String]) -> Result { - let (target, args) = target(args)?; + let (own, args) = Own::parse(args)?; let Some(args) = Args::parse(&args, USAGE)? else { return Ok(ExitCode::SUCCESS); }; // Read now, so a malformed config fails before the check. Config::load(&args.config)?; - let target = target.unwrap_or_else(|| rustc_session::config::host_tuple().to_string()); - let (facade, patch) = facade(&args.manifest, &target, &args.cargo)?; - let output = match &args.output { - Output::Dir(out) => (config::env::OUT, out.as_os_str()), - Output::Census(census) => (config::env::CENSUS, census.as_os_str()), + let mut targets = own.targets; + if targets.is_empty() { + targets.push(rustc_session::config::host_tuple().to_string()); + } + let output = match (&args.output, own.api) { + (Output::Dir(out), None) => (config::env::OUT, out.clone()), + (Output::Census(census), None) => (config::env::CENSUS, census.clone()), + (Output::Dir(_), Some(api)) => (config::env::API, api), + (Output::Census(_), Some(_)) => return Err(format!("--api and --census are each an output\n\n{USAGE}")), }; + if output.0 != config::env::OUT && (targets.len() > 1 || !own.with.is_empty()) { + return Err(format!( + "--census and --api write one target's: give at most one --target and no --with\n\n{USAGE}" + )); + } + let (facade, patch) = facade(&args.manifest, &targets[0], &args.cargo)?; let line = facade.line(); - Check { - tool: "rust2-ffi", - manifest: &args.manifest, - package: &facade.name, - root: None, - // Always named, so cargo's config never picks another: the exports are found on it. - target: Some(&target), - args: &args.cargo, - env: &[ - (config::env::CONFIG, args.config.as_os_str()), - (config::env::PACKAGE, line.as_ref()), - (config::env::PATCH, patch.as_ref()), - output, - ], + // A staged API's spans are relative to the workspace and cargo's home, which hold the + // same files at other paths on another host. + let cargo_home = std::env::var_os("CARGO_HOME") + .map(PathBuf::from) + .or_else(|| std::env::home_dir().map(|home| home.join(".cargo"))); + let workspace = rust2_driver::cargo::workspace_root(Some(&args.manifest))?; + let roots = std::env::join_paths(std::iter::once(workspace).chain(cargo_home)) + .map_err(|err| format!("joining the roots of spans: {err}"))?; + let check = |target: &str, output: (&str, &std::ffi::OsStr), merge: &std::ffi::OsStr| { + Check { + tool: "rust2-ffi", + manifest: &args.manifest, + package: &facade.name, + root: None, + // Always named, so cargo's config never picks another: the exports are found on it. + target: Some(target), + args: &args.cargo, + env: &[ + (config::env::CONFIG, args.config.as_os_str()), + (config::env::PACKAGE, line.as_ref()), + (config::env::PATCH, patch.as_ref()), + (config::env::MERGE, merge), + (config::env::ROOTS, roots.as_ref()), + output, + ], + } + .run() + }; + let (last, rest) = targets.split_last().expect("a target"); + // Each target but the last stages its API for the last one's check to merge. + let stage = + rust2_driver::cargo::target_dir("rust2-ffi", Some(&args.manifest))?.join(format!("api-{}", std::process::id())); + let run = || { + let mut merge = own.with.clone(); + for target in rest { + let staged = crate::merge::staged(&stage, target); + let code = check(target, (config::env::API, staged.as_os_str()), "".as_ref())?; + if code != ExitCode::SUCCESS { + return Ok(code); + } + merge.push(staged); + } + let merge = std::env::join_paths(&merge).map_err(|err| format!("joining the paths to merge: {err}"))?; + check(last, (output.0, output.1.as_os_str()), &merge) + }; + let code = run(); + if stage.exists() { + std::fs::remove_dir_all(&stage).map_err(|err| format!("removing {}: {err}", stage.display()))?; } - .run() + code } -/// `--target`'s value, and the rest of `args` for the shared parser, which knows no -/// `--target`. -fn target(args: &[String]) -> Result<(Option, Vec), String> { - let mut target = None; - let mut rest = Vec::new(); - let mut args = args.iter(); - while let Some(arg) = args.next() { - match arg.as_str() { - "--target" => { - let Some(value) = args.next() else { - return Err(format!("--target needs a value\n\n{USAGE}")); - }; - if target.replace(value.clone()).is_some() { - return Err(format!("--target is given twice\n\n{USAGE}")); +/// The args rust2-ffi takes beyond the shared parser's. +struct Own { + /// Each `--target`, in order. + targets: Vec, + /// Each `--with`: another target's API, as `--api` wrote it. + with: Vec, + /// `--api`: write the target's API here instead of exporting. + api: Option, +} + +impl Own { + /// The args, and the rest for the shared parser, which knows none of them. `--api` + /// stands in that rest as `--out`, the output the shared parser requires. + fn parse(args: &[String]) -> Result<(Self, Vec), String> { + let mut own = Self { + targets: Vec::new(), + with: Vec::new(), + api: None, + }; + let mut rest = Vec::new(); + let mut args = args.iter(); + while let Some(arg) = args.next() { + let flag = arg.as_str(); + if !matches!(flag, "--target" | "--with" | "--api") { + // The cargo args, and the rest after them, are cargo's. + if flag == "--" { + rest.push(arg.clone()); + rest.extend(args.cloned()); + break; } - } - // The cargo args, and the rest after them, are cargo's. - "--" => { rest.push(arg.clone()); - rest.extend(args.cloned()); - break; + continue; + } + let Some(value) = args.next() else { + return Err(format!("{flag} needs a value\n\n{USAGE}")); + }; + match flag { + "--target" if own.targets.contains(value) => { + return Err(format!("--target {value} is given twice\n\n{USAGE}")); + } + "--target" => own.targets.push(value.clone()), + "--with" => own.with.push(absolute(value)?), + _ if own.api.is_some() => return Err(format!("--api is given twice\n\n{USAGE}")), + _ => { + let api = absolute(value)?; + rest.extend(["--out".to_string(), api.display().to_string()]); + own.api = Some(api); + } } - _ => rest.push(arg.clone()), } + if own.api.is_some() && rest.iter().take_while(|a| *a != "--").filter(|a| *a == "--out").count() > 1 { + return Err(format!("--api and --out are each an output\n\n{USAGE}")); + } + Ok((own, rest)) } - Ok((target, rest)) +} + +fn absolute(path: &str) -> Result { + std::path::absolute(path).map_err(|err| format!("resolving {path}: {err}")) } /// The facade, the package of `manifest`, then the patches it resolved with, as diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index 605fe8ad4..51abe7c2e 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -30,7 +30,7 @@ use rustc_trait_selection::traits::query::evaluate_obligation::InferCtxtExt as _ use crate::config; use crate::ir::{ Access, Api, Callback, Collection, Constant, Dyn, Enum, Family, Field, Function, Home, Int, Literal, Mutability, - Object, Param, Path, Receiver, Record, Task, TaskKind, Traits, Type, Variant, + Object, On, Param, Path, Receiver, Record, Task, TaskKind, Traits, Type, Variant, }; /// Why something is refused: a construct, ranked by the census, and what it names. @@ -303,6 +303,7 @@ pub fn collect<'tcx>(tcx: TyCtxt<'tcx>, facade: &Facade<'_>) -> Collected { functions: Vec::new(), constants: Vec::new(), crates: Vec::new(), + targets: vec![tcx.sess.opts.target_triple.tuple().to_string()], }, }; c.walk(); @@ -1177,6 +1178,7 @@ impl<'tcx> Collector<'tcx> { shown, args: Vec::new(), span, + on: On::ALL, docs, fields: Vec::new(), tuple: false, @@ -1192,6 +1194,7 @@ impl<'tcx> Collector<'tcx> { shown, args: Vec::new(), span, + on: On::ALL, docs, variants: Vec::new(), exhaustive: true, @@ -1211,6 +1214,7 @@ impl<'tcx> Collector<'tcx> { shown, args: Vec::new(), span, + on: On::ALL, docs, sync, copy, @@ -1228,6 +1232,7 @@ impl<'tcx> Collector<'tcx> { path, home: home.clone(), span, + on: On::ALL, docs, methods: Vec::new(), }); @@ -1264,6 +1269,7 @@ impl<'tcx> Collector<'tcx> { shown: vec![self.report.root.clone(), krate.to_string(), name.to_string()], args: Vec::new(), span, + on: On::ALL, docs: format!( "`Arc<{}>`: a handle to another crate's value, which Rust takes back, with none of its API.", tcx.def_path_str(def_id) @@ -1300,6 +1306,7 @@ impl<'tcx> Collector<'tcx> { path: path("parse"), shown: shown("parse"), span, + on: On::ALL, docs: docs.to_string(), receiver: None, params: vec![Param { @@ -1340,6 +1347,7 @@ impl<'tcx> Collector<'tcx> { path: path("to_text"), shown: shown("to_text"), span, + on: On::ALL, docs: "The document as JSON text.".to_string(), receiver: Some(Receiver::Ref), params: Vec::new(), @@ -1368,6 +1376,7 @@ impl<'tcx> Collector<'tcx> { shown: shown(""), args: Vec::new(), span, + on: On::ALL, docs: docs.to_string(), sync: true, copy: false, @@ -1625,6 +1634,7 @@ impl<'tcx> Collector<'tcx> { discriminant, fields, tuple, + on: On::ALL, }); } match errs.is_empty() { @@ -1864,6 +1874,7 @@ impl<'tcx> Collector<'tcx> { path: [&site.path[..], std::slice::from_ref(&name)].concat(), shown: shown.clone(), span, + on: On::ALL, docs: docs(tcx, field.did), receiver: Some(receiver), params, @@ -2465,6 +2476,7 @@ impl<'tcx> Collector<'tcx> { path, shown, span: tcx.def_span(def_id), + on: On::ALL, docs: docs(tcx, def_id), receiver, params, @@ -3807,6 +3819,7 @@ impl<'tcx> Collector<'tcx> { path, home, span, + on: On::ALL, docs: docs(tcx, def_id), value, }); @@ -3826,6 +3839,7 @@ impl<'tcx> Collector<'tcx> { path: path.clone(), shown: path, span, + on: On::ALL, docs: docs(tcx, def_id), receiver: None, params: Vec::new(), diff --git a/ffi/src/config.rs b/ffi/src/config.rs index 47b94d6b4..bc274c43a 100644 --- a/ffi/src/config.rs +++ b/ffi/src/config.rs @@ -80,6 +80,14 @@ pub mod env { pub const OUT: &str = "RUST2_FFI_OUT"; /// The directory to write the census into instead of exporting, absolute. pub const CENSUS: &str = "RUST2_FFI_CENSUS"; + /// The file to write the target's API into instead of exporting, absolute, for the + /// export of several targets to merge. + pub const API: &str = "RUST2_FFI_API"; + /// The other targets' APIs the export merges with its own, as a list of paths. + pub const MERGE: &str = "RUST2_FFI_MERGE"; + /// The directories a staged API's spans are relative to, as a list of paths: the + /// workspace's root and cargo's home, so an API staged on another host keeps them. + pub const ROOTS: &str = "RUST2_FFI_ROOTS"; /// The facade's [`Package`](super::Package). pub const PACKAGE: &str = "RUST2_FFI_PACKAGE"; /// The `[patch]` resolving the facade as cargo did, as TOML with each path absolute. diff --git a/ffi/src/dart.rs b/ffi/src/dart.rs index e4c87a357..0b6bbaa9a 100644 --- a/ffi/src/dart.rs +++ b/ffi/src/dart.rs @@ -27,7 +27,7 @@ use std::fmt::Write; use crate::abi::{self, Abi, Sig, Ty, pascal}; use crate::glue::rs; -use crate::ir::{Access, Family, Function, Int, Literal, Receiver, Type}; +use crate::ir::{Access, Family, Function, Int, Literal, On, Receiver, Type}; use crate::kotlin::strip_refs; const RUNTIME: &str = include_str!("../runtime/rt.dart"); @@ -65,6 +65,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result, packages: &str, contract: u64) -> Result _{p}_contract() == 0x{contract:016x};\n\n" + bool _contract() => _{p}_contract() == 0x{contract:016x};\n\n\ + /// The index of the library's target among those this file serves.\n\ + final int _target = _{p}_target();\n\n\ + /// Throws unless the library's target is among those of [on], as the item [what] is.\n\ + void _on(int on, String what) {{\n\tif ((on >>> _target) & 1 == 0) {{\n\t\tthrow UnsupportedError(\"$what: it is not on this library's target\");\n\t}}\n}}\n\n" ); for (name, params, ret, dart_params, dart_ret) in [ ("contract", "", "ffi.Uint64", "", "int"), + ("target", "", "ffi.Uint32", "", "int"), ("buf_free", "_Buf", "ffi.Void", "_Buf p0", "void"), ( "dart_init", @@ -162,7 +168,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result format!("_{p}_contract"), + "contract" | "target" => format!("_{p}_{name}"), "buf_free" => "_bufFree".to_string(), other => format!("_{}", camel(other)), }; @@ -315,6 +321,8 @@ struct Gen<'a> { /// Each closure type, whose trampoline is `_t`. trampolines: Vec, fresh: usize, + /// The targets of the item being written, whose calls check theirs. + on: On, } impl<'a> Gen<'a> { @@ -663,7 +671,30 @@ impl<'a> Gen<'a> { call.name, args.join(", ") ); - self.native_fn(&name, params, call.ret.clone()) + let native = self.native_fn(&name, params, call.ret.clone()); + let api = self.abi.api; + if api.everywhere(self.on) { + return native; + } + // On a target its item is not on, the call throws before it calls in. + let checked = format!("{native}_on"); + if !self.natives.contains_key(&checked) { + let decl = self.natives[&name].replacen(&format!("{native}("), &format!("{checked}("), 1); + let params: Vec = (0..=call.params.len()).map(|n| format!("p{n}")).collect(); + let declared = &decl[decl.find("external ").expect("a native") + "external ".len()..]; + let ret = &declared[..declared.find(' ').expect("a native's return")]; + let typed = + &declared[declared.find('(').expect("a native's parameters")..declared.rfind(");").expect("a native")]; + let wrapper = format!( + "{ret} {native}{typed}) {{\n\t_on(0x{:x}, '{}');\n\treturn {checked}({});\n}}\n\n", + self.on.0 & api.all().0, + call.name, + params.join(", ") + ); + self.natives.insert(name.clone(), decl); + self.natives.insert(checked, wrapper); + } + native } /// The Dart functions owning a handle of the object `c`, whose C type is `handle`: one @@ -824,6 +855,11 @@ impl<'a> Gen<'a> { for (n, variant) in e.variants.iter().enumerate() { let case = variant_class(&class_name, &variant.name); let mut body = vec![format!("c.$tag = {n};")]; + // A value of a variant the library's target lacks is refused as it crosses. + if !abi.api.everywhere(variant.on) { + let on = variant.on.0 & abi.api.all().0; + body.insert(0, format!("_on(0x{on:x}, '{c}.{}');", variant.name)); + } let member = abi::escape(&abi::snake(&variant.name)); for field in &variant.fields { let e = format!("v.{}", field_name(&field.name)); @@ -1274,6 +1310,7 @@ impl<'a> Gen<'a> { fn record(&mut self, i: usize) { let abi = self.abi; let record = &abi.api.records[i]; + self.on = record.on; let c = abi.record(i); let name = self.class(&c); let rust = abi.api.rust(&record.path, &record.args); @@ -1317,6 +1354,7 @@ impl<'a> Gen<'a> { fn enumeration(&mut self, i: usize) { let abi = self.abi; let e = &abi.api.enums[i]; + self.on = e.on; let c = abi.enumeration(i); let name = self.class(&c); let rust = abi.api.rust(&e.path, &e.args); @@ -1326,17 +1364,34 @@ impl<'a> Gen<'a> { let mut body = Vec::new(); let text = format!("{rust}\n\n{}", e.docs); if e.plain() { + // A value of a variant the library's target lacks is refused as it crosses, when + // its C value is read. + let limited = e.variants.iter().any(|v| !abi.api.everywhere(v.on)); let values: Vec = (e.variants.iter()) .map(|v| { let value = enum_value(&v.name); members.push(format!("static {value}")); - format!("\t{value}({})", v.discriminant) + match limited { + true => format!( + "\t{value}({}, 0x{:x}, '{c}.{}')", + v.discriminant, + v.on.0 & abi.api.all().0, + v.name + ), + false => format!("\t{value}({})", v.discriminant), + } }) .collect(); - body.push(format!( - "{};\n\n\tconst {name}(this._c);\n\n\t/// Its C value.\n\tfinal int _c;\n", - values.join(",\n") - )); + body.push(match limited { + true => format!( + "{};\n\n\tconst {name}(this._value, this._targets, this._what);\n\n\tfinal int _value;\n\tfinal int _targets;\n\tfinal String _what;\n\n\t/// Its C value.\n\tint get _c {{\n\t\t_on(_targets, _what);\n\t\treturn _value;\n\t}}\n", + values.join(",\n") + ), + false => format!( + "{};\n\n\tconst {name}(this._c);\n\n\t/// Its C value.\n\tfinal int _c;\n", + values.join(",\n") + ), + }); self.traits(&ty, &c, e.traits, &mut body, &mut members); let owner = abi.ty(&ty); self.methods(&e.methods, &owner, &ty, &mut body, &mut members); @@ -1396,6 +1451,7 @@ impl<'a> Gen<'a> { fn object(&mut self, i: usize) { let abi = self.abi; let object = &abi.api.objects[i]; + self.on = object.on; let c = abi.object(i); let name = self.class(&c); let rust = abi.api.rust(&object.path, &object.args); @@ -1426,6 +1482,7 @@ impl<'a> Gen<'a> { let abi = self.abi; let p = abi.prefix.clone(); let callback = &abi.api.callbacks[i]; + self.on = callback.on; let c = abi.callback(i); let name = self.class(&c); let rust = callback.path.join("::"); @@ -1664,6 +1721,18 @@ impl<'a> Gen<'a> { function: &Function, owner: Option<&Ty>, owner_ty: Option<&Type>, + ) -> Option<(String, String)> { + let on = std::mem::replace(&mut self.on, function.on); + let code = self.function_on(function, owner, owner_ty); + self.on = on; + code + } + + fn function_on( + &mut self, + function: &Function, + owner: Option<&Ty>, + owner_ty: Option<&Type>, ) -> Option<(String, String)> { let abi = self.abi; let _p = abi.prefix.clone(); diff --git a/ffi/src/driver.rs b/ffi/src/driver.rs index 29f0f3206..eaa60c058 100644 --- a/ffi/src/driver.rs +++ b/ffi/src/driver.rs @@ -15,7 +15,8 @@ use rustc_span::Span; use crate::abi::Abi; use crate::config::{self, Config}; -use crate::{census, collect, glue}; +use crate::ir::Api; +use crate::{census, collect, glue, merge}; /// Run as rustc, with `args` as cargo passed them: the wrapped rustc's path first. pub fn run(args: Vec) -> ExitCode { @@ -32,6 +33,11 @@ struct Export { facade: config::Package, /// The facade's workspace's patches, for the glue's workspace. patch: String, + /// The other targets' APIs to merge with this one's, as [`crate::merge::Target::write`] + /// wrote them. + merge: Vec, + /// The directories a staged API's spans are relative to. + roots: Vec, } /// What the job writes. @@ -40,6 +46,8 @@ enum Output { Dir(PathBuf), /// A census, into this directory, instead of an export. Census(PathBuf), + /// The target's API, into this file, for an export of several targets to merge. + Api(PathBuf), } impl Export { @@ -57,17 +65,28 @@ impl Export { let output = match ( std::env::var_os(config::env::OUT), std::env::var_os(config::env::CENSUS), + std::env::var_os(config::env::API), ) { - (Some(out), None) => Output::Dir(out.into()), - (None, Some(census)) => Output::Census(census.into()), + (Some(out), None, None) => Output::Dir(out.into()), + (None, Some(census), None) => Output::Census(census.into()), + (None, None, Some(api)) => Output::Api(api.into()), _ => { return Err(format!( - "exactly one of {} and {} must be set", + "exactly one of {}, {}, and {} must be set", config::env::OUT, - config::env::CENSUS + config::env::CENSUS, + config::env::API )); } }; + let merge = std::env::var_os(config::env::MERGE).unwrap_or_default(); + let merge = std::env::split_paths(&merge) + .filter(|p| !p.as_os_str().is_empty()) + .collect(); + let roots = std::env::var_os(config::env::ROOTS).unwrap_or_default(); + let roots = std::env::split_paths(&roots) + .filter(|p| !p.as_os_str().is_empty()) + .collect(); facade.features = args .windows(2) .filter(|w| w[0] == "--cfg") @@ -81,6 +100,8 @@ impl Export { output, facade, patch, + merge, + roots, })) } } @@ -96,62 +117,154 @@ impl Callbacks for Export { // A census counts the whole API, whatever the config leaves out. exclude: match self.output { Output::Census(_) => &[], - Output::Dir(_) => &self.config.exclude, + Output::Dir(_) | Output::Api(_) => &self.config.exclude, }, }; let collected = collect::collect(tcx, &facade); let report = &collected.report; - let abi = Abi::new(&collected.api, &self.facade.lib); - let written = match &self.output { - Output::Census(dir) => { - let text = census::render(&collected.api, report, &abi.clashes, &|span| at(tcx, span)); - // A census reports what a crate uses, but a missed instance is the exporter's bug. - for refusal in &report.refusals { - if refusal.why.iter().any(|why| why.construct == collect::NOT_INFERRED) { - refuse(tcx, refusal, "inference missed an instance, a bug in rust2-ffi"); - } + if let Output::Census(dir) = &self.output { + let abi = Abi::new(&collected.api, &self.facade.lib); + let text = census::render(&collected.api, report, &abi.clashes, &|span| at(tcx, span)); + // A census reports what a crate uses, but a missed instance is the exporter's bug. + for refusal in &report.refusals { + if refusal.why.iter().any(|why| why.construct == collect::NOT_INFERRED) { + refuse(tcx, refusal, "inference missed an instance, a bug in rust2-ffi"); } - write(&dir.join(format!("{}.md", report.package)), &text) } - Output::Dir(out) => { - for refusal in &report.refusals { - refuse(tcx, refusal, "exclude it in the config to leave it out"); - } - for (path, count) in &report.excluded { - if *count == 0 && !configured_out(tcx, path) { - tcx.dcx().err(format!( - "rust2-ffi: exclude `{path}` in the config names nothing exported" - )); - } - } - for (name, (first, first_span), (second, second_span)) in &abi.clashes { - tcx.dcx() - .struct_span_err( - *second_span, - format!("rust2-ffi: {second} and {first} are both `{name}` in C"), - ) - .with_span_note(*first_span, format!("{first} is here")) - .emit(); - } - if tcx.dcx().has_errors().is_some() { - return Compilation::Stop; - } - let glue = glue::Crate { - abi: &abi, - facade: &self.facade, - patch: &self.patch, - target: tcx.sess.opts.target_triple.tuple(), - }; - glue.write(out) + if let Err(err) = write(&dir.join(format!("{}.md", report.package)), &text) { + tcx.dcx().err(format!("rust2-ffi: {err}")); } + return stop_on_errors(tcx); + } + for refusal in &report.refusals { + refuse(tcx, refusal, "exclude it in the config to leave it out"); + } + for (path, count) in &report.excluded { + if *count == 0 && !configured_out(tcx, path) { + tcx.dcx().err(format!( + "rust2-ffi: exclude `{path}` in the config names nothing exported" + )); + } + } + // Refusals still let the export go on to report each clash, but write nothing. + let target = merge::Target { + package: self.facade.name.clone(), + features: self.facade.features.clone(), + exclude: self.config.exclude.clone(), + api: collected.api, + }; + let written = match &self.output { + Output::Census(_) => unreachable!("written above"), + Output::Api(_) if tcx.dcx().has_errors().is_some() => Ok(()), + Output::Api(path) => target.write(path, tcx.sess.source_map(), &self.roots), + Output::Dir(out) => match self.merged(tcx, target) { + Some(api) => self.export(tcx, &api, out), + None => Ok(()), + }, }; if let Err(err) = written { tcx.dcx().err(format!("rust2-ffi: {err}")); } - match tcx.dcx().has_errors() { - None => Compilation::Continue, - Some(_) => Compilation::Stop, + stop_on_errors(tcx) + } +} + +impl Export { + /// This target's API merged with the other targets', or none once a difference + /// between two of them is an error. + fn merged(&self, tcx: TyCtxt<'_>, ours: merge::Target) -> Option { + if self.merge.is_empty() { + return Some(ours.api); + } + let mut apis = vec![ours.api]; + let mut unmergeable = false; + for path in &self.merge { + let theirs = match merge::Target::read(path, tcx.sess.source_map(), &self.roots) { + Ok(theirs) => theirs, + Err(err) => { + tcx.dcx().err(format!("rust2-ffi: {err}")); + unmergeable = true; + continue; + } + }; + let target = theirs.api.targets.join(", "); + let differs = [ + ("package", theirs.package != ours.package), + ("features", theirs.features != ours.features), + ("config's exclude", theirs.exclude != ours.exclude), + ]; + for (what, _) in differs.iter().filter(|(_, differs)| *differs) { + unmergeable = true; + tcx.dcx().err(format!( + "rust2-ffi: {target}'s API, from {}, has another {what} than this export's", + path.display() + )); + } + if apis.iter().any(|api| api.targets == theirs.api.targets) { + unmergeable = true; + tcx.dcx().err(format!("rust2-ffi: {target} is exported twice")); + } + apis.push(theirs.api); + } + if apis.len() > 64 { + unmergeable = true; + tcx.dcx().err("rust2-ffi: one binding serves at most 64 targets"); + } + if unmergeable { + return None; + } + let (api, differs) = merge::merge(apis); + for differs in &differs { + let (mut first, mut second) = (&differs.first, &differs.second); + // Where a staged span's file is not on this host as it was, the error is at the other. + if first.1.is_dummy() { + std::mem::swap(&mut first, &mut second); + } + let ((first, first_span), (second, second_span)) = (first, second); + let mut err = tcx.dcx().struct_span_err( + *first_span, + format!( + "rust2-ffi: {} differs between {first} and {second}, which one binding cannot serve", + differs.what + ), + ); + err = match second_span.is_dummy() { + true => err.with_note(format!("{second}'s API was staged where its file is elsewhere")), + false => err.with_span_note(*second_span, format!("on {second}, it is here")), + }; + err.emit(); } + differs.is_empty().then_some(api) + } + + /// Write the export of `api` into `out`, or fail on two items that share a C name. + fn export(&self, tcx: TyCtxt<'_>, api: &Api, out: &Path) -> Result<(), String> { + let abi = Abi::new(api, &self.facade.lib); + for (name, (first, first_span), (second, second_span)) in &abi.clashes { + tcx.dcx() + .struct_span_err( + *second_span, + format!("rust2-ffi: {second} and {first} are both `{name}` in C"), + ) + .with_span_note(*first_span, format!("{first} is here")) + .emit(); + } + if tcx.dcx().has_errors().is_some() { + return Ok(()); + } + let glue = glue::Crate { + abi: &abi, + facade: &self.facade, + patch: &self.patch, + }; + glue.write(out) + } +} + +fn stop_on_errors(tcx: TyCtxt<'_>) -> Compilation { + match tcx.dcx().has_errors() { + None => Compilation::Continue, + Some(_) => Compilation::Stop, } } diff --git a/ffi/src/glue.rs b/ffi/src/glue.rs index 9c526a9ac..21eff7efc 100644 --- a/ffi/src/glue.rs +++ b/ffi/src/glue.rs @@ -8,27 +8,35 @@ use std::fmt::Write; use std::path::Path; -use crate::abi::{self, Abi, Ty}; +use crate::abi::{self, Abi, CFG, Ty}; use crate::config::Package; -use crate::ir::{Access, Collection, Family, Function, Literal, Mutability, Receiver, Task, TaskKind, Traits, Type}; +use crate::ir::{ + Access, Collection, Family, Function, Literal, Mutability, On, Receiver, Task, TaskKind, Traits, Type, +}; const RUNTIME: &str = include_str!("../runtime/rt.rs"); -/// The crate's build script: on macOS, a CPython extension leaves Python's symbols for the -/// interpreter that loads it to resolve. -/// The crate's build script, which refuses to build for a target but `target`: on -/// another, an item that target compiles would be missing, or one it leaves out left over. -fn build(target: &str) -> String { +/// The crate's build script, which refuses to build for a target but `targets`, on which an +/// item a target compiles would be missing, or one it leaves out left over, and tells the +/// crate which it builds for, as the `cfg` [`CFG`]. On macOS, a CPython extension leaves +/// Python's symbols for the interpreter that loads it to resolve. +fn build(targets: &[String]) -> String { + let list: Vec = targets.iter().map(|t| format!("{t:?}")).collect(); + let list = list.join(", "); + let names = targets.join(", "); format!( "// Generated by rust2-ffi. Do not edit.\n\n\ fn main() {{\n\ \tlet target = std::env::var(\"TARGET\").expect(\"cargo sets TARGET\");\n\ - \tif target != {target:?} {{\n\ - \t\tpanic!(\"exported for {target}, not {{target}}: export again with --target {{target}}\");\n\ + \tprintln!(\"cargo::rustc-check-cfg=cfg({CFG}, values({}))\");\n\ + \tif ![{list}].contains(&target.as_str()) {{\n\ + \t\tpanic!(\"exported for {names}, not {{target}}: export again with --target {{target}}\");\n\ \t}}\n\ + \tprintln!(\"cargo::rustc-cfg={CFG}=\\\"{{target}}\\\"\");\n\ \t#[cfg(feature = \"python\")]\n\ \tpyo3_build_config::add_extension_module_link_args();\n\ - }}\n" + }}\n", + list.replace('"', "\\\"") ) } @@ -93,8 +101,6 @@ pub struct Crate<'a> { /// The `[patch]` table the crate's own workspace resolves the exports with, as the /// check did. pub patch: &'a str, - /// The target the check compiled for, the only one the crate builds for. - pub target: &'a str, } impl Crate<'_> { @@ -185,12 +191,13 @@ impl Crate<'_> { contract, dependency: dependency(&facade.root()), lends: false, + on: On::ALL, out: String::new(), }; generator.lib(); crate::driver::write(&out.join("Cargo.toml"), &manifest)?; crate::driver::write(&out.join("src/lib.rs"), &generator.out)?; - crate::driver::write(&out.join("build.rs"), &build(self.target))?; + crate::driver::write(&out.join("build.rs"), &build(&self.abi.api.targets))?; crate::driver::write(&out.join("src/rt.rs"), RUNTIME)?; crate::driver::write(&out.join("include").join(format!("{}.h", self.abi.prefix)), &header)?; crate::driver::write(&out.join("include").join(format!("{}.hpp", self.abi.prefix)), &cpp)?; @@ -308,15 +315,55 @@ struct Gen<'a> { /// Whether the function being written returns a borrow lent from its receiver's /// handle, which `lender` holds. lends: bool, + /// The targets of the item being written, which compile what names its Rust types. + on: On, out: String, } impl Gen<'_> { + /// A C type, or anything else every target compiles. fn line(&mut self, line: impl AsRef) { self.out.push_str(line.as_ref()); self.out.push('\n'); } + fn cfg(&self, on: On) -> Option { + self.abi.cfg(on) + } + + /// What names the facade's types, on the targets of the item being written. + fn item(&mut self, item: String) { + if let Some(cfg) = self.cfg(self.on) { + self.out.push_str(&format!("#[cfg({cfg})]\n")); + } + self.line(item); + } + + /// A C function, which on a target its item is not on fails when called: the binding + /// serves those targets too, and checks a call's target before making it. + fn export(&mut self, function: String) { + let Some(cfg) = self.cfg(self.on) else { + return self.line(function); + }; + let at = function.find(" extern \"C\" fn ").expect("a C function"); + let head = &function[..at + function[at..].find(" {\n").expect("a C function's body")]; + let name = &head[head.rfind(" fn ").expect("a C function") + 4..]; + let name = &name[..name.find('(').expect("a C function's parameters")]; + let stub = format!( + "#[cfg(not({cfg}))]\n#[allow(unused_variables, reason = \"a call fails before reading any\")]\n{head} {{\n\trt::fail(\"{name}\", \"it is not on this library's target\")\n}}\n" + ); + self.out.push_str(&format!("#[cfg({cfg})]\n")); + self.line(function); + self.line(stub); + } + + /// The `#[cfg]` of a match arm for `variant`, if not on every target. + fn arm(&self, on: On) -> String { + self.cfg(on) + .map(|cfg| format!("\n\t\t#[cfg({cfg})]")) + .unwrap_or_default() + } + /// A Rust path of the facade's, through the crate's dependency on it. fn path(&self, path: &[String]) -> String { format!("::{}", self.dependency) + &path[1..].iter().map(|s| format!("::{s}")).collect::() @@ -642,6 +689,14 @@ impl Gen<'_> { "#[unsafe(no_mangle)]\npub extern \"C\" fn {p}_contract() -> u64 {{\n\t0x{:016x}\n}}\n", self.contract )); + // Its index among the targets the header serves, by which a binding checks a call of + // an item only some have before making it. + for (i, target) in api.targets.iter().enumerate() { + self.line(format!("#[cfg({CFG} = {target:?})]\nconst TARGET: u32 = {i};\n")); + } + self.line(format!( + "#[unsafe(no_mangle)]\npub extern \"C\" fn {p}_target() -> u32 {{\n\tTARGET\n}}\n" + )); for (name, ty) in &self.abi.ranges { let (kind, int) = match ty { @@ -655,10 +710,11 @@ impl Gen<'_> { for (name, elem) in &self.abi.lists.clone() { let c = rs(&self.abi.ty(elem)); self.line(format!("pub type {name} = rt::List<{c}>;\n")); + self.on = self.abi.on(elem); let free = self .free(&Type::List(Collection::Vec, Box::new(elem.clone())), "this") .expect("an array is freed"); - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_free(this: {name}) {{\n\t{free}\n}}\n" )); } @@ -686,13 +742,14 @@ impl Gen<'_> { self.callback(i); } for (i, object) in api.objects.iter().enumerate() { + self.on = object.on; let name = self.abi.object(i); let path = self.object_path(i); - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_free(this: *const ::std::ffi::c_void) {{\n\tunsafe {{ rt::Object::<{path}>::free(this) }}\n}}\n" )); if object.shared { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_borrowed(this: *const ::std::ffi::c_void) -> bool {{\n\tunsafe {{ rt::Object::<{path}>::borrowed(this, \"{name}_borrowed\") }}\n}}\n" )); } @@ -717,32 +774,32 @@ impl Gen<'_> { fn traits(&mut self, name: &str, path: &str, traits: Traits, borrow: impl Fn(&str) -> String) { let handle = "*const ::std::ffi::c_void"; if traits.clone { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_clone(this: {handle}) -> {handle} {{\n\tlet w = \"{name}_clone\";\n\tlet this = {};\n\trt::Object::new(<{path} as ::std::clone::Clone>::clone(&*this))\n}}\n", borrow("this") )); } if traits.eq { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_eq(a: {handle}, b: {handle}) -> bool {{\n\tlet w = \"{name}_eq\";\n\tlet a = {};\n\tlet b = {};\n\t<{path} as ::std::cmp::PartialEq>::eq(&*a, &*b)\n}}\n", borrow("a"), borrow("b") )); } if traits.display { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_to_string(this: {handle}) -> rt::Buf {{\n\tlet w = \"{name}_to_string\";\n\tlet this = {};\n\trt::Buf::string(::std::string::ToString::to_string(&*this))\n}}\n", borrow("this") )); } if traits.debug { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_debug(this: {handle}) -> rt::Buf {{\n\tlet w = \"{name}_debug\";\n\tlet this = {};\n\trt::Buf::string(format!(\"{{:?}}\", &*this))\n}}\n", borrow("this") )); } if traits.default { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_default() -> {handle} {{\n\trt::Object::new(<{path} as ::std::default::Default>::default())\n}}\n" )); } @@ -756,29 +813,29 @@ impl Gen<'_> { let lift_b = self.lift(ty, "unsafe { *b }"); let c = rs(&self.abi.ty(ty)); if traits.eq { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_eq(a: *const {c}, b: *const {c}) -> bool {{\n\tlet w = \"{name}_eq\";\n\t<{path} as ::std::cmp::PartialEq>::eq(&{lift_a}, &{lift_b})\n}}\n" )); } if traits.display { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_to_string(this: *const {c}) -> rt::Buf {{\n\tlet w = \"{name}_to_string\";\n\trt::Buf::string(::std::string::ToString::to_string(&{lift}))\n}}\n" )); } if traits.debug { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_debug(this: *const {c}) -> rt::Buf {{\n\tlet w = \"{name}_debug\";\n\trt::Buf::string(format!(\"{{:?}}\", &{lift}))\n}}\n" )); } if traits.default { - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_default() -> {c} {{\n\t{}\n}}\n", self.lower(ty, &format!("<{path} as ::std::default::Default>::default()")) )); } if !matches!(self.abi.ty(ty), Ty::Enum(_)) { let free = self.free(ty, "unsafe { this.read() }").unwrap_or_default(); - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}_free(this: *mut {c}) {{\n\tif this.is_null() {{\n\t\treturn;\n\t}}\n\t{free}\n}}\n" )); } @@ -786,6 +843,7 @@ impl Gen<'_> { fn record(&mut self, i: usize) { let record = &self.abi.api.records[i]; + self.on = record.on; let name = self.abi.record(i); let path = self.rust(&Type::Record(i)); let ty = Type::Record(i); @@ -814,7 +872,7 @@ impl Gen<'_> { format!("{path} {{ {} }}", fields.join(", ")) } }; - self.line(format!( + self.item(format!( "fn lift_{name}(c: &{name}, w: &str) -> {path} {{\n\tlet _ = w;\n\t{build}\n}}\n" )); let bindings: Vec = (0..record.fields.len()).map(|n| format!("f{n}")).collect(); @@ -836,7 +894,7 @@ impl Gen<'_> { .enumerate() .map(|(n, f)| format!("f{n}: {}", self.lower(&f.ty, &format!("f{n}")))) .collect(); - self.line(format!( + self.item(format!( "fn lower_{name}(v: {path}) -> {name} {{\n\tlet {pattern} = v;\n\t{name} {{ {} }}\n}}\n", lowered.join(", ") )); @@ -847,7 +905,7 @@ impl Gen<'_> { .enumerate() .filter_map(|(n, f)| self.free(&f.ty, &format!("c.f{n}"))) .collect(); - self.line(format!("fn free_{name}(c: {name}) {{\n\t{}\n}}\n", frees.join("\n\t"))); + self.item(format!("fn free_{name}(c: {name}) {{\n\t{}\n}}\n", frees.join("\n\t"))); } self.value_traits(&name, &path, &ty, record.traits); let owner = Ty::Struct(name.clone()); @@ -858,6 +916,7 @@ impl Gen<'_> { fn enumeration(&mut self, i: usize) { let e = &self.abi.api.enums[i]; + self.on = e.on; let name = self.abi.enumeration(i); let path = self.rust(&Type::Enum(i)); let ty = Type::Enum(i); @@ -866,21 +925,31 @@ impl Gen<'_> { false => "\n\t\t_ => unreachable!(\"a variant this export does not know\"),", }; if e.plain() { - let lift: Vec = e - .variants - .iter() - .map(|v| format!("\n\t\t{} => {path}::{},", v.discriminant, v.name)) + let lift: Vec = (e.variants.iter()) + .map(|v| { + let missing = self.cfg(v.on).map(|cfg| { + format!( + "\n\t\t#[cfg(not({cfg}))]\n\t\t{} => rt::fail(w, \"the variant is not on this library's target\"),", + v.discriminant + ) + }); + let arm = self.arm(v.on); + format!( + "{arm}\n\t\t{} => {path}::{},{}", + v.discriminant, + v.name, + missing.unwrap_or_default() + ) + }) .collect(); - let lower: Vec = e - .variants - .iter() - .map(|v| format!("\n\t\t{path}::{} => {},", v.name, v.discriminant)) + let lower: Vec = (e.variants.iter()) + .map(|v| format!("{}\n\t\t{path}::{} => {},", self.arm(v.on), v.name, v.discriminant)) .collect(); - self.line(format!( + self.item(format!( "fn lift_{name}(c: i32, w: &str) -> {path} {{\n\tmatch c {{{}\n\t\t_ => rt::fail(w, \"an enum value is invalid\"),\n\t}}\n}}\n", lift.concat() )); - self.line(format!( + self.item(format!( "fn lower_{name}(v: {path}) -> i32 {{\n\tmatch v {{{}{wildcard}\n\t}}\n}}\n", lower.concat() )); @@ -921,11 +990,18 @@ impl Gen<'_> { format!("{path}::{} {{ {} }}", variant.name, fields.join(", ")) } }; + let arm = self.arm(variant.on); + if let Some(cfg) = self.cfg(variant.on) { + let _ = write!( + lift, + "\n\t\t#[cfg(not({cfg}))]\n\t\t{n} => rt::fail(w, \"the variant is not on this library's target\")," + ); + } if variant.fields.is_empty() { - let _ = write!(lift, "\n\t\t{n} => {pattern},"); + let _ = write!(lift, "{arm}\n\t\t{n} => {pattern},"); let _ = write!( lower, - "\n\t\t{pattern} => {name} {{ tag: {n}, u: unsafe {{ ::std::mem::zeroed() }} }}," + "{arm}\n\t\t{pattern} => {name} {{ tag: {n}, u: unsafe {{ ::std::mem::zeroed() }} }}," ); continue; } @@ -950,7 +1026,7 @@ impl Gen<'_> { }; let _ = write!( lift, - "\n\t\t{n} => {{\n\t\t\tlet p = unsafe {{ c.u.v{n} }};\n\t\t\t{build}\n\t\t}}" + "{arm}\n\t\t{n} => {{\n\t\t\tlet p = unsafe {{ c.u.v{n} }};\n\t\t\t{build}\n\t\t}}" ); let lowered: Vec = variant .fields @@ -960,7 +1036,7 @@ impl Gen<'_> { .collect(); let _ = write!( lower, - "\n\t\t{pattern} => {name} {{ tag: {n}, u: {name}_union {{ v{n}: {payload} {{ {} }} }} }},", + "{arm}\n\t\t{pattern} => {name} {{ tag: {n}, u: {name}_union {{ v{n}: {payload} {{ {} }} }} }},", lowered.join(", ") ); let frees: Vec = variant @@ -972,19 +1048,19 @@ impl Gen<'_> { if !frees.is_empty() { let _ = write!( free, - "\n\t\t{n} => {{\n\t\t\tlet p = unsafe {{ c.u.v{n} }};\n\t\t\t{}\n\t\t}}", + "{arm}\n\t\t{n} => {{\n\t\t\tlet p = unsafe {{ c.u.v{n} }};\n\t\t\t{}\n\t\t}}", frees.join("\n\t\t\t") ); } } - self.line(format!( + self.item(format!( "fn lift_{name}(c: &{name}, w: &str) -> {path} {{\n\tmatch c.tag {{{lift}\n\t\t_ => rt::fail(w, \"an enum tag is invalid\"),\n\t}}\n}}\n" )); - self.line(format!( + self.item(format!( "fn lower_{name}(v: {path}) -> {name} {{\n\tmatch v {{{lower}{wildcard}\n\t}}\n}}\n" )); if self.owns(&ty) { - self.line(format!( + self.item(format!( "fn free_{name}(c: {name}) {{\n\tmatch c.tag {{{free}\n\t\t_ => {{}}\n\t}}\n}}\n" )); } @@ -1000,6 +1076,7 @@ impl Gen<'_> { /// trait by calling them. fn callback(&mut self, i: usize) { let callback = &self.abi.api.callbacks[i]; + self.on = callback.on; let name = self.abi.callback(i); let path = self.path(&callback.path); let mut table = String::from("\tpub free: Option,\n"); @@ -1047,10 +1124,10 @@ impl Gen<'_> { self.line(format!( "#[repr(C)]\n#[derive(Clone, Copy)]\npub struct {name} {{\n\tpub vtable: *const {name}_vtable,\n\tpub data: *mut ::std::ffi::c_void,\n}}\n" )); - self.line(format!( + self.item(format!( "/// The foreign implementation, which frees its data when dropped.\npub struct Foreign_{name}({name});\n\n// SAFETY: the callback's contract: its functions may be called from any thread.\nunsafe impl Send for Foreign_{name} {{}}\n// SAFETY: as above.\nunsafe impl Sync for Foreign_{name} {{}}\n\nimpl Drop for Foreign_{name} {{\n\tfn drop(&mut self) {{\n\t\tif let Some(free) = unsafe {{ (*self.0.vtable).free }} {{\n\t\t\tunsafe {{ free(self.0.data) }};\n\t\t}}\n\t}}\n}}\n" )); - self.line(format!("impl {path} for Foreign_{name} {{\n{methods}}}\n")); + self.item(format!("impl {path} for Foreign_{name} {{\n{methods}}}\n")); } /// The C value lending the Rust value `e` of type `ty` to a callback for its call. @@ -1233,6 +1310,12 @@ impl Gen<'_> { /// An exported function, a method of `owner` if it has one: its C type, and its type. fn function(&mut self, function: &Function, owner: Option<(&Ty, Type)>) { + let on = std::mem::replace(&mut self.on, function.on); + self.function_on(function, owner); + self.on = on; + } + + fn function_on(&mut self, function: &Function, owner: Option<(&Ty, Type)>) { let export = self.abi.export(function, owner.as_ref().map(|(ty, _)| *ty)); let name = export.call.name.clone(); // An eager call borrows its arguments only for the call, as a synchronous one does. @@ -1415,7 +1498,7 @@ impl Gen<'_> { .enumerate() .map(|(n, out)| format!("let {out} = outs[{n}];\n\t\t\t")) .collect(); - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}({}) -> *mut rt::Future {{\n\tlet w = \"{name}\";\n\tlet _ = w;\n\t{body}{now}\n\trt::Future::new(async move {{\n\t\tlet result = {later};\n\t\t::std::boxed::Box::new(move |outs: &[*mut ::std::ffi::c_void], err: *mut ::std::ffi::c_void| -> i32 {{\n\t\t\tlet _ = (outs, err);\n\t\t\t{bind}{}\n\t\t}}) as rt::Done\n\t}})\n}}\n", params.join(", "), self.finish(function, &ok, ("v", "rt::READY", "rt::ERROR")) @@ -1430,13 +1513,13 @@ impl Gen<'_> { Some(_) => "err.cast()", None => "::std::ptr::null_mut()", }; - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {}(future: *mut rt::Future, waker: rt::Waker{}) -> i32 {{\n\tunsafe {{ rt::Future::poll(future, waker, &[{outs}], {err}, \"{}\") }}\n}}\n", poll.name, poll_params.iter().map(|p| format!(", {p}")).collect::(), poll.name )); - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {}(future: *mut rt::Future) {{\n\tunsafe {{ rt::Future::free(future) }}\n}}\n", free.name )); @@ -1454,7 +1537,7 @@ impl Gen<'_> { None => self.finish(function, &function.ok, ("", "", "")), }; self.lends = false; - self.line(format!( + self.export(format!( "#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn {name}({}) -> {ret} {{\n\tlet w = \"{name}\";\n\tlet _ = w;\n\t{body}\n\tlet result = {};{writes}\n\t{finish}\n}}\n", params.join(", "), match function.collect { diff --git a/ffi/src/go.rs b/ffi/src/go.rs index 80ca9bb10..3b22e69e1 100644 --- a/ffi/src/go.rs +++ b/ffi/src/go.rs @@ -37,7 +37,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::Write; use crate::abi::{self, Abi, Ty, pascal}; -use crate::ir::{Access, Family, Function, Home, Int, Literal, Param, Receiver, Type}; +use crate::ir::{Access, Family, Function, Home, Int, Literal, On, Param, Receiver, Type}; const RUNTIME: &str = include_str!("../runtime/rt.go"); const CONVERSIONS: &str = include_str!("../runtime/ffi_cgo.go"); @@ -444,7 +444,11 @@ impl<'a> Gen<'a> { func cBufsFree(bufs []cBuf) {{\n\tC.{go}_bufs_free(&bufs[0], C.size_t(len(bufs)))\n}}\n\n\ // Go packages and a library from different builds must not mix: checked when the\n\ // runtime loads, before any other call.\n\ - func init() {{\n\tif C.{p}_contract() != 0x{contract:016x} || C.{upper}_CONTRACT != 0x{contract:016x} {{\n\t\tpanic(\"the Go of {p} and the library are from different builds\")\n\t}}\n}}\n\n\ + func init() {{\n\tif C.{p}_contract() != 0x{contract:016x} || C.{upper}_CONTRACT != 0x{contract:016x} {{\n\t\tpanic(\"the Go of {p} and the library are from different builds\")\n\t}}\n\ttarget_ = uint64(C.{p}_target())\n}}\n\n\ + // target_ is the index of the library's target among those the packages serve.\n\ + var target_ uint64\n\n\ + // On_ panics unless the library's target is among those of on, as the item what is.\n\ + func On_(on uint64, what string) {{\n\tif on>>target_&1 == 0 {{\n\t\tpanic(what + \": it is not on this library's target\")\n\t}}\n}}\n\n\ //export {go}_call\nfunc {go}_call(data unsafe.Pointer) {{\n\tgiven_(data).(func())()\n}}\n\n\ //export {go}_release\nfunc {go}_release(data unsafe.Pointer) {{\n\trelease_(data)\n}}\n\n" ); @@ -452,6 +456,17 @@ impl<'a> Gen<'a> { out } + /// The statement that panics unless the library's target is among those of `on`, as the + /// item `what` is, or none if it is on every target. + fn on(&mut self, on: On, what: &str) -> Option { + let api = self.abi.api; + if api.everywhere(on) { + return None; + } + let check = self.qualify(Packages::RT, "On_"); + Some(format!("{check}({}, \"{what}\")", on.0 & api.all().0)) + } + /// The Go name of `name`, an item of `krate`'s package, from this package: imported, or /// its own. fn qualify(&mut self, krate: &str, name: &str) -> String { @@ -1305,6 +1320,14 @@ impl<'a> Gen<'a> { body: format!("return {lift}"), } }); + let (mut methods, mut default) = (methods, default); + // On a target a value's type is not on, only the binding makes one, which calls in + // for none of these. + if let Some(check) = self.on(self.abi.on(ty), c) { + for method in methods.iter_mut().chain(default.as_mut()) { + method.body = format!("{check}\n{}", method.body); + } + } (methods, default) } @@ -1534,19 +1557,31 @@ impl<'a> Gen<'a> { let c = abi.enumeration(i); let ty = Type::Enum(i); let name = self.qualified(&ty); + // A value of a variant the library's target lacks is refused as it crosses. + let checks: Vec> = (e.variants.iter()) + .map(|v| self.on(v.on, &format!("{c}.{}", v.name))) + .collect(); if e.plain() { + let cases: String = (e.variants.iter().zip(&checks)) + .filter_map(|(v, check)| Some(format!("\tcase {}:\n\t\t{}\n", v.discriminant, check.as_ref()?))) + .collect(); + let check = match cases.is_empty() { + true => String::new(), + false => format!("\tswitch v {{\n{cases}\t}}\n"), + }; return format!( - "func lift_{c}(c C.{c}) {name} {{\n\treturn {name}(c)\n}}\n\nfunc lower_{c}(v {name}) C.{c} {{\n\treturn C.{c}(v)\n}}\n\n" + "func lift_{c}(c C.{c}) {name} {{\n\treturn {name}(c)\n}}\n\nfunc lower_{c}(v {name}) C.{c} {{\n{check}\treturn C.{c}(v)\n}}\n\n" ); } let mut lifts = Vec::new(); let mut lowers = Vec::new(); - for (n, variant) in e.variants.iter().enumerate() { + for ((n, variant), check) in e.variants.iter().enumerate().zip(checks) { let vname = format!("{name}{}", pascal(&variant.name)); let payload = abi::payload(&c, &variant.name); + let check = check.map(|check| format!("\t\t{check}\n")).unwrap_or_default(); if variant.fields.is_empty() { lifts.push(format!("\tcase {n}:\n\t\treturn {vname}{{}}\n")); - lowers.push(format!("\tcase {vname}:\n\t\tc.tag = {n}\n")); + lowers.push(format!("\tcase {vname}:\n{check}\t\tc.tag = {n}\n")); continue; } let lifted: Vec = (variant.fields.iter()) @@ -1566,7 +1601,7 @@ impl<'a> Gen<'a> { lifted.join(", ") )); lowers.push(format!( - "\tcase {vname}:\n\t\tc.tag = {n}\n\t\t*(*C.{payload})(unsafe.Pointer(&c.anon0)) = C.{payload}{{{}}}\n", + "\tcase {vname}:\n{check}\t\tc.tag = {n}\n\t\t*(*C.{payload})(unsafe.Pointer(&c.anon0)) = C.{payload}{{{}}}\n", lowered.join(", ") )); } @@ -2179,6 +2214,9 @@ impl<'a> Gen<'a> { "\n\nIt blocks until Rust's future is ready, or ctx is done: then it cancels the future in\nRust, losing nothing, and returns ctx's error.", ); } + if let Some(check) = self.on(function.on, &c_name) { + body.insert(0, check); + } Method { docs: text, receiver: go_receiver, diff --git a/ffi/src/ir.rs b/ffi/src/ir.rs index b519efe27..3a3ce90b9 100644 --- a/ffi/src/ir.rs +++ b/ffi/src/ir.rs @@ -7,6 +7,7 @@ use rustc_span::Span; /// The exported crates' API. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Api { pub records: Vec, pub enums: Vec, @@ -16,9 +17,29 @@ pub struct Api { pub constants: Vec, /// The crates items belong to, each after every crate it depends on. pub crates: Vec, + /// The targets it serves, sorted: one for a single target's, several merged. + pub targets: Vec, } impl Api { + /// Whether an item on `on` is on every target the API serves. + pub fn everywhere(&self, on: On) -> bool { + on.0 & self.all().0 == self.all().0 + } + + /// Every target the API serves. + pub fn all(&self) -> On { + On(u64::MAX >> (64 - self.targets.len())) + } + + /// The targets of `on`, by name. + pub fn named(&self, on: On) -> Vec<&str> { + (self.targets.iter().enumerate()) + .filter(|(i, _)| on.0 >> i & 1 == 1) + .map(|(_, target)| target.as_str()) + .collect() + } + /// A type's Rust path with its instance's arguments, such as /// `moq_mux::catalog::Producer`. pub fn rust(&self, path: &Path, args: &[Type]) -> String { @@ -53,12 +74,22 @@ impl Api { } } +/// The targets an item compiles on, as a bit for each index into [`Api::targets`]: at +/// most 64 targets. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)] +pub struct On(pub u64); + +impl On { + /// On every target, as a single target's API is. + pub const ALL: On = On(u64::MAX); +} + /// A Rust path: its export's package name in snake case, unique among the exports, then /// the path from that crate's root, such as `["moq_net", "track", "Producer", "append_group"]`. pub type Path = Vec; /// The crate an item belongs to, and how much of its path lies outside that crate. -#[derive(Clone, Default, PartialEq, Eq, Debug)] +#[derive(Clone, Default, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)] pub struct Home { /// The crate's name, followed by `#` and a number for a later crate of a name an /// earlier one has: the one defining the item, or for an instance, the last of its own @@ -71,6 +102,7 @@ pub struct Home { } /// A struct whose fields are all `pub`: a value, copied across. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Record { pub path: Path, pub home: Home, @@ -79,7 +111,10 @@ pub struct Record { pub shown: Path, /// The type and const arguments of the instance it is, empty unless the type is generic. pub args: Vec, + #[serde(with = "crate::merge::span")] pub span: Span, + /// The targets it compiles on. + pub on: On, pub docs: String, pub fields: Vec, /// Whether its fields are positional, named `0`, `1`, ... @@ -88,6 +123,7 @@ pub struct Record { pub traits: Traits, } +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Field { /// Its Rust name: a number for a positional field. pub name: String, @@ -97,12 +133,16 @@ pub struct Field { /// An enum: a value, copied across. Without fields in any variant, it is a plain /// enumeration. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Enum { pub path: Path, pub home: Home, pub shown: Path, pub args: Vec, + #[serde(with = "crate::merge::span")] pub span: Span, + /// The targets it compiles on. + pub on: On, pub docs: String, pub variants: Vec, /// Whether another crate can match it without a wildcard arm. @@ -111,12 +151,15 @@ pub struct Enum { pub traits: Traits, } +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Variant { pub name: String, /// Its discriminant, for an enum without fields. pub discriminant: i128, pub fields: Vec, pub tuple: bool, + /// The targets it compiles on. + pub on: On, } impl Enum { @@ -128,12 +171,16 @@ impl Enum { /// Any other type: a handle to a value the foreign side never sees inside, shared behind /// an `Arc`, whose receivers borrow it as Rust's do, checked at run time. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Object { pub path: Path, pub home: Home, pub shown: Path, pub args: Vec, + #[serde(with = "crate::merge::span")] pub span: Span, + /// The targets it compiles on. + pub on: On, pub docs: String, /// Whether it is `Sync`: if not, `&self` borrows it on one thread at a time. pub sync: bool, @@ -157,17 +204,21 @@ pub struct Object { } /// A `pub` trait: implemented by the foreign side, through a table of functions. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Callback { pub path: Path, pub home: Home, + #[serde(with = "crate::merge::span")] pub span: Span, + /// The targets it compiles on. + pub on: On, pub docs: String, /// Its required methods, each taking `&self` or `&mut self`. pub methods: Vec, } /// The std traits a type's impls export. -#[derive(Default, Clone, Copy)] +#[derive(Default, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Traits { pub clone: bool, pub eq: bool, @@ -177,13 +228,17 @@ pub struct Traits { } /// A function or method. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Function { /// Its path, ending in its own name: a method's goes through its type's. pub path: Path, /// Its path with the arguments of its type's instance and its own, which the config /// names it by: `moq_mux::catalog::Producer::stats`. pub shown: Path, + #[serde(with = "crate::merge::span")] pub span: Span, + /// The targets it compiles on. + pub on: On, /// Its doc comment, as Markdown. pub docs: String, pub receiver: Option, @@ -220,6 +275,7 @@ pub struct Function { /// An instance of a generic function or method, at one of the arguments a type or const /// parameter exports at. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Instance { /// The arguments of its declared type and const parameters, which the call names, as /// Rust cannot infer one no argument names; its argument infers an `impl Trait` one, @@ -230,7 +286,7 @@ pub struct Instance { } /// What an exported function reads. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum Access { /// It calls the function. Call, @@ -254,6 +310,7 @@ impl Function { } /// A parameter a function polls its task through. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Task { /// Its place among the parameters after `self`. pub at: usize, @@ -261,6 +318,7 @@ pub struct Task { } /// What a [`Task`] parameter is. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum TaskKind { /// `&Context` or `&mut Context`: made from C's waker, or the future's own. Context(Mutability), @@ -270,7 +328,7 @@ pub enum TaskKind { } /// How a method takes `self`. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum Receiver { /// `&self` Ref, @@ -280,6 +338,7 @@ pub enum Receiver { Owned, } +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Param { pub name: String, pub ty: Type, @@ -289,15 +348,19 @@ pub struct Param { } /// A `pub const`. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Constant { pub path: Path, pub home: Home, + #[serde(with = "crate::merge::span")] pub span: Span, + /// The targets it compiles on. + pub on: On, pub docs: String, pub value: Literal, } -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)] pub enum Literal { Bool(bool), Int(Int, i128), @@ -314,7 +377,7 @@ impl std::fmt::Display for Literal { } /// A type as the API uses it, in the exact form the Rust side takes or returns. -#[derive(Clone, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)] pub enum Type { Unit, Bool, @@ -372,7 +435,7 @@ pub enum Type { } /// The Rust collection a [`Type::List`] is, which Rust builds from its items in order. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)] pub enum Collection { /// `Vec`, or behind a reference, `[T]`. Vec, @@ -385,7 +448,7 @@ pub enum Collection { } /// The addresses a socket address type holds. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)] pub enum Family { /// IPv4 or IPv6: `SocketAddr`. Any, @@ -393,14 +456,14 @@ pub enum Family { V6, } -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)] pub enum Mutability { Not, Mut, } /// How a parameter takes a foreign implementation of a trait. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)] pub enum Dyn { /// A type parameter or `impl Trait`, given the foreign type. Impl, @@ -410,7 +473,7 @@ pub enum Dyn { Arc, } -#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum Int { U8, U16, diff --git a/ffi/src/kotlin.rs b/ffi/src/kotlin.rs index a175f91e4..e3b3e5b92 100644 --- a/ffi/src/kotlin.rs +++ b/ffi/src/kotlin.rs @@ -24,7 +24,7 @@ use std::hash::Hasher; use rustc_data_structures::stable_hash::StableHasher; use crate::abi::{self, Abi, Ty, pascal}; -use crate::ir::{Access, Family, Function, Int, Literal, Param, Receiver, Type}; +use crate::ir::{Access, Family, Function, Int, Literal, On, Param, Receiver, Type}; const RUNTIME: &str = include_str!("../runtime/rt.kt"); pub const GLUE_RUNTIME: &str = include_str!("../runtime/jni.rs"); @@ -54,6 +54,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result, packages: &str, contract: u64) -> Result, packages: &str, contract: u64) -> Result jint {{\n\ + \tcrate::{p}_target() as jint\n\ + }}\n\n\ + #[unsafe(no_mangle)]\n\ pub extern \"system\" fn Java_{pkg}_Native__1_1waker_1new(env: *mut JNIEnv, _: jclass, waker: jobject) -> jlong {{\n\ \trt::node(env, waker) as usize as jlong\n\ }}\n\n\ @@ -299,6 +304,8 @@ struct Gen<'a> { tuples: BTreeSet, /// The Kotlin functions the glue calls, which the rules keep. dispatchers: Vec, + /// The targets of the item being written, whose native methods check theirs. + on: On, } impl<'a> Gen<'a> { @@ -926,6 +933,7 @@ impl<'a> Gen<'a> { fn record(&mut self, i: usize) { let abi = self.abi; let record = &abi.api.records[i]; + self.on = record.on; let c = abi.record(i); let name = self.class(&c); let rust = abi.api.rust(&record.path, &record.args); @@ -1028,6 +1036,7 @@ impl<'a> Gen<'a> { fn enumeration(&mut self, i: usize) { let abi = self.abi; let e = &abi.api.enums[i]; + self.on = e.on; let c = abi.enumeration(i); let name = self.class(&c); let rust = abi.api.rust(&e.path, &e.args); @@ -1054,11 +1063,25 @@ impl<'a> Gen<'a> { members.push("fun of".to_string()); members.push("val value".to_string()); self.distinct(&rust, &members); - let entries: Vec = e - .variants - .iter() - .map(|v| format!("\t{}({}),", abi::snake(&v.name).to_uppercase(), v.discriminant)) + // An entry the library's target lacks is refused as it crosses, when its value is read. + let entries: Vec = (e.variants.iter()) + .map(|v| { + let entry = abi::snake(&v.name).to_uppercase(); + match abi.api.everywhere(v.on) { + true => format!("\t{entry}({}),", v.discriminant), + false => format!( + "\t{entry}({}) {{\n\t\toverride val value: Int\n\t\t\tget() = super.value.also {{ Native.check({}uL, \"{c}.{}\") }}\n\t}},", + v.discriminant, + v.on.0 & abi.api.all().0, + v.name + ), + } + }) .collect(); + let open = match e.variants.iter().all(|v| abi.api.everywhere(v.on)) { + true => "", + false => "open ", + }; let cases: Vec = e .variants .iter() @@ -1072,7 +1095,7 @@ impl<'a> Gen<'a> { let entries = entries.strip_suffix(',').unwrap_or(&entries); let _ = write!( self.types, - "{}enum class {name}(internal val value: Int) {{\n{entries};\n\n{}\tcompanion object {{\n{}\t}}\n}}\n\n", + "{}enum class {name}(internal {open}val value: Int) {{\n{entries};\n\n{}\tcompanion object {{\n{}\t}}\n}}\n\n", docs(&e.docs, ""), body.iter().map(|b| format!("{b}\n")).collect::(), statics.join("\n") @@ -1093,6 +1116,11 @@ impl<'a> Gen<'a> { .map(|f| format!("val {}: {}", field_name(&f.name), self.kt(&f.ty))) .collect(); let mut write = vec![format!("w.i32({n})")]; + // A value of a variant the library's target lacks is refused as it crosses. + if !abi.api.everywhere(variant.on) { + let on = variant.on.0 & abi.api.all().0; + write.insert(0, format!("Native.check({on}uL, \"{c}.{}\")", variant.name)); + } for f in &variant.fields { let e = format!("v.{}", field_name(&f.name)); write.push(self.write_value(&f.ty, "w", &e)); @@ -1162,6 +1190,7 @@ impl<'a> Gen<'a> { fn object(&mut self, i: usize) { let abi = self.abi; let object = &abi.api.objects[i]; + self.on = object.on; let c = abi.object(i); let name = self.class(&c); let bare = self.bare(&c).to_string(); @@ -1213,6 +1242,7 @@ impl<'a> Gen<'a> { fn callback(&mut self, i: usize) { let abi = self.abi; let callback = &abi.api.callbacks[i]; + self.on = callback.on; let c = abi.callback(i); let name = self.class(&c); let bare = self.bare(&c).to_string(); @@ -1387,6 +1417,26 @@ impl<'a> Gen<'a> { .enumerate() .map(|(n, (_, k))| format!("p{n}: {k}")) .collect(); + // On a target its item is not on, the native method throws before it calls in. + let api = self.abi.api; + let external = match api.everywhere(self.on) { + true => bare.to_string(), + false => { + let external = format!("{bare}__on"); + let args: Vec = (0..params.len()).map(|n| format!("p{n}")).collect(); + let _ = write!( + self.natives, + "\t@JvmStatic fun {bare}({}): {} {{\n\t\tcheck({}uL, \"{}_{bare}\")\n\t\treturn {external}({})\n\t}}\n\n", + kt.join(", "), + ret.1, + self.on.0 & api.all().0, + self.abi.prefix, + args.join(", ") + ); + external + } + }; + let bare = external.as_str(); let _ = write!( self.natives, "\t@JvmStatic external fun {bare}({}): {}\n\n", @@ -1431,6 +1481,19 @@ impl<'a> Gen<'a> { owner: Option<&Ty>, owner_ty: Option<&Type>, place: Place, + ) -> Option { + let on = std::mem::replace(&mut self.on, function.on); + let code = self.function_on(function, owner, owner_ty, place); + self.on = on; + code + } + + fn function_on( + &mut self, + function: &Function, + owner: Option<&Ty>, + owner_ty: Option<&Type>, + place: Place, ) -> Option { let abi = self.abi; let export = abi.export(function, owner); diff --git a/ffi/src/main.rs b/ffi/src/main.rs index 930d11f0f..1e01d30ab 100644 --- a/ffi/src/main.rs +++ b/ffi/src/main.rs @@ -32,6 +32,7 @@ mod glue; mod go; mod ir; mod kotlin; +mod merge; mod python; mod swift; diff --git a/ffi/src/merge.rs b/ffi/src/merge.rs new file mode 100644 index 000000000..b895e1950 --- /dev/null +++ b/ffi/src/merge.rs @@ -0,0 +1,523 @@ +//! One API for several targets: each target's, collected by its own check, merged into the +//! union of their items, each marked with the targets it compiles on. +//! +//! An item two targets both compile must be the same on each, since one binding declares +//! it once; one that differs, such as a function taking `u32` on one and `u64` on another, +//! a record with a field only one has, or a constant of another value, is refused at its +//! span, naming both targets. An enum's variants merge as its items do, each on its own +//! targets, so a value of a variant its target lacks is refused when it crosses. + +use std::path::{Path, PathBuf}; + +use rustc_span::source_map::SourceMap; +use rustc_span::{DUMMY_SP, Span}; + +use crate::ir::{Api, Callback, Constant, Enum, Field, Function, Object, On, Record, TaskKind, Type, Variant}; + +/// A target's API, as a check writes it for the export of several targets. +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Target { + /// The facade's package, which every target's must share. + pub package: String, + /// The features the facade compiled with, which every target's must share, since the + /// crate enables one set. + pub features: Vec, + /// The config's `exclude`, which every target's must share. + pub exclude: Vec, + pub api: Api, +} + +impl Target { + /// Write it to `path`, with each span as its file and offsets in `source_map`, the + /// file relative to the first of `roots` it is in. + pub fn write(&self, path: &Path, source_map: &SourceMap, roots: &[PathBuf]) -> Result<(), String> { + let json = span::with(source_map, roots, || serde_json::to_string(self)) + .map_err(|err| format!("writing {}: {err}", path.display()))?; + crate::driver::write(path, &json) + } + + /// Read what [`Self::write`] wrote, with each span loaded into `source_map` from + /// `roots`, or none where its file is not on this host as it was. + pub fn read(path: &Path, source_map: &SourceMap, roots: &[PathBuf]) -> Result { + let json = std::fs::read_to_string(path).map_err(|err| format!("reading {}: {err}", path.display()))?; + span::with(source_map, roots, || serde_json::from_str(&json)) + .map_err(|err| format!("reading {}: {err}", path.display())) + } +} + +/// An item two targets compile differently, which one binding cannot serve: what it is, +/// then each target with its span there. +pub struct Differs { + pub what: String, + pub first: (String, Span), + pub second: (String, Span), +} + +/// The APIs of several targets, one each, merged into one serving them all, with every +/// item that differs between two of them. +pub fn merge(mut apis: Vec) -> (Api, Vec) { + apis.sort_by(|a, b| a.targets.cmp(&b.targets)); + let targets: Vec = apis.iter().flat_map(|api| api.targets.clone()).collect(); + let mut apis = apis.into_iter().enumerate().map(|(i, mut api)| { + mark(&mut api, On(1 << i)); + api + }); + let mut merged = Merged { + api: apis.next().expect("an export has a target"), + differs: Vec::new(), + }; + merged.api.targets = targets; + for api in apis { + merged.add(api); + } + (merged.api, merged.differs) +} + +/// Mark every item of `api` as on `on`. +fn mark(api: &mut Api, on: On) { + let functions = |methods: &mut [Function]| methods.iter_mut().for_each(|m| m.on = on); + for record in &mut api.records { + record.on = on; + functions(&mut record.methods); + } + for e in &mut api.enums { + e.on = on; + e.variants.iter_mut().for_each(|v| v.on = on); + functions(&mut e.methods); + } + for object in &mut api.objects { + object.on = on; + functions(&mut object.methods); + } + for callback in &mut api.callbacks { + callback.on = on; + functions(&mut callback.methods); + } + functions(&mut api.functions); + api.constants.iter_mut().for_each(|c| c.on = on); +} + +struct Merged { + api: Api, + differs: Vec, +} + +/// Where each type of the API being added lands among the merged API's. +#[derive(Default)] +struct Maps { + records: Vec, + enums: Vec, + objects: Vec, + callbacks: Vec, +} + +impl Maps { + /// Each index `find` finds an equal item at, and past `len` for each it does not, in order. + fn of(items: &[T], len: usize, find: impl Fn(&T) -> Option) -> Vec { + let mut next = len; + (items.iter()) + .map(|item| { + find(item).unwrap_or_else(|| { + next += 1; + next - 1 + }) + }) + .collect() + } + + fn ty(&self, ty: &mut Type) { + match ty { + Type::Record(i) => *i = self.records[*i], + Type::Enum(i) => *i = self.enums[*i], + Type::Object(i) => *i = self.objects[*i], + Type::Callback(i, _) => *i = self.callbacks[*i], + Type::List(_, inner) | Type::Option(inner) | Type::Box(inner) | Type::Ref(_, inner) => self.ty(inner), + Type::Closure(params, ret) => { + params.iter_mut().for_each(|p| self.ty(p)); + self.ty(ret); + } + Type::Tuple(elements) => elements.iter_mut().for_each(|e| self.ty(e)), + _ => {} + } + } + + fn types(&self, types: &mut [Type]) { + types.iter_mut().for_each(|ty| self.ty(ty)); + } + + fn fields(&self, fields: &mut [Field]) { + fields.iter_mut().for_each(|f| self.ty(&mut f.ty)); + } + + fn function(&self, f: &mut Function) { + f.params.iter_mut().for_each(|p| self.ty(&mut p.ty)); + self.ty(&mut f.ok); + if let Some(err) = &mut f.err { + self.ty(err); + } + if let Some(TaskKind::Made { object, .. }) = f.task.as_mut().map(|task| &mut task.kind) { + *object = self.objects[*object]; + } + if let Some(instance) = &mut f.instance { + instance.args.iter_mut().flatten().for_each(|ty| self.ty(ty)); + self.types(&mut instance.named); + } + } + + fn functions(&self, functions: &mut [Function]) { + functions.iter_mut().for_each(|f| self.function(f)); + } +} + +/// `item` with what may differ between targets alike cleared: its span, docs, targets, +/// and `clear`'s, which clears its members, compared on their own. +fn bare(item: &T, clear: impl Fn(&mut T)) -> T { + let mut item = item.clone(); + clear(&mut item); + item +} + +fn bare_function(f: &Function) -> Function { + bare(f, |f| { + f.span = DUMMY_SP; + f.docs.clear(); + f.on = On::ALL; + }) +} + +fn bare_record(r: &Record) -> Record { + bare(r, |r| { + r.span = DUMMY_SP; + r.docs.clear(); + r.on = On::ALL; + r.methods.clear(); + r.fields.iter_mut().for_each(|f| f.docs.clear()); + }) +} + +fn bare_enum(e: &Enum) -> Enum { + bare(e, |e| { + e.span = DUMMY_SP; + e.docs.clear(); + e.on = On::ALL; + e.methods.clear(); + e.variants.clear(); + }) +} + +fn bare_variant(v: &Variant) -> Variant { + bare(v, |v| { + v.on = On::ALL; + v.fields.iter_mut().for_each(|f| f.docs.clear()); + }) +} + +fn bare_object(o: &Object) -> Object { + bare(o, |o| { + o.span = DUMMY_SP; + o.docs.clear(); + o.on = On::ALL; + o.methods.clear(); + }) +} + +fn bare_callback(c: &Callback) -> Callback { + bare(c, |c| { + c.span = DUMMY_SP; + c.docs.clear(); + c.on = On::ALL; + c.methods = c.methods.iter().map(bare_function).collect(); + }) +} + +fn bare_constant(c: &Constant) -> Constant { + bare(c, |c| { + c.span = DUMMY_SP; + c.docs.clear(); + c.on = On::ALL; + }) +} + +impl Merged { + /// The first target `on` names. + fn first(&self, on: On) -> String { + self.api.named(on).first().expect("an item is on a target").to_string() + } + + /// Refuse `what`, which the merged API has on `ours` at `span` and `other` on its target + /// at `theirs`. + fn differ(&mut self, what: String, (ours, span): (On, Span), (other, theirs): (On, Span)) { + let first = (self.first(ours), span); + let second = (self.first(other), theirs); + self.differs.push(Differs { what, first, second }); + } + + /// Add `api`'s items, of one target, to the merged API's. + fn add(&mut self, mut api: Api) { + let maps = Maps { + records: Maps::of(&api.records, self.api.records.len(), |r| { + (self.api.records.iter()).position(|m| m.shown == r.shown) + }), + enums: Maps::of(&api.enums, self.api.enums.len(), |e| { + (self.api.enums.iter()).position(|m| m.shown == e.shown) + }), + objects: Maps::of(&api.objects, self.api.objects.len(), |o| { + (self.api.objects.iter()).position(|m| m.shown == o.shown && m.foreign == o.foreign) + }), + callbacks: Maps::of(&api.callbacks, self.api.callbacks.len(), |c| { + (self.api.callbacks.iter()).position(|m| m.path == c.path) + }), + }; + for record in &mut api.records { + maps.fields(&mut record.fields); + maps.types(&mut record.args); + maps.functions(&mut record.methods); + } + for e in &mut api.enums { + e.variants.iter_mut().for_each(|v| maps.fields(&mut v.fields)); + maps.types(&mut e.args); + maps.functions(&mut e.methods); + } + for object in &mut api.objects { + maps.types(&mut object.args); + maps.functions(&mut object.methods); + } + for callback in &mut api.callbacks { + maps.functions(&mut callback.methods); + } + maps.functions(&mut api.functions); + for krate in api.crates { + if !self.api.crates.contains(&krate) { + self.api.crates.push(krate); + } + } + for (record, at) in api.records.into_iter().zip(maps.records) { + let Some(ours) = self.api.records.get(at) else { + self.api.records.push(record); + continue; + }; + if bare_record(ours) != bare_record(&record) { + let what = format!("struct `{}`", record.shown.join("::")); + self.differ(what, (ours.on, ours.span), (record.on, record.span)); + } + self.api.records[at].on.0 |= record.on.0; + let methods = std::mem::take(&mut self.api.records[at].methods); + self.api.records[at].methods = self.functions(methods, record.methods); + } + for (e, at) in api.enums.into_iter().zip(maps.enums) { + let Some(ours) = self.api.enums.get(at) else { + self.api.enums.push(e); + continue; + }; + let what = format!("enum `{}`", e.shown.join("::")); + if bare_enum(ours) != bare_enum(&e) { + self.differ(what.clone(), (ours.on, ours.span), (e.on, e.span)); + } + self.variants(at, &e, what); + self.api.enums[at].on.0 |= e.on.0; + let methods = std::mem::take(&mut self.api.enums[at].methods); + self.api.enums[at].methods = self.functions(methods, e.methods); + } + for (object, at) in api.objects.into_iter().zip(maps.objects) { + let Some(ours) = self.api.objects.get(at) else { + self.api.objects.push(object); + continue; + }; + if bare_object(ours) != bare_object(&object) { + let what = format!("type `{}`", object.shown.join("::")); + self.differ(what, (ours.on, ours.span), (object.on, object.span)); + } + self.api.objects[at].on.0 |= object.on.0; + let methods = std::mem::take(&mut self.api.objects[at].methods); + self.api.objects[at].methods = self.functions(methods, object.methods); + } + for (callback, at) in api.callbacks.into_iter().zip(maps.callbacks) { + let Some(ours) = self.api.callbacks.get(at) else { + self.api.callbacks.push(callback); + continue; + }; + if bare_callback(ours) != bare_callback(&callback) { + let what = format!("trait `{}`", callback.path.join("::")); + self.differ(what, (ours.on, ours.span), (callback.on, callback.span)); + } + let ours = &mut self.api.callbacks[at]; + ours.on.0 |= callback.on.0; + ours.methods.iter_mut().for_each(|m| m.on.0 |= callback.on.0); + } + let functions = std::mem::take(&mut self.api.functions); + self.api.functions = self.functions(functions, api.functions); + for constant in api.constants { + let Some(at) = self.api.constants.iter().position(|c| c.path == constant.path) else { + self.api.constants.push(constant); + continue; + }; + let ours = &self.api.constants[at]; + if bare_constant(ours) != bare_constant(&constant) { + let what = format!("const `{}`", constant.path.join("::")); + self.differ(what, (ours.on, ours.span), (constant.on, constant.span)); + } + self.api.constants[at].on.0 |= constant.on.0; + } + } + + /// `theirs` merged into `ours`, each function matched by its path with its instance's + /// arguments, and what it reads. + fn functions(&mut self, mut ours: Vec, theirs: Vec) -> Vec { + for function in theirs { + // A field's getter and setter share its path. + let Some(at) = ours + .iter() + .position(|f| f.shown == function.shown && f.access == function.access) + else { + ours.push(function); + continue; + }; + if bare_function(&ours[at]) != bare_function(&function) { + let what = format!("fn `{}`", function.shown.join("::")); + self.differ(what, (ours[at].on, ours[at].span), (function.on, function.span)); + } + ours[at].on.0 |= function.on.0; + } + ours + } + + /// The variants of `theirs` merged into the merged API's enum `at`, each after the one + /// it follows in `theirs`. + fn variants(&mut self, at: usize, theirs: &Enum, what: String) { + let span = self.api.enums[at].span; + let mut after = 0; + for variant in &theirs.variants { + let ours = &mut self.api.enums[at].variants; + match ours.iter().position(|v| v.name == variant.name) { + Some(found) => { + if bare_variant(&ours[found]) != bare_variant(variant) { + let on = ours[found].on; + self.differ(what.clone(), (on, span), (variant.on, theirs.span)); + } + self.api.enums[at].variants[found].on.0 |= variant.on.0; + after = found + 1; + } + None => { + ours.insert(after, variant.clone()); + after += 1; + } + } + } + // A C enum's values name its variants on every target alike. + let e = &self.api.enums[at]; + if e.plain() { + for (i, a) in e.variants.iter().enumerate() { + if let Some(b) = e.variants[i + 1..].iter().find(|b| b.discriminant == a.discriminant) { + let (a, b) = (a.on, b.on); + self.differ(what.clone(), (a, span), (b, theirs.span)); + return; + } + } + } + } +} + +/// Spans as their file and byte offsets, which a later session loads that file for. +pub mod span { + use std::cell::Cell; + use std::path::{Path, PathBuf}; + + use rustc_span::source_map::SourceMap; + use rustc_span::{BytePos, DUMMY_SP, FileName, Span}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + thread_local! { + static CONTEXT: Cell> = const { Cell::new(None) }; + } + + /// Run `f` with spans read and written through `source_map`, their files relative to + /// `roots`. + pub fn with(source_map: &SourceMap, roots: &[PathBuf], f: impl FnOnce() -> R) -> R { + let previous = CONTEXT.replace(Some((source_map, roots))); + let result = f(); + CONTEXT.set(previous); + result + } + + fn context(f: impl FnOnce(&SourceMap, &[PathBuf]) -> R) -> R { + let (map, roots) = CONTEXT + .get() + .expect("a span is read or written only within `span::with`"); + // SAFETY: `with` sets them to references that outlive `f`'s run, then restores them. + f(unsafe { &*map }, unsafe { &*roots }) + } + + /// A span's file, relative to the root at its index if it is in one, with `/` between + /// its components whatever the host, else absolute; its offsets; and the file's length, + /// which tells a file changed since, or checked out with other line endings. + #[derive(Serialize, Deserialize)] + struct Located { + root: Option, + file: PathBuf, + lo: u32, + hi: u32, + len: u32, + } + + /// `path` relative to the first of `roots` it is in. + fn relative(path: &Path, roots: &[PathBuf]) -> Option<(usize, PathBuf)> { + roots.iter().enumerate().find_map(|(i, root)| { + let rest = path.strip_prefix(root).ok()?; + let parts: Option> = rest.components().map(|c| c.as_os_str().to_str()).collect(); + Some((i, PathBuf::from(parts?.join("/")))) + }) + } + + pub fn serialize(span: &Span, s: S) -> Result { + let located = context(|map, roots| { + let span = span.source_callsite(); + let file = map.lookup_source_file(span.lo()); + let FileName::Real(real) = &file.name else { + return None; + }; + let path = real.local_path()?; + let (root, path) = match relative(path, roots) { + Some((root, path)) => (Some(root), path), + None => (None, path.to_path_buf()), + }; + Some(Located { + root, + file: path, + lo: (span.lo() - file.start_pos).0, + hi: (span.hi() - file.start_pos).0, + len: (file.end_position() - file.start_pos).0, + }) + }); + located.serialize(s) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let Some(located) = Option::::deserialize(d)? else { + return Ok(DUMMY_SP); + }; + Ok(context(|map, roots| { + let path = match located.root { + Some(root) => match roots.get(root) { + Some(root) => root.join(&located.file), + None => return DUMMY_SP, + }, + None => located.file, + }; + match map.load_file(&path) { + Ok(file) if (file.end_position() - file.start_pos).0 == located.len && located.hi <= located.len => { + Span::with_root_ctxt( + file.start_pos + BytePos(located.lo), + file.start_pos + BytePos(located.hi), + ) + } + // Its file is not on this host, or not as it was. + _ => DUMMY_SP, + } + })) + } +} + +/// The path a target's API is staged at in `dir`. +pub fn staged(dir: &Path, target: &str) -> PathBuf { + dir.join(format!("{target}.json")) +} diff --git a/ffi/src/python.rs b/ffi/src/python.rs index 3f2a75b5c..3c188053f 100644 --- a/ffi/src/python.rs +++ b/ffi/src/python.rs @@ -26,7 +26,7 @@ use std::hash::Hasher; use rustc_data_structures::stable_hash::StableHasher; use crate::abi::{self, Abi, Ty, pascal}; -use crate::ir::{Access, Family, Function, Int, Literal, Receiver, Traits, Type}; +use crate::ir::{Access, Family, Function, Int, Literal, On, Receiver, Traits, Type}; use crate::kotlin::{indent_by, rust_c, strip_refs}; const RUNTIME: &str = include_str!("../runtime/rt.py"); @@ -104,6 +104,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result { /// Each module-level Python name, with what claimed it. names: BTreeMap, errors: Vec, + /// The targets of the item being written, whose native functions check theirs. + on: On, } impl<'a> Gen<'a> { @@ -505,6 +508,14 @@ impl<'a> Gen<'a> { Type::Box(inner) => self.lift(inner, o, m), Type::Ref(_, inner) => self.lift(inner, o, "false"), Type::Record(_) => format!("r_{}({o}, s)", self.c_struct(ty)), + Type::Enum(i) if abi.api.enums[*i].variants.iter().any(|v| !abi.api.everywhere(v.on)) => { + // Checks the member a value is against the library's target. + let c = abi.enumeration(*i); + match abi.api.enums[*i].plain() { + true => format!("rt::member_in({o}, {}).and_then(on_{c})", self.t(&c)), + false => format!("r_{c}({o}, s)"), + } + } Type::Enum(i) => match abi.api.enums[*i].plain() { true => format!("rt::member_in({o}, {})", self.t(&abi.enumeration(*i))), false => format!("r_{}({o}, s)", abi.enumeration(*i)), @@ -908,6 +919,7 @@ impl<'a> Gen<'a> { fn record(&mut self, i: usize) { let abi = self.abi; let record = &abi.api.records[i]; + self.on = record.on; let c = abi.record(i); let name = self.class(&c); let rust = abi.api.rust(&record.path, &record.args); @@ -970,6 +982,7 @@ impl<'a> Gen<'a> { fn enumeration(&mut self, i: usize) { let abi = self.abi; let e = &abi.api.enums[i]; + self.on = e.on; let c = abi.enumeration(i); let name = self.class(&c); let rust = abi.api.rust(&e.path, &e.args); @@ -980,6 +993,22 @@ impl<'a> Gen<'a> { self.traits(&ty, &c, e.traits, &mut body); let owner = abi.ty(&ty); if e.plain() { + let missing: String = (e.variants.iter()) + .filter_map(|v| { + let cfg = abi.cfg(v.on)?; + let d = v.discriminant; + Some(format!( + "\t\t#[cfg(not({cfg}))]\n\t\t{d} => rt::missing(\"{c}.{}\"),\n", + v.name + )) + }) + .collect(); + if !missing.is_empty() { + let _ = write!( + self.glue, + "fn on_{c}(v: i32) -> R {{\n\tmatch v {{\n{missing}\t\t_ => Ok(v),\n\t}}\n}}\n\n" + ); + } let data: Vec = e.variants.iter().map(|v| abi::snake(&v.name).to_uppercase()).collect(); // An `Enum`'s members have these. self.methods(&e.methods, (&ty, &owner, &rust), &data, &["name", "value"], &mut body); @@ -1042,10 +1071,18 @@ impl<'a> Gen<'a> { self.py, "@_dataclass(slots=True{repr})\nclass {class}({name}):\n{fields}\n\n{class}.__name__ = \"{vname}\"\n{class}.__qualname__ = \"{name}.{vname}\"\nif not _typing.TYPE_CHECKING:\n\t{name}.{vname} = {class}\n\n\n" ); + // A value of a variant the library's target lacks is refused as it crosses. + let missing = match abi.cfg(variant.on) { + Some(cfg) => format!( + "if !cfg!({cfg}) {{\n\t\t\treturn rt::missing(\"{c}.{}\");\n\t\t}}\n\t\t", + variant.name + ), + None => String::new(), + }; match variant.fields.is_empty() { true => { reads.push(format!( - "if rt::is(o, {tv}) {{\n\t\treturn Ok(crate::{c} {{ tag: {n}, u: unsafe {{ std::mem::zeroed() }} }});\n\t}}" + "if rt::is(o, {tv}) {{\n\t\t{missing}return Ok(crate::{c} {{ tag: {n}, u: unsafe {{ std::mem::zeroed() }} }});\n\t}}" )); writes.push(format!("\t\t{n} => rt::record({tv}, vec![]),")); } @@ -1061,7 +1098,7 @@ impl<'a> Gen<'a> { lowers.push(format!("({key}, {})", self.lower(&f.ty, &format!("p.f{k}")))); } reads.push(format!( - "if rt::is(o, {tv}) {{\n\t\t{}\n\t\treturn Ok(crate::{c} {{ tag: {n}, u: crate::{c}_union {{ v{n}: crate::{payload} {{ {} }} }} }});\n\t}}", + "if rt::is(o, {tv}) {{\n\t\t{missing}{}\n\t\treturn Ok(crate::{c} {{ tag: {n}, u: crate::{c}_union {{ v{n}: crate::{payload} {{ {} }} }} }});\n\t}}", binds.join("\n\t\t"), fields.join(", ") )); @@ -1083,6 +1120,7 @@ impl<'a> Gen<'a> { fn object(&mut self, i: usize) { let abi = self.abi; let object = &abi.api.objects[i]; + self.on = object.on; let c = abi.object(i); let name = self.class(&c); let rust = abi.api.rust(&object.path, &object.args); @@ -1114,6 +1152,7 @@ impl<'a> Gen<'a> { fn callback(&mut self, i: usize) { let abi = self.abi; let callback = &abi.api.callbacks[i]; + self.on = callback.on; let c = abi.callback(i); let name = self.class(&c); let rust = callback.path.join("::"); @@ -1234,6 +1273,7 @@ impl<'a> Gen<'a> { /// A native function `bare`, which the generated Python calls by position with `n` /// arguments as `a`, whose glue runs `body` with a scope `s`, returning an `R`. fn native(&mut self, bare: &str, n: usize, body: &str) { + let body = self.checked(bare, body); let _ = write!( self.glue, "unsafe extern \"C\" fn f_{bare}(_: *mut PyObject, a: *mut *mut PyObject, n: Py_ssize_t) -> *mut PyObject {{\n\trt::wrap(|| unsafe {{\n\t\tlet a = rt::args(a, n, {n}, \"{bare}\")?;\n\t\tlet s = &mut rt::Scope::default();\n\t\t{body}\n\t}})\n}}\n\n" @@ -1241,10 +1281,22 @@ impl<'a> Gen<'a> { self.table.push(format!("rt::method(c\"{bare}\", f_{bare})")); } + /// `body`, raising first on a target the item being written is not on. + fn checked(&self, bare: &str, body: &str) -> String { + match self.abi.cfg(self.on) { + Some(cfg) => format!( + "if !cfg!({cfg}) {{\n\t\t\treturn rt::missing(\"{}_{bare}\");\n\t\t}}\n\t\t{body}", + self.abi.prefix + ), + None => body.to_string(), + } + } + /// A native function `bare` that Python calls as `d` describes, with no Python /// frame between. Its glue runs `body` like a [`Gen::native`]'s, with `a` holding /// `self` first if it has one. fn direct(&mut self, bare: &str, d: Direct<'_>, body: &str) { + let body = &self.checked(bare, body); let member = d.member; let class = d.owner.map(|c| (self.t(c), self.class(c))); if d.property { @@ -1298,6 +1350,19 @@ impl<'a> Gen<'a> { owner: Option<&Ty>, owner_ty: Option<(&Type, &[&str])>, place: Place, + ) -> Option
{ + let on = std::mem::replace(&mut self.on, function.on); + let form = self.function_on(function, owner, owner_ty, place); + self.on = on; + form + } + + fn function_on( + &mut self, + function: &Function, + owner: Option<&Ty>, + owner_ty: Option<(&Type, &[&str])>, + place: Place, ) -> Option { let abi = self.abi; let export = abi.export(function, owner); diff --git a/ffi/src/swift.rs b/ffi/src/swift.rs index 9201e4d35..677aa1719 100644 --- a/ffi/src/swift.rs +++ b/ffi/src/swift.rs @@ -19,7 +19,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::Write; use crate::abi::{self, Abi, Ty, pascal}; -use crate::ir::{Access, Function, Int, Literal, Param, Receiver, Type}; +use crate::ir::{Access, Function, Int, Literal, On, Param, Receiver, Type}; const RUNTIME: &str = include_str!("../runtime/rt.swift"); @@ -50,6 +50,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result, packages: &str, contract: u64) -> Result Swift.Bool {{\n\t\t{p}_contract() == 0x{contract:016x} && {upper}_CONTRACT == 0x{contract:016x}\n\t}}\n\n\ + \t/// The index of the library's target among those this file serves.\n\ + \tstatic let target = Swift.UInt64({p}_target())\n\n\ + \t/// Traps unless the library's target is among those of `on`, as the item `what` is.\n\ + \tstatic func on(_ on: Swift.UInt64, _ what: Swift.String) {{\n\t\tif (on >> target) & 1 == 0 {{\n\t\t\tfatalError(\"\\(what): it is not on this library's target\")\n\t\t}}\n\t}}\n\n\ \tstatic func free(_ buf: Buf) {{\n\t\t{p}_buf_free(buf)\n\t}}\n\n\ \tstatic func load(_ state: UnsafeMutablePointer) -> Swift.Int {{\n\t\t{p}_atomic_load(state)\n\t}}\n\n\ \tstatic func store(_ state: UnsafeMutablePointer, _ value: Swift.Int) {{\n\t\t{p}_atomic_store(state, value)\n\t}}\n\n\ @@ -184,9 +189,21 @@ struct Gen<'a> { /// The exported types thrown as errors, which conform to `Swift.Error`. conforms: BTreeSet, fresh: usize, + /// The targets of the item being written, whose calls check theirs. + on: On, } impl<'a> Gen<'a> { + /// The statement that traps unless the library's target is among those of `on`, as the + /// item `what` is, then `then`; or `then` alone if it is on every target. + fn on(&self, on: On, what: &str, then: &str) -> String { + let api = self.abi.api; + match api.everywhere(on) { + true => then.to_string(), + false => format!("Native.on({}, \"{what}\"){then}", on.0 & api.all().0), + } + } + fn fresh(&mut self, base: &str) -> String { self.fresh += 1; format!("{base}{}_", self.fresh) @@ -570,7 +587,8 @@ impl<'a> Gen<'a> { }; ( format!( - "Native.check()\n\t\t{arena}var c_ = self._lower({})\n\t\treturn ", + "{}Native.check()\n\t\t{arena}var c_ = self._lower({})\n\t\treturn ", + self.on(self.on, c, "\n\t\t"), self.arena_arg(ty) ), "&c_", @@ -615,7 +633,8 @@ impl<'a> Gen<'a> { }; let arg = self.arena_arg(ty); format!( - "Native.check()\n\t\t{arena}var ca_ = a._lower({arg})\n\t\tvar cb_ = b._lower({arg})\n\t\treturn {c}_eq(&ca_, &cb_)" + "{}Native.check()\n\t\t{arena}var ca_ = a._lower({arg})\n\t\tvar cb_ = b._lower({arg})\n\t\treturn {c}_eq(&ca_, &cb_)", + self.on(self.on, c, "\n\t\t") ) } }; @@ -630,7 +649,8 @@ impl<'a> Gen<'a> { _ => format!("{name}._lift({c}_default())"), }; body.push(format!( - "\t/// Rust's `Default`.\n\tpublic static func `default`() -> {name} {{\n\t\tNative.check()\n\t\treturn {lift}\n\t}}\n" + "\t/// Rust's `Default`.\n\tpublic static func `default`() -> {name} {{\n\t\t{}Native.check()\n\t\treturn {lift}\n\t}}\n", + self.on(self.on, c, "\n\t\t") )); } } @@ -655,6 +675,7 @@ impl<'a> Gen<'a> { fn record(&mut self, i: usize) { let abi = self.abi; let record = &abi.api.records[i]; + self.on = record.on; let c = abi.record(i); let name = self.class(&c); let rust = abi.api.rust(&record.path, &record.args); @@ -722,6 +743,7 @@ impl<'a> Gen<'a> { fn enumeration(&mut self, i: usize) { let abi = self.abi; let e = &abi.api.enums[i]; + self.on = e.on; let c = abi.enumeration(i); let name = self.class(&c); let rust = abi.api.rust(&e.path, &e.args); @@ -752,9 +774,11 @@ impl<'a> Gen<'a> { true => cases.push(format!("\tcase {case}\n")), false => cases.push(format!("\tcase {case}({})\n", fields.join(", "))), } + // A value of a variant the library's target lacks is refused as it crosses. + let refused = self.on(variant.on, &format!("{c}.{}", variant.name), "\n\t\t\t"); if e.plain() { lifts.push(format!("\t\tcase {tag}: return .{case}")); - lowers.push(format!("\t\tcase .{case}: return {tag}")); + lowers.push(format!("\t\tcase .{case}:\n\t\t\t{refused}return {tag}")); continue; } let lifted: Vec = (variant.fields.iter()) @@ -774,12 +798,12 @@ impl<'a> Gen<'a> { match variant.fields.is_empty() { true => { lifts.push(format!("\t\tcase {tag}: return .{case}")); - lowers.push(format!("\t\tcase .{case}: c.tag = {tag}")); + lowers.push(format!("\t\tcase .{case}:\n\t\t\t{refused}c.tag = {tag}")); } false => { lifts.push(format!("\t\tcase {tag}: return .{case}({})", lifted.join(", "))); lowers.push(format!( - "\t\tcase .{case}({}):\n\t\t\tc.tag = {tag}\n\t\t\tc.{member} = {payload}({})", + "\t\tcase .{case}({}):\n\t\t\t{refused}c.tag = {tag}\n\t\t\tc.{member} = {payload}({})", binds.join(", "), lowered.join(", ") )); @@ -827,6 +851,7 @@ impl<'a> Gen<'a> { fn object(&mut self, i: usize) { let abi = self.abi; let object = &abi.api.objects[i]; + self.on = object.on; let c = abi.object(i); let name = self.class(&c); let rust = abi.api.rust(&object.path, &object.args); @@ -857,6 +882,7 @@ impl<'a> Gen<'a> { fn callback(&mut self, i: usize) { let abi = self.abi; let callback = &abi.api.callbacks[i]; + self.on = callback.on; let c = abi.callback(i); let name = self.class(&c); let rust = callback.path.join("::"); @@ -1056,6 +1082,9 @@ impl<'a> Gen<'a> { if !object { prelude.push("Native.check()".to_string()); } + if !abi.api.everywhere(function.on) { + prelude.push(self.on(function.on, &c_name, "")); + } let mut arena = false; let mut args: Vec = Vec::new(); let mut c_params = export.call.params.iter(); diff --git a/ffi/tests/api.c b/ffi/tests/api.c index e84bf17a0..0030ca324 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -1239,6 +1239,13 @@ static int broken(const char *which) { api_vec_u16 ports = api_ports((api_vec_socket_addr){0}); ports.len = 1; api_vec_u16_free(ports); + } else if (strcmp(which, "target") == 0) { + /* The header serves wasm32 too, whose items the host's library lacks. */ + api_wasm_only(); + } else if (strcmp(which, "variant") == 0) { + api_pointer_bits((api_Platform){.tag = API_PLATFORM_WASM, .wasm = {.bits = 32}}); + } else if (strcmp(which, "plain variant") == 0) { + api_is_family(API_FAMILY_WASM); } return 0; } diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index 7d724153b..30e5e5bd3 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -797,9 +797,30 @@ Future owned() async { final log = CountLog(); unclosedLogger(log); await collect(() => log.closes == 1, 'the garbage collector frees what an object it frees holds'); + targets(); print('owned ok'); } +/// `f` throws the UnsupportedError of `what`, which the library's target lacks. +void lacks(void Function() f, String what) { + try { + f(); + } on UnsupportedError catch (e) { + check(e.message == "$what: it is not on this library's target", '$what: $e'); + return; + } + check(false, what); +} + +/// The bindings serve wasm32 too, whose items the host's library lacks. +void targets() { + check(api.pointerBits(api.platform()) == 64, "the host's variant crosses"); + check(api.isFamily(api.Family.native), "the host's plain variant crosses"); + lacks(() => api.wasmOnly(), 'api_wasm_only'); + lacks(() => api.pointerBits(const api.PlatformWasm(bits: 32)), 'api_Platform.Wasm'); + lacks(() => api.isFamily(api.Family.wasm), 'api_Family.Wasm'); +} + @pragma('vm:never-inline') void unclosed() { api.Token(3); diff --git a/ffi/tests/api.go b/ffi/tests/api.go index fcd596a4f..34cf06e2c 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -1065,6 +1065,13 @@ func broken(which string) { // A Rust Duration longer than Go's longest. case "duration": api.Extended(math.MaxInt64, 1) + // The packages serve wasm32 too, whose items the host's library lacks. + case "target": + api.WasmOnly() + case "variant": + api.PointerBits(api.PlatformWasm{Bits: 32}) + case "plain variant": + api.IsFamily(api.FamilyWasm) default: check(false, "no case "+which) } diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index 3f8f71d59..76229ec3e 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -693,6 +693,18 @@ fun owned() { check(dropped() == before + 1u, "closing again frees nothing") check(runCatching { token.idGet }.exceptionOrNull()?.message == "Token is closed", "a call after close throws") + // The file serves wasm32 too: what only wasm32 has throws on the host's library. + check(pointerBits(platform()) == 64u && pointerBits(Platform.Native(64u)) == 64u, "the host is native") + check(isFamily(Family.NATIVE), "and of the native family") + for ((what, call) in listOf Unit>>( + "api_wasm_only" to { wasmOnly() }, + "api_Platform.Wasm" to { pointerBits(Platform.Wasm(32u)) }, + "api_Family.Wasm" to { isFamily(Family.WASM) }, + )) { + val thrown = runCatching(call).exceptionOrNull() + check(thrown is UnsupportedOperationException && thrown.message == "$what: it is not on this library's target", "$what throws: $thrown") + } + // Bytes outlive the object that returned them and the record that held them. val body = ByteArray(2048) { 'b'.code.toByte() } val message = Message.new(Stamp.fromMillis(1u), body) diff --git a/ffi/tests/api.py b/ffi/tests/api.py index 2382f2a9a..545407b9a 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -841,6 +841,20 @@ def raises(error, f, what): check(api.doubled_ttl(td.max) == td.max, "timedelta's max is Duration::MAX") raises(ValueError, lambda: api.doubled_ttl(td(seconds=-1)), "a negative duration raises") + # The module serves wasm32 too: what only wasm32 has raises on the host's library. + check(api.pointer_bits(api.platform()) == api.pointer_bits(api.Platform.Native(64)) == 64, "the host is native") + check(api.is_family(api.Family.NATIVE), "and of the native family") + for f, what in [ + (api.wasm_only, "api_wasm_only"), + (lambda: api.pointer_bits(api.Platform.Wasm(32)), "api_Platform.Wasm"), + (lambda: api.is_family(api.Family.WASM), "api_Family.Wasm"), + ]: + try: + f() + check(False, f"{what} raises") + except NotImplementedError as e: + check(str(e) == f"{what}: it is not on this library's target", repr(e)) + if os.name == "posix": path = api.stray() check(os.fsencode(path) == b"\xff", "a path that is not UTF-8 keeps its bytes") diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index 706e0f6d1..2d058807a 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -807,6 +807,13 @@ func broken(_ which: String) { _ = stray() case "lent stray": visitStray { _ in } + // The file serves wasm32 too, whose items the host's library lacks. + case "target": + _ = wasmOnly() + case "variant": + _ = pointerBits(.wasm(bits: 32)) + case "plain variant": + _ = isFamily(.wasm) default: check(false, "no case \(which)") } diff --git a/ffi/tests/fixtures.rs b/ffi/tests/fixtures.rs index 8b7393ded..0dd92f329 100644 --- a/ffi/tests/fixtures.rs +++ b/ffi/tests/fixtures.rs @@ -19,6 +19,21 @@ fn scratch() -> PathBuf { PathBuf::from(env!("CARGO_TARGET_TMPDIR")) } +/// The host's target, which rustc builds for by default. +fn host() -> &'static str { + static HOST: OnceLock = OnceLock::new(); + HOST.get_or_init(|| { + let rustc = run(Command::new(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into())).arg("-vV")); + let version = String::from_utf8_lossy(&rustc.stdout).into_owned(); + let host = version.lines().find_map(|line| line.strip_prefix("host: ")); + host.expect("rustc -vV names the host").to_string() + }) +} + +/// The target the api fixture's binding serves besides the host's, which has an item the +/// host lacks and a variant of an enum. +const WASM: &str = "wasm32-unknown-unknown"; + fn cargo() -> Command { Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) } @@ -34,16 +49,16 @@ fn run(cmd: &mut Command) -> Output { output } -/// Export the fixture `name` into `out` for `target`, else the host, with these cargo -/// args, returning what the exporter printed. -fn export(name: &str, out: &Path, target: Option<&str>, cargo: &[&str]) -> Output { +/// Export the fixture `name` into `out` for `targets` into one binding, else the host, +/// with these cargo args, returning what the exporter printed. +fn export(name: &str, out: &Path, targets: &[&str], cargo: &[&str]) -> Output { let fixture = manifest_dir().join("fixtures").join(name); Command::new(env!("CARGO_BIN_EXE_rust2-ffi")) .arg("--config") .arg(fixture.join("ffi.toml")) .arg("--out") .arg(out) - .args(target.map(|target| ["--target", target]).into_iter().flatten()) + .args(targets.iter().flat_map(|target| ["--target", target])) .arg("--") .args(cargo) .env("CARGO_TARGET_DIR", scratch().join(name).join("build")) @@ -79,7 +94,8 @@ fn api() -> &'static Api { // A feature of the facade's that enables one in a crate it re-exports, which the C // library must build with. let features = ["--features", "doubled"]; - let output = export("api", &out, None, &features); + // One binding for the host and wasm32, which only the host builds and runs. + let output = export("api", &out, &[host(), WASM], &features); assert!( output.status.success(), "exporting failed:\n{}", @@ -187,6 +203,15 @@ fn api_c() { "lent shared", "api_Meter_reset: a handle a call lent shared is borrowed exclusively", ), + ("target", "api_wasm_only: it is not on this library's target"), + ( + "variant", + "api_pointer_bits: the variant is not on this library's target", + ), + ( + "plain variant", + "api_is_family: the variant is not on this library's target", + ), ] { let output = Command::new(&exe).arg(case).output().unwrap(); assert!(!output.status.success(), "{case} did not abort"); @@ -426,7 +451,7 @@ fn api_android() { abi => panic!("the android shell's toolchain has no target for the device's ABI {abi}"), }; let out = dir.join("out"); - let output = export("api", &out, Some(target), &["--features", "doubled"]); + let output = export("api", &out, &[target, WASM], &["--features", "doubled"]); assert!( output.status.success(), "exporting for {target} failed:\n{}", @@ -737,6 +762,9 @@ fn api_swift() { ("outlived", OUTLIVED), ("stray", "a path from Rust is not UTF-8"), ("lent stray", "a path from Rust is not UTF-8"), + ("target", "api_wasm_only: it is not on this library's target"), + ("variant", "api_Platform.Wasm: it is not on this library's target"), + ("plain variant", "api_Family.Wasm: it is not on this library's target"), ] { let output = Command::new(&exe).arg(case).output().unwrap(); assert!(!output.status.success(), "Swift {case} did not fail"); @@ -986,6 +1014,9 @@ fn api_go() { ("text", "api_greet: a string is not UTF-8"), ("callback panicked", "a log failed"), ("duration", "a Rust Duration past time.Duration's longest"), + ("target", "api_wasm_only: it is not on this library's target"), + ("variant", "api_Platform.Wasm: it is not on this library's target"), + ("plain variant", "api_Family.Wasm: it is not on this library's target"), ] { let output = Command::new(&exe).arg(case).output().unwrap(); assert!(!output.status.success(), "Go {case} did not fail"); @@ -1033,7 +1064,7 @@ fn go_module() { ) .unwrap(); let out = module.join("sub").join("out"); - let output = export("api", &out, None, &[]); + let output = export("api", &out, &[], &[]); assert!( output.status.success(), "exporting failed:\n{}", @@ -1216,7 +1247,7 @@ fn relative_target() { #[test] fn reject() { let out = scratch().join("reject").join("out"); - let output = export("reject", &out, None, &[]); + let output = export("reject", &out, &[], &[]); assert!(!output.status.success(), "exporting reject succeeded"); let stderr = String::from_utf8_lossy(&output.stderr); // Each error at its span, then each of its notes at its own. @@ -1261,7 +1292,7 @@ fn reject() { #[test] fn feature_of_dependency() { let out = scratch().join("feature").join("out"); - let output = export("api", &out, None, &["--features", "bytes/std"]); + let output = export("api", &out, &[], &["--features", "bytes/std"]); assert!(!output.status.success(), "exporting with bytes/std succeeded"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( @@ -1541,37 +1572,37 @@ fn configured_out() { #[test] fn target() { let package = package("targeted"); - write_all( - &package, - &[( - "src/lib.rs", - "pub const MAX: usize = usize::MAX;\n\n\ - pub fn halve(n: usize) -> usize {\n\tn / 2\n}\n\n\ - #[cfg(target_arch = \"wasm32\")]\npub fn wasm() -> u32 {\n\t32\n}\n\n\ - #[cfg(not(target_arch = \"wasm32\"))]\npub fn desktop() -> u32 {\n\t64\n}\n", - )], - ); - let export = |name: &str, target: &[&str]| { - let out = package.join(name); - run(Command::new(env!("CARGO_BIN_EXE_rust2-ffi")) - .arg("--config") + let shared = "pub fn halve(n: usize) -> usize {\n\tn / 2\n}\n\n\ + #[cfg(target_arch = \"wasm32\")]\npub fn wasm() -> u32 {\n\t32\n}\n\n\ + #[cfg(not(target_arch = \"wasm32\"))]\npub fn desktop() -> u32 {\n\t64\n}\n"; + // Items whose definitions differ between targets, which one binding cannot serve. + let differing = "pub const MAX: usize = usize::MAX;\n\n\ + #[cfg(target_arch = \"wasm32\")]\npub fn width() -> u32 {\n\t32\n}\n\n\ + #[cfg(not(target_arch = \"wasm32\"))]\npub fn width() -> u64 {\n\t64\n}\n"; + write_all(&package, &[("src/lib.rs", &format!("{shared}\n{differing}"))]); + let exporter = |args: &[&str]| { + let mut cmd = Command::new(env!("CARGO_BIN_EXE_rust2-ffi")); + cmd.arg("--config") .arg(package.join("ffi.toml")) - .arg("--out") - .arg(&out) - .args(target) - .env("CARGO_TARGET_DIR", package.join("build"))); + .args(args) + .env("CARGO_TARGET_DIR", package.join("build")); + cmd + }; + let export = |name: &str, args: &[&str]| { + let out = package.join(name); + run(exporter(&["--out", out.to_str().unwrap()]).args(args)); std::fs::copy(package.join("Cargo.lock"), out.join("Cargo.lock")).unwrap(); let header = std::fs::read_to_string(out.join("include/targeted.h")).unwrap(); (out, header) }; - let (host, header) = export("host", &[]); + let (host_out, header) = export("host", &[]); assert!( header.contains("targeted_desktop(") && !header.contains("targeted_wasm("), "{header}" ); assert!(header.contains(&u64::MAX.to_string()), "{header}"); - build_offline(&host, &package.join("glue")); - let (wasm, header) = export("wasm", &["--target", "wasm32-unknown-unknown"]); + build_offline(&host_out, &package.join("glue")); + let (wasm, header) = export("wasm", &["--target", WASM]); assert!( header.contains("targeted_wasm(") && !header.contains("targeted_desktop("), "{header}" @@ -1580,7 +1611,7 @@ fn target() { header.contains(&u32::MAX.to_string()) && !header.contains(&u64::MAX.to_string()), "{header}" ); - let glue = |target: &[&str]| { + let glue = |out: &Path, target: &[&str]| { let mut cmd = cargo(); cmd.args([ "check", @@ -1590,18 +1621,92 @@ fn target() { "kotlin,dart", "--manifest-path", ]) - .arg(wasm.join("Cargo.toml")) + .arg(out.join("Cargo.toml")) .arg("--target-dir") .arg(package.join("glue")) .args(target) .env("RUSTFLAGS", "-D warnings"); cmd }; - run(&mut glue(&["--target", "wasm32-unknown-unknown"])); - let output = glue(&[]).output().unwrap(); + run(&mut glue(&wasm, &["--target", WASM])); + let output = glue(&wasm, &[]).output().unwrap(); let stderr = String::from_utf8_lossy(&output.stderr); assert!(!output.status.success(), "wasm32's export built for the host"); - assert!(stderr.contains("exported for wasm32-unknown-unknown, not "), "{stderr}"); + assert!(stderr.contains(&format!("exported for {WASM}, not ")), "{stderr}"); + + // One binding cannot serve an item whose definition differs between its targets: each + // is refused at its span, naming both. + let merged = package.join("merged"); + // From an earlier run's export. + let _ = std::fs::remove_dir_all(&merged); + let output = exporter(&["--out", merged.to_str().unwrap(), "--target", host(), "--target", WASM]) + .output() + .unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(!output.status.success(), "differing items exported"); + let (first, second) = match host() < WASM { + true => (host(), WASM), + false => (WASM, host()), + }; + for what in ["const `targeted::MAX`", "fn `targeted::width`"] { + let differs = format!("rust2-ffi: {what} differs between {first} and {second}"); + assert!(stderr.contains(&differs), "{stderr}"); + } + assert!(!merged.exists(), "a refused export wrote {}", merged.display()); + + // Staged on another host, with the checkout elsewhere, an item keeps its span there. + let staged = package.join("differing.json"); + run(&mut exporter(&["--api", staged.to_str().unwrap(), "--target", WASM])); + let moved = scratch().join("targeted-moved"); + for file in ["Cargo.toml", "ffi.toml", "src/lib.rs"] { + std::fs::create_dir_all(moved.join(file).parent().unwrap()).unwrap(); + std::fs::copy(package.join(file), moved.join(file)).unwrap(); + } + let output = Command::new(env!("CARGO_BIN_EXE_rust2-ffi")) + .arg("--config") + .arg(moved.join("ffi.toml")) + .arg("--out") + .arg(moved.join("out")) + .arg("--with") + .arg(&staged) + .env("CARGO_TARGET_DIR", moved.join("build")) + .output() + .unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(!output.status.success(), "differing items exported"); + // Each target's span shows its own line of the moved checkout. + assert!( + stderr.contains("pub fn width() -> u32") && stderr.contains("pub fn width() -> u64"), + "{stderr}" + ); + + // Without them, one binding serves both targets, each with only its own items. + write_all(&package, &[("src/lib.rs", shared)]); + let (merged, header) = export("merged", &["--target", host(), "--target", WASM]); + assert!( + header.contains("targeted_desktop(") + && header.contains("targeted_wasm(") + && header.contains("targeted_target("), + "{header}" + ); + run(&mut glue(&merged, &["--target", WASM])); + run(&mut glue(&merged, &[])); + + // A target staged on its own host merges into the same binding. + let staged = package.join("wasm.json"); + run(&mut exporter(&["--api", staged.to_str().unwrap(), "--target", WASM])); + let (_, joined) = export("joined", &["--with", staged.to_str().unwrap()]); + assert_eq!(joined, header); + + // A census or a staged API is of one target. + for output in ["--census", "--api"] { + let out = package.join("one"); + let output = exporter(&[output, out.to_str().unwrap(), "--target", host(), "--target", WASM]) + .output() + .unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(!output.status.success() && stderr.contains("one --target"), "{stderr}"); + } } /// Writes each `(path, text)` into `dir`. diff --git a/quest/m1/ffi-export/moq-targets.md b/quest/m1/ffi-export/moq-targets.md new file mode 100644 index 000000000..e03dc5996 --- /dev/null +++ b/quest/m1/ffi-export/moq-targets.md @@ -0,0 +1,41 @@ +# [M] Export moq for every target it ships + +## Goal + +Nightly exports moq's stand-in facade for each target moq ships into one binding, as +[rust2-ffi](/ffi/README.md#targets) shares a binding across targets, and fails on an item +whose definition differs between two of them. Each target's API is staged with `--api` +on a host with its C toolchain and merged with `--with` on Linux, whose export then builds +and runs `just ffi moq` as today. + +## Plan + +moq ships one binding per language across targets +([moq's release workflows](https://github.com/moq-dev/moq/tree/main/.github/workflows)): + +- Kotlin: three Android ABIs, Linux x86_64 and aarch64, macOS, and Windows. +- Dart: those, plus Intel macOS and three iOS targets. +- Go: Linux x86_64 and aarch64, macOS, and Windows. +- Swift: iOS, the iOS simulator, and macOS. +- Python wheels and the C library ship per target, so they need nothing more. + +So the union is the three Android ABIs (in `nix develop .#android`, which has the NDK), +Linux x86_64 and aarch64, macOS on aarch64 and x86_64 and the iOS device and simulator +targets (on a macOS runner), and Windows MSVC (on a Windows runner): moq's `aws-lc-sys` +and `ring` compile C in their build scripts even for a check, so each target stages where +its toolchain is. Each staging job uploads its JSON as an artifact, and the Linux job +downloads them all. + +At moq `ffcb7cd9`, the stand-in facade's items that differ by target are all in +moq-tokio: `tokio::Error::Unix`, `tokio::listen::Config::unix`, and the `tokio::unix` +module exist only on unix, and `tokio::tls::Error::AndroidInit`, `tokio::tls::init_android`, +and the `jni` re-export only on Android. Each is an item or a variant only some targets +have, which the binding serves, so none should be refused: `tokio::listen::Config` holds a +`pub(crate)` field, so it exports as an object, whose `unix` field's accessors are only +unix's, rather than as a record whose fields differ, which the export refuses. + +## Hosts + +- linux +- macos - the Apple targets' checks need Apple's SDK +- windows - the MSVC target's check needs MSVC diff --git a/quest/m1/ffi-export/target.md b/quest/m1/ffi-export/target.md deleted file mode 100644 index aff169d5c..000000000 --- a/quest/m1/ffi-export/target.md +++ /dev/null @@ -1,42 +0,0 @@ -# [L] Share a binding across targets - -## Goal - -One binding serves every target its package ships to, such as one Kotlin file for each -Android ABI and the desktops, though their exports differ by `cfg`: on each target, every -item that target compiles works exactly and no other item does. Proven by a fixture whose -API differs by target, exported for the host and wasm32 into one binding and driven on the -host, and by moq's census on each target moq ships. - -## Plan - -`rust2-ffi --target ` exports one target's API, with that target's `cfg`s and -layouts, into a crate whose build script refuses any other target; the `target` test in -`ffi/tests/fixtures.rs` proves it for the host and wasm32. moq ships one binding per -language across targets ([moq's release workflows](https://github.com/moq-dev/moq/tree/main/.github/workflows)): - -- Kotlin: three Android ABIs, Linux x86_64 and aarch64, macOS, and Windows. -- Dart: those, plus Intel macOS and three iOS targets. -- Go: Linux x86_64 and aarch64, macOS, and Windows. -- Swift: iOS, the iOS simulator, and macOS. -- Python wheels and the C library ship per target, so they need nothing here. - -At moq `ffcb7cd9`, the stand-in facade's items that differ by target are all in -moq-tokio: `tokio::Error::Unix`, `tokio::listen::Config::unix`, and the `tokio::unix` -module exist only on unix, and `tokio::tls::Error::AndroidInit`, `tokio::tls::init_android`, -and the `jni` re-export only on Android. Swift's targets differ in none of them; Kotlin's, -Dart's, and Go's differ in `tokio::Error`, moq's top-level error. - -Each target's census has to run where its C toolchain is: moq's dependencies `aws-lc-sys` -and `ring` compile C in their build scripts even for a check, so a Linux host without the -NDK, Apple's SDK, or MSVC can census only itself. - -Decided on 2026-11-10, taken unattended under the maintainer's standing instruction to -take the recommended option, so it may be revisited: how a shared binding treats an item -only some of its targets have. ✅ (b) export every target's items: on a target that lacks -one, calling it raises the language's own error, and a value holding a field it lacks is -refused; only an item whose signature differs between targets is refused at export, at its -span, naming both. Rejected: (a) refuse such items, which would refuse moq's -`tokio::Error`, `tokio::listen::Config`, and `tokio::tls::Error` in Kotlin, Dart, and Go -and need moq source changes, against the 2026-11-09 rule that rust2 asks moq for none; -(c) one binding per target, which the JVM, Dart, and Go cannot load under one name. From 057a4e97b07bf338a198412dd187ee5652dec1df Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:05:07 -0800 Subject: [PATCH 3/8] fix(ffi): gate a callback's every impl, reserve `target`, and merge order by use A callback only some targets have gates its Send, Sync, and Drop impls too. The runtime's `_target` is reserved, so an item of that name is a clash at its span. A data enum's variants merge whatever their Rust discriminants, which only a plain enum's C values are. The merged crates follow each crate whose types their items name, on any target. Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/api/src/lib.rs | 31 ++++++- ffi/fixtures/reject/expected.txt | 2 + ffi/fixtures/reject/src/lib.rs | 5 ++ ffi/src/abi.rs | 1 + ffi/src/glue.rs | 12 ++- ffi/src/merge.rs | 135 +++++++++++++++++++++++++++---- 6 files changed, 165 insertions(+), 21 deletions(-) diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 2d0c774f7..b28df3f00 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -2182,13 +2182,14 @@ pub fn refuse(reason: &str) -> Result> { } /// The kind of target the library was built for. One binding serves the host and wasm32, -/// so it has every target's variants, and a target refuses one it lacks. +/// so it has every target's variants, and a target refuses one it lacks. As wasm32's comes +/// first, `Native`'s Rust discriminant differs between the two, which its C tag does not. pub enum Platform { - /// A native target, with its pointers' width in bits. - Native(u32), /// wasm32, which only that target has. #[cfg(target_arch = "wasm32")] Wasm { bits: u32 }, + /// A native target, with its pointers' width in bits. + Native(u32), } /// The library's target's platform. @@ -2250,3 +2251,27 @@ impl Worker { self.id } } + +/// A record only wasm32 has. +#[cfg(target_arch = "wasm32")] +pub struct Page { + pub size: u32, +} + +/// wasm32's page. +#[cfg(target_arch = "wasm32")] +pub fn page() -> Page { + Page { size: 65536 } +} + +/// A callback only wasm32 has. +#[cfg(target_arch = "wasm32")] +pub trait Beat: Send + Sync { + fn beat(&self) -> u32; +} + +/// What `beat` counts. +#[cfg(target_arch = "wasm32")] +pub fn beats(beat: Box) -> u32 { + beat.beat() +} diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index 7e1e02b0d..31202768f 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -105,4 +105,6 @@ fixtures/reject/src/lib.rs:864:9: rust2-ffi: cannot export use `renamed`: re-exp fixtures/reject/src/lib.rs:875:2: rust2-ffi: cannot export method `Tally::poll_bump`: `&mut self` of a value; exclude it in the config to leave it out fixtures/reject/src/lib.rs:895:1: rust2-ffi: cannot export fn `poll_risky`: Poll with no Context or waiter parameter `std::task::Poll<()>`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:911:1: rust2-ffi: cannot export fn `poll_loose`: Poll with no Context or waiter parameter `std::task::Poll<()>`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:917:1: rust2-ffi: fn `reject::target` and the runtime are both `reject_target` in C + fixtures/reject/src/lib.rs:133:1: note: the runtime is here fixtures/reject/src/lib.rs:94:1: rust2-ffi: cannot export fn `first`: borrowed return that is not Clone `Opaque`; exclude it in the config to leave it out diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index 90de6f5a4..cf01305ba 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -912,3 +912,8 @@ pub fn poll_loose(waiter: &Loose) -> std::task::Poll<()> { waiter.0.wake_by_ref(); std::task::Poll::Ready(()) } + +/// The runtime's `reject_target`, which names the library's target. +pub fn target() -> u32 { + 0 +} diff --git a/ffi/src/abi.rs b/ffi/src/abi.rs index d35a0f848..a0fc05eaa 100644 --- a/ffi/src/abi.rs +++ b/ffi/src/abi.rs @@ -634,6 +634,7 @@ impl<'a> Abi<'a> { // package, and Swift's in its module and the atomics header its runtime adds. for runtime in [ "contract", + "target", "buf", "buf_free", "u128", diff --git a/ffi/src/glue.rs b/ffi/src/glue.rs index 21eff7efc..417a6e3cc 100644 --- a/ffi/src/glue.rs +++ b/ffi/src/glue.rs @@ -1124,8 +1124,18 @@ impl Gen<'_> { self.line(format!( "#[repr(C)]\n#[derive(Clone, Copy)]\npub struct {name} {{\n\tpub vtable: *const {name}_vtable,\n\tpub data: *mut ::std::ffi::c_void,\n}}\n" )); + // Each item apart, as each takes the target's `#[cfg]`. self.item(format!( - "/// The foreign implementation, which frees its data when dropped.\npub struct Foreign_{name}({name});\n\n// SAFETY: the callback's contract: its functions may be called from any thread.\nunsafe impl Send for Foreign_{name} {{}}\n// SAFETY: as above.\nunsafe impl Sync for Foreign_{name} {{}}\n\nimpl Drop for Foreign_{name} {{\n\tfn drop(&mut self) {{\n\t\tif let Some(free) = unsafe {{ (*self.0.vtable).free }} {{\n\t\t\tunsafe {{ free(self.0.data) }};\n\t\t}}\n\t}}\n}}\n" + "/// The foreign implementation, which frees its data when dropped.\npub struct Foreign_{name}({name});\n" + )); + self.item(format!( + "// SAFETY: the callback's contract: its functions may be called from any thread.\nunsafe impl Send for Foreign_{name} {{}}\n" + )); + self.item(format!( + "// SAFETY: as above.\nunsafe impl Sync for Foreign_{name} {{}}\n" + )); + self.item(format!( + "impl Drop for Foreign_{name} {{\n\tfn drop(&mut self) {{\n\t\tif let Some(free) = unsafe {{ (*self.0.vtable).free }} {{\n\t\t\tunsafe {{ free(self.0.data) }};\n\t\t}}\n\t}}\n}}\n" )); self.item(format!("impl {path} for Foreign_{name} {{\n{methods}}}\n")); } diff --git a/ffi/src/merge.rs b/ffi/src/merge.rs index b895e1950..6ea741d19 100644 --- a/ffi/src/merge.rs +++ b/ffi/src/merge.rs @@ -65,12 +65,13 @@ pub fn merge(mut apis: Vec) -> (Api, Vec) { let mut merged = Merged { api: apis.next().expect("an export has a target"), differs: Vec::new(), + renumbered: Vec::new(), }; merged.api.targets = targets; for api in apis { merged.add(api); } - (merged.api, merged.differs) + merged.finish() } /// Mark every item of `api` as on `on`. @@ -100,6 +101,9 @@ fn mark(api: &mut Api, on: On) { struct Merged { api: Api, differs: Vec, + /// Each variant whose discriminant differs between two targets, with its enum: an + /// error only if the merged enum is plain, whose C values are its discriminants. + renumbered: Vec<(usize, Differs)>, } /// Where each type of the API being added lands among the merged API's. @@ -208,6 +212,8 @@ fn bare_enum(e: &Enum) -> Enum { fn bare_variant(v: &Variant) -> Variant { bare(v, |v| { v.on = On::ALL; + // Compared apart, as only a plain enum's matter. + v.discriminant = 0; v.fields.iter_mut().for_each(|f| f.docs.clear()); }) } @@ -246,10 +252,41 @@ impl Merged { /// Refuse `what`, which the merged API has on `ours` at `span` and `other` on its target /// at `theirs`. - fn differ(&mut self, what: String, (ours, span): (On, Span), (other, theirs): (On, Span)) { + fn differ(&mut self, what: String, ours: (On, Span), theirs: (On, Span)) { + let differs = self.differs(what, ours, theirs); + self.differs.push(differs); + } + + fn differs(&self, what: String, (ours, span): (On, Span), (other, theirs): (On, Span)) -> Differs { let first = (self.first(ours), span); let second = (self.first(other), theirs); - self.differs.push(Differs { what, first, second }); + Differs { what, first, second } + } + + /// The merged API, and what differs between its targets, once every target is added. + fn finish(mut self) -> (Api, Vec) { + let renumbered = std::mem::take(&mut self.renumbered); + for (at, differs) in renumbered { + if self.api.enums[at].plain() { + self.differs.push(differs); + } + } + // A C enum's values name its variants on every target alike. + for e in &self.api.enums { + if !e.plain() { + continue; + } + let what = format!("enum `{}`", e.shown.join("::")); + for (i, a) in e.variants.iter().enumerate() { + if let Some(b) = e.variants[i + 1..].iter().find(|b| b.discriminant == a.discriminant) { + let differs = self.differs(what.clone(), (a.on, e.span), (b.on, e.span)); + self.differs.push(differs); + break; + } + } + } + self.api.crates = order_crates(&self.api); + (self.api, self.differs) } /// Add `api`'s items, of one target, to the merged API's. @@ -286,10 +323,16 @@ impl Merged { maps.functions(&mut callback.methods); } maps.functions(&mut api.functions); - for krate in api.crates { - if !self.api.crates.contains(&krate) { - self.api.crates.push(krate); + // In dependency order, as each list is: one only this target has goes before the + // first crate after it here that the merged list has already. + for (i, krate) in api.crates.iter().enumerate() { + if self.api.crates.contains(krate) { + continue; } + let at = (api.crates[i + 1..].iter()) + .find_map(|next| self.api.crates.iter().position(|c| c == next)) + .unwrap_or(self.api.crates.len()); + self.api.crates.insert(at, krate.clone()); } for (record, at) in api.records.into_iter().zip(maps.records) { let Some(ours) = self.api.records.get(at) else { @@ -390,9 +433,12 @@ impl Merged { let ours = &mut self.api.enums[at].variants; match ours.iter().position(|v| v.name == variant.name) { Some(found) => { + let on = ours[found].on; if bare_variant(&ours[found]) != bare_variant(variant) { - let on = ours[found].on; self.differ(what.clone(), (on, span), (variant.on, theirs.span)); + } else if ours[found].discriminant != variant.discriminant { + let differs = self.differs(what.clone(), (on, span), (variant.on, theirs.span)); + self.renumbered.push((at, differs)); } self.api.enums[at].variants[found].on.0 |= variant.on.0; after = found + 1; @@ -403,18 +449,73 @@ impl Merged { } } } - // A C enum's values name its variants on every target alike. - let e = &self.api.enums[at]; - if e.plain() { - for (i, a) in e.variants.iter().enumerate() { - if let Some(b) = e.variants[i + 1..].iter().find(|b| b.discriminant == a.discriminant) { - let (a, b) = (a.on, b.on); - self.differ(what.clone(), (a, span), (b, theirs.span)); - return; - } - } + } +} + +/// `api`'s crates in an order where each follows every crate whose types its items name, +/// on any target, as each target's order has it; else in the order they were merged in. +/// Two targets may order crates differently where neither names the other's types. +fn order_crates(api: &Api) -> Vec { + let crates = &api.crates; + let index = |krate: &str| crates.iter().position(|c| c == krate); + // Each crate's home, then a type its items name. + let mut uses: Vec<(&str, &Type)> = Vec::new(); + fn functions<'a>(uses: &mut Vec<(&'a str, &'a Type)>, functions: &'a [Function]) { + for f in functions { + let types = f.params.iter().map(|p| &p.ty).chain([&f.ok]).chain(&f.err); + uses.extend(types.map(|ty| (f.home.krate.as_str(), ty))); } } + for r in &api.records { + let types = r.fields.iter().map(|f| &f.ty).chain(&r.args); + uses.extend(types.map(|ty| (r.home.krate.as_str(), ty))); + functions(&mut uses, &r.methods); + } + for e in &api.enums { + let fields = e.variants.iter().flat_map(|v| &v.fields); + uses.extend( + fields + .map(|f| &f.ty) + .chain(&e.args) + .map(|ty| (e.home.krate.as_str(), ty)), + ); + functions(&mut uses, &e.methods); + } + for o in &api.objects { + uses.extend(o.args.iter().map(|ty| (o.home.krate.as_str(), ty))); + functions(&mut uses, &o.methods); + } + for c in &api.callbacks { + functions(&mut uses, &c.methods); + } + functions(&mut uses, &api.functions); + let mut before = vec![std::collections::BTreeSet::new(); crates.len()]; + for (home, ty) in uses { + let Some(home) = index(home) else { continue }; + crate::ir::each(ty, &mut |named| { + let krate = match named { + Type::Record(i) => &api.records[*i].home.krate, + Type::Enum(i) => &api.enums[*i].home.krate, + Type::Object(i) => &api.objects[*i].home.krate, + Type::Callback(i, _) => &api.callbacks[*i].home.krate, + _ => return, + }; + match index(krate) { + Some(named) if named != home => _ = before[home].insert(named), + _ => {} + } + }); + } + let mut placed = vec![false; crates.len()]; + let mut order = Vec::with_capacity(crates.len()); + while order.len() < crates.len() { + let next = (0..crates.len()) + .find(|&i| !placed[i] && before[i].iter().all(|&j| placed[j])) + .expect("crates whose items name each other's types, which Rust's dependencies cannot"); + placed[next] = true; + order.push(crates[next].clone()); + } + order } /// Spans as their file and byte offsets, which a later session loads that file for. From faf2dde4ae7a4f11345af3c576f2be5b2620f36b Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:07:19 -0800 Subject: [PATCH 4/8] docs(ffi): a plain enum's values must agree across targets Co-Authored-By: Claude Opus 5.5 --- ffi/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffi/README.md b/ffi/README.md index 7cf4ed02f..83dfedfc6 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -155,8 +155,8 @@ item that target compiles works exactly, and no other does: - A constant only some targets have keeps its value everywhere, since reading it calls nothing. - An item whose definition differs between two targets, such as a function whose - signature does, a record with a field only one has, or a constant of another value, is - refused at its span, with a note at the other's, naming both targets. + signature does, a record with a field only one has, a plain enum whose variant has + another value, or a constant of another value, is refused at its span, with a note at the other's, naming both targets. The crate's build script sets `cfg(rust2_ffi_target = "")` for the target it builds for, which gates each item, and refuses a target it was not exported for. From 44a63bf5af326134f7d5e1c285883be26030ef13 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:13:04 -0800 Subject: [PATCH 5/8] fix(ffi): refuse crates whose items name each other's types across targets An instance's crate is the last of its arguments' on its own target, so where two targets order independent crates differently, the merged crates can name each other's types. That is now a refusal at an item of each, naming both targets, rather than a panic. Co-Authored-By: Claude Opus 5.5 --- ffi/src/merge.rs | 126 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 26 deletions(-) diff --git a/ffi/src/merge.rs b/ffi/src/merge.rs index 6ea741d19..6eb49b27f 100644 --- a/ffi/src/merge.rs +++ b/ffi/src/merge.rs @@ -12,7 +12,7 @@ use std::path::{Path, PathBuf}; use rustc_span::source_map::SourceMap; use rustc_span::{DUMMY_SP, Span}; -use crate::ir::{Api, Callback, Constant, Enum, Field, Function, Object, On, Record, TaskKind, Type, Variant}; +use crate::ir::{Api, Callback, Constant, Enum, Field, Function, Home, Object, On, Record, TaskKind, Type, Variant}; /// A target's API, as a check writes it for the export of several targets. #[derive(serde::Serialize, serde::Deserialize)] @@ -285,7 +285,15 @@ impl Merged { } } } - self.api.crates = order_crates(&self.api); + match order_crates(&self.api) { + Ok(crates) => self.api.crates = crates, + Err(cycle) => { + let names: Vec = cycle.crates.iter().map(|c| format!("`{c}`")).collect(); + let what = format!("the order of crates {}", names.join(" and ")); + let differs = self.differs(what, cycle.first, cycle.second); + self.differs.push(differs); + } + } (self.api, self.differs) } @@ -455,43 +463,44 @@ impl Merged { /// `api`'s crates in an order where each follows every crate whose types its items name, /// on any target, as each target's order has it; else in the order they were merged in. /// Two targets may order crates differently where neither names the other's types. -fn order_crates(api: &Api) -> Vec { +/// +/// An instance lives in the last crate of its own and its arguments' on its target, which +/// on another target, where those crates' order differs, may name types of crates after +/// it. Then crates name each other's types, which none of their orders serves: that is +/// the error, with an item of each crate that names the other's. +fn order_crates(api: &Api) -> Result, Cycle> { let crates = &api.crates; let index = |krate: &str| crates.iter().position(|c| c == krate); - // Each crate's home, then a type its items name. - let mut uses: Vec<(&str, &Type)> = Vec::new(); - fn functions<'a>(uses: &mut Vec<(&'a str, &'a Type)>, functions: &'a [Function]) { + // Each type an item names, with the item's crate, targets, and span. + let mut uses: Vec<(&Home, On, Span, &Type)> = Vec::new(); + fn functions<'a>(uses: &mut Vec<(&'a Home, On, Span, &'a Type)>, functions: &'a [Function]) { for f in functions { let types = f.params.iter().map(|p| &p.ty).chain([&f.ok]).chain(&f.err); - uses.extend(types.map(|ty| (f.home.krate.as_str(), ty))); + uses.extend(types.map(|ty| (&f.home, f.on, f.span, ty))); } } for r in &api.records { let types = r.fields.iter().map(|f| &f.ty).chain(&r.args); - uses.extend(types.map(|ty| (r.home.krate.as_str(), ty))); + uses.extend(types.map(|ty| (&r.home, r.on, r.span, ty))); functions(&mut uses, &r.methods); } for e in &api.enums { - let fields = e.variants.iter().flat_map(|v| &v.fields); - uses.extend( - fields - .map(|f| &f.ty) - .chain(&e.args) - .map(|ty| (e.home.krate.as_str(), ty)), - ); + let fields = e.variants.iter().flat_map(|v| &v.fields).map(|f| &f.ty); + uses.extend(fields.chain(&e.args).map(|ty| (&e.home, e.on, e.span, ty))); functions(&mut uses, &e.methods); } for o in &api.objects { - uses.extend(o.args.iter().map(|ty| (o.home.krate.as_str(), ty))); + uses.extend(o.args.iter().map(|ty| (&o.home, o.on, o.span, ty))); functions(&mut uses, &o.methods); } for c in &api.callbacks { functions(&mut uses, &c.methods); } functions(&mut uses, &api.functions); - let mut before = vec![std::collections::BTreeSet::new(); crates.len()]; - for (home, ty) in uses { - let Some(home) = index(home) else { continue }; + // Each crate's predecessors, each with the first item naming its types. + let mut before = vec![std::collections::BTreeMap::new(); crates.len()]; + for (home, on, span, ty) in uses { + let Some(home) = index(&home.krate) else { continue }; crate::ir::each(ty, &mut |named| { let krate = match named { Type::Record(i) => &api.records[*i].home.krate, @@ -500,22 +509,63 @@ fn order_crates(api: &Api) -> Vec { Type::Callback(i, _) => &api.callbacks[*i].home.krate, _ => return, }; - match index(krate) { - Some(named) if named != home => _ = before[home].insert(named), - _ => {} + if let Some(named) = index(krate).filter(|&named| named != home) { + before[home].entry(named).or_insert((on, span)); } }); } let mut placed = vec![false; crates.len()]; let mut order = Vec::with_capacity(crates.len()); while order.len() < crates.len() { - let next = (0..crates.len()) - .find(|&i| !placed[i] && before[i].iter().all(|&j| placed[j])) - .expect("crates whose items name each other's types, which Rust's dependencies cannot"); + let ready = (0..crates.len()).find(|&i| !placed[i] && before[i].keys().all(|&j| placed[j])); + let Some(next) = ready else { + return Err(cycle(crates, &before, &placed)); + }; placed[next] = true; order.push(crates[next].clone()); } - order + Ok(order) +} + +/// Crates whose items name each other's types, across targets: their names, each naming +/// the next's types and the last the first's; then the first's item naming the next's, +/// and another crate's item naming its next's on a target the first item is not on. +struct Cycle { + crates: Vec, + first: (On, Span), + second: (On, Span), +} + +/// A cycle among the crates not `placed`, each of which has a predecessor not placed. +fn cycle(crates: &[String], before: &[std::collections::BTreeMap], placed: &[bool]) -> Cycle { + let unplaced = |i: usize| { + (before[i].iter()) + .find(|&(&j, _)| !placed[j]) + .map(|(&j, &item)| (j, item)) + }; + let mut path = vec![placed.iter().position(|p| !p).expect("a crate not placed")]; + let start = loop { + let (next, _) = unplaced(*path.last().unwrap()).expect("a crate not placed has one before it"); + if let Some(at) = path.iter().position(|&i| i == next) { + break at; + } + path.push(next); + }; + let path = &path[start..]; + // Each crate's item naming its predecessor's types, along the cycle. + let items: Vec<(On, Span)> = path.iter().map(|&i| unplaced(i).unwrap().1).collect(); + let first = items[0]; + // No one target has the whole cycle, so an item on a target the first is not on. + let lowest = first.0.0 & first.0.0.wrapping_neg(); + let second = (items[1..].iter()) + .find(|(on, _)| on.0 & lowest == 0) + .copied() + .unwrap_or(items[items.len() - 1]); + Cycle { + crates: path.iter().map(|&i| crates[i].clone()).collect(), + first, + second, + } } /// Spans as their file and byte offsets, which a later session loads that file for. @@ -622,3 +672,27 @@ pub mod span { pub fn staged(dir: &Path, target: &str) -> PathBuf { dir.join(format!("{target}.json")) } + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use rustc_span::DUMMY_SP; + + use super::{On, cycle}; + + #[test] + fn names_a_cycle_across_targets() { + // `a` names `c`'s types on the first target and `c` names `a`'s on the second, while + // `b`, which `a` also names, is placed. + let crates = ["a", "b", "c"].map(String::from); + let before = [ + BTreeMap::from([(1, (On(1), DUMMY_SP)), (2, (On(1), DUMMY_SP))]), + BTreeMap::new(), + BTreeMap::from([(0, (On(2), DUMMY_SP))]), + ]; + let cycle = cycle(&crates, &before, &[false, true, false]); + assert_eq!(cycle.crates, ["a", "c"]); + assert_eq!((cycle.first.0.0, cycle.second.0.0), (1, 2)); + } +} From 7c7b62d56b246ff13cbaf8a9261632864eb3095c Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:57:28 -0800 Subject: [PATCH 6/8] fix(ffi): name a variant's own targets in a crate-order cycle Co-Authored-By: Claude Opus 5.5 --- ffi/src/merge.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/ffi/src/merge.rs b/ffi/src/merge.rs index 6eb49b27f..265106f1d 100644 --- a/ffi/src/merge.rs +++ b/ffi/src/merge.rs @@ -485,8 +485,10 @@ fn order_crates(api: &Api) -> Result, Cycle> { functions(&mut uses, &r.methods); } for e in &api.enums { - let fields = e.variants.iter().flat_map(|v| &v.fields).map(|f| &f.ty); - uses.extend(fields.chain(&e.args).map(|ty| (&e.home, e.on, e.span, ty))); + for v in &e.variants { + uses.extend(v.fields.iter().map(|f| (&e.home, v.on, e.span, &f.ty))); + } + uses.extend(e.args.iter().map(|ty| (&e.home, e.on, e.span, ty))); functions(&mut uses, &e.methods); } for o in &api.objects { @@ -679,7 +681,8 @@ mod tests { use rustc_span::DUMMY_SP; - use super::{On, cycle}; + use super::{On, cycle, order_crates}; + use crate::ir::{Api, Enum, Field, Home, Traits, Type, Variant}; #[test] fn names_a_cycle_across_targets() { @@ -695,4 +698,50 @@ mod tests { assert_eq!(cycle.crates, ["a", "c"]); assert_eq!((cycle.first.0.0, cycle.second.0.0), (1, 2)); } + + #[test] + fn names_a_variants_own_targets() { + // `a`'s enum, on both targets, names `c`'s only in a variant on the second; `c`'s, + // on the first, names `a`'s. + let enumeration = |krate: &str, on: u64, variant: u64, ty: usize| Enum { + path: vec![krate.into()], + home: Home { + krate: krate.into(), + outside: 0, + }, + shown: vec![krate.into()], + args: Vec::new(), + span: DUMMY_SP, + on: On(on), + docs: String::new(), + variants: vec![Variant { + name: "V".into(), + discriminant: 0, + fields: vec![Field { + name: "0".into(), + docs: String::new(), + ty: Type::Enum(ty), + }], + tuple: true, + on: On(variant), + }], + exhaustive: true, + methods: Vec::new(), + traits: Traits::default(), + }; + let api = Api { + records: Vec::new(), + enums: vec![enumeration("a", 3, 2, 1), enumeration("c", 1, 1, 0)], + objects: Vec::new(), + callbacks: Vec::new(), + functions: Vec::new(), + constants: Vec::new(), + crates: ["a", "c"].map(String::from).to_vec(), + targets: ["x", "y"].map(String::from).to_vec(), + }; + let Err(cycle) = order_crates(&api) else { + panic!("a cycle") + }; + assert_eq!((cycle.first.0.0, cycle.second.0.0), (2, 1)); + } } From 53d79dbafd54344d7fcd41ad46011fc29d3d12fd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 11 Nov 2026 10:20:24 -0800 Subject: [PATCH 7/8] fix(ffi): check a Dart call's target before lowering its arguments A call to an item this target lacks gave Rust its implementations and wakers before the checked native threw, leaking them and serving every later call from a worker thread. Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/api/src/lib.rs | 6 ++++++ ffi/src/dart.rs | 4 ++++ ffi/tests/api.dart | 2 ++ 3 files changed, 12 insertions(+) diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 2cc868d5c..0e4dcc490 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -2252,6 +2252,12 @@ pub fn wasm_only() -> u32 { 32 } +/// Only wasm32 has it: a call on another target fails before it takes `log`. +#[cfg(target_arch = "wasm32")] +pub fn wasm_log(log: Box) { + log.log(0, "wasm"); +} + /// A type only wasm32 has. #[cfg(target_arch = "wasm32")] pub struct Worker { diff --git a/ffi/src/dart.rs b/ffi/src/dart.rs index a5001bf88..58639dd44 100644 --- a/ffi/src/dart.rs +++ b/ffi/src/dart.rs @@ -1886,6 +1886,10 @@ impl<'a> Gen<'a> { } } let outs: Vec<(String, Ty)> = c_params.cloned().collect(); + // A call this target lacks throws before any argument is lowered or given to Rust. + if !abi.api.everywhere(self.on) { + pre.insert(0, format!("_on(0x{:x}, '{c_name}');", self.on.0 & abi.api.all().0)); + } // A tuple of one returned whole is its element, written through its one out pointer. let ok = strip_refs(&function.ok); diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index 0415c5be7..4222b28a3 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -878,6 +878,8 @@ void closedObjects() { closed(() => api.logCounts(spared, 0, open, [c]), 'Counter', 'a closed object in a list after an implementation'); check(spared.closes == 0, 'a call that throws gives Rust no implementation'); check(api.onFirstThread(), "a call after one that threw runs on the isolate's thread"); + lacks(() => api.wasmLog(spared), 'api_wasm_log'); + check(api.onFirstThread(), "a call after one this target lacks runs on the isolate's thread"); check(api.logCounts(spared, 0, open, []) == 1, 'an implementation a call that threw gave none of'); check(spared.lines == 1 && spared.closes == 1, 'an implementation given once is closed once'); From 214a90d1b72ad920f1a82e4582f9aa8ab396f35e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 11 Nov 2026 10:25:05 -0800 Subject: [PATCH 8/8] fix(ffi): fill the merged IR's new fields Co-Authored-By: Claude Opus 5.5 --- ffi/src/collect.rs | 2 ++ ffi/src/merge.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index aca89dc5c..2aacc2740 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -1296,6 +1296,7 @@ impl<'tcx> Collector<'tcx> { shown: vec![self.report.root.clone(), "Level".to_string()], args: Vec::new(), span: tcx.def_span(def_id), + on: On::ALL, docs: "`tracing::Level`: how verbose a span or event is, from the most to the least.".to_string(), variants: (variants.iter().zip(0..)) .map(|(name, discriminant)| Variant { @@ -1303,6 +1304,7 @@ impl<'tcx> Collector<'tcx> { discriminant, fields: Vec::new(), tuple: false, + on: On::ALL, }) .collect(), // A match sees through its constants to the five levels they are. diff --git a/ffi/src/merge.rs b/ffi/src/merge.rs index 265106f1d..0905cec95 100644 --- a/ffi/src/merge.rs +++ b/ffi/src/merge.rs @@ -728,6 +728,7 @@ mod tests { exhaustive: true, methods: Vec::new(), traits: Traits::default(), + foreign: false, }; let api = Api { records: Vec::new(),