לדלג לתוכן

10.6 פרויקט סיכום פרויקט

פרויקט סיכום - מערכת פלאגינים - plugin system

הקדמה

בפרויקט הזה נבנה מערכת פלאגינים (plugins) בשפת C. הפרויקט מחבר את כל מה שלמדנו בפרק 10:

  • מצביעי פונקציות (פרק 10.5) - ממשק הפלאגין הוא struct עם מצביעי פונקציות
  • טעינה דינמית (פרק 5.10 + 10.5) - dlopen/dlsym לטעינת ספריות בזמן ריצה
  • פעולות אטומיות (פרק 10.4) - מונה סטטיסטיקות thread-safe
  • טריקים ביטיים (פרק 10.3) - דגלים למצב הפלאגין

הרעיון: תוכנית ראשית (host) שטוענת פלאגינים מתיקייה, מפעילה אותם על קלט, ומנהלת סטטיסטיקות. כל פלאגין הוא ספרייה משותפת (.so) שמממשת ממשק קבוע.

הפרויקט בנוי בשלבים - כל שלב מוסיף יכולת חדשה.


ממשק הפלאגין - plugin interface

כל פלאגין חייב לחשוף struct מסוג plugin_t דרך פונקציה בשם plugin_get_info:

/* plugin_api.h - הממשק המשותף בין ה-host לפלאגינים */
#ifndef PLUGIN_API_H
#define PLUGIN_API_H

#define PLUGIN_API_VERSION 1

typedef struct plugin {
    const char *name;           /* שם הפלאגין */
    const char *description;    /* תיאור קצר */
    int api_version;            /* גרסת API - חייב להיות PLUGIN_API_VERSION */

    int  (*init)(void);                     /* אתחול, מחזיר 0 בהצלחה */
    void (*cleanup)(void);                  /* ניקוי משאבים */
    char *(*process)(const char *input);    /* עיבוד קלט, מחזיר תוצאה */
} plugin_t;

/* כל פלאגין חייב לממש את הפונקציה הזו */
typedef plugin_t *(*plugin_get_info_fn)(void);

#endif

שמרו את הקובץ הזה כ-plugin_api.h. גם ה-host וגם הפלאגינים ישתמשו בו.


שלב 1 - כתיבת פלאגינים

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

פלאגין 1 - uppercase - המרה לאותיות גדולות:

/* plugin_upper.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "plugin_api.h"

static int upper_init(void) {
    printf("[upper] initialized\n");
    return 0;
}

static void upper_cleanup(void) {
    printf("[upper] cleaned up\n");
}

static char *upper_process(const char *input) {
    char *result = strdup(input);
    if (!result) return NULL;
    for (int i = 0; result[i]; i++)
        result[i] = toupper(result[i]);
    return result;
}

static plugin_t plugin = {
    .name = "uppercase",
    .description = "converts text to uppercase",
    .api_version = PLUGIN_API_VERSION,
    .init = upper_init,
    .cleanup = upper_cleanup,
    .process = upper_process,
};

plugin_t *plugin_get_info(void) {
    return &plugin;
}

פלאגין 2 - reverse - היפוך מחרוזת:

כתבו פלאגין שמקבל מחרוזת ומחזיר אותה הפוכה. ממשו את reverse_process באופן דומה.

פלאגין 3 - wordcount - ספירת מילים:

כתבו פלאגין שמקבל מחרוזת ומחזיר מחרוזת עם מספר המילים (למשל, "3 words").

קומפילציה של הפלאגינים:

gcc -shared -fPIC -o plugins/plugin_upper.so plugin_upper.c
gcc -shared -fPIC -o plugins/plugin_reverse.so plugin_reverse.c
gcc -shared -fPIC -o plugins/plugin_wordcount.so plugin_wordcount.c

שימו לב: צרו תיקיית plugins/ לפני הקומפילציה.


שלב 2 - טעינת פלאגינים

כתבו את התוכנית הראשית שסורקת תיקיית plugins/, מוצאת קבצי .so, וטוענת כל אחד עם dlopen.

הנה קוד התחלתי:

/* host.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <dirent.h>
#include "plugin_api.h"

#define MAX_PLUGINS 32

struct loaded_plugin {
    void *handle;       /* dlopen handle */
    plugin_t *info;     /* מידע על הפלאגין */
};

struct loaded_plugin plugins[MAX_PLUGINS];
int plugin_count = 0;

int load_plugin(const char *path) {
    void *handle = dlopen(path, RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlopen(%s): %s\n", path, dlerror());
        return -1;
    }

    dlerror(); /* ניקוי שגיאות */

    plugin_get_info_fn get_info = dlsym(handle, "plugin_get_info");
    if (!get_info) {
        fprintf(stderr, "dlsym: %s\n", dlerror());
        dlclose(handle);
        return -1;
    }

    plugin_t *info = get_info();
    if (info->api_version != PLUGIN_API_VERSION) {
        fprintf(stderr, "plugin %s: incompatible API version %d (expected %d)\n",
                path, info->api_version, PLUGIN_API_VERSION);
        dlclose(handle);
        return -1;
    }

    /* TODO: קראו ל-info->init() ובדקו את ערך ההחזרה */

    plugins[plugin_count].handle = handle;
    plugins[plugin_count].info = info;
    plugin_count++;

    printf("loaded plugin: %s (%s)\n", info->name, info->description);
    return 0;
}

int scan_plugins(const char *dir_path) {
    /* TODO: פתחו את התיקייה עם opendir,
       עברו על הקבצים עם readdir,
       טענו כל קובץ שמסתיים ב-.so */
    return 0;
}

מה צריך לממש:
1. השלימו את scan_plugins - סרקו תיקייה, מצאו קבצי .so, טענו כל אחד עם load_plugin
2. קראו ל-init() של כל פלאגין אחרי הטעינה
3. טפלו בשגיאות - פלאגין שנכשל באתחול לא צריך להישמר ברשימה

רמז: השתמשו ב-opendir, readdir, closedir מ-<dirent.h>. כדי לבדוק אם קובץ מסתיים ב-.so, השתמשו ב-strstr או strcmp על הסיומת.


שלב 3 - עיבוד קלט

הוסיפו לולאה ראשית ל-host שקוראת שורות מהמשתמש ומעבירה כל שורה לכל הפלאגינים:

void process_input(const char *input) {
    for (int i = 0; i < plugin_count; i++) {
        char *result = plugins[i].info->process(input);
        if (result) {
            printf("  [%s]: %s\n", plugins[i].info->name, result);
            free(result);
        }
    }
}

int main(int argc, char **argv) {
    const char *plugin_dir = (argc > 1) ? argv[1] : "./plugins";

    scan_plugins(plugin_dir);
    if (plugin_count == 0) {
        printf("no plugins loaded\n");
        return 1;
    }

    printf("\nready. enter text (or 'quit' to exit):\n");

    char input[1024];
    while (fgets(input, sizeof(input), stdin)) {
        /* הסרת newline */
        input[strcspn(input, "\n")] = '\0';

        if (strcmp(input, "quit") == 0)
            break;

        process_input(input);
    }

    /* ניקוי */
    for (int i = 0; i < plugin_count; i++) {
        plugins[i].info->cleanup();
        dlclose(plugins[i].handle);
    }

    return 0;
}

דוגמת הרצה:

loaded plugin: uppercase (converts text to uppercase)
loaded plugin: reverse (reverses text)
loaded plugin: wordcount (counts words)

ready. enter text (or 'quit' to exit):
hello world
  [uppercase]: HELLO WORLD
  [reverse]: dlrow olleh
  [wordcount]: 2 words
quit


שלב 4 - סטטיסטיקות עם atomics

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

#include <stdatomic.h>

struct loaded_plugin {
    void *handle;
    plugin_t *info;
    atomic_int call_count;       /* כמה פעמים process נקרא */
    atomic_long total_input_len; /* סך אורך הקלט שעובד */
};
  1. אתחלו את המונים ל-0 בזמן הטעינה.
  2. בכל קריאה ל-process, הגדילו את call_count ב-1 ואת total_input_len באורך הקלט.
  3. השתמשו ב-memory_order_relaxed (כי זה רק מונים).
  4. הוסיפו פקודה "stats" שמדפיסה את הסטטיסטיקות של כל פלאגין.

שלב 5 - עיבוד מרובה threads

שנו את process_input כך שכל פלאגין ירוץ ב-thread נפרד:

  1. לכל פלאגין, צרו thread שמריץ את process.
  2. חכו לכל הthreads עם pthread_join.
  3. הדפיסו את התוצאות.
#include <pthread.h>

struct process_args {
    struct loaded_plugin *plugin;
    const char *input;
    char *result;
};

void *process_thread(void *arg) {
    struct process_args *pa = arg;
    /* TODO: קראו ל-process, שמרו תוצאה,
       עדכנו מונים אטומיים */
    return NULL;
}

ודאו שהמונים האטומיים עובדים נכון גם כשכמה threads מעדכנים אותם בו-זמנית.


Makefile

הנה Makefile להמליץ על תהליך הבנייה:

CC = gcc
CFLAGS = -Wall -Wextra -g
LDFLAGS = -ldl -lpthread

# תוכנית ראשית
host: host.c plugin_api.h
    $(CC) $(CFLAGS) -o host host.c $(LDFLAGS)

# פלאגינים
plugins/plugin_upper.so: plugin_upper.c plugin_api.h
    mkdir -p plugins
    $(CC) $(CFLAGS) -shared -fPIC -o $@ $<

plugins/plugin_reverse.so: plugin_reverse.c plugin_api.h
    mkdir -p plugins
    $(CC) $(CFLAGS) -shared -fPIC -o $@ $<

plugins/plugin_wordcount.so: plugin_wordcount.c plugin_api.h
    mkdir -p plugins
    $(CC) $(CFLAGS) -shared -fPIC -o $@ $<

# בנייה של הכל
all: host plugins/plugin_upper.so plugins/plugin_reverse.so plugins/plugin_wordcount.so

clean:
    rm -f host plugins/*.so

.PHONY: all clean

שימוש:

make all
./host


רעיונות להרחבה

דגלי מצב עם ביטים:
הוסיפו שדה unsigned int flags ל-loaded_plugin עם דגלים כמו:
- PLUGIN_LOADED (1 << 0) - נטען בהצלחה
- PLUGIN_INITIALIZED (1 << 1) - אותחל
- PLUGIN_ENABLED (1 << 2) - מופעל
- PLUGIN_ERROR (1 << 3) - שגיאה

הוסיפו פקודות "enable"/"disable" שמדליקות/מכבות את דגל ENABLED, ועיבוד רק לפלאגינים שהדגל שלהם דלוק.

תיעדוף פלאגינים:
הוסיפו שדה priority ל-plugin_t. מיינו את הפלאגינים לפי עדיפות (עם qsort ופונקציית השוואה) והריצו אותם בסדר.

שרשור פלאגינים - pipeline:
במקום להריץ כל פלאגין על הקלט המקורי, העבירו את הפלט של פלאגין אחד כקלט לפלאגין הבא.

הוספת פלאגין בזמן ריצה:
הוסיפו פקודה "load" שמקבלת נתיב לso חדש וטוענת אותו תוך כדי ריצה.

בהצלחה!