יישום תכנות מונחה עצמים בשפת C – ‏GObject

ערב טוב.

לשם שינוי, לא אכתוב על שינויים ב־GNOME, על אף שיש רבים כאלה, אלא קצת על מה שנמצא מתחת למכסה המנוע בפיתוח GNOME.

יישומי GNOME וספריות התכנה שלה כתובים ברובם בשפת C, ומבוססים כולם על ספרייה בשם GLib, הכתובה גם היא בשפת C.

GLib מחולקת למעשה לשלוש ספריות:

  • GLib – ‏ API למטרות כלליות, כמו מבני נתונים שונים, טיפול ב־UTF-8, טיפול במחרוזות ועוד רשימה ארוכה של כלים
  • GIO – קלט פלט על גבי VFS, למעשה API נוח לשימוש מעל POSIX, או טיפול בתקשורת בין תהליכים (D-Bus וכו׳)
  • GObject – ‏מימוש תכנות מונחה עצמים בשפת C, מספק API מאוד נוח מסביב

ברשומה זו לא אגע ב־API המסופק על ידי GObject או במימוש המדויק בספרייה זו, אלא אביא את הרעיון הכללי.

מימוש תכנות מונחה עצמים בשפת C

שם המשחק הוא שימוש נכון במצביעים.

מצביע הוא משתנה המכיל מספר שלם המייצג כתובת בזיכרון. ככורח יוצא שגודל משתנה זה, גודלו של מצביע, תלוי בזיכרון הקיים (או: במערכת ההפעלה) ולא במשתנה אליו מתייחסת הכתובת. כלומר, מצביע למשתנה מסוג int גודלו כגודל מצביע למבנה זה או אחר כמו למשתנה מסוג double.

struct A
{
        int dum;
};

אין כאן שום דבר מעניין. רק הגדרתי מבנה עם משתנה דמה.

struct A *a;

a = malloc (sizeof (struct A) * 1);
a->dum = 0;

כאן כבר יש קוד מאוד מעניין, גם אם לא כך נראה ממבט ראשון.

a->dum = 0;

כאשר אני מנסה לאתחל את המשתנה dum מהמבנה A, המהדר מחשב מהכתובת המתקבלת (המצביע, המשתנה a) את המקום של המשתנה dum מתחילת המבנה (0, המשתנה מופיע בתחילת המבנה), משם מחשב את הגודל של המשתנה dum (גודל של משתנה int), וכך יודע עד לאיזו כתובת מופיע המשתנה dum. ובקיצור: המהדר מוצא את הכתובת המתחילה והכתובת המסיימת את המשתנה dum, ומאתחל את המקום לערך 0, כפי שהתבקש.

מה אני מנסה להדגיש בשורה פשוטה זו של קוד ? כיוון שהמשתנה dum מופיע בתחילת המבנה, ניתן להתייחס לכתובת של a ככתובת של dum.

*((int *) a) = 5;

בשורה זו ^ התייחסתי למשתנה a כאל מצביע למשתנה מסוג int (במקום למצביע למבנה A), והשמתי את הערך ל־5.

בדיקה קצרה (להלן קוד הניתן להידור) תראה שאכן הכל כאן נכון ו„חוקי”:

#include <stdlib.h>
#include <stdio.h>
 
struct A
{
        int dum;
};
 
int main (int argc, char **argv)
{
        struct A *a;

        a = malloc (sizeof (struct A) * 1);
        a->dum = 0;

        *((int *) a) = 5;

        printf ("%d\n", a->dum);

        free (a);

        return 0;
}

כעת אנסה לעשות משהו מועיל יותר מהקוד הנ״ל.

אגדיר מבנה נוסף, בשם B, בתחילתו אשים מופע של המבנה A, ורק לאחריו משתנה דמה נוסף.

ברור כעת כי למצביע למבנה B ניתן להתייחס גם כמצביע למבנה A, או אף למצביע למשתנה מסוג int.

להלן הקוד המשחק עם זה.

#include <stdlib.h>
#include <stdio.h>
 
struct A
{
        int dum;
};
 
struct B
{
        struct A a;
        int dum;
};
 
int main (int argc, char **argv)
{
        struct A *a;
        struct B *b;

        b = malloc (sizeof (struct B) * 1);
        b->a.dum = 1;
        b->dum = 2;

        a = (struct A *) b;

        printf ("b->dum: %d\n", b->dum);
        printf ("a->dum: %d\n", a->dum);
        printf ("a.dum: %d\n\n", *((int *) b));

        printf ("a-pointer: %p\n", a);
        printf ("b-pointer: %p\n", b);
        printf ("int-pointer: %p\n", (int *) b);

        free (b);

        return 0;
}

מכאן הדרך ליישום מלא של תכונת ההורשה, יחד עם הסתרת משתנים פרטיים, קצרה מאוד.

להלן קוד שכתבתי במיוחד לכך, מחולק לקבצים.

#ifndef ___A___
#define ___A___

typedef struct _A               A;
typedef struct _APrivate        APrivate;

struct _A
{
        APrivate *priv;
};

A *     a_new     ();
void    a_init    (A    *a);
void    a_free    (A    *a);

