פתרון פרויקט עטיפה בטוחה לספריית זיכרון זרה¶
נתחיל בכותרת blob.h. המצביע הוא לידית אטומה, ולכן המשתמש אינו רשאי להסתמך על גודל המבנה הפנימי.
#include <stddef.h>
#include <stdint.h>
struct blob;
struct blob *blob_create(const uint8_t *data, size_t len);
const uint8_t *blob_data(const struct blob *handle);
size_t blob_len(const struct blob *handle);
void blob_destroy(struct blob *handle);
size_t blob_live(void);
המימוש הבא בקובץ blob.c שומר הקצאה נפרדת עבור הנתונים. המונה מיועד לתרגיל סינכרוני בלבד ואינו בטוח לשימוש מקבילי.
#include "blob.h"
#include <stdlib.h>
#include <string.h>
struct blob { size_t len; uint8_t *data; };
static size_t live = 0;
struct blob *blob_create(const uint8_t *data, size_t len) {
if (!data && len) return NULL;
struct blob *result = malloc(sizeof(*result));
if (!result) return NULL;
result->data = malloc(len ? len : 1);
if (!result->data) { free(result); return NULL; }
if (len) memcpy(result->data, data, len);
result->len = len;
++live;
return result;
}
const uint8_t *blob_data(const struct blob *handle) { return handle->data; }
size_t blob_len(const struct blob *handle) { return handle->len; }
void blob_destroy(struct blob *handle) {
free(handle->data);
free(handle);
--live;
}
size_t blob_live(void) { return live; }
הקוד הבא הוא תוכנית ראסט מלאה בקובץ main.rs. הסמן עם Rc מבטל העברה ושיתוף אוטומטיים בין תהליכונים, בלי להקצות אובייקט Rc בפועל.
use std::{ffi::c_void, marker::PhantomData, ptr::NonNull, rc::Rc};
unsafe extern "C" {
fn blob_create(data: *const u8, len: usize) -> *mut c_void;
fn blob_data(handle: *const c_void) -> *const u8;
fn blob_len(handle: *const c_void) -> usize;
fn blob_destroy(handle: *mut c_void);
fn blob_live() -> usize;
}
struct ForeignBytes {
handle: NonNull<c_void>,
single_thread: PhantomData<Rc<()>>,
}
impl ForeignBytes {
fn new(bytes: &[u8]) -> Result<Self, &'static str> {
let handle = unsafe { blob_create(bytes.as_ptr(), bytes.len()) };
let handle = NonNull::new(handle).ok_or("allocation failed")?;
Ok(Self { handle, single_thread: PhantomData })
}
fn as_slice(&self) -> &[u8] {
unsafe {
let pointer = blob_data(self.handle.as_ptr());
let len = blob_len(self.handle.as_ptr());
// C keeps one initialized, immutable allocation alive until Drop.
// Creation copied a Rust slice, so len cannot exceed isize::MAX.
std::slice::from_raw_parts(pointer, len)
}
}
}
impl Drop for ForeignBytes {
fn drop(&mut self) { unsafe { blob_destroy(self.handle.as_ptr()); } }
}
fn early_return() -> Result<(), &'static str> {
let _bytes = ForeignBytes::new(&[1, 2, 3])?;
Err("later operation failed")
}
fn main() {
assert_eq!(unsafe { blob_live() }, 0);
{
let empty = ForeignBytes::new(&[]).unwrap();
let data = ForeignBytes::new(&[3, 0, 7]).unwrap();
let large = ForeignBytes::new(&vec![9; 100_000]).unwrap();
assert!(empty.as_slice().is_empty());
assert_eq!(data.as_slice(), [3, 0, 7]);
assert_eq!(large.as_slice().len(), 100_000);
assert_eq!(unsafe { blob_live() }, 3);
}
assert!(early_return().is_err());
assert_eq!(unsafe { blob_live() }, 0);
}
הפקודות הבאות בונות את שני הצדדים בסביבת לינוקס.
cc -std=c11 -Wall -Wextra -c blob.c -o blob.o
ar rcs libblob.a blob.o
rustc --edition=2024 main.rs -L . -l static=blob -o foreign-bytes
./foreign-bytes
האינווריאנט הראשון הוא שכל ForeignBytes חי מחזיק ידית אחת שנוצרה בהצלחה ולא הושמדה. האינווריאנט השני הוא שהספרייה אינה משנה את הנתונים ואינה מזיזה אותם עד ההשמדה. האינווריאנט השלישי הוא שהשאלה מ־as_slice אינה יכולה להאריך ימים מעבר להידית, משום שהחיים המושמטים בחתימה נקשרים ל־&self.
בדיקת הקומפילציה השלילית הבאה מיועדת להיכשל כאשר מכניסים אותה במקום main. היא מנסה להשתמש בפרוסה אחרי יציאה מה־scope שמחזיק את הבעלים.
fn main() {
let escaped;
{
let bytes = ForeignBytes::new(&[1, 2]).unwrap();
escaped = bytes.as_slice();
}
println!("{}", escaped.len());
}
הפתרון אינו מוסיף Clone, משום ששכפול פשוט של המצביע היה מייצר השמדה כפולה. אפשר להוסיף שכפול אמיתי באמצעות קריאה ל־blob_create(self.as_slice()), ואז כל ידית מחזיקה הקצאה נפרדת. אפשר גם לתכנן שיתוף של בעלים אחד, אבל אז נצטרך לשנות את חוזה התהליכונים ואת המונה האבחוני.
הכשל לאחר הקצאת המבנה ולפני הקצאת הנתונים מטופל בתוך סי, ולכן ראסט מקבלת או ידית שלמה או null. לעומת זאת, אם ספרייה זרה מחזירה ידית חלקית עם קוד שגיאה, נדרש חוזה אחר שמגדיר האם צריך להשמיד אותה. ההבדל הזה צריך להופיע בעטיפה ולא להישאר כהנחה בלתי כתובה.