🎓 A1Learner
🔍

Rust FFI Ownership Transfer

An FFI ownership transfer passes a Rust value to C and back. Box::into_raw and Box::from_raw are the two hands that pass ownership across the border.

Hand a Box to the border

into_raw lets go of the Box. The value does not drop. It waits at the same address.

let boxed = Box::new(41);
let raw = Box::into_raw(boxed);

Take it back exactly once

from_raw rebuilds the Box. Dropping that Box frees the value, just like a normal Box.

let back = unsafe { Box::from_raw(raw) };
println!("value = {}", *back + 1);

The create and destroy pair

A small Rust library can offer C two functions. One makes a value. One ends it.

#[unsafe(no_mangle)]
pub extern "C" fn tally_new(start: i32) -> *mut Tally {
    Box::into_raw(Box::new(Tally { total: start }))
}

Borrow without taking

A read-only call can look through the pointer. The ownership stays with the caller.

if tally.is_null() {
    return -1;
}
unsafe { (*tally).total }

Strings cross the same way

CString has the same pair of hands. into_raw gives C a text pointer. from_raw takes the string back.

let raw = CString::new("hello").unwrap().into_raw();
let back = unsafe { CString::from_raw(raw) };

Example

use std::ffi::CString;

pub struct Tally {
    total: i32,
}

#[unsafe(no_mangle)]
pub extern "C" fn tally_new(start: i32) -> *mut Tally {
    Box::into_raw(Box::new(Tally { total: start }))
}

#[unsafe(no_mangle)]
pub extern "C" fn tally_add(tally: *mut Tally, amount: i32) {
    if tally.is_null() {
        return;
    }
    let tally = unsafe { &mut *tally };
    tally.total += amount;
}

#[unsafe(no_mangle)]
pub extern "C" fn tally_read(tally: *const Tally) -> i32 {
    if tally.is_null() {
        return -1;
    }
    unsafe { (*tally).total }
}

#[unsafe(no_mangle)]
pub extern "C" fn tally_free(tally: *mut Tally) {
    if tally.is_null() {
        return;
    }
    let _ = unsafe { Box::from_raw(tally) };
}

fn main() {
    let tally = tally_new(7);
    tally_add(tally, 5);
    println!("tally = {}", tally_read(tally));
    let raw = CString::new("owned by C now").unwrap().into_raw();
    let back = unsafe { CString::from_raw(raw) };
    println!("text back = {}", back.to_str().unwrap());
    tally_free(tally);
    println!("freed cleanly");
}

Key points

  • Box::into_raw turns a Box into a raw pointer.
  • The value stays alive.
  • Nobody owns it until someone takes it back
  • Box::from_raw is the matching hand.
  • It rebuilds the Box.
  • The value drops when that Box drops
  • Call from_raw exactly once for each into_raw.
  • Twice means a double free.
  • Never means a leak
  • Write one create function and one destroy function for each type you share.
  • The pair makes the rule easy to check
  • A function that only reads through the pointer should borrow it.
  • It should not rebuild the Box.
  • Rebuilding at the wrong moment frees the value early
  • Ownership across the border passes by hand: into_raw hands it out, and from_raw takes it back.
  • A raw pointer carries no ownership information.
  • The contract lives in your API design and in your comments.
  • Who frees the value is the one question that decides every transfer.

In this example, main plays the part of a C caller. tally_new builds a Tally inside a Box. into_raw hands out the raw pointer. tally_add borrows through the pointer and adds 5. The total becomes 12. The CString makes the same trip. into_raw hands its text pointer out. from_raw takes the string back, so Rust owns it again. tally_free ends the story with from_raw. The rebuilt Box drops at once. The memory is freed exactly once.

Note

It does not live in the type

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

A C program received a pointer from Box::into_raw. It is finished with the value. What must happen?