Μια εγγενής σύνδεση Cairo, που αποστέλλεται#
Το κεφάλαιο των vector σχεδίασε με το 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 και τα υπόλοιπα, με τις υπογραφές μεθόδων του upstream. Κώδικας γραμμένος για το Cairo του CPAN τρέχει αμετάβλητος· μεταφερμένες σουίτες δοκιμών Cairo περνούν επάνω του. Το πρόγραμμα από το κεφάλαιο των vector είναι το πρόγραμμα εδώ:
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: ο αδιαφανής χειριστής ως blessed ακέραιος#
Το Cairo επιστρέφει ένα cairo_surface_t* και ένα cairo_t*: δείκτες σε structs της C των οποίων τα εσωτερικά δεν αγγίζετε ποτέ. Η σύνδεση δεν χρειάζεται να κατανοεί τη διάταξή τους, μόνο να κρατά τον δείκτη και να τον επιστρέφει σε κάθε κλήση. Η pperl αναπαριστά έναν τέτοιον χειριστή με τον ίδιο τρόπο που το Math::GMP κρατά τον δείκτη του mpz_t: ως έναν ακέραιο μέσα σε μια blessed αναφορά.
Cairo::ImageSurface object = bless(\ (surface_ptr as integer), 'Cairo::ImageSurface')
Cairo::Context object = bless(\ (context_ptr as integer), 'Cairo::Context')
Όταν καλείται μια μέθοδος, η σύνδεση διαβάζει τον ακέραιο πίσω από τη blessed αναφορά, τον μετατρέπει στον δείκτη C, και καλεί τη συνάρτηση. Το bless σάς δίνει αποστολή μεθόδων ($cr->stroke), κληρονομικότητα (κάθε συγκεκριμένη κλάση επιφάνειας είναι υποκλάση Cairo::Surface) και, κρίσιμα, ένα hook DESTROY. Η μόνη διαφορά από το Math::GMP είναι ότι το αδιαφανές πράγμα είναι μια επιφάνεια σχεδίασης αντί για έναν μεγάλο ακέραιο.
Βήμα 3: η διάρκεια ζωής δεμένη στο DESTROY#
Ένα αντικείμενο γραφικών C πρέπει να αποδεσμεύεται ακριβώς μία φορά. Αν το διαρρεύσετε καίτε μνήμη σε κάθε σχεδίαση· αν το αποδεσμεύσετε δύο φορές διαφθείρετε τον σωρό. Η σύνδεση δένει τη ζωή του αντικειμένου C σε αυτήν του αντικειμένου της Perl: όταν η blessed αναφορά βγαίνει εκτός εμβέλειας, η Perl καλεί το DESTROY, και η σύνδεση καλεί τον αντίστοιχο καταστροφέα C, σεβόμενη την καταμέτρηση αναφορών του cairo.
Cairo::Context DESTROY -> cairo_destroy(context_ptr)
Cairo::Surface DESTROY -> cairo_surface_destroy(surface_ptr)
Το Math::GMP κάνει ακριβώς αυτό: ο καταστροφέας του καλεί το mpz_clear και αποδεσμεύει το mpz_t. Εσείς, ο χρήστης, δεν καλείτε ποτέ μια συνάρτηση αποδέσμευσης· τα αντικείμενα σχεδίασης καθαρίζονται όταν εξαφανίζονται οι μεταβλητές της Perl που τα κρατούν. Η σύνδεση επιβάλλει τη σειρά που απαιτεί το cairo, αποδεσμεύοντας ένα context πριν από την επιφάνεια στην οποία σχεδιάζει.
Βήμα 4: οι μέθοδοι#
Κάθε μέθοδος της Perl είναι μια μικρή εγγενής συνάρτηση που διαβάζει τον χειριστή, διαβάζει τα βαθμωτά ορίσματα, και κάνει μία κλήση C. Το Cairo::Context::move_to διαβάζει τον δείκτη context και δύο 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 είναι ο τρίτος από τους τρεις λόγους για τους οποίους μια επιμελημένη σύνδεση νικά τις διάσπαρτες ακατέργαστες κλήσεις: η διάρκεια ζωής του αντικειμένου, η εργονομία, και η υπογραφή γραμμένη μία φορά.
Τι προσεγγίζει αυτό, και τι όχι#
Με αυτά τα τέσσερα κομμάτια (άνοιγμα μία φορά, χειριστής ως blessed ακέραιος, το DESTROY αποδεσμεύει, μέθοδοι πάνω από τον επιλυμένο πίνακα συναρτήσεων) η pperl παραδίδει ένα πραγματικό, ασφαλές ως προς τη μνήμη Cairo χωρίς εγκατεστημένη διανομή CPAN, χωρίς μεταγλωττιστή C, και χωρίς βήμα κατασκευής. Ολόκληρο το μοντέλο σχεδίασης του κεφαλαίου των vector είναι διαθέσιμο, και τα αντικείμενα διαχειρίζονται τον εαυτό τους.
Αυτό που το πρότυπο δεν προσεγγίζει από μόνο του είναι το ίδιο σύνορο που ονόμασε ο εισαγωγικός οδηγός. Το Cairo αποφεύγει όλο αυτό στο πυρηνικό API σχεδίασής του, γι” αυτό είναι η πρώτη βιβλιοθήκη γραφικών που σηκώνεται με αυτόν τον τρόπο. Μια βιβλιοθήκη που περνά χρώματα ή σημεία κατά τιμή ως structs, που καλεί πίσω τον κώδικά σας, ή που σας δίνει συμβάντα τύπου union χρειάζεται περισσότερα από όσα έχει αυτό το πρότυπο: οι περιπτώσεις κατά τιμή και union περιμένουν το στρώμα περιγραφής δεδομένων, και τα callbacks χρειάζονται υποστήριξη δεικτών συναρτήσεων που ξαναμπαίνει στην Perl. Το ολοκληρωτικό έργο του 2D παιχνιδιού χτυπά ακριβώς σε αυτό το τείχος με τις βιβλιοθήκες παραθύρων και συμβάντων, και το λέει στο σημείο που το κάνει.
Το συμπέρασμα#
Μια βιβλιοθήκη γραφικών της οποίας το API της C είναι βαθμωτά συν αδιαφανείς χειριστές μπορεί να σηκωθεί στην pperl ως μια επιμελημένη εγγενής σύνδεση, και το Cairo είναι η πρώτη που αποστέλλεται: άνοιγμα της βιβλιοθήκης μία φορά, κράτημα κάθε αντικειμένου C ως blessed ακεραίου, αποδέσμευσή του στο DESTROY, και στήριξη κάθε μεθόδου με την κλήση C της ρυθμισμένη σε ένα σημείο. Το αποτέλεσμα είναι το upstream API του Cairo, να τρέχει εγγενώς. Άλλες βιβλιοθήκες του ίδιου σχήματος είναι καθ” οδόν. Το επόμενο κεφάλαιο σταματά να περιγράφει και αρχίζει να κατασκευάζει: μια εφαρμογή σχεδίασης πάνω στη συντηρούμενη στοίβα εργαλειοθηκών, συναρμολογώντας ολόκληρο τον οδηγό σε κάτι που τρέχει.