Skip to content

Commit

Permalink
Merge pull request #74 from ZettaScaleLabs/btrees
Browse files Browse the repository at this point in the history
Introduce BTrees, Fix nightly lints, Make btree the default vtset
  • Loading branch information
p-avital authored Jun 7, 2024
2 parents 8194b9c + 39f5803 commit 31dc42a
Show file tree
Hide file tree
Showing 20 changed files with 1,105 additions and 146 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# 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.
- When mutating one of their nodes (either because an entry is changed, or because a child node required a carry-over operation to complete its own mutation), one of two cases will happen:
- The node wasn't shared: it is mutated in place.
- The node was shared: it is copied (increasing its children's reference counts), mutated, and the parent node is mutated to replace the shared node by its mutated copy. This behaviour keeps recursing until the root if necessary.
- `AtomicArcBtreeSet` is a lock-free set based on `ArcBtreeSet`: when a node is inserted, the root pointer is cloned, the clone is mutated (causing its root pointer to change), and replaced. If the root pointer changed since reading it, the process is tried again.
- 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
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
# A Stable ABI for Rust with compact sum-types
`stabby` is your one-stop-shop to create stable binary interfaces for your shared libraries easily, without having your sum-types (enums) explode in size.

Your main vector of interraction with `stabby` will be the `#[stabby::stabby]` proc-macro, with which you can annotate a lot of things.
Your main vector of interaction with `stabby` will be the `#[stabby::stabby]` proc-macro, with which you can annotate a lot of things.

## Why would I _want_ a stable ABI? And what even _is_ an ABI?
ABI stands for _Application Binary Interface_, and is like API's more detail focused sibbling. While an API defines what type of data a function expects, and what properties these types should have; ABI defines _how_ this data should be laid out in memory, and how a function call even works.
ABI stands for _Application Binary Interface_, and is like API's more detail focused sibling. While an API defines what type of data a function expects, and what properties these types should have; ABI defines _how_ this data should be laid out in memory, and how a function call even works.

How data is laid out in memory is often called "representation": field ordering, how variants of enums are distinguished, padding, size... In order to communicate using certain types, two software units _must_ agree on what these types look like in memory.

Function calls are also highly complex under the hood (although it's rare for developers to need to think about them): is the callee or caller responsible for protecting the caller's register from callee's operations? In which registers / order on the stack should arguments be passed? What CPU instruction is used to actually trigger the call? A set of replies to these questions (and a few more) is called "calling convention".

In Rust, unless you explicitly select a known representation for your types through `#[repr(_)]`, or an explicit calling convention for your functions with `extern "_"`, the compiler is free to do whatever it pleases with these aspects of your software: the process by which it does that is explicitly unstable, and depends on your compiler version, the optimization level you selected, some llama's mood in a whool farm near Berkshire... who knows?
In Rust, unless you explicitly select a known representation for your types through `#[repr(_)]`, or an explicit calling convention for your functions with `extern "_"`, the compiler is free to do whatever it pleases with these aspects of your software: the process by which it does that is explicitly unstable, and depends on your compiler version, the optimization level you selected, some llama's mood in a wool farm near Berkshire... who knows?

The problem with that comes when dynamic linkage is involved: since the ABI for most things in Rust is unstable, software units (such as a dynamic library and the executable that requires it) that have been built through different compiler calls may disagree on these decisions about ABI, even though there's no way for the linker to know that they did.

Expand All @@ -44,7 +44,7 @@ When you annotate an enum with `#[stabby::stabby]`, you may select an existing s

Note that `#[repr(stabby)]` does lose you the ability to pattern-match.

Due to limitations of the trait solver, `#[repr(stabby)]` enums have a few papercuts:
Due to limitations of the trait solver, `#[repr(stabby)]` enums have a few paper-cuts:
- Compilation times suffer from `#[repr(stabby)]` enums.
- Additional trait bounds are required when writing `impl`-blocks for generic enums. They will always be of the form of one or multiple `A: stabby::abi::IDeterminantProvider<B>` bounds (although `rustc`'s error may suggest more complex bounds, the bounds should always be of this `IDeterminantProvider` shape).

Expand All @@ -58,15 +58,15 @@ Due to limitations of the trait solver, `#[repr(stabby)]` enums have a few paper
If you want to make your own internally tagged unions, you can tag them with `#[stabby::stabby]` to let `stabby` check that you only used stable variants, and let it know the size and alignment of your unions. Note that `stabby` will always consider that unions have no niches.

## Traits
When you annotate a trait with `#[stabby::stabby]`, an ABI-stable vtable is generated for it. You can then use any of the following type equivalence:
When you annotate a trait with `#[stabby::stabby]`, an ABI-stable v-table is generated for it. You can then use any of the following type equivalence:
- `&'a dyn Traits``DynRef<'a, vtable!(Traits)>` __or__ `dynptr!(&'a dyn Trait)`
- `&'a mut dyn Traits``Dyn<&'a mut (), vtable!(Traits)>` __or__ `dynptr!(&'a mut dyn Traits)`
- `Box<dyn Traits + 'a>``Dyn<'a, Box<()>, vtable!(Traits)>` __or__ `dynptr!(Box<dyn Traits + 'a>)`
- `Arc<dyn Traits + 'a>``Dyn<'a, Arc<()>, vtable!(Traits)>` __or__ `dynptr!(Arc<dyn Traits + 'a>)`

Note that `vtable!(Traits)` and `dynptr!(..dyn Traits..)` support any number of traits: `vtable!(TraitA + TraitB<Output = u8>)` or `dynptr!(Box<dyn TraitA + TraitB<Output = u8>>)` are perfectly valid, but ordering must remain consistent.

However, the vtables generated by stabby will not take supertraits into account.
However, the v-tables generated by stabby will not take super-traits into account.

In order for `stabby::dynptr!(Box<dyn Traits + 'a>)` to have `Trait`'s methods, you will need to `use trait::{TraitDyn, TraitDynMut};`, so make sure you don't accidentally seal these traits which are automatically declared with the same visibility as your `Trait`.

Expand Down Expand Up @@ -149,10 +149,9 @@ However, our experience in software engineering has shown that type-size matters

My hope with `stabby` comes in two flavors:
- Adoption in the Rust ecosystem: this is my least favorite option, but this would at least let people have a better time with Rust in situations where they need dynamic linkage.
- Triggering a discussion about providing not a stable, but versioned ABI for Rust: `stabby` essentially provides a versioned ABI already through the selected version of the `stabby-abi` crate. However, having a library implement type-layout, which is normally the compiler's job, forces abi-stability to be per-type explicit, instead of applicable to a whole compilation unit. In my opinion, a `abi = "<stabby/crabi/c>"` key in the cargo manifest would be a much better way to do this. Better yet, merging that idea with [RFC 3435](https://github.com/rust-lang/rfcs/pull/3435) to allow selecting an ABI on a per-function basis, and letting the compiler contaminate the types at the annotated functions' interfaces with the selected stable ABI, would be much more granular, but would still allow end users to become ABI-stable by commiting to a single version of their dependencies.
- Triggering a discussion about providing not a stable, but versioned ABI for Rust: `stabby` essentially provides a versioned ABI already through the selected version of the `stabby-abi` crate. However, having a library implement type-layout, which is normally the compiler's job, forces abi-stability to be per-type explicit, instead of applicable to a whole compilation unit. In my opinion, a `abi = "<stabby/crabi/c>"` key in the cargo manifest would be a much better way to do this. Better yet, merging that idea with [RFC 3435](https://github.com/rust-lang/rfcs/pull/3435) to allow selecting an ABI on a per-function basis, and letting the compiler contaminate the types at the annotated functions' interfaces with the selected stable ABI, would be much more granular, but would still allow end users to become ABI-stable by committing to a single version of their dependencies.

# `stabby`'s semver policy
Stabby's semver policy is built as such:
- patch: `stabby` will never break your builds _or_ your ABI, whatever sections of it you are using, through a patch update. New APIs may be added, and implementations may be refined, provided those refinements don't break ABI, including implicit invariants.
- minor: `stabby` reserves the right to break API _and_ ABI for a small set of types on minor releases. These breaks shall be clearly highlighted in the CHANGELOG, and will be avoided unless they provide major benefits (extreme performance increase, important new feature, or vulnerability fix).
- major: these releases indicate that `stabby`'s ABI has changed in a global way: binaries that use different major releases of `stabby` are unlikely to be able to interact correctly. For examples, if `#[repr(stabby)]`'s implementation for enums was to change, this would be a major release. Note that if `stabby` is unable to detect such a change at runtime through its reflection system, this shall be explicitly stated in the changelog. Note that this applies to bugfixes: if a widely used type from `stabby` needs to have its layout or invariants changed in order to fix a bug, the fix will still be a major release.
# `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.
5 changes: 3 additions & 2 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.0.1"
version = "5.1.0"
edition = "2021"
authors = { workspace = true }
license = { workspace = true }
Expand All @@ -27,6 +27,7 @@ description = "stabby's core ABI, you shouldn't add this crate to your dependenc
default = ["std"]
std = ["libc"]
libc = ["dep:libc"]
test = []

abi_stable = ["dep:abi_stable"]
abi_stable-channels = ["abi_stable", "abi_stable/channels"]
Expand All @@ -38,7 +39,7 @@ abi_stable-channels = ["abi_stable", "abi_stable/channels"]
# unsafe_wakers = [] # unsafe_wakers is no longer a feature, but a compile option: you can enable them using `RUST_FLAGS='--cfg unsafe_wakers="true"'`

[dependencies]
stabby-macros = { path = "../stabby-macros/", version = "5.0.1" }
stabby-macros = { path = "../stabby-macros/", version = "5.1.0" }
abi_stable = { workspace = true, optional = true }
libc = { workspace = true, optional = true }
rustversion = { workspace = true }
Expand Down
43 changes: 35 additions & 8 deletions stabby-abi/src/alloc/boxed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use crate::IntoDyn;

use super::{vec::*, AllocPtr, AllocSlice, IAlloc};
use core::fmt::Debug;
use core::{fmt::Debug, ptr::NonNull};

/// An ABI-stable Box, provided `Alloc` is ABI-stable.
#[crate::stabby]
Expand All @@ -24,6 +24,8 @@ pub struct Box<T, Alloc: IAlloc = super::DefaultAllocator> {
}
unsafe impl<T: Send, Alloc: IAlloc + Send> Send for Box<T, Alloc> {}
unsafe impl<T: Sync, Alloc: IAlloc> Sync for Box<T, Alloc> {}
unsafe impl<T: Send, Alloc: IAlloc + Send> Send for BoxedSlice<T, Alloc> {}
unsafe impl<T: Sync, Alloc: IAlloc> Sync for BoxedSlice<T, Alloc> {}
#[cfg(feature = "libc")]
impl<T> Box<T> {
/// Attempts to allocate [`Self`], initializing it with `constructor`.
Expand Down Expand Up @@ -71,9 +73,11 @@ impl<T, Alloc: IAlloc> Box<T, Alloc> {
None => return Err((constructor, alloc)),
};
unsafe {
constructor(core::mem::transmute::<&mut T, _>(ptr.as_mut()));
constructor(ptr.as_mut());
}
Ok(Self { ptr })
Ok(Self {
ptr: unsafe { ptr.assume_init() },
})
}
/// Attempts to allocate a [`Self`] and store `value` in it
/// # Errors
Expand Down Expand Up @@ -108,9 +112,11 @@ impl<T, Alloc: IAlloc> Box<T, Alloc> {
None => panic!("Allocation failed"),
};
unsafe {
constructor(core::mem::transmute::<&mut T, _>(ptr.as_mut()));
constructor(ptr.as_mut());
}
Self {
ptr: unsafe { ptr.assume_init() },
}
Self { ptr }
}
/// Attempts to allocate [`Self`] and store `value` in it.
///
Expand Down Expand Up @@ -185,7 +191,7 @@ impl<T, Alloc: IAlloc> crate::IPtrOwned for Box<T, Alloc> {
fn drop(this: &mut core::mem::ManuallyDrop<Self>, drop: unsafe extern "C" fn(&mut ())) {
let rthis = &mut ***this;
unsafe {
drop(core::mem::transmute(rthis));
drop(core::mem::transmute::<&mut T, &mut ()>(rthis));
}
this.free();
}
Expand All @@ -203,7 +209,7 @@ impl<T, Alloc: IAlloc> IntoDyn for Box<T, Alloc> {
type Target = T;
fn anonimize(self) -> Self::Anonymized {
let original_prefix = self.ptr.prefix_ptr();
let anonymized = unsafe { core::mem::transmute::<_, Self::Anonymized>(self) };
let anonymized = unsafe { core::mem::transmute::<Self, Self::Anonymized>(self) };
let anonymized_prefix = anonymized.ptr.prefix_ptr();
assert_eq!(anonymized_prefix, original_prefix, "The allocation prefix was lost in anonimization, this is definitely a bug, please report it.");
anonymized
Expand All @@ -223,6 +229,10 @@ pub struct BoxedSlice<T, Alloc: IAlloc = super::DefaultAllocator> {
pub(crate) alloc: Alloc,
}
impl<T, Alloc: IAlloc> BoxedSlice<T, Alloc> {
/// Constructs an empty boxed slice with a given capacity.
pub fn with_capacity_in(capacity: usize, alloc: Alloc) -> Self {
Vec::with_capacity_in(capacity, alloc).into()
}
/// The number of elements in the boxed slice.
pub const fn len(&self) -> usize {
ptr_diff(self.slice.end, self.slice.start.ptr)
Expand All @@ -239,6 +249,23 @@ impl<T, Alloc: IAlloc> BoxedSlice<T, Alloc> {
pub fn as_slice_mut(&mut self) -> &mut [T] {
unsafe { core::slice::from_raw_parts_mut(self.slice.start.as_ptr(), self.len()) }
}
/// Attempts to add an element to the boxed slice without reallocating.
/// # Errors
/// Returns the value if pushing would require reallocating.
pub fn try_push(&mut self, value: T) -> Result<(), T> {
if self.slice.len()
>= unsafe { self.slice.start.prefix() }
.capacity
.load(core::sync::atomic::Ordering::Relaxed)
{
return Err(value);
}
unsafe {
core::ptr::write(self.slice.end.as_ptr(), value);
self.slice.end = NonNull::new_unchecked(self.slice.end.as_ptr().add(1));
}
Ok(())
}
pub(crate) fn into_raw_components(self) -> (AllocSlice<T, Alloc>, usize, Alloc) {
let slice = self.slice;
let alloc = unsafe { core::ptr::read(&self.alloc) };
Expand Down Expand Up @@ -315,7 +342,7 @@ impl<T, Alloc: IAlloc> From<BoxedSlice<T, Alloc>> for Vec<T, Alloc> {
start: slice.start,
end: slice.end,
capacity: if core::mem::size_of::<T>() == 0 {
unsafe { core::mem::transmute(usize::MAX) }
unsafe { core::mem::transmute::<usize, NonNull<T>>(usize::MAX) }
} else {
slice.start.ptr
},
Expand Down
Loading

0 comments on commit 31dc42a

Please sign in to comment.