12. Unsafe Rust et FFI (Foreign Function Interface)

unsafe permet de désactiver certaines garanties du compilateur pour des opérations que Rust ne peut pas vérifier. Le FFI permet d’appeler du code C depuis Rust. Ces deux outils sont nécessaires pour interagir avec CUDA (cudarc), les bibliothèques C (cuBLAS, NCCL), et pour des optimisations bas niveau.


12.1 Unsafe — Quand et Pourquoi

Opérations unsafe

5 super-pouvoirs de unsafe :

// 1. Déréférencer un pointeur brut
let ptr = &42 as *const i32;
unsafe {
    println!("{}", *ptr);
}
 
// 2. Appeler une fonction unsafe (FFI)
unsafe {
    libc::printf(b"hello\0".as_ptr() as *const i8);
}
 
// 3. Accéder à un union (C-like)
// 4. Modifier une variable statique mutable
static mut COUNTER: usize = 0;
unsafe {
    COUNTER += 1;
}
 
// 5. Implémenter un trait unsafe (Send, Sync)
struct PtrWrapper(*mut f64);
unsafe impl Send for PtrWrapper {}
unsafe impl Sync for PtrWrapper {}

Règles de l’unsafe

// UNSAFE ne désactive PAS le borrow checker !
// Il donne accès à 5 opérations supplémentaires.
 
// ❌ Ceci est toujours interdit (même dans unsafe) :
let mut v = vec![1, 2, 3];
let r1 = &v;
unsafe {
    let r2 = &mut v;  // ⛔ violation de borrowing !
}

Exemple : Allouer un buffer contigu sur le heap

use std::alloc::{alloc, Layout, dealloc};
 
struct AlignedBuffer {
    ptr: *mut u8,
    size: usize,
}
 
impl AlignedBuffer {
    fn new(size: usize, alignment: usize) -> Self {
        let layout = Layout::from_size_align(size, alignment).unwrap();
        let ptr = unsafe { alloc(layout) };
        if ptr.is_null() { panic!("allocation échouée"); }
        Self { ptr, size }
    }
 
    fn as_slice(&self) -> &[u8] {
        unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
    }
 
    fn as_mut_slice(&mut self) -> &mut [u8] {
        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.size) }
    }
}
 
impl Drop for AlignedBuffer {
    fn drop(&mut self) {
        let layout = Layout::from_size_align(self.size, 64).unwrap();
        unsafe { dealloc(self.ptr, layout); }
    }
}
 
// Utilisation : buffer aligné pour transfert GPU
let mut buf = AlignedBuffer::new(1024 * 1024, 256); // 1 Mo, aligné à 256 octets
buf.as_mut_slice()[0] = 42;

12.2 FFI — Appeler du C depuis Rust

Déclarer des fonctions C

extern "C" {
    fn cudaMalloc(ptr: *mut *mut std::ffi::c_void, size: usize) -> i32;
    fn cudaMemcpy(dst: *mut std::ffi::c_void, src: *const std::ffi::c_void,
                  size: usize, kind: i32) -> i32;
    fn cudaFree(ptr: *mut std::ffi::c_void) -> i32;
}
 
// Constantes
const cudaMemcpyDeviceToHost: i32 = 2;
const cudaMemcpyHostToDevice: i32 = 1;
 
// Wrapper sûr
fn allocate_gpu(size: usize) -> Result<*mut std::ffi::c_void, String> {
    let mut ptr: *mut std::ffi::c_void = std::ptr::null_mut();
    let err = unsafe { cudaMalloc(&mut ptr as *mut *mut _, size) };
    if err != 0 {
        Err(format!("cudaMalloc a échoué avec code {err}"))
    } else {
        Ok(ptr)
    }
}

Structures compatibles C

#[repr(C)]  // garantit l'ordre et l'alignement C (pas de réorganisation)
struct CudaBuffer {
    ptr: *mut f64,   // 8 octets
    size: usize,      // 8 octets (sur 64-bit)
}
 
#[repr(C, packed)]  // pas de padding (attention : alignement potentiellement lent)
struct PackedGradient {
    value: f64,
    worker_id: i32,
}

Intégration avec un build script

// build.rs — exécuté avant la compilation
fn main() {
    println!("cargo:rustc-link-lib=cudart");  // lie CUDA runtime
    println!("cargo:rustc-link-search=/usr/local/cuda/lib64");
}
 
// Puis dans src/
extern "C" {
    fn cudaSetDevice(device: i32) -> i32;
}

12.3 Intégration CUDA avec cudarc

// Exemple cudarc (bindings CUDA Driver API, pas de kernel writing)
use cudarc::driver::*;
use std::rc::Rc;
 
