כריכת Cairo מובנית, נשלחת#

פרק הווקטור צייר עם Cairo כמודול CPAN רגיל. פרק זה מראה את הצד השני של אותו קוד: pperl שולח מימוש מחדש מובנה של Cairo, כך ש־use Cairo עובד על pperl בלי הפצת Cairo של CPAN מותקנת ובלי מהדר C מעורב. ה־Perl שאתם כותבים זהה; מה שמגבה אותו הוא ה־FFI של pperl עצמו המגיע ל־libcairo ישירות. Cairo מהווה את הדוגמה המעובדת האידיאלית מפני שה־API של C שלו הוא כמעט כולו סקלרים וידית אטומה אחת, וזו בדיוק הצורה שמגיעה בנקיות היום.

זוהי כריכה אמיתית, נשלחת, לא סקיצה. קריאתה אומרת לכם מהי כריכת גרפיקה מובנית אצורה, כיצד החלקים משתלבים, וכיצד לזהות (או לבקש) את אותו טיפול עבור ספרייה אחרת. בניית כריכה כזו היא משימת זמן־ריצה; מה שאתם כותבים כמשתמשים הוא ה־Perl שבצידה הרחוק, וה־Perl הזה הוא רק ה־API של Cairo שכבר ראיתם.

ה־API של Perl: בדיוק זה של CPAN#

הכריכה המובנית חושפת את שמות החבילות והמתודות האמיתיים של Cairo: Cairo::ImageSurface, Cairo::Context, Cairo::PdfSurface והשאר, עם חתימות המתודה של מעלה־הזרם. קוד שנכתב עבור Cairo של CPAN רץ ללא שינוי; חבילות בדיקה של Cairo שהוסבו עוברות מולו. התוכנית מפרק הווקטור היא התוכנית כאן:

use Cairo;

my $surface = Cairo::ImageSurface->create('argb32', 200, 200);
my $cr      = Cairo::Context->create($surface);

$cr->set_source_rgb(1, 1, 1);
$cr->paint;                                  # white background
$cr->set_source_rgb(0, 0, 0);
$cr->set_line_width(3);
$cr->move_to(20, 20);
$cr->line_to(180, 20);
$cr->stroke;
$cr->arc(100, 100, 40, 0, 6.28318);
$cr->set_source_rgba(1, 0, 0, 0.5);
$cr->fill;

$surface->write_to_png('out.png');
# the surface and context free their C objects when they go out of scope

שום דבר בתוכנית הזו אינו רומז כיצד היא ממומשת. אין dlopen, אין מחרוזת חתימה, אין אובייקט כריכה: רק Cairo. שאר הפרק הזה הוא מה שגורם ל־use Cairo לעשות זאת על pperl, והתבנית זהה לזו שמאחורי Math::GMP.

צעד 1: פתחו את הספרייה פעם אחת, פתרו את הסמלים#

הכריכה פותחת את libcairo.so.2 פעם יחידה, כשהמודול נטען, ופותרת כל פונקציית C הדרושה לה לתוך טבלה של מצביעי פונקציה המוחזקת למשך חיי התהליך. זו אותה צורה ככריכת Math::GMP, הפותחת את libgmp פעם אחת ופותרת את סמלי ה־mpz_* שלה. עבור Cairo הסמלים הפתורים הם אלה שמאחורי המתודות לעיל:

open libcairo.so.2 once when the module loads
resolve and store, as function pointers:
    cairo_image_surface_create(format, width, height) -> surface*
    cairo_create(surface*)                             -> context*
    cairo_move_to(context*, x, y)
    cairo_line_to(context*, x, y)
    cairo_arc(context*, xc, yc, radius, a1, a2)
    cairo_set_source_rgb(context*, r, g, b)
    cairo_set_source_rgba(context*, r, g, b, a)
    cairo_set_line_width(context*, w)
    cairo_stroke(context*)  cairo_fill(context*)  cairo_paint(context*)
    cairo_surface_write_to_png(surface*, path) -> status
    cairo_destroy(context*)  cairo_surface_destroy(surface*)

אם libcairo אינה מותקנת במערכת, הכריכה מדווחת על כך פעם אחת עם הודעה מועילה ולא קורסת. לכל אחת מפונקציות ה־C האלה יש חתימה שקודי הטיפוס של ה־FFI כבר מבטאים: הארגומנטים הם הידית האטומה (כתובת, כלומר מספר שלם), doubles, ונתיב מחרוזת. אין struct לפי־ערך ב־API הציור המרכזי, וזו הסיבה ש־Cairo היא הדוגמה הנקייה.

Cairo היא ספרייה גדולה, ולכן הכריכה האמיתית מאורגנת לפי חבילה: Cairo::Surface, Cairo::Context, Cairo::Path, Cairo::Pattern, Cairo::Matrix, Cairo::Font ו־Cairo::Region, כשכל אחת פותרת את הסמלים הדרושים לה. זו נוחות מימוש; המשתמש רואה Cairo אחד.

צעד 2: הידית האטומה כמספר שלם מבורך#

Cairo מחזירה cairo_surface_t* ו־cairo_t*: מצביעים ל־structs של C שבפנימיותם לעולם אינכם נוגעים. הכריכה אינה צריכה להבין את הפריסה שלהם, רק להחזיק את המצביע ולהעביר אותו בחזרה בכל קריאה. pperl מייצג ידית כזו באותו אופן שMath::GMP מחזיק את מצביע ה־mpz_t שלו: כמספר שלם בתוך הפניה מבורכת.