int     a_get_dum (A    *a);
void    a_set_dum (A    *a,
                   int   dum);

#endif
#ifndef ___B___
#define ___B___

#include "a.h"

typedef struct _B               B;
typedef struct _BPrivate        BPrivate;

struct _B
{
        A parent;
        BPrivate *priv;
};

B *     b_new     ();
void    b_init    (B    *b);
void    b_free    (B    *b);

int     b_get_dum (B    *b);
void    b_set_dum (B    *b,
                   int   dum);

#endif
#ifndef ___C___
#define ___C___

#include "b.h"

typedef struct _C               C;
typedef struct _CPrivate        CPrivate;

struct _C
{
        B parent;
        CPrivate *priv;
};

C *     c_new     ();
void    c_init    (C    *c);
void    c_free    (C    *c);

int     c_get_dum (C    *c);
void    c_set_dum (C    *c,
                   int   dum);

#endif

המימוש:

#include <stdlib.h>
#include <stdio.h>

#include "a.h"

struct _APrivate
{
        int dum;
};

A *
a_new ()
{
        A *a;

        a = malloc (sizeof (A) * 1);
        a_init (a);

        printf ("a_new\n");

        return a;
}

void
a_init (A *a)
{
        if (!a)
                return;

        a->priv = malloc (sizeof (APrivate) * 1);
        a->priv->dum = -1;

        printf ("a_init\n");
}

int
a_get_dum (A *a)
{
        return (a ? a->priv->dum : -1);
}

void
a_set_dum (A *a,
           int dum)
{
        if (a)
                a->priv->dum = dum;
}

void
a_free (A *a)
{
        if (!a)
                return;

        free (a->priv);

        printf ("a_free\n");
}
#include <stdlib.h>
#include <stdio.h>

#include "b.h"

struct _BPrivate
{
        int dum;
};

B *
b_new ()
{
        B *b;

        b = malloc (sizeof (B) * 1);
        b_init (b);

        printf ("b_new\n");

        return b;
}

void
b_init (B *b)
{
        if (!b)
                return;

        a_init (&(b->parent));
        b->priv = malloc (sizeof (BPrivate) * 1);
        b->priv->dum = -1;

        printf ("b_init\n");
}

int
b_get_dum (B *b)
{
        return (b ? b->priv->dum : -1);
}

void
b_set_dum (B *b,
           int dum)
{
        if (b)
                b->priv->dum = dum;
}

void
b_free (B *b)
{
        if (!b)
                return;

        a_free (&(b->parent));
        free (b->priv);

        printf ("b_free\n");
}
#include <stdlib.h>
#include <stdio.h>

#include "c.h"

struct _CPrivate
{
        int dum;
};

C *
c_new ()
{
        C *c;

        c = malloc (sizeof (C) * 1);
        c_init (c);

        printf ("c_new\n");

        return c;
}

void
c_init (C *c)
{
        if (!c)
                return;

        b_init (&(c->parent));
        c->priv = malloc (sizeof (CPrivate) * 1);
        c->priv->dum = -1;

        printf ("c_init\n");
}

int
c_get_dum (C *c)
{
        return (c ? c->priv->dum : -1);
}

void
c_set_dum (C *c,
           int dum)
{
        if (c)
                c->priv->dum = dum;
}

void
c_free (C *c)
{
        if (!c)
                return;

        b_free (&(c->parent));
        free (c->priv);

        printf ("c_free\n");
}

וכן תכנית קצרה העושה שימוש בקוד הנ״ל:

#include <stdlib.h>
#include <stdio.h>

#include "c.h"

int main (int argc, char **argv)
{
        C *c;
        B *b;
        A *a;

        c = c_new ();
        c_set_dum (c, 15);

        b = (B *) c;
        b_set_dum (b, 10);

        a = (A *) b;
        a_set_dum (a, 5);

        printf ("c: %d\n", c_get_dum (c));
        printf ("b: %d\n", b_get_dum (b));
        printf ("a: %d\n", a_get_dum (a));

        c_free (c);
        free (c);
        
        return 0;
}

נראה לי שההסבר למעלה מסביר מה בדיוק עושה הקוד (אם כי הוא כתוב בצורה רצינית ומסודרת יותר :-)).

את הקוד ניתן להדר באמצעות מהדרים שונים, אך אני תמיד מעדיף להדר עם valac, זה פשוט חוסך העברה של דגלים מיותרים:

valac main.c a.c b.c c.c && ./main

ברשומה הבאה אכתוב על מימוש ניהול זיכרון בשפת C, בשיטה פשוטה של ספירת הפניות, ובזו שאחריה אכתוב על השימוש ב־API של GObject.
אציין שהמימוש ב־GLib/GObject משוכלל ומסובך לאין ערוך מזה המוצג פה, המציג את הרעיון הכללי מאחור.

בברכה,

יוסף אור

GNOME Developer Experience Hackfest, 2014

בוקר טוב.

בבוקר יום שלישי האחרון טסתי לברלין, גרמניה, להאקתון פיתוח של GNOME, ‏Developer Experience, ‏2014.

