Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/<prefix>.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 <dependency>/<feature>`, 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
Expand Down Expand Up @@ -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 = "<triple>")` for the target it
builds for, which gates each item, and refuses a target it was not exported for.
`<prefix>_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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
101 changes: 101 additions & 0 deletions ffi/fixtures/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2271,6 +2271,107 @@ pub fn refuse(reason: &str) -> Result<u32, Box<Refusal>> {
}))
}

/// 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<dyn Log>) {
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<dyn Beat>) -> 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)]
Expand Down
10 changes: 10 additions & 0 deletions ffi/runtime/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ pub fn raise<T>(exc: *mut PyObject, msg: &str) -> R<T> {
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<T>(what: &str) -> R<T> {
// SAFETY: a static exception type.
raise(
unsafe { PyExc_NotImplementedError },
&format!("{what}: it is not on this library's target"),
)
}

pub fn type_error<T>(msg: &str) -> R<T> {
// SAFETY: a static exception type.
raise(unsafe { PyExc_TypeError }, msg)
Expand Down
33 changes: 32 additions & 1 deletion ffi/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -127,6 +127,9 @@ fn owned(ty: &Type) -> Type {
/// The suffix naming each instance of a generic item, by its arguments.
type Suffixes = HashMap<Vec<Type>, 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,
Expand Down Expand Up @@ -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<String> {
if self.api.everywhere(on) {
return None;
}
let targets: Vec<String> = 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)
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions ffi/src/c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
));
Expand Down Expand Up @@ -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};"));
Expand Down
Loading