יצירת קישורים ובנייה ליעד אחר¶
נניח שהממשק הזר שלנו כולל מאה מבנים וכמה מאות פונקציות. העתקה ידנית של כל החתימות יוצרת מקום נוח לשגיאות, במיוחד כאשר כותרת משתנה. הכלי bindgen יכול לקרוא כותרות דרך clang ולהפיק הכרזות ראסט, אבל הוא אינו יודע שהספרייה שומרת את הבאפר לשלוש שניות או מחייבת השמדה על אותו תהליכון. אנחנו משתמשים בו כדי לתרגם ייצוגים, ומשאירים את תרגום הבעלות לשכבת עטיפה נפרדת.
נבנה ספרייה קטנה כדי לראות את הגבול. צרו פרויקט ספרייה באמצעות cargo new --lib native_probe, ובשורש שלו שמרו את הכותרת הבאה בקובץ wrapper.h.
#include <stdint.h>
struct probe_result { uint32_t code; uint64_t value; };
uint64_t probe_double(uint32_t value);
שמרו את המימוש בקובץ probe.c באותה תיקייה.
הוסיפו תלויות בנייה. הכלי bindgen דורש גם התקנת libclang המתאימה לסביבת הפיתוח, ולכן כשל בטעינתה הוא כשל בסביבת כלי הבנייה ולא בהכרח שגיאה בכותרת.
הקובץ הבא הוא build.rs מלא בשורש הפרויקט. הוא מקמפל את קוד הסי לארכיון, מייצר הכרזות לתיקיית הפלט ומציין אילו קבצים מחייבים בנייה מחדש.
use std::{env, path::PathBuf};
fn main() {
println!("cargo:rerun-if-changed=probe.c");
println!("cargo:rerun-if-changed=wrapper.h");
cc::Build::new().file("probe.c").compile("probe");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.allowlist_function("probe_double")
.allowlist_type("probe_result")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate().expect("binding generation failed");
bindings.write_to_file(PathBuf::from(env::var_os("OUT_DIR").unwrap())
.join("bindings.rs")).expect("binding write failed");
}
הקובץ הבא מחליף את src/lib.rs. המודול הגולמי אינו הופך אוטומטית לחלק מהממשק הציבורי של הספרייה.
#[allow(non_camel_case_types, non_snake_case, non_upper_case_globals)]
mod raw { include!(concat!(env!("OUT_DIR"), "/bindings.rs")); }
pub fn double(value: u32) -> u64 {
// SAFETY: probe_double accepts every u32 and retains no state.
unsafe { raw::probe_double(value) }
}
#[test]
fn handles_maximum() {
assert_eq!(double(u32::MAX), u64::from(u32::MAX) * 2);
}
הריצו cargo test -v וקראו גם את פלט הבנייה. חפשו את ההכרזה שנוצרה בתיקיית target, ובדקו שהטיפוס של uint64_t מתאים ליעד. הספרייה cc מטפלת בהפעלת קומפיילר סי ובפרטי הקישור הנדרשים, אבל עדיין דורשת שכלי היעד יהיו מותקנים ומוגדרים נכון.
הקושי האמיתי מתחיל בבנייה למכונה אחרת. בתהליך הזה קיימים לפחות שני יעדים שונים: המחשב שמריץ את build.rs, והמחשב שעליו ירוץ הבינארי. הקוד של תסריט הבנייה רץ על המארח, ואילו probe.c חייב להיבנות ליעד. כותרות מערכת שנקראו מהמארח עלולות לתאר ABI שונה מזה של הספרייה שאליה נקשר בסוף.
הפקודה rustup target add aarch64-unknown-linux-gnu מוסיפה את רכיבי ראסט של היעד, אך אינה מתקינה בהכרח linker, קומפיילר סי או sysroot מתאים. סביבת בנייה צריכה לספק את כולם. לפני שמתקנים חתימה של ראסט, בדקו באמצעות file ו־readelf -h אם קובץ האובייקט הזר נבנה בכלל לארכיטקטורה הנכונה.
ההבדל בין GNU ל־musl אינו רק סיומת בשם. בחירת ספריית סי משנה את ספריות זמן הריצה, את סביבת הקישור ולעיתים את האופן שבו שירותי מערכת כמו פתרון שמות משתלבים במערכת. בינארי שמקושר סטטית עדיין עשוי להזדקק לקבצי תצורה, אישורים או נתוני אזור זמן. בדקו תלות בפועל באמצעות כותרות ELF והרצה בסביבה נקייה, ואל תסיקו עצמאות מלאה מהיעדר libc.so ברשימה.
שמרו את Cargo.lock עם תרגיל הבנייה וציינו את גרסאות המהדרים. בעת עדכון bindgen, השוו את הקוד שנוצר ואת בדיקות הפריסה. אם הכותרת מכריזה על טיפוס אטום, עדיף לשמור אותו אטום גם בעטיפה במקום לשחזר מבנה פנימי שהספרייה רשאית לשנות.
הזרימה של יצירת הכרזות מתועדת במדריך הבנייה של bindgen, והפרדת המארח והיעד מתוארת בתיעוד תסריטי הבנייה של Cargo. היכולת להסביר איזה כלי יצר כל קובץ חשובה יותר מזכירת אוסף משתני סביבה בעל פה.