You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Rust中有无无UB的指针与U64互转方法?含无锁栈场景

32位Rust无锁栈的ABA问题与UB规避方案

问题背景

你实现的无锁栈采用头指针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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 20:22:46