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