Skip to content

Commit 8c51e57

Browse files
committed
Use the allocator to properly realign heap buffers when converting between String and SmartString.
Closes bodil#28
1 parent e3b6d55 commit 8c51e57

File tree

7 files changed

+113
-30
lines changed

7 files changed

+113
-30
lines changed

.github/workflows/ci.yml

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,18 @@ jobs:
110110
token: ${{ secrets.GITHUB_TOKEN }}
111111
args: --all-features
112112

113-
# miri:
114-
# name: Miri
115-
# runs-on: ubuntu-latest
116-
# steps:
117-
# - uses: actions/checkout@v2
118-
# - uses: actions-rs/toolchain@v1
119-
# with:
120-
# profile: minimal
121-
# toolchain: nightly
122-
# override: true
123-
# components: miri
124-
# - name: Run Miri
125-
# run: |
126-
# cargo miri setup
127-
# cargo miri test -- -Zmiri-disable-isolation -- --skip proptest --skip ser_de
113+
miri:
114+
name: Miri
115+
runs-on: ubuntu-latest
116+
steps:
117+
- uses: actions/checkout@v2
118+
- uses: actions-rs/toolchain@v1
119+
with:
120+
profile: minimal
121+
toolchain: nightly
122+
override: true
123+
components: miri
124+
- name: Run Miri
125+
run: |
126+
cargo miri setup
127+
env MIRIFLAGS=-Zmiri-disable-isolation cargo miri test -- --skip proptest

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project
66
adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### FIXED
11+
12+
- To avoid an issue where allocated heap memory may be deallocated with a different layout
13+
alignment than it was officially allocated with when converting between `std::string::String`
14+
and `SmartString`, even if otherwise correctly aligned, the respective `From` implementations
15+
now use `std::alloc::Allocator::grow()` to re-align the heap data as necessary. An unfortunate
16+
consequence of this is that because the `std::alloc::Allocator` API hasn't been stabilised yet,
17+
unless you're on nightly or some future stable rustc version after `allocator_api` has been
18+
stabilised, converting between `String` and `SmartString` will always reallocate and copy
19+
(making it always O(n) rather than O(1) when correctly aligned and O(n) otherwise).
20+
([#28](https://github.com/bodil/smartstring/issues/28))
21+
822
## [1.0.0] - 2022-02-24
923

1024
### CHANGED

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ categories = ["data-structures"]
1212
keywords = ["cache-local", "cpu-cache", "small-string", "sso", "inline-string"]
1313
exclude = ["release.toml", "proptest-regressions/**"]
1414
rust-version = "1.57"
15+
build = "./build.rs"
1516

1617
[package.metadata.docs.rs]
1718
features = ["arbitrary", "proptest", "serde"]
@@ -40,3 +41,7 @@ proptest-derive = "0.3"
4041
criterion = "0.3"
4142
rand = "0.8"
4243
serde_test = "1"
44+
45+
[build-dependencies]
46+
version_check = "0.9"
47+
autocfg = "1"

build.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
use version_check as rustc;
6+
7+
fn main() {
8+
let ac = autocfg::new();
9+
let has_feature = Some(true) == rustc::supports_feature("allocator_api");
10+
let has_api = ac.probe_trait("alloc::alloc::Allocator");
11+
if has_feature || has_api {
12+
autocfg::emit("has_allocator");
13+
}
14+
if has_feature {
15+
autocfg::emit("needs_allocator_feature");
16+
}
17+
autocfg::rerun_path("build.rs");
18+
}

src/boxed.rs

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use alloc::{alloc::Layout, string::String};
66
use core::{
7-
mem::{align_of, forget},
7+
mem::align_of,
88
ops::{Deref, DerefMut},
99
ptr::NonNull,
1010
};
@@ -167,30 +167,63 @@ impl DerefMut for BoxedString {
167167
}
168168

169169
impl From<String> for BoxedString {
170+
#[allow(unsafe_code, unused_mut)]
170171
fn from(mut s: String) -> Self {
171172
if s.is_empty() {
172173
Self::new(s.capacity())
173174
} else {
174-
// If the `String`'s buffer isn't word aligned, we can't reuse it.
175-
if check_alignment(s.as_ptr()) {
176-
return Self::from_str(s.capacity(), &s);
175+
#[cfg(has_allocator)]
176+
{
177+
// TODO: Use String::into_raw_parts when stabilised, meanwhile let's get unsafe
178+
let len = s.len();
179+
let cap = s.capacity();
180+
#[allow(unsafe_code)]
181+
let ptr = unsafe { NonNull::new_unchecked(s.as_mut_ptr()) };
182+
let old_layout = Layout::array::<u8>(cap).unwrap();
183+
184+
use alloc::alloc::Allocator;
185+
let allocator = alloc::alloc::Global;
186+
if let Ok(aligned_ptr) =
187+
unsafe { allocator.grow(ptr, old_layout, Self::layout_for(cap)) }
188+
{
189+
core::mem::forget(s);
190+
Self {
191+
cap,
192+
len,
193+
ptr: aligned_ptr.cast(),
194+
}
195+
} else {
196+
Self::from_str(cap, &s)
197+
}
177198
}
178-
// TODO: Use String::into_raw_parts when stabilised, meanwhile let's get unsafe
179-
let len = s.len();
180-
let cap = s.capacity();
181-
#[allow(unsafe_code)]
182-
let ptr = unsafe { NonNull::new_unchecked(s.as_mut_ptr()) };
183-
forget(s);
184-
Self { cap, len, ptr }
199+
#[cfg(not(has_allocator))]
200+
Self::from_str(s.capacity(), &s)
185201
}
186202
}
187203
}
188204

189205
impl From<BoxedString> for String {
206+
#[allow(unsafe_code)]
190207
fn from(s: BoxedString) -> Self {
191-
#[allow(unsafe_code)]
192-
let out = unsafe { String::from_raw_parts(s.ptr.as_ptr(), s.len(), s.capacity()) };
193-
forget(s);
194-
out
208+
#[cfg(has_allocator)]
209+
{
210+
let ptr = s.ptr;
211+
let cap = s.cap;
212+
let len = s.len;
213+
let new_layout = Layout::array::<u8>(cap).unwrap();
214+
215+
use alloc::alloc::Allocator;
216+
let allocator = alloc::alloc::Global;
217+
if let Ok(aligned_ptr) =
218+
unsafe { allocator.grow(ptr, BoxedString::layout_for(cap), new_layout) }
219+
{
220+
core::mem::forget(s);
221+
unsafe { String::from_raw_parts(aligned_ptr.as_ptr().cast(), len, cap) }
222+
} else {
223+
String::from(s.deref())
224+
}
225+
}
226+
#[cfg(not(has_allocator))]
227+
String::from(s.deref())
195228
}
196229
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
#![deny(nonstandard_style)]
102102
#![warn(unreachable_pub, missing_debug_implementations, missing_docs)]
103103
#![cfg_attr(not(feature = "std"), no_std)]
104+
#![cfg_attr(needs_allocator_feature, feature(allocator_api))]
104105

105106
extern crate alloc;
106107

src/test.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,4 +551,16 @@ mod tests {
551551
s.clear();
552552
assert_eq!(Discriminant::Inline, s.discriminant());
553553
}
554+
555+
#[test]
556+
fn from_string() {
557+
let std_s =
558+
String::from("I am a teapot short and stout; here is my handle, here is my snout");
559+
let smart_s: SmartString<LazyCompact> = std_s.clone().into();
560+
assert_eq!(std_s, smart_s);
561+
let unsmart_s: String = smart_s.clone().into();
562+
assert_eq!(smart_s, unsmart_s);
563+
assert_eq!(std_s, unsmart_s);
564+
// This test exists just to provoke a Miri problem when dropping a string created by SmartString::into::<String>() (#28)
565+
}
554566
}

0 commit comments

Comments
 (0)