fn median_on_gpu(grads: &[Vec<f64>]) -> Result<Vec<f64>, Box<dyn std::error::Error>> {
    let dev = CudaDevice::new(0)?;  // premier GPU
 
    let n = grads.len();
    let d = grads[0].len();
    let total_size = n * d;
 
    // Allouer sur GPU
    let d_grads = dev.alloc::<f64>(total_size)?;  // DeviceBuffer<f64>
    let d_result = dev.alloc::<f64>(d)?;
 
    // Copier CPU → GPU
    let flat: Vec<f64> = grads.iter().flat_map(|g| g.iter().copied()).collect();
    d_grads.copy_from(&flat)?;
 
    // Lancer un kernel CUDA (écrit en .cu, chargé ici)
    // La fonction "median_kernel" est définie dans un fichier .cu compilé
    let ptx = std::fs::read_to_string("kernels/median.ptx")?;
    dev.load_ptx("median.ptx", "median_module", &[&ptx])?;
 
    let kernel = dev.get_kernel("median_module", "median_kernel")?;
    unsafe {
        dev.launch_kernel1d(
            d as u32,  // 1 bloc par dimension
            kernel,
            (&d_grads, n as i32, d as i32, &d_result),
        )?;
    }
 
    // Copier GPU → CPU
    let mut result = vec![0.0; d];
    d_result.copy_into(&mut result)?;
 
    Ok(result)
}

Avec des kernels CUDA C++ compilés à côté

// kernels/median.cu
extern "C" __global__ void median_kernel(
    const double* grads, int n, int d, double* result
) {
    int j = blockIdx.x;  // une dimension par bloc
    if (j >= d) return;
 
    // Copier la colonne j dans la mémoire partagée
    extern __shared__ double col[];
    for (int i = 0; i < n; i++) {
        col[i] = grads[i * d + j];
    }
 
    // Trier (bitonic sort pour la démo)
    for (int i = 0; i < n - 1; i++) {
        for (int k = 0; k < n - i - 1; k++) {
            if (col[k] > col[k + 1]) {
                double tmp = col[k];
                col[k] = col[k + 1];
                col[k + 1] = tmp;
            }
        }
    }
 
    result[j] = col[n / 2];
}

12.4 Exposer Rust en C (l’inverse)

// src/lib.rs — librairie Rust appelable depuis C/Python
 
#[no_mangle]  // empêche le name mangling Rust
pub extern "C" fn median_aggregate(
    grads: *const f64,
    n: i32,
    d: i32,
    result: *mut f64,
) -> i32 {
    // Convertir les pointeurs C en slices Rust
    let n = n as usize;
    let d = d as usize;
 
    let grads_slice = unsafe {
        std::slice::from_raw_parts(grads, n * d)
    };
    let result_slice = unsafe {
        std::slice::from_raw_parts_mut(result, d)
    };
 
    // Construire la matrice
    let mut matrix: Vec<Vec<f64>> = Vec::with_capacity(n);
    for i in 0..n {
        let start = i * d;
        matrix.push(grads_slice[start..start + d].to_vec());
    }
 
    // Calculer la médiane
    for j in 0..d {
        let mut col: Vec<f64> = matrix.iter().map(|g| g[j]).collect();
        col.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
        result_slice[j] = col[n / 2];
    }
 
    0  // succès
}
# Compiler en librairie C dynamique
cargo build --release
# target/release/libgradient_core.so  (Linux)
# target/release/libgradient_core.dylib (macOS)
# target/release/gradient_core.dll  (Windows)
# Python — appeler la librairie via ctypes sans PyO3
import ctypes
 
lib = ctypes.CDLL("target/release/libgradient_core.dylib")
lib.median_aggregate.argtypes = [
    ctypes.POINTER(ctypes.c_double),
    ctypes.c_int32,
    ctypes.c_int32,
    ctypes.POINTER(ctypes.c_double),
]
lib.median_aggregate.restype = ctypes.c_int32
 
grads = [1.0, 5.0, 3.0, 2.0, 2.0, 8.0, 9.0, 1.0, 4.0]
n, d = 3, 3
grads_arr = (ctypes.c_double * len(grads))(*grads)
result_arr = (ctypes.c_double * d)()
 
lib.median_aggregate(grads_arr, n, d, result_arr)
print(list(result_arr))  # [3.0, 2.0, 4.0]

12.5 Règles pour l’Unsafe

RègleExplication
Minimiser l’unsafeL’encapsuler dans la plus petite fonction possible
Wrapper sûrToujours proposer une API safe autour de l’unsafe
Documenter les invariants// SAFETY: ptr doit être non-null et aligné
TesterLes fonctions unsafe nécessitent encore plus de tests
MiriUtiliser cargo miri test pour détecter les UB
Préférer les cratesNe pas réinventer : cudarc, libc, winapi
/// Calcule la somme d'un tableau de f64.
///
/// # Safety
/// - `ptr` doit pointer sur un tableau valide de `len` éléments
/// - `len` doit être > 0
unsafe fn sum_array(ptr: *const f64, len: usize) -> f64 {
    let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
    slice.iter().sum()
}

🔗 Voir aussi