Skip to content

Commit 22e0609

Browse files
Kijewskihuacnlee
andauthored
Use arc_swap to implement AtomicStr (#72)
* Use `arc_swap` to implement `AtomicStr` The previous implementation allowed use-after-free in a multi-threaded context. This PR fixes the problem by using `ArcSwap`, which is implements thread-safe swapping of `Arc`s, and is widely used. * Use `triomphe::Arc` to mitigate performance losses This change replaces `std::sync::Arc` with `triomphe::Arc`. The latter has no weak references, and is a lot faster because of that. * Update atomic_str.rs * Update lib.rs * Update lib.rs --------- Co-authored-by: Jason Lee <huacnlee@gmail.com>
1 parent 37aa93a commit 22e0609

File tree

5 files changed

+83
-61
lines changed

5 files changed

+83
-61
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ You can use `rust_i18n::set_locale` to set the global locale at runtime, so that
192192
rust_i18n::set_locale("zh-CN");
193193
194194
let locale = rust_i18n::locale();
195-
assert_eq!(*locale, "zh-CN");
195+
assert_eq!(&*locale, "zh-CN");
196196
```
197197

198198
### Extend Backend

crates/support/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ repository = "https://github.com/longbridgeapp/rust-i18n"
88
version = "3.0.0"
99

1010
[dependencies]
11+
arc-swap = "1.6.0"
1112
globwalk = "0.8.1"
1213
once_cell = "1.10.0"
1314
proc-macro2 = "1.0"
@@ -18,3 +19,4 @@ toml = "0.7.4"
1819
normpath = "1.1.1"
1920
lazy_static = "1"
2021
regex = "1"
22+
triomphe = { version = "0.1.11", features = ["arc-swap"] }

crates/support/src/atomic_str.rs

+39-54
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,65 @@
11
use std::fmt;
2-
use std::sync::atomic::{AtomicPtr, Ordering};
3-
use std::sync::Arc;
2+
use std::ops::Deref;
3+
4+
use arc_swap::{ArcSwapAny, Guard};
5+
use triomphe::Arc;
46

57
/// A thread-safe atomically reference-counting string.
6-
pub struct AtomicStr(AtomicPtr<String>);
8+
pub struct AtomicStr(ArcSwapAny<Arc<String>>);
9+
10+
/// A thread-safe view the string that was stored when `AtomicStr::as_str()` was called.
11+
struct GuardedStr(Guard<Arc<String>>);
12+
13+
impl Deref for GuardedStr {
14+
type Target = str;
15+
16+
fn deref(&self) -> &Self::Target {
17+
self.0.as_str()
18+
}
19+
}
720

821
impl AtomicStr {
922
/// Create a new `AtomicStr` with the given value.
10-
pub fn new(value: impl Into<String>) -> Self {
23+
pub fn new(value: &str) -> Self {
1124
let arced = Arc::new(value.into());
12-
Self(AtomicPtr::new(Arc::into_raw(arced) as _))
25+
Self(ArcSwapAny::new(arced))
1326
}
1427

1528
/// Get the string slice.
16-
pub fn as_str(&self) -> &str {
17-
unsafe {
18-
let arced_ptr = self.0.load(Ordering::SeqCst);
19-
assert!(!arced_ptr.is_null());
20-
&*arced_ptr
21-
}
22-
}
23-
24-
/// Get the cloned inner `Arc<String>`.
25-
pub fn clone_string(&self) -> Arc<String> {
26-
unsafe {
27-
let arced_ptr = self.0.load(Ordering::SeqCst);
28-
assert!(!arced_ptr.is_null());
29-
Arc::increment_strong_count(arced_ptr);
30-
Arc::from_raw(arced_ptr)
31-
}
29+
pub fn as_str(&self) -> impl Deref<Target = str> {
30+
GuardedStr(self.0.load())
3231
}
3332

34-
/// Replaces the value at self with src, returning the old value, without dropping either.
35-
pub fn replace(&self, src: impl Into<String>) -> Arc<String> {
36-
unsafe {
37-
let arced_new = Arc::new(src.into());
38-
let arced_old_ptr = self.0.swap(Arc::into_raw(arced_new) as _, Ordering::SeqCst);
39-
assert!(!arced_old_ptr.is_null());
40-
Arc::from_raw(arced_old_ptr)
41-
}
33+
/// Replaces the value at self with src.
34+
pub fn replace(&self, src: impl Into<String>) {
35+
let arced = Arc::new(src.into());
36+
self.0.store(arced);
4237
}
4338
}
4439

45-
impl Drop for AtomicStr {
46-
fn drop(&mut self) {
47-
unsafe {
48-
let arced_ptr = self.0.swap(std::ptr::null_mut(), Ordering::SeqCst);
49-
assert!(!arced_ptr.is_null());
50-
let _ = Arc::from_raw(arced_ptr);
51-
}
40+
impl From<&str> for AtomicStr {
41+
fn from(value: &str) -> Self {
42+
Self::new(value)
5243
}
5344
}
5445

55-
impl AsRef<str> for AtomicStr {
56-
fn as_ref(&self) -> &str {
57-
self.as_str()
46+
impl fmt::Display for AtomicStr {
47+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48+
f.write_str(&self.as_str())
5849
}
5950
}
6051

61-
impl<T> From<T> for AtomicStr
62-
where
63-
T: Into<String>,
64-
{
65-
fn from(value: T) -> Self {
66-
Self::new(value)
67-
}
68-
}
52+
#[cfg(test)]
53+
mod tests {
54+
use super::*;
6955

70-
impl From<&AtomicStr> for Arc<String> {
71-
fn from(value: &AtomicStr) -> Self {
72-
value.clone_string()
56+
fn test_str(s: &str) {
57+
assert_eq!(s, "hello");
7358
}
74-
}
7559

76-
impl fmt::Display for AtomicStr {
77-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78-
write!(f, "{}", self.as_str())
60+
#[test]
61+
fn test_atomic_str() {
62+
let s = AtomicStr::from("hello");
63+
test_str(&s.as_str());
7964
}
8065
}

src/lib.rs

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#![doc = include_str!("../README.md")]
22

3+
use std::ops::Deref;
4+
35
use once_cell::sync::Lazy;
4-
use std::sync::Arc;
56

67
#[doc(hidden)]
78
pub use once_cell;
@@ -16,8 +17,8 @@ pub fn set_locale(locale: &str) {
1617
}
1718

1819
/// Get current locale
19-
pub fn locale() -> Arc<String> {
20-
CURRENT_LOCALE.clone_string()
20+
pub fn locale() -> impl Deref<Target = str> {
21+
CURRENT_LOCALE.as_str()
2122
}
2223

2324
/// Replace patterns and return a new string.
@@ -110,7 +111,7 @@ pub fn replace_patterns(input: &str, patterns: &[&str], values: &[String]) -> St
110111
macro_rules! t {
111112
// t!("foo")
112113
($key:expr) => {
113-
crate::_rust_i18n_translate(rust_i18n::locale().as_str(), $key)
114+
crate::_rust_i18n_translate(&rust_i18n::locale(), $key)
114115
};
115116

116117
// t!("foo", locale = "en")
@@ -184,7 +185,7 @@ mod tests {
184185

185186
#[test]
186187
fn test_locale() {
187-
assert_locale_type(locale().as_str(), CURRENT_LOCALE.as_str());
188-
assert_locale_type(&locale(), CURRENT_LOCALE.as_str());
188+
assert_locale_type(&locale(), &CURRENT_LOCALE.as_str());
189+
assert_eq!(locale().deref(), "en");
189190
}
190191
}

tests/multi_threading.rs

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use std::ops::Add;
2+
use std::thread::spawn;
3+
use std::time::{Duration, Instant};
4+
5+
use rust_i18n::{set_locale, t};
6+
7+
rust_i18n::i18n!("locales", fallback = "en");
8+
9+
#[test]
10+
fn test_load_and_store() {
11+
let end = Instant::now().add(Duration::from_secs(3));
12+
let store = spawn(move || {
13+
let mut i = 0u32;
14+
while Instant::now() < end {
15+
for _ in 0..100 {
16+
i = i.wrapping_add(1);
17+
if i % 2 == 0 {
18+
set_locale(&format!("en-{i}"));
19+
} else {
20+
set_locale(&format!("fr-{i}"));
21+
}
22+
}
23+
}
24+
});
25+
let load = spawn(move || {
26+
while Instant::now() < end {
27+
for _ in 0..100 {
28+
t!("hello");
29+
}
30+
}
31+
});
32+
store.join().unwrap();
33+
load.join().unwrap();
34+
}

0 commit comments

Comments
 (0)