diff --git a/ffi/README.md b/ffi/README.md index cba1a46be..f5912c532 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 @@ -139,6 +139,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, 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. +`_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 @@ -711,7 +747,9 @@ nix develop .#bench --command just ffi bench # the binding benchmark: bench/ffi ``` 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 @@ -744,7 +782,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 a9fef13c6..437d46a44 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -2271,6 +2271,107 @@ 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. As wasm32's comes +/// first, `Native`'s Rust discriminant differs between the two, which its C tag does not. +pub enum Platform { + /// 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. +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 +} + +/// 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 { + 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 + } +} + +/// 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() +} + /// A cancellation at a step, named as a class Dart's runtime declares: Dart's name takes a /// trailing `_`. #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/ffi/runtime/python.rs b/ffi/runtime/python.rs index 753143f14..cf1bc9b77 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 f620ccfe1..18d1b415e 100644 --- a/ffi/src/abi.rs +++ b/ffi/src/abi.rs @@ -16,7 +16,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 one item claims twice, which no rename can tell apart: the name, then what /// claimed it first and second. @@ -127,6 +127,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, @@ -175,6 +178,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 of `item`, renamed if its own clashed, else `name`. fn renamed(&self, item: Item, name: impl FnOnce() -> String) -> String { self.renames.get(&item).cloned().unwrap_or_else(name) @@ -797,6 +827,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/c.rs b/ffi/src/c.rs index af09f0833..efc2906d4 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 7f5043b94..2aacc2740 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. @@ -326,6 +326,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(); @@ -1209,6 +1210,7 @@ impl<'tcx> Collector<'tcx> { shown, args: Vec::new(), span, + on: On::ALL, docs, fields: Vec::new(), tuple: false, @@ -1224,6 +1226,7 @@ impl<'tcx> Collector<'tcx> { shown, args: Vec::new(), span, + on: On::ALL, docs, variants: Vec::new(), exhaustive: true, @@ -1244,6 +1247,7 @@ impl<'tcx> Collector<'tcx> { shown, args: Vec::new(), span, + on: On::ALL, docs, sync, copy, @@ -1261,6 +1265,7 @@ impl<'tcx> Collector<'tcx> { path, home: home.clone(), span, + on: On::ALL, docs, methods: Vec::new(), }); @@ -1291,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 { @@ -1298,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. @@ -1335,6 +1342,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) @@ -1371,6 +1379,7 @@ impl<'tcx> Collector<'tcx> { path: path("parse"), shown: shown("parse"), span, + on: On::ALL, docs: docs.to_string(), receiver: None, params: vec![Param { @@ -1422,6 +1431,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(), @@ -1456,6 +1466,7 @@ impl<'tcx> Collector<'tcx> { shown: shown(""), args: Vec::new(), span, + on: On::ALL, docs: docs.to_string(), sync: true, copy: implements(tcx, self_ty, def_id, sym::Copy), @@ -1712,6 +1723,7 @@ impl<'tcx> Collector<'tcx> { discriminant, fields, tuple, + on: On::ALL, }); } match errs.is_empty() { @@ -1977,6 +1989,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, @@ -2578,6 +2591,7 @@ impl<'tcx> Collector<'tcx> { path, shown, span: tcx.def_span(def_id), + on: On::ALL, docs: docs(tcx, def_id), receiver, params, @@ -3952,6 +3966,7 @@ impl<'tcx> Collector<'tcx> { path, home, span, + on: On::ALL, docs: docs(tcx, def_id), value, }); @@ -3971,6 +3986,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 85911cb3a..dee4264bc 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"); @@ -81,6 +81,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", @@ -189,7 +195,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)), }; @@ -349,6 +355,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, /// The namespace of the glue's C names, apart from the export's. space: String, } @@ -705,7 +713,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 @@ -866,6 +897,11 @@ impl<'a> Gen<'a> { for (n, variant) in e.variants.iter().enumerate() { let case = self.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)); @@ -1312,6 +1348,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); @@ -1354,6 +1391,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); @@ -1362,17 +1400,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); @@ -1431,6 +1486,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); @@ -1459,6 +1515,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("::"); @@ -1697,6 +1754,19 @@ impl<'a> Gen<'a> { owner: Option<&Ty>, owner_ty: Option<&Type>, taken: &[String], + ) -> Option<(String, String)> { + let on = std::mem::replace(&mut self.on, function.on); + let code = self.function_on(function, owner, owner_ty, taken); + self.on = on; + code + } + + fn function_on( + &mut self, + function: &Function, + owner: Option<&Ty>, + owner_ty: Option<&Type>, + taken: &[String], ) -> Option<(String, String)> { let abi = self.abi; let _p = abi.prefix.clone(); @@ -1817,6 +1887,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/src/driver.rs b/ffi/src/driver.rs index 291c2c1ec..04334df13 100644 --- a/ffi/src/driver.rs +++ b/ffi/src/driver.rs @@ -19,7 +19,8 @@ use rustc_span::{FileName, Span, sym}; 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 { @@ -36,6 +37,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. @@ -44,6 +50,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 { @@ -61,17 +69,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") @@ -85,6 +104,8 @@ impl Export { output, facade, patch, + merge, + roots, })) } } @@ -100,62 +121,163 @@ 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; + } + }; + if theirs.api.targets.len() != 1 { + unmergeable = true; + tcx.dcx().err(format!( + "rust2-ffi: the API from {} lists {} targets, not one", + path.display(), + theirs.api.targets.len() + )); + continue; + } + let target = &theirs.api.targets[0]; + 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 e62bc594a..2c3e3931e 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('"', "\\\"") ) } @@ -103,8 +111,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<'_> { @@ -203,12 +209,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)?; @@ -326,15 +333,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::() @@ -674,6 +721,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 { @@ -687,10 +742,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" )); } @@ -718,13 +774,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" )); } @@ -749,32 +806,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" )); } @@ -788,29 +845,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" )); } @@ -818,6 +875,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); @@ -846,7 +904,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(); @@ -868,7 +926,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(", ") )); @@ -879,7 +937,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()); @@ -890,6 +948,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); @@ -903,21 +962,31 @@ impl Gen<'_> { false => v.name.clone(), }; if e.plain() { - let lift: Vec = e - .variants - .iter() - .map(|v| format!("\n\t\t{} => {path}::{},", v.discriminant, variant(v))) + 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, + variant(v), + missing.unwrap_or_default() + ) + }) .collect(); - let lower: Vec = e - .variants - .iter() - .map(|v| format!("\n\t\t{path}::{} => {},", variant(v), v.discriminant)) + let lower: Vec = (e.variants.iter()) + .map(|v| format!("{}\n\t\t{path}::{} => {},", self.arm(v.on), variant(v), 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() )); @@ -958,11 +1027,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; } @@ -987,7 +1063,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 @@ -997,7 +1073,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 @@ -1009,19 +1085,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" )); } @@ -1037,6 +1113,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"); @@ -1084,10 +1161,20 @@ 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!( - "/// 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" + // 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" + )); + 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.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. @@ -1270,6 +1357,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. @@ -1452,7 +1545,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")) @@ -1467,13 +1560,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 )); @@ -1491,7 +1584,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 3110c6b85..12a51dcca 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"); @@ -552,7 +552,11 @@ impl<'a> Gen<'a> { func cWakerDrop(w cWaker) {{\n\tC.{go}_waker_drop(w)\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" ); @@ -560,6 +564,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 { @@ -1403,6 +1418,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) } @@ -1675,20 +1698,32 @@ 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(); let (krate, _) = self.named(&ty); - for (n, variant) in e.variants.iter().enumerate() { + for ((n, variant), check) in e.variants.iter().enumerate().zip(checks) { let vname = self.qualify(krate, &self.variants[&(c.clone(), variant.name.clone())]); 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()) @@ -1708,7 +1743,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(", ") )); } @@ -2319,6 +2354,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 731eab63c..791a7ad67 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. @@ -115,12 +155,15 @@ pub struct Enum { pub foreign: bool, } +#[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 { @@ -132,12 +175,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, @@ -161,17 +208,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, @@ -181,13 +232,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, @@ -224,6 +279,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, @@ -234,7 +290,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, @@ -258,6 +314,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, @@ -265,6 +322,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), @@ -274,7 +332,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, @@ -284,6 +342,7 @@ pub enum Receiver { Owned, } +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Param { pub name: String, pub ty: Type, @@ -293,15 +352,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), @@ -318,7 +381,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, @@ -377,7 +440,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, @@ -390,7 +453,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, @@ -398,14 +461,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, @@ -415,7 +478,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 0ae351da1..4d5fc5fb7 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"); @@ -56,6 +56,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\ @@ -325,6 +330,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> { @@ -947,6 +954,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); @@ -1055,6 +1063,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); @@ -1085,11 +1094,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() @@ -1103,7 +1126,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") @@ -1124,6 +1147,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)); @@ -1193,6 +1221,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(); @@ -1248,6 +1277,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(); @@ -1421,6 +1451,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", @@ -1466,6 +1516,20 @@ impl<'a> Gen<'a> { owner_ty: Option<&Type>, place: Place, taken: &[String], + ) -> Option { + let on = std::mem::replace(&mut self.on, function.on); + let code = self.function_on(function, owner, owner_ty, place, taken); + self.on = on; + code + } + + fn function_on( + &mut self, + function: &Function, + owner: Option<&Ty>, + owner_ty: Option<&Type>, + place: Place, + taken: &[String], ) -> Option { let abi = self.abi; self.params = locals(&function.params); diff --git a/ffi/src/main.rs b/ffi/src/main.rs index b25d2b10e..95e2938f4 100644 --- a/ffi/src/main.rs +++ b/ffi/src/main.rs @@ -35,6 +35,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..0905cec95 --- /dev/null +++ b/ffi/src/merge.rs @@ -0,0 +1,748 @@ +//! 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, 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)] +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(), + renumbered: Vec::new(), + }; + merged.api.targets = targets; + for api in apis { + merged.add(api); + } + merged.finish() +} + +/// 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, + /// 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. +#[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; + // Compared apart, as only a plain enum's matter. + v.discriminant = 0; + 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: (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); + 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; + } + } + } + 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) + } + + /// 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); + // 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 { + 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) => { + let on = ours[found].on; + if bare_variant(&ours[found]) != bare_variant(variant) { + 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; + } + None => { + ours.insert(after, variant.clone()); + after += 1; + } + } + } + } +} + +/// `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. +/// +/// 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 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, 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, r.on, r.span, ty))); + functions(&mut uses, &r.methods); + } + for e in &api.enums { + 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 { + 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); + // 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, + 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, + }; + 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 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()); + } + 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. +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")) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use rustc_span::DUMMY_SP; + + use super::{On, cycle, order_crates}; + use crate::ir::{Api, Enum, Field, Home, Traits, Type, Variant}; + + #[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)); + } + + #[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(), + foreign: false, + }; + 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)); + } +} diff --git a/ffi/src/python.rs b/ffi/src/python.rs index be15e468a..f35321fa0 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"); @@ -105,6 +105,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> { @@ -499,6 +502,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)), @@ -903,6 +914,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); @@ -964,6 +976,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); @@ -973,6 +986,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); @@ -1035,10 +1064,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![]),")); } @@ -1054,7 +1091,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(", ") )); @@ -1076,6 +1113,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); @@ -1106,6 +1144,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("::"); @@ -1225,6 +1264,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" @@ -1232,10 +1272,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 { @@ -1289,6 +1341,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 80d23a9eb..89a7d772f 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\ @@ -187,9 +192,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) @@ -572,7 +589,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_", @@ -617,7 +635,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") ) } }; @@ -632,7 +651,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") )); } } @@ -657,6 +677,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); @@ -723,6 +744,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); @@ -856,6 +881,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 9b311249f..f47849f86 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -1322,6 +1322,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 14a8222a5..3f468bec7 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -836,9 +836,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); @@ -887,6 +908,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'); diff --git a/ffi/tests/api.go b/ffi/tests/api.go index 096e06094..47260ad70 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -1116,6 +1116,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 ca0fdd452..a2c43995c 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -727,6 +727,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 709df8100..3d6f4ee5e 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -886,6 +886,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 f3cc94cb0..5b6e01c27 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -840,6 +840,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 e2778ce95..b56efb18b 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{}", @@ -739,6 +764,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"); @@ -988,6 +1016,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"); @@ -1035,7 +1066,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{}", @@ -1218,7 +1249,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. @@ -1263,7 +1294,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!( @@ -1549,37 +1580,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}" @@ -1588,7 +1619,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", @@ -1598,18 +1629,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.