Unsafe Rust
unsafe Rust
What you’ll learn: What
unsafepermits (raw pointers, FFI, unchecked casts), safe wrapper patterns, C# P/Invoke vs Rust FFI for calling native code, and the safety checklist forunsafeblocks.
本章将学到什么:unsafe到底开放了哪些能力,例如裸指针、FFI、未检查转换;如何把危险实现包进安全封装;C# 的 P/Invoke 和 Rust FFI 在调用原生代码时怎么对应;以及写unsafe块时该遵守的安全检查清单。Difficulty: 🔴 Advanced
难度: 🔴 进阶
Unsafe Rust allows operations that the borrow checker cannot verify. It should be used sparingly, and every use最好都带着清晰的边界与说明。
unsafe Rust 允许开发者做一些借用检查器无法验证的操作。它不是洪水猛兽,但确实应该少用,而且每一处都得把边界和理由讲明白。
Advanced coverage: For safe abstraction patterns over unsafe code, such as arena allocators, lock-free structures, and custom vtables, see Rust Patterns.
更深入的延伸阅读: 如果想继续看如何在 unsafe 之上建立安全抽象,例如 arena 分配器、无锁结构和自定义 vtable,可以去读 Rust Patterns。
When You Need Unsafe
什么时候会需要 unsafe
#![allow(unused)]
fn main() {
// 1. Dereferencing raw pointers
let mut value = 42;
let ptr = &mut value as *mut i32;
// SAFETY: ptr points to a valid, live local variable.
unsafe {
*ptr = 100; // Must be in unsafe block
}
// 2. Calling unsafe functions
unsafe fn dangerous() {
// Internal implementation that requires caller to maintain invariants
}
// SAFETY: no invariants to uphold for this example function.
unsafe {
dangerous(); // Caller takes responsibility
}
// 3. Accessing mutable static variables
static mut COUNTER: u32 = 0;
// SAFETY: single-threaded context; no concurrent access to COUNTER.
unsafe {
COUNTER += 1; // Not thread-safe — caller must ensure synchronization
}
// 4. Implementing unsafe traits
unsafe trait UnsafeTrait {
fn do_something(&self);
}
}
Rust 并不是见到 unsafe 就自动失控。准确地说,unsafe 只是把一小块区域标记成“这里的正确性证明,交给开发者自己负责”。
也就是说,unsafe 不会关闭整个 Rust 的安全系统,它只是局部放开几个原本被严格限制的操作。
C# Comparison: unsafe Keyword
和 C# unsafe 的对比
// C# unsafe - similar concept, different scope
unsafe void UnsafeExample()
{
int value = 42;
int* ptr = &value;
*ptr = 100;
// C# unsafe is about pointer arithmetic
// Rust unsafe is about ownership/borrow rule relaxation
}
// C# fixed - pinning managed objects
unsafe void PinnedExample()
{
byte[] buffer = new byte[100];
fixed (byte* ptr = buffer)
{
// ptr is valid only within this block
}
}
C# 里的 unsafe 更多是为了直接操作指针、和托管内存系统短接。Rust 里的 unsafe 范围更广一些,它不只和指针有关,也包括别名规则、FFI 边界、可变静态变量和 trait 安全契约。
所以 C# 开发者刚接触 Rust 时,容易误以为“unsafe 就是指针区”,其实 Rust 的 unsafe 语义更系统化,也更强调局部证明责任。
Safe Wrappers
安全封装
#![allow(unused)]
fn main() {
/// The key pattern: wrap unsafe code in a safe API
pub struct SafeBuffer {
data: Vec<u8>,
}
impl SafeBuffer {
pub fn new(size: usize) -> Self {
SafeBuffer { data: vec![0; size] }
}
/// Safe API — bounds-checked access
pub fn get(&self, index: usize) -> Option<u8> {
self.data.get(index).copied()
}
/// Fast unchecked access — unsafe but wrapped safely with bounds check
pub fn get_unchecked_safe(&self, index: usize) -> Option<u8> {
if index < self.data.len() {
// SAFETY: we just checked that index is in bounds
Some(unsafe { *self.data.get_unchecked(index) })
} else {
None
}
}
}
}
这就是 Rust 里最值钱的思路之一:把不安全操作关进一个很小的实现细节里,对外暴露 100% 安全的 API。
标准库里的 Vec、String、HashMap 其实也都靠类似思路活着,内部有 unsafe,但接口本身尽量保持安全。
Interop with C# via FFI
通过 FFI 和 C# 互操作
Rust can expose C-compatible functions that C# calls through P/Invoke.
Rust 可以导出符合 C ABI 的函数,C# 再通过 P/Invoke 去调用它们。
graph LR
subgraph "C# Process"
CS["C# Code<br/>C# 代码"] -->|"P/Invoke"| MI["Marshal Layer<br/>UTF-16 → UTF-8<br/>结构体布局"]
end
MI -->|"C ABI call"| FFI["FFI Boundary<br/>FFI 边界"]
subgraph "Rust cdylib (.so / .dll)"
FFI --> RF["extern \"C\" fn<br/>#[no_mangle]"]
RF --> Safe["Safe Rust<br/>内部实现"]
end
style FFI fill:#fff9c4,color:#000
style MI fill:#bbdefb,color:#000
style Safe fill:#c8e6c9,color:#000
Rust Library (compiled as cdylib)
Rust 侧库(编译成 cdylib)
#![allow(unused)]
fn main() {
// src/lib.rs
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn process_string(input: *const std::os::raw::c_char) -> i32 {
let c_str = unsafe {
if input.is_null() {
return -1;
}
// SAFETY: input is non-null (checked inside) and assumed null-terminated by caller.
std::ffi::CStr::from_ptr(input)
};
match c_str.to_str() {
Ok(s) => s.len() as i32,
Err(_) => -1,
}
}
}
# Cargo.toml
[lib]
crate-type = ["cdylib"]
C# Consumer (P/Invoke)
C# 侧调用方(P/Invoke)
using System.Runtime.InteropServices;
public static class RustInterop
{
[DllImport("my_rust_lib", CallingConvention = CallingConvention.Cdecl)]
public static extern int add_numbers(int a, int b);
[DllImport("my_rust_lib", CallingConvention = CallingConvention.Cdecl)]
public static extern int process_string(
[MarshalAs(UnmanagedType.LPUTF8Str)] string input);
}
// Usage
int sum = RustInterop.add_numbers(5, 3);
int len = RustInterop.process_string("Hello from C#!");
FFI Safety Checklist
FFI 安全检查清单
When exposing Rust functions to C#, the following rules avoid many common crashes and ABI mismatches:
Rust 往 C# 暴露函数时,下面这些规则能挡掉一大堆常见炸点和 ABI 不匹配问题。
- Always use
extern "C"— otherwise Rust uses its own unstable calling convention.
1. 一定要用extern "C",不然调用约定就对不上。 - Add
#[no_mangle]— otherwise C# often找不到符号。
2. 补上#[no_mangle],否则 C# 经常连导出名都找不到。 - Never let a panic cross the FFI boundary — unwinding into foreign code is undefined behavior.
3. 绝对别让 panic 穿过 FFI 边界,Rust unwind 到外部语言里属于未定义行为。 - Use
#[repr(C)]for transparent structs that foreign code reads directly.
4. 如果外部语言要直接读结构体字段,就必须用#[repr(C)]。 - Always validate pointers before dereferencing.
5. 所有裸指针解引用之前都先判空。 - Document string encoding clearly — C# 内部是 UTF-16,Rust
CStr常常期待 UTF-8。
6. 把字符串编码规则写清楚,别让 UTF-16 和 UTF-8 在边界上互相埋雷。
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn safe_ffi_function() -> i32 {
match std::panic::catch_unwind(|| {
42
}) {
Ok(result) => result,
Err(_) => -1,
}
}
}
#![allow(unused)]
fn main() {
// Opaque handle — no #[repr(C)] needed when C# only stores IntPtr
pub struct Connection { /* Rust-only fields */ }
// Transparent data — C# reads fields directly
#[repr(C)]
pub struct Point { pub x: f64, pub y: f64 }
}
End-to-End Example: Opaque Handle with Lifecycle Management
完整例子:带生命周期管理的不透明句柄
This is a very common production pattern: Rust owns the object, C# only holds an opaque handle, and explicit create/free functions manage lifetime.
这是一种非常常见的生产写法:对象真实所有权归 Rust,C# 只拿一个不透明句柄,再通过显式的创建和释放函数管理生命周期。
Rust side:
Rust 侧:
#![allow(unused)]
fn main() {
use std::ffi::{c_char, CStr};
pub struct ImageProcessor {
width: u32,
height: u32,
pixels: Vec<u8>,
}
#[no_mangle]
pub extern "C" fn processor_new(width: u32, height: u32) -> *mut ImageProcessor {
if width == 0 || height == 0 {
return std::ptr::null_mut();
}
let proc = ImageProcessor {
width,
height,
pixels: vec![0u8; (width * height * 4) as usize],
};
Box::into_raw(Box::new(proc))
}
#[no_mangle]
pub extern "C" fn processor_grayscale(ptr: *mut ImageProcessor) -> i32 {
// SAFETY: ptr was created by Box::into_raw (non-null), still valid.
let proc = match unsafe { ptr.as_mut() } {
Some(p) => p,
None => return -1,
};
for chunk in proc.pixels.chunks_exact_mut(4) {
let gray = (0.299 * chunk[0] as f64
+ 0.587 * chunk[1] as f64
+ 0.114 * chunk[2] as f64) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
}
0
}
#[no_mangle]
pub extern "C" fn processor_free(ptr: *mut ImageProcessor) {
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)); }
}
}
}
C# side:
C# 侧:
using System.Runtime.InteropServices;
public sealed class ImageProcessor : IDisposable
{
[DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr processor_new(uint width, uint height);
[DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
private static extern int processor_grayscale(IntPtr ptr);
[DllImport("image_rust", CallingConvention = CallingConvention.Cdecl)]
private static extern void processor_free(IntPtr ptr);
private IntPtr _handle;
public ImageProcessor(uint width, uint height)
{
_handle = processor_new(width, height);
if (_handle == IntPtr.Zero)
throw new ArgumentException("Invalid dimensions");
}
public void Grayscale()
{
if (processor_grayscale(_handle) != 0)
throw new InvalidOperationException("Processor is null");
}
public void Dispose()
{
if (_handle != IntPtr.Zero)
{
processor_free(_handle);
_handle = IntPtr.Zero;
}
}
}
using var proc = new ImageProcessor(1920, 1080);
proc.Grayscale();
Key insight: This is very close to the spirit of C#
SafeHandle. Rust usesBox::into_raw/Box::from_rawto hand ownership across the FFI boundary, and the C#IDisposablewrapper makes cleanup explicit and reliable.
关键点:这套思路和 C# 的SafeHandle很接近。Rust 用Box::into_raw/Box::from_raw转移所有权,C# 再用IDisposable把释放动作明确地兜住。
Exercises
练习
🏋️ Exercise: Safe Wrapper for Raw Pointer 🏋️ 练习:给裸指针做安全封装
You receive a raw pointer from a C library. Write a safe Rust wrapper:
假设从一个 C 库拿到裸指针,尝试给它写一个安全 Rust 包装层:
#![allow(unused)]
fn main() {
// Simulated C API
extern "C" {
fn lib_create_buffer(size: usize) -> *mut u8;
fn lib_free_buffer(ptr: *mut u8);
}
}
Requirements:
要求:
- Create a
SafeBufferstruct that wraps the raw pointer
1. 定义一个SafeBuffer结构包住裸指针。 - Implement
Dropto calllib_free_buffer
2. 实现Drop,在析构时调用lib_free_buffer。 - Provide a safe
&[u8]view viaas_slice()
3. 通过as_slice()暴露一个安全的&[u8]视图。 - Ensure
SafeBuffer::new()returnsNoneif the pointer is null
4. 如果指针为空,SafeBuffer::new()必须返回None。
🔑 Solution 参考答案
struct SafeBuffer {
ptr: *mut u8,
len: usize,
}
impl SafeBuffer {
fn new(size: usize) -> Option<Self> {
// SAFETY: lib_create_buffer returns a valid pointer or null (checked below).
let ptr = unsafe { lib_create_buffer(size) };
if ptr.is_null() {
None
} else {
Some(SafeBuffer { ptr, len: size })
}
}
fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
}
impl Drop for SafeBuffer {
fn drop(&mut self) {
unsafe { lib_free_buffer(self.ptr); }
}
}
fn process(buf: &SafeBuffer) {
let data = buf.as_slice();
println!("First byte: {}", data[0]);
}
Key pattern: keep the unsafe in one tiny place, attach // SAFETY: reasoning, and present a fully safe public API.
核心模式:把 unsafe 尽量缩成一个很小的实现块,配上 // SAFETY: 注释说明理由,然后对外提供纯安全 API。