קריאות חוזרות ופאניקה בגבול שפה¶
נניח שספריית סי סורקת קובץ ורוצה לדווח לנו על כל רשומה. אנחנו יכולים להעביר אליה פונקציה, אבל הפונקציה לבדה אינה מכילה את מצב הסריקה של המשתמש. ממשקים רבים פותרים זאת באמצעות זוג של מצביע לפונקציה ומצביע אטום בשם context. האחריות שלנו היא להוכיח שהמצביע האטום עדיין מצביע על אובייקט חי כאשר הספרייה קוראת לפונקציה.
נתחיל בחוזה מצומצם בכוונה. הספרייה קוראת ל־callback רק בתוך הקריאה ל־visit_three, מאותו תהליכון, ברצף, ואינה שומרת את המצביע לאחר החזרה. אלה תנאי הממשק, לא ניחוש מתוך הרצה מוצלחת. שמרו בקובץ visit.c את המימוש הבא.
typedef int (*visitor)(void *, unsigned int);
int visit_three(visitor callback, void *context) {
for (unsigned int i = 1; i <= 3; ++i) {
int status = callback(context, i);
if (status != 0) return status;
}
return 0;
}
נשתמש במצב מקומי שמכיל closure. העטיפה הבאה היא תוכנית מלאה המקושרת לארכיון libvisit.a, בהתאם לפקודות הבנייה מההרצאה הקודמת.
use std::ffi::{c_int, c_uint, c_void};
use std::panic::{catch_unwind, AssertUnwindSafe};
unsafe extern "C" {
fn visit_three(callback: unsafe extern "C" fn(*mut c_void, c_uint) -> c_int,
context: *mut c_void) -> c_int;
}
struct Context<F> { callback: F, panicked: bool }
unsafe extern "C" fn trampoline<F: FnMut(u32)>(raw: *mut c_void,
value: c_uint) -> c_int {
// SAFETY: visit owns a live Context<F>, and C never overlaps callbacks.
let context = unsafe { &mut *raw.cast::<Context<F>>() };
match catch_unwind(AssertUnwindSafe(|| (context.callback)(value))) {
Ok(()) => 0,
Err(payload) => {
context.panicked = true;
// A panic payload can itself have a panicking destructor.
std::mem::forget(payload);
-1
}
}
}
fn visit<F: FnMut(u32)>(callback: F) -> Result<(), &'static str> {
let mut context = Context { callback, panicked: false };
let status = unsafe {
visit_three(trampoline::<F>, (&mut context as *mut Context<F>).cast())
};
if context.panicked { Err("callback panicked") }
else if status != 0 { Err("foreign failure") }
else { Ok(()) }
}
fn main() {
let mut total = 0;
assert_eq!(visit(|value| total += value), Ok(()));
assert_eq!(total, 6);
assert!(visit(|value| if value == 2 { panic!("stop") }).is_err());
}
הפונקציה trampoline היא גשר בין חתימה קבועה בסי לבין טיפוס ה־closure שנבחר בזמן קומפילציה. המצביע האטום אינו מוחק את חובות החיים והטיפוס, אלא רק מסתיר אותן מהקומפיילר. ההמרה חזרה ל־Context<F> תקינה משום שאותה התמחות של trampoline נמסרה עם אותו סוג של context.
ההשאלה המשתנה נשארת תקינה משום שאין שתי קריאות חופפות עם אותו context. אם ספרייה יכולה לקרוא מחדש לאותו callback לפני שהקריאה הקודמת הסתיימה, יצירת &mut Context<F> נוספת עלולה להפר את חוזה הייחודיות. נעילה אינה פתרון אוטומטי לכניסה חוזרת מאותו תהליכון, משום שנעילה שאינה תומכת בכך יכולה להיתקע. במקרה כזה נדרש ממשק מצב אחר, בדיקת כניסה חוזרת לפני יצירת הפניה, או איסור מפורש שהספרייה אכן מקיימת.
החוזה של unwind נפרד מהחוזה של המצביע. אנחנו לא מאפשרים לפאניקה לחצות את גבול extern "C", ולכן תופסים אותה בתוך ה־callback. הפונקציה catch_unwind תופסת פאניקות שמתבצעות באמצעות unwind, ואינה תופסת תהליך שמוגדר עם panic=abort. גם panic hook יכול להדפיס הודעה לפני שהפאניקה נתפסת, ולכן הופעת הודעה אינה מוכיחה שהתהליך קרס.
הדוגמה שוכחת במכוון את מטען הפאניקה בנתיב החריג. הסיבה היא שהשמדת מטען שרירותי יכולה בעצמה להיכנס לפאניקה, ואנחנו צריכים למנוע גם את הפאניקה השנייה מלחצות את הגבול. זהו מחיר של דליפה בנתיב כשל, ולא מדיניות כללית מומלצת לכל מערכת. במוצר אמיתי אפשר לבחור סיום תהליך מבוקר או חוזה callback שאינו משתמש בפאניקה לדיווח על כשל צפוי.
העטיפה מחזירה רק הודעת שגיאה אחרי הפאניקה ואינה מפעילה שוב את ה־closure. ההכרזה AssertUnwindSafe אינה מתקנת מצב חלקי שה־closure השאיר אחריו. אם הוא עדכן חצי רשומה, המשתמש צריך לקבל מדיניות מפורשת לגבי הנתונים האלה. לעיתים הפתרון הוא לבנות תוצאה זמנית ולפרסם אותה רק אחרי שהקריאה מצליחה.
הגרסה הזאת אינה מתאימה לספרייה ששומרת callback ומפעילה אותו בהמשך או מתהליכון זר. במקרה כזה המחסנית של visit כבר עשויה להיעלם, ונדרש פרוטוקול הרשמה, ביטול הרשמה והמתנה לסיום קריאות פעילות. נבנה את הפרוטוקול רק אחרי שנלמד סנכרון בפרק השביעי ונשלב אותו בפרק השמיני.
הפרטים של גבולות unwind מתועדים בפרק הממשקים הזרים של הנומיקון. בדקו תמיד אם החוזה שלכם משתמש ב־ABI שמאפשר unwind ומה נתמך בשני הצדדים, במקום להסתמך על כך שהסטאק נראה דומה בדיבאגר.