זו לי הפעם הראשונה מחוץ לישראל, כמו גם הטיסה הראשונה בכלל.

דבר אחד על ברלין – יש שם תחבורה ציבורית מדהימה, חלומית בקנה מידה של מה שיש לנו כאן.

ההאקתון עצמו נועד לימים רביעי, חמישי ושישי, ואילו אני הגעתי ביום שלישי, מה שהתברר כנכון (לקח לי זמן למצוא את המקום), וכן חזרתי ביום שישי בבוקר ובכך הפסדתי את היום האחרון (לא רציתי לשהות בשבת בחו״ל, כל שכן בברלין).

בשבילי הימים הללו היו בעיקר כדי לפגוש את האנשים. לעבור מכינויים ב־IRC לפרצופים ולאנשים. לראות שאותם אנשים קיימים גם במציאות, לדבר ולהכיר.

ואמנם ביום הראשון כלל לא כתבתי קוד. במחשב ה־Chromepixel (אחד משישה שנתרמו לקרן GNOME, כדי לאפשר פיתוח תמיכה במסך מגע ובהבחנה גבוהה) שעמנואלה בשיא הביא לי לשימוש בכנס, הייתה מותקנת מערכת Chrome OS (מערכת שנראית מגניבה ממבט ראשון, אך חסרת כל שימושיות לאחד דקה או שניים – מערכת שהיא כולה דפדפן), מה שהביא אותי להתקין את פדורה 20, דבר שארך זמן כיוון שקצת מסובך להפעיל תמונה חיה במחשבים אלה. לאחר ששדרגתי ל־Rawhide המערכת נשברה, מה שגרר בזבוז נוסף של זמן בניסיון לתיקון המערכת.

ביום השני התקנתי מחדש את פדורה 20, ולאחר מכן התקנתי את GNOME 3.12 מהמאגרים של ריצ׳רד, במקום לשדרג ל־Rawhide. כך יכולתי לכתוב קוד ויחד עם זאת להשתמש בסביבה לא מיושנת מדי.

יצא שכתבתי טלאי אחד (קטן ופשוט) והכנסתי טלאי אחר ל־Git. כלומר לא באמת כתבתי קוד.

למעשה, בעיקר דיברתי עם אנשים, היו דיונים על עיצובים, מצאתי המון באגים יחד עם ג׳ון (חלקם קשורים למסך מגע ותמיכה בהבחנה גבוהה), וכן דיברתי עם אלן על הבעיות בעיצוב הנוכחי של הצגת פריטים ביישומים השונים, כאשר מוצגים יותר מדי פריטים, מה גם שלחיצה על „טעינת נוספים” או גלילה מטה אלו דרכים לא נוחות במיוחד להצגת פריטים נוספים וכו׳. הצעתי גישה אחרת של דפים כרעיון כללי.

גם דיברתי עם ג׳ון וג'ייקוב על עיצוב לניהול לשוניות (בדפדפן GNOME), אך זה האחרון לא מעוניין שננסה לנהל לשוניות, זה לא יעבוד הוא אומר.

דבר מעניין נוסף הוא ניהול הורדות בדפדפן GNOME. ג׳ון טוען כי התיקייה „הורדות” צריכה להיעלם, היא פשוט עיצוב שגוי. הרעיון העיצובי (שקיים כבר זמן רב) הוא שהמשתמש יראה תצוגה מקדימה של מסמך, ואם ירצה יוכל לשמור אותו איפה שירצה, בצורה מסודרת. אני בהחלט מסכים אתו – תיקיית ההורדות שלי מכילה אוסף של קבצים שהורדתי ומיד שכחתי מהם – אני לא יודע מה איפה וכו׳. לאחר שאני מוריד קובץ וגם רוצה שהוא ישמר לי, אני טורח להעביר אותו מהתיקייה „הורדות” למקום מסודר. כלומר בשבילי תיקייה זו כתיקיית „אשפה”.

מעבר לזה היו כמובן דיונים נוספים, הרבה שכתבתו קוד, אך על אלה ניתן לקרוא ב־Planet GNOME :-)

בסוף היום השני, כשניגשתי להחזיר את ה־Chromepixel לעמנואלה, אלן הציע לי לקחת את המחשב איתי ולהחזיר אותו ב־GUADEC, אם אגיע, כיוון שעמנואלה לא משתמש בו. משום מה עניתי „כן”, לא לפני שווידאתי שאכן אף אחד אחר לא צריך את המחשב.

ביום שישי נסעתי לשדה התעופה, וכך מספר שעות לפני שבת נחתתי בישראל. נורא נהניתי מהתזמון בו נחתתי – מספר ימים לפני יום העצמאות, כך שהכל מקושט בדגלי ישראל :-)

כך ישראל נראית מהמטוס:

אסיים בתודה לקרן GNOME, שמממנת את הטיסה שלי ושהותי במלון, כמו של חברים נוספים שהגיעו להאקתון זה. גם ה־Chromepixel שהשתמשתי בו במהלך הכנס, ושלקחתי איתי, שייך לקרן GNOME.

בברכה,

יוסף אור
sponsored-badge-simple