כללים לביצוע binding

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

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

typedef int card32

אומר שהגדרנו כרגע טיפוס בשם card32 והוא בעצם מסוג int.

אפשר לראות את זה גם בהגדרות משתנים:

int age

אומר שהגדרנו משתנה בשם age שהוא מסוג int. ההפוך הזה הוא הפוך מבחינה לוגית של חשיבה אנושית בכל שפה אנושית שאני מכיר. כי בד"כ אנחנו אומרים "גיל כמספר שלם" או "age as an integer" ואנחנו לא אומרים "מספר שלם גיל" או "integer age", כך שצריך להבין את הלוגיקה של C ועד כמה התפיסה בה שונה מבחינת תפיסת החשיבה של שפות בני אדם א איך אנחנו בכלל קוראים בכלל את הקוד.

אם אתם עובדים עם FPC, תשתמשו ביחידה ctypes. היחידה מחזיקה בתוכה טיפוסים זהים לטיפוסים של C באותו כמות הביטים, וכך במקום שתנסו לזכור לבד מה הביט של כל דבר, תוכלו להשתמש במשהו שכבר מוגדר.

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

void say_hi(void)

התרגום יהיה כזה:

procedure say_hi;

ה void בסוגריים אומר שאין פרמטרים, ובפסקל כאשר אין פרמטרים, פשוט לא שמים סוגריים …

כאשר יש שימוש ב void עם כוכבית:

void * bla

אז התרגום הוא לטיפוס בשם Pointer. היות ו * void למצביע שרק שומר את המיקום, אבל ללא טיפוס מיוחד:

bla : pointer

כאשר זה מופיע בcallback מדובר בכתובת של פונקציית ה callback והטיפול בו מעט שונה (למטה אני מציג איך זה מופיע).

כאשר יש הגדרה של struct אנחנו נתרגם אתה ל record. חשוב אבל לדאוג להגיד למהדר שהרשומות הן רשומות C בצורה שהוא דוחס אותן :

struct type_name {
  int    a;
  double angle;
  char   arr[31];
}

יתורגם ל:

{$packrecords c} // once in a unit is more then enough
...
type
  type_name = record
    a     : cint;
    angle : cdouble;
    arr   : array[0..31] of char; // or byte, if it's pure number rather then characters
  end;

וכאשר יש שימוש ב union (מתוך הגדרה של a.out.h) :

struct nlist
{
  union
  {
    char *n_name;
    struct nlist *n_next;
    long n_strx;
  } n_un;
  unsigned char n_type;
  char n_other;
  short n_desc;
  unsigned long n_value;
};

יהיה תרגום בצורה הבאה:

type
  pnlist = ^nlist; // create a new type of pointer of nlist
  nlist = record
    n_un : record
      case clong of // union ...
        0 : ( n_name : PChar );
        1 : ( n_next : pnlist );
        2 : ( n_strx : clong );
      end;
    n_type : byte;
    n_other : char;
    n_desc : cshort;
    n_value : culong;
  end;

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

במידה ומדובר ב callback למשל, כאן יש שוני משמעותי. דבר ראשון אי אפשר להגדיר call back בפונקציה בפסקל. כמו עם מצביע, צריך להגדיר טיפוס שהוא callback. אבל גם ההגדרה של callback שונה לגמרי ממה שרגילים

ב C אנחנו נראה ש callback מוגדר בצורה הבאה:

void list_values (int (*getNextValue)(void))

בפסקל לעומת זאת אנחנו נגדיר אותו בצורה הבאה:

type
  getNextValue = function : cint; cdecl;
procedure list_values(next_value : getNextValue); cdecl;

חשוב לדעת ש callback ב c אפשר להגדיר גם ב typedef, אבל זה נראה גם לא ברור באותה מידה (ההגדרה זהה לחלוטין רק קיימת ב typedef), ונדיר מאוד למצוא את זה בפרוייקטים (לפחות אלו שאני עשיתי להם bind). כמו כן, השתמשתי באפשרות להגיד גם ל callback וגם לפרוצדורה מה סדר הפרמטרים שלה. במקרה הזה השתמשתי ב cdecl, אשר נמצא בשימוש בד"כ במהדרי unix והלינוקס השונים. gcc גם בWindows עובד בברירת מהחדל עם סדר פרמטרים זה (אלא עם הוגדר במפורש משהו שונה).

כאשר יש שימוש ב va_list או בשלוש נקודת:

int printf(const char *format, …);

אז אנחנו נגדיר את הפונקציה בפסקל בצורה הבאה:

function printf(const format : PChar, params : array of const) : cint; cdecl;

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

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

כאשר אנחנו רואים קוד של enum, אנחנו נעשה 2 דברים:

  1. ניצור טיפוס בשם של הenum והוא יהיה כנראה cint (אלא אם יש צורך במספר גדול יותר, או קטן יותר, או דורש סימן – כל זה תלוי ב enum עצמו)
  2. את הערכים נשים תחת const.

הסיבה שנשים את הערכים ב const היא בגלל שב C אין באמת enum. והוא בכלל לא זהה לצורה שפסקל עושה מתנהגת עם enum. בפסקל  enum הוא מניה אמיתית, ואפשר לעבור על כל איבר ואיבר בלולאה ולהוסיף עוד איבר ל enum בלי לשנות את הלולאה ועדיין נדע את האורך המלא ללא טריק מיוחד, בעוד שב C אנחנו חייבים לדעת את הערך האחרון. במידה והערך של האיבר הראשון קיבל אינדקס שונה מ0, אז צריך לדעת בC גם מה האיבר הראשון.

כאשר יש לנו הגדרה של קבועים בC עם #define (דיברתי על מאקרו מקודם), אז נתרגם גם אותם ל const.

הנה כמה סוגי קבועים, והתרגום שלהם:

#define ONE 1
#define SHIFT_LEFT 10 << 1
#define SHIFT_RIGHT 10 >> 1
#define BITWISE_OR 10 | SHIFT_LEFT
#define BITWISE_AND 10 & SHIFT_RIGHT
#define LONG_NUMBER 2L
#define HEXA 0xFF

בפסקל זה יראה ככה:

const
  ONE         = 1;
  SHIFT_LEFT  = 10 shl 1;
  SHIFT_RIGHT = 10 shr 1;
  BITWISE_OR  = 10 or  SHIFT_LEFT;
  BITWISE_AND = 10 and SHIFT_RIGHT;
  LONG_NUMBER = clong(2);
  HEXA        = $FF;

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

(* 1 *) function get_voices(const language : cint) : PPChar; cdecl; external 'library_name';
(* 2 *) {$LINKLIB c} // usually in the head of the unit
...
function strlen (P : pchar) : cint; cdecl; external;

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

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

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

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

כאשר נרצה להשתמש בשם אחר אשר קיים בספרייה, נוכל לעשות גם את זה בצורה הבאה:

function slen (P : pchar) : cint; cdecl; external 'c' name 'strlen';

אינני יודע אם זה המצב בעולם היוניקס, אבל ב Windows לכל פונקציה יש גם סדר בו היא מופיעה בספרייה המקושרת. במידה ואנחנו רוצים לציין במפורש לקחת את פונקציה מספר 18 (למשל) נעשה את זה במקום name 'strlen' את המילה השמורה index ואחריו המספר 18:

function slen (P : pchar) : cint; cdecl; external 'c' index 18;

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

7 מחשבות על “כללים לביצוע binding

  1. a

    כשהתחלת לדבר על מצביעים, ובמיוחג על מצביעים לפונקציות, נזכרתי למה אני אוהב את C בכלל, ואת הגישה שלה בפרט…

  2. a

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

  3. ארתיום

    האמת,

    a – אל תגזים, התחביר של הגדרת טיפוס או משתנה ב־C לא מי יודע מה מוצלח, כי כדי לפענח אותו אתה צריך להבין בתחביר פורמלי ואפילו בספר של K&R מופיעה "אלגורמתם" כיצד להבין הגדרה.

    לא שאני אוהב את התחביר של Pascal, אבל להגיד שתחביר של C (ואני אוהב C) מוצלח זה לא נכון.

  4. ik_5 מאת

    a – בפסקל אתה לא צריך להגדיר תמיד טיפוס מיוחד למצביע. למשל ברשומה או בהגדרת משתנה var רגילה ועוד הרבה מקומות למינהם לא צריך להגדיר במיוחד.

    Pointer זה מצביע שכל התפקיד שלו זה לשמור כתובת והוא זהה לחלוטין ל *void. כלומר איפה שיש לך את *void כמעט תמיד בטוח שזה יתורגם ל Pointer (אלא אם מדובר ב callback ואז אתה יודע שזה הכתובת של ה callback שאתה לא צריך להעביר אותה בהגדרת ה callback).

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

    כל מה שהראתי בסופו של דבר כאן זה שמאוד לא מסובך להגדיר מקרים גלובליים בין השפות. אבל יש מקרים בהם זה מאוד מסובך לא בגלל מחסור ביכולות, אלא בגלל חשיבה לגמרי שונה שדורשת מאמץ בשביל לקשר בין 2 החשיבות.

    דא" עכשיו שמתי לב שיש לי bug בהגדרת union שכתבתי בפסקל ואף אחד לא שם לב וזה חסר לי שם end של ה record המקונן.

  5. פינגבק: דלפות זיכרון בשפות דינמיות « לראות שונה

  6. פינגבק: תגיד משהו השתנה מאז ? « לראות שונה

כתיבת תגובה

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

הלוגו של WordPress.com

אתה מגיב באמצעות חשבון WordPress.com שלך. לצאת מהמערכת / לשנות )

תמונת Twitter

אתה מגיב באמצעות חשבון Twitter שלך. לצאת מהמערכת / לשנות )

תמונת Facebook

אתה מגיב באמצעות חשבון Facebook שלך. לצאת מהמערכת / לשנות )

תמונת גוגל פלוס

אתה מגיב באמצעות חשבון Google+ שלך. לצאת מהמערכת / לשנות )

מתחבר ל-%s