Skip to content

Commit

Permalink
Merge pull request #78 from ZettaScaleLabs/prep-safer-ffi
Browse files Browse the repository at this point in the history
Release 6.1.1
  • Loading branch information
p-avital authored Jun 25, 2024
2 parents 3569122 + 228c6a6 commit 802267e
Show file tree
Hide file tree
Showing 36 changed files with 460 additions and 501 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 6.1.1 (api=1.0.0, abi=1.0.0)
- Add support for multi-fields variants in `repr(C, u*)` enums.
- Deprecate support for `repr(C)` by deprecating any enum that uses it without also specifying a determinant size.
- Add support for `Result<T, core::conver::Infallible>`, which is fully identical to `T` down to its reports.
- Remove `CompilerVersion_X_Y_Z` types: keeping them up to date with new compiler release was too much maintenance.
- Remove `CurrentCompilerVersion` type alias: as it could break your code if you upgraded to a version of the compiler that `stabby` didn't know of.
- Prepare integration of `stabby` and [`safer-ffi`](https://crates.io/crates/safer-ffi) by adding `CType` and `is_invalid` to `IStable`.
- Switch versioning system to [SemVer Prime](https://p-avital.github.io/semver-prime), using the `api, abi` as the key.
- [RFC 3633](https://github.com/rust-lang/rfcs/pull/3633) which seeks to address the breaking change in Rust 1.78 is scheduled to be discussed in July 2024.

# 5.1.0
- Introducing `stabby::collections::arc_btree`, a set of copy-on-write btrees:
- `ArcBtreeSet` and `ArcBtreeMap` behave like you would expect, but share nodes with their clones.
Expand All @@ -8,7 +18,6 @@
- This is notably how `stabby` global set of vtables is implemented to support stable Rust from version 1.78 onward, until the [static-promotion regression](https://github.com/rust-lang/rust/issues/123281) is [fixed](https://github.com/rust-lang/rfcs/pull/3633), and this global set can be removed.
- Add some missing `Send` and `Sync` implementations for container types.
- Fix a lot of nightly lints.
- Officially switch to [Humane SemVer](https://p-avital.github.io/humane-semver)

# 5.0.1
- Fix a regression in MSRV
Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ license = " EPL-2.0 OR Apache-2.0"
categories = ["development-tools::ffi", "no-std::no-alloc"]
repository = "https://github.com/ZettaScaleLabs/stabby"
readme = "stabby/README.md"
version = "6.1.1" # Track

[workspace.dependencies]
stabby-macros = { path = "./stabby-macros/", version = "6.1.1", default-features = false } # Track
stabby-abi = { path = "./stabby-abi/", version = "6.1.1", default-features = false } # Track
stabby = { path = "./stabby/", version = "6.1.1", default-features = false } # Track

abi_stable = "0.11.2"
criterion = "0.5.1"
lazy_static = "1.4.0"
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The problem with that comes when dynamic linkage is involved: since the ABI for

Concretely, this could mean that your executable thinks the leftmost 8 bytes of `Vec<Potato>` is the pointer to the heap allocation, while the library believes them to be its length. This could also mean the library thinks it's free to clobber registers when its functions are called, while the executable relied on it to save them and restore them before returning.

`stabby` seeks to help you solve these issues by helping you pin the ABI for a subset of your program, while helping you retain some of the layout optimizations `rustc` provides when using its unstable ABI. On top of this, stabby allows you to annotate function exports and imports in a way that also serves as a check of your dependency versionning for types that are `stabby::abi::IStable`.
`stabby` seeks to help you solve these issues by helping you pin the ABI for a subset of your program, while helping you retain some of the layout optimizations `rustc` provides when using its unstable ABI. On top of this, stabby allows you to annotate function exports and imports in a way that also serves as a check of your dependency versioning for types that are `stabby::abi::IStable`.

## Structures
When you annotate structs with `#[stabby::stabby]`, two things happen:
Expand Down Expand Up @@ -72,7 +72,7 @@ In order for `stabby::dynptr!(Box<dyn Traits + 'a>)` to have `Trait`'s methods,

`stabby::closure` exports the `CallN`, `CallMutN` and `CallOnceN` traits, where `N` (in `0..=9`) is the number of arguments, as ABI-stable equivalents of `Fn`, `FnMut` and `FnOnce` respectively.

Since version `1.0.1`, the v-tables generated by `#[stabby::stabby]` always assume all of their method arguments to be ABI-stable, to prevent the risk of freezing rustc.
Since version `1.0.1`, the v-tables generated by `#[stabby::stabby]` always assume all of their method arguments to be ABI-stable, to prevent the risk of freezing `rustc`.
Unless your trait has methods referencing its own v-table, it's advised to use `#[stabby::stabby(checked)]` instead to avoid the v-table being marked as stable despite some types in its
interface not actually being stable.

Expand Down Expand Up @@ -154,4 +154,10 @@ My hope with `stabby` comes in two flavors:
# `stabby`'s SemVer policy
Stabby includes all of its `stabby_abi::IStable` implementation in its public API: any change to an `IStable` type's memory representation is a breaking change which will lead to a `MAJOR` version change.

Stabby follows [Humane SemVer](https://p-avital.github.io/humane-semver): small major bumps should require no thinking to perform (they may break binary compatibility, though); while large major bump indicate that you may have to think a bit before upgrading.
From `6.1.1` onwards, Stabby follows [SemVer Prime](https://p-avital.github.io/semver-prime), using the `api, abi` as the key. Here's a few ways you can interpret that:
- `stabby.version[level] = 2^(api[level]) * 3^(abi[level])` lets you compute the exact versions of stabby's ABI and API.
- When upgrading stabby, you can check what has changed by dividing the new version by the previous one: if the division result is a multiple of 2, the change affected API; and it affected ABI if it's a multiple of 3.
- ABI versioning:
- Adding a new type to the set of ABI stable type will bump ABI patch.
- Modifying an existing type's ABI in any way will bump the ABI major.
- API versioning strictly follows SemVer policy. Any API visible in the docs is considered public, as well as whatever contracts are mentioned in said docs.
4 changes: 2 additions & 2 deletions examples/dynlinkage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ version = "0.1.0"
edition = "2021"

[dependencies]
stabby = { path = "../../stabby/" }
library = { path = "../library/" } # This ensures proper compilation order
stabby.workspace = true
library = { path = "../library/" } # This ensures proper compilation order
2 changes: 1 addition & 1 deletion examples/libloading/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
stabby = { path = "../../stabby/", features = ["libloading"] }
stabby = { workspace = true, features = ["libloading"] }
libloading = "0.8"
2 changes: 1 addition & 1 deletion examples/library/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ crate-type = ["cdylib", "staticlib"]


[dependencies]
stabby = { path = "../../stabby/" }
stabby.workspace = true
40 changes: 30 additions & 10 deletions release.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import sys, os, re
ws_root = os.path.dirname(__file__)
crates = ["stabby-macros", "stabby-abi", "stabby"]

def factor(x, base):
n = 0
while x > 1 and x % base == 0:
x /= base
n += 1
return n

def factor_version(version, base):
return ".".join([str(factor(int(x), base)) for x in version.split(".")])

if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "publish":
for crate in crates:
Expand All @@ -9,19 +20,28 @@
raise f"Failed to release {crate}, stopping publication"
else:
changelog = f"{ws_root}/CHANGELOG.md"
print("Close the CHANGELOG to continue, the topmost version will be picked")
os.system(f"code --wait {changelog}")
version = None
changelog_text = None
with open(changelog) as clog:
while version is None:
line = clog.readline()
versions = re.findall("^#\s+([^:\n]+)", line)
changelog_text = clog.read()
for line in changelog_text.splitlines():
versions = re.findall(r"^#\s+([\d\.]+)", line)
version = versions[0] if len(versions) else None
if version is not None:
break
header = f"# {version} (api={factor_version(version, 2)}, abi={factor_version(version, 3)})"
print(header)
changelog_text = re.sub(r"^#\s+([\d\.]+)\s*(\(api[^\)]+\))?", header, changelog_text)
with open(changelog, "w") as clog:
clog.write(changelog_text)

print(f"Updating Cargo.tomls with version={version}")
for crate in crates:
vupdate = f"""sed -i -E 's/^version\s*=.*/version = \"{version}\"/g' {ws_root}/{crate}/Cargo.toml"""
os.system(vupdate)
print(vupdate)
dupdate = f"""sed -i -E 's/(stabby-.*version\s*=\s*)\"[^\"]*\"(.*)/\\1\"{version}\"\\2/g' {ws_root}/{crate}/Cargo.toml"""
print(dupdate)
os.system(dupdate)

ws_toml = None
with open(f"{ws_root}/Cargo.toml") as toml:
ws_toml = toml.read()
ws_toml = re.sub(r"version\s*=\s*\"[^\"]+(?P<rest>\".*Track)", f"version = \"{version}\\g<rest>", ws_toml)
with open(f"{ws_root}/Cargo.toml", "w") as toml:
toml.write(ws_toml)
7 changes: 4 additions & 3 deletions stabby-abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

[package]
name = "stabby-abi"
version = "5.1.0"
version = { workspace = true }
edition = "2021"
authors = { workspace = true }
license = { workspace = true }
Expand All @@ -36,10 +36,11 @@ abi_stable-channels = ["abi_stable", "abi_stable/channels"]
# `stabby::future::Future::poll` may need to allocate in order to provide stable wakers.
# If you're confident enough that `core::task::Waker`'s ABI will not change between your targetted versions
# of rustc, you may enable this feature to pass them across the FFI boundary directly.
# unsafe_wakers = [] # unsafe_wakers is no longer a feature, but a compile option: you can enable them using `RUST_FLAGS='--cfg unsafe_wakers="true"'`
# stabby_unsafe_wakers = [] # stabby_unsafe_wakers is no longer a feature, but a compile option: you can enable them using `RUST_FLAGS='--cfg stabby_unsafe_wakers="true"'`

[dependencies]
stabby-macros = { path = "../stabby-macros/", version = "5.1.0" }
stabby-macros.workspace = true

abi_stable = { workspace = true, optional = true }
libc = { workspace = true, optional = true }
rustversion = { workspace = true }
Expand Down
93 changes: 77 additions & 16 deletions stabby-abi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
// Pierre Avital, <[email protected]>
//

use std::{
fmt::Write as FmtWrite,
fs::File,
io::{BufWriter, Write},
path::PathBuf,
};

fn u(mut i: u128) -> String {
let mut result = "UTerm".into();
let mut ids = Vec::new();
Expand All @@ -26,35 +33,89 @@ fn u(mut i: u128) -> String {
result
}

fn main() {
use std::{
fs::File,
io::{BufWriter, Write},
path::PathBuf,
};
fn typenum_unsigned() -> std::io::Result<()> {
const SEQ_MAX: u128 = 1000;
let padding_rs = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join("unsigned.rs");
let mut padding_file = BufWriter::new(File::create(padding_rs).unwrap());
let filename = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join("unsigned.rs");
let mut file = BufWriter::new(File::create(filename).unwrap());
for i in 0..=SEQ_MAX {
let u = u(i);
writeln!(padding_file, "/// {i}\npub type U{i} = {u};").unwrap();
writeln!(padding_file, "/// {i}\npub type Ux{i:X} = {u};").unwrap();
writeln!(padding_file, "/// {i}\npub type Ub{i:b} = {u};").unwrap();
writeln!(file, "/// {i}\npub type U{i} = {u};")?;
writeln!(file, "/// {i}\npub type Ux{i:X} = {u};")?;
writeln!(file, "/// {i}\npub type Ub{i:b} = {u};")?;
}
for i in 0..39 {
let ipow = 10u128.pow(i);
let u = u(ipow);
writeln!(padding_file, "/// {i}\npub type U10pow{i} = {u};").unwrap();
writeln!(file, "/// {i}\npub type U10pow{i} = {u};")?;
if ipow > SEQ_MAX {
writeln!(padding_file, "/// {i}\npub type U{ipow} = {u};").unwrap();
writeln!(padding_file, "/// {i}\npub type Ux{ipow:X} = {u};").unwrap();
writeln!(padding_file, "/// {i}\npub type Ub{ipow:b} = {u};").unwrap();
writeln!(file, "/// {i}\npub type U{ipow} = {u};")?;
writeln!(file, "/// {i}\npub type Ux{ipow:X} = {u};")?;
writeln!(file, "/// {i}\npub type Ub{ipow:b} = {u};")?;
}
}
for i in 0..128 {
let u = u(1 << i);
writeln!(padding_file, "/// {i}\npub type U2pow{i} = {u};").unwrap();
writeln!(file, "/// {i}\npub type U2pow{i} = {u};")?;
}
Ok(())
}

fn tuples(max_tuple: usize) -> std::io::Result<()> {
let filename = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join("tuples.rs");
let mut file = BufWriter::new(File::create(filename).unwrap());
for i in 0..=max_tuple {
writeln!(
file,
r##"/// An ABI stable tuple of {i} elements.
#[crate::stabby]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Tuple{i}<{generics}>({fields});
impl<{generics}> From<({generics})> for Tuple{i}<{generics}> {{
fn from(value: ({generics})) -> Self {{
let ({named_fields}) = value;
Self({named_fields})
}}
}}
#[allow(clippy::unused_unit)]
impl<{generics}> From<Tuple{i}<{generics}>> for ({generics}) {{
fn from(value: Tuple{i}<{generics}>) -> Self {{
let Tuple{i}({named_fields}) = value;
({named_fields})
}}
}}
"##,
generics = (0..i).fold(String::new(), |mut acc, it| {
write!(acc, "T{it}, ").unwrap();
acc
}),
fields = (0..i).fold(String::new(), |mut acc, it| {
write!(acc, "pub T{it}, ").unwrap();
acc
}),
named_fields = (0..i).fold(String::new(), |mut acc, it| {
write!(acc, "field{it}, ").unwrap();
acc
}),
)?;
}
Ok(())
}

fn main() {
typenum_unsigned().unwrap();
println!("cargo:rustc-check-cfg=cfg(stabby_max_tuple, values(any()))");
let max_tuple = std::env::var("CARGO_CFG_STABBY_MAX_TUPLE")
.map_or(32, |s| s.parse().unwrap_or(32))
.max(10);
tuples(max_tuple).unwrap();
println!("cargo:rustc-check-cfg=cfg(stabby_nightly, values(none()))");
println!(
r#"cargo:rustc-check-cfg=cfg(stabby_check_unreachable, values(none(), "true", "false"))"#
);
println!(r#"cargo:rustc-check-cfg=cfg(stabby_unsafe_wakers, values(none(), "true", "false"))"#);
println!(
r#"cargo:rustc-check-cfg=cfg(stabby_vtables, values(none(), "vec", "btree", "no_alloc"))"#
);
if let Ok(toolchain) = std::env::var("RUSTUP_TOOLCHAIN") {
if toolchain.starts_with("nightly") {
println!("cargo:rustc-cfg=stabby_nightly");
Expand Down
3 changes: 2 additions & 1 deletion stabby-abi/src/alloc/collections/arc_btree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ where
type UnusedBits = <*const T as IStable>::UnusedBits;
type HasExactlyOneNiche = crate::B0;
type ContainsIndirections = crate::B1;
type CType = <*const T as IStable>::CType;
const REPORT: &'static crate::report::TypeReport = &crate::report::TypeReport {
name: crate::str::Str::new("ArcBTreeSet"),
module: crate::str::Str::new("stabby_abi::alloc::collections::arc_btree"),
Expand Down Expand Up @@ -248,7 +249,7 @@ impl<T: Ord + Clone, const REPLACE_ON_INSERT: bool, const SPLIT_LIMIT: usize>
const fn as_ptr(
&self,
) -> *mut ArcBTreeSetNodeInner<T, DefaultAllocator, REPLACE_ON_INSERT, SPLIT_LIMIT> {
unsafe { core::mem::transmute_copy(self) }
unsafe { core::mem::transmute(core::ptr::read(self)) }
}
fn copy_from_ptr(
ptr: *const ArcBTreeSetNodeInner<T, DefaultAllocator, REPLACE_ON_INSERT, SPLIT_LIMIT>,
Expand Down
6 changes: 6 additions & 0 deletions stabby-abi/src/alloc/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,16 @@ impl<T, Alloc: IAlloc> Arc<T, Alloc> {
pub fn downgrade(this: &Self) -> Weak<T, Alloc> {
this.into()
}
#[rustversion::since(1.73)]
/// Returns a reference to the allocator used to construct `this`
pub const fn allocator(this: &Self) -> &Alloc {
unsafe { &this.ptr.prefix().alloc }
}
#[rustversion::before(1.73)]
/// Returns a reference to the allocator used to construct `this`
pub fn allocator(this: &Self) -> &Alloc {
unsafe { &this.ptr.prefix().alloc }
}
}
impl<T, Alloc: IAlloc> Drop for Arc<T, Alloc> {
fn drop(&mut self) {
Expand Down
3 changes: 3 additions & 0 deletions stabby-abi/src/enums/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ unsafe impl IStable for BitDeterminant {
type UnusedBits = Array<U0, U254, End>;
type HasExactlyOneNiche = Saturator;
type ContainsIndirections = B0;
type CType = u8;
primitive_report!("BitDeterminant");
}

Expand Down Expand Up @@ -95,6 +96,7 @@ unsafe impl<Offset, Value, Tail: IStable> IStable for ValueIsErr<Offset, Value,
type UnusedBits = Tail::UnusedBits;
type HasExactlyOneNiche = Tail::HasExactlyOneNiche;
type ContainsIndirections = B0;
type CType = ();
primitive_report!("ValueIsErr");
}
impl<Offset: Unsigned, Value: Unsigned, Tail: IDeterminant + core::fmt::Debug> core::fmt::Debug
Expand Down Expand Up @@ -186,6 +188,7 @@ unsafe impl<Determinant: IStable> IStable for Not<Determinant> {
type UnusedBits = Determinant::UnusedBits;
type HasExactlyOneNiche = Determinant::HasExactlyOneNiche;
type ContainsIndirections = Determinant::ContainsIndirections;
type CType = Determinant::CType;
primitive_report!("Not", Determinant);
}
impl<Determinant: IDeterminant> IDeterminant for Not<Determinant>
Expand Down
8 changes: 4 additions & 4 deletions stabby-abi/src/future.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ use crate::vtable::HasDropVt;
use crate::{IPtrMut, IPtrOwned, IStable};

pub use stable_waker::StableWaker;
#[cfg(unsafe_wakers = "true")]
#[cfg(stabby_unsafe_wakers = "true")]
mod stable_waker {
use core::task::Waker;

use crate::StableLike;
/// A waker that promises to be ABI stable.
///
/// With the `unsafe_wakers` feature enabled, this is actually a lie: the waker is not guaranteed to
/// With the `stabby_unsafe_wakers` feature enabled, this is actually a lie: the waker is not guaranteed to
/// be ABI-stable! However, since building ABI-stable wakers that are compatible with Rust's wakers is
/// costly in terms of runtime, you might want to experiment with unsafe wakers, to bench the cost of
/// stable wakers if nothing else.
Expand All @@ -47,7 +47,7 @@ mod stable_waker {
}
}
}
#[cfg(any(not(unsafe_wakers), unsafe_wakers = "false"))]
#[cfg(any(not(stabby_unsafe_wakers), stabby_unsafe_wakers = "false"))]
mod stable_waker {
use core::{
mem::ManuallyDrop,
Expand Down Expand Up @@ -114,7 +114,7 @@ mod stable_waker {
///
/// While this is the only way to guarantee ABI-stability when interacting with futures, this does add
/// a layer of indirection, and cloning this waker will cause an allocation. To bench the performance cost
/// of this wrapper and decide if you want to risk ABI-unstability on wakers, you may use `RUST_FLAGS='--cfg unsafe_wakers="true"'`, which will turn [`StableWaker`] into a newtype of [`core::task::Waker`].
/// of this wrapper and decide if you want to risk ABI-unstability on wakers, you may use `RUSTFLAGS='--cfg stabby_unsafe_wakers="true"'`, which will turn [`StableWaker`] into a newtype of [`core::task::Waker`].
#[crate::stabby]
pub struct StableWaker<'a, Alloc: IAlloc = crate::alloc::DefaultAllocator> {
waker: StableLike<&'a Waker, &'a ()>,
Expand Down
Loading

0 comments on commit 802267e

Please sign in to comment.