יישום תכנות מונחה עצמים בשפת 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 משוכלל ומסובך לאין ערוך מזה המוצג פה, המציג את הרעיון הכללי מאחור.

בברכה,

יוסף אור

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *