在Rust中有无无UB的指针与U64互转方法?含无锁栈场景
问题背景
你实现的无锁栈采用头指针CAS算法,为解决ABA问题,64位系统可将计数器塞进指针高位,但32位系统需把32位指针+32位计数器打包进AtomicU64,直接从整数转指针会触发Rust的未定义行为(UB)——这是因为该操作破坏了指针溯源(pointer provenance)规则。
稳定版Rust可行方案
1. 结合NonNull与unsafe位操作
用AtomicU64存储打包后的指针+计数器值,拆分时提取低32位作为指针地址,通过NonNull::new_unchecked恢复指针:
use std::sync::atomic::{AtomicU64, Ordering}; use std::ptr::NonNull; #[derive(Debug)] struct Node<T> { val: T, next: Option<NonNull<Node<T>>>, } struct LockFreeStack<T> { head: AtomicU64, // 低32位:指针地址,高32位:计数器 } impl<T> LockFreeStack<T> { pub fn push(&self, val: T) { let new_node = Box::into_raw(Box::new(Node { val, next: None })); let new_ptr = NonNull::new(new_node).unwrap(); loop { let current_head = self.head.load(Ordering::Acquire); // 提取指针地址并恢复指针 let current_ptr = unsafe { NonNull::new_unchecked((current_head & 0xFFFF_FFFF) as *mut Node<T>) }; let current_count = (current_head >> 32) as u32; // 更新新节点的next指针 unsafe { (*new_ptr.as_ptr()).next = Some(current_ptr); } // 打包新指针与递增后的计数器 let new_head = ((current_count + 1) as u64) << 32 | (new_ptr.as_ptr() as u64); if self.head.compare_exchange(current_head, new_head, Ordering::Release, Ordering::Acquire).is_ok() { break; } } } }
注意:此方案需严格保证指针地址始终指向有效已分配内存,
NonNull::new_unchecked是unsafe操作,一旦地址无效会直接触发UB。只要遵守内存安全规则,该方法可在稳定版中规避UB。
2. 基于repr(C)结构体与transmute
定义对齐的结构体存储指针和计数器,通过transmute在u64和结构体间转换:
use std::sync::atomic::{AtomicU64, Ordering}; use std::ptr::NonNull; #[repr(C)] struct TaggedPtr<T> { ptr: *mut T, count: u32, } #[derive(Debug)] struct Node<T> { val: T, next: Option<NonNull<Node<T>>>, } struct LockFreeStack<T> { head: AtomicU64, } impl<T> LockFreeStack<T> { pub fn push(&self, val: T) { let new_node = Box::into_raw(Box::new(Node { val, next: None })); let new_ptr = NonNull::new(new_node).unwrap(); loop { let current_head = self.head.load(Ordering::Acquire); // 从u64转成TaggedPtr let tagged = unsafe { std::mem::transmute::<u64, TaggedPtr<Node<T>>>(current_head) }; unsafe { (*new_ptr.as_ptr()).next = NonNull::new(tagged.ptr); } // 打包新指针与递增后的计数器 let new_tagged = TaggedPtr { ptr: new_ptr.as_ptr(), count: tagged.count + 1 }; let new_head = unsafe { std::mem::transmute::<TaggedPtr<Node<T>>, u64>(new_tagged) }; if self.head.compare_exchange(current_head, new_head, Ordering::Release, Ordering::Acquire).is_ok() { break; } } } }
repr(C)保证结构体内存布局为指针(32位)在前、计数器(32位)在后,刚好填满64位。transmute的安全性完全依赖于内存布局的正确性,需确保目标平台指针长度为32位。
Nightly版Rust方案:显式处理指针溯源
Nightly版提供ptr::expose_addr和ptr::from_exposed_addr,专门解决整数与指针转换时的溯源问题:
#![feature(ptr_expose_addr)] use std::sync::atomic::{AtomicU64, Ordering}; use std::ptr; use std::ptr::NonNull; #[derive(Debug)] struct Node<T> { val: T, next: Option<NonNull<Node<T>>>, } struct LockFreeStack<T> { head: AtomicU64, } impl<T> LockFreeStack<T> { pub fn push(&self, val: T) { let new_node = Box::into_raw(Box::new(Node { val, next: None })); let new_addr = ptr::expose_addr(new_node); // 暴露指针地址,剥离溯源 loop { let current_head = self.head.load(Ordering::Acquire); let current_addr = (current_head & 0xFFFF_FFFF) as usize; let current_count = (current_head >> 32) as u32; // 从地址恢复指针,告知编译器指针来源未知 let current_ptr = unsafe { ptr::from_exposed_addr(current_addr) }; unsafe { (*new_node).next = NonNull::new(current_ptr); } let new_head = ((current_count + 1) as u64) << 32 | (new_addr as u64); if self.head.compare_exchange(current_head, new_head, Ordering::Release, Ordering::Acquire).is_ok() { break; } } } }
ptr::from_exposed_addr会创建带有"通用溯源"的指针,直接告知编译器该指针来自未知来源、允许任意别名,完美契合你的需求。
关于指针别名的编译器告知
Rust中*mut T本身允许别名,但编译器会基于指针溯源做优化。上述方案(尤其是nightly的from_exposed_addr)已经显式打破常规溯源规则,编译器会放弃基于指针唯一性的优化,从而避免别名导致的UB。
内容的提问来源于stack exchange,提问作者Eloff