Cairo::ImageSurface object  =  bless(\ (surface_ptr as integer), 'Cairo::ImageSurface')
Cairo::Context object       =  bless(\ (context_ptr as integer), 'Cairo::Context')

כשמתודה נקראת, הכריכה קוראת את המספר השלם בחזרה מתוך ההפניה המבורכת, ממירה אותו למצביע ה־C, וקוראת לפונקציה. הברכה נותנת לכם שיגור מתודות ($cr->stroke), הורשה (כל מחלקת משטח קונקרטית היא תת־מחלקה של Cairo::Surface), ובאופן מכריע, וו DESTROY. ההבדל היחיד מ־Math::GMP הוא שהדבר האטום הוא משטח ציור במקום מספר שלם גדול.

צעד 3: משך חיים קשור ל־DESTROY#

אובייקט גרפיקה של C חייב להשתחרר בדיוק פעם אחת. הדליפו אותו ותשרפו זיכרון בכל ציור; שחררו אותו פעמיים ותשחיתו את הערמה. הכריכה קושרת את חיי אובייקט ה־C לאלה של אובייקט ה־Perl: כשההפניה המבורכת יוצאת מתחום, Perl קורא ל־DESTROY, והכריכה קוראת ל־destructor התואם של C, תוך כיבוד ספירת ההפניות של cairo.

Cairo::Context DESTROY  ->  cairo_destroy(context_ptr)
Cairo::Surface DESTROY  ->  cairo_surface_destroy(surface_ptr)

Math::GMP עושה בדיוק זאת: ה־destructor שלו קורא ל־mpz_clear ומשחרר את ה־mpz_t. אתם, המשתמשים, לעולם אינכם קוראים לפונקציית שחרור; אובייקטי הציור מתנקים כשמשתני ה־Perl המחזיקים בהם נעלמים. הכריכה אוכפת את הסדר ש־cairo דורשת, ומשחררת הקשר לפני המשטח שהוא מצייר אליו.

צעד 4: המתודות#

כל מתודת Perl היא פונקציה מובנית קטנה הקוראת את הידית, קוראת את הארגומנטים הסקלריים, ומבצעת קריאת C אחת. Cairo::Context::move_to קוראת את מצביע ההקשר ושני doubles וקוראת ל־cairo_move_to; Cairo::Context::set_source_rgba קוראת את המצביע וארבעה doubles וקוראת ל־cairo_set_source_rgba; Cairo::Surface::write_to_png קוראת את המצביע ומחרוזת נתיב. המיפוי הוא מתודה אחת לקריאת C אחת:

Cairo::Context::move_to($cr, $x, $y)
    -> read context ptr from $cr; call cairo_move_to(ptr, x, y)

Cairo::Context::set_source_rgba($cr, $r, $g, $b, $a)
    -> call cairo_set_source_rgba(ptr, r, g, b, a)

Cairo::Surface::write_to_png($surface, $path)
    -> call cairo_surface_write_to_png(ptr, path)

חתימות רמת־ה־C חיות בכריכה ומיושבות שם פעם אחת. משתמש הקורא $cr->set_source_rgba(1, 0, 0, 0.5) לעולם אינו כותב חתימה ואינו יכול לטעות באחת. נקודת האמת היחידה הזו לקריאות ה־C היא השלישית משלוש הסיבות שכריכה אצורה מנצחת קריאות גולמיות מפוזרות: משך חיי אובייקט, ארגונומיה, והחתימה שנרשמה פעם אחת.

למה זה מגיע, ולמה לא#

עם ארבעת החלקים האלה (פתיחה פעם אחת, ידית כמספר שלם מבורך, DESTROY משחרר, מתודות מעל טבלת הפונקציות הפתורה) pperl מספק Cairo אמיתי ובטוח־זיכרון בלי הפצת CPAN מותקנת, בלי מהדר C, ובלי צעד בנייה. מודל הציור המלא של פרק הווקטור זמין, והאובייקטים מנהלים את עצמם.

מה שהתבנית אינה מגיעה אליו, מעצמה, הוא אותה חזית שהפרימר נקב בשמה. Cairo נמנעת מכולה ב־API הציור המרכזי שלה, וזו הסיבה שהיא ספריית הגרפיקה הראשונה המובאת בדרך זו. ספרייה המעבירה צבעים או נקודות לפי ערך כ־structs, שקוראת בחזרה אל הקוד שלכם, או שמוסרת לכם אירועים מטיפוס union זקוקה ליותר ממה שיש לתבנית הזו: המקרים של לפי־ערך ושל union ממתינים לשכבת תיאור־הנתונים, ו־callbacks זקוקים לתמיכת מצביע־פונקציה שנכנסת מחדש אל Perl. פרויקט הגולה של המשחק הדו־ממדי נתקל בדיוק בקיר הזה עם ספריות חלונות ואירועים, ואומר זאת בנקודה שבה זה קורה.

המסקנה#

ספריית גרפיקה שה־API של C שלה הוא סקלרים בתוספת ידיות אטומות יכולה להיות מובאת על pperl ככריכה מובנית אצורה, ו־Cairo היא הראשונה שנשלחה: פתחו את הספרייה פעם אחת, החזיקו כל אובייקט C כמספר שלם מבורך, שחררו אותו ב־DESTROY, וגבו כל מתודה עם קריאת ה־C שלה מיושבת במקום אחד. התוצאה היא ה־API של Cairo של מעלה־הזרם, רץ באופן מובנה. ספריות אחרות מאותה צורה בדרך. הפרק הבא מפסיק לתאר ומתחיל לבנות: יישום ציור על מחסנית ערכות הכלים המתוחזקת, המרכיב את כל המדריך למשהו שרץ.