ספריית ctypes של פייתון מאפשרת לנו לטעון כל DLL ולהפעיל כל פונקציה ממנו בצורה שקופה מתוך תוכנית פייתון. הרבה ספריות C עובדות בצורה פשוטה: אנחנו קוראים לפונקציה, C מחשב משהו ומחזיר תוצאה. לדוגמה התוכנית הבאה משתמשת ב libm כדי להעלות את המספר 2 בחזקת 3 ומדפיסה את התוצאה 8:
from ctypes import *
from ctypes.util import find_library
libm = CDLL(find_library("m"))
libm.pow.restype = c_double
print(libm.pow(c_double(2), c_double(3)))
זה היה כיף, אבל לפעמים יהיו לנו קטעי קוד יותר מורכבים בשפת C, קטעי קוד שיקחו הרבה זמן ושצריכים לדווח על התקדמות במהלך העבודה שלהם, או קטעי קוד שצריכים עוד מידע ב Python לפני שיוכלו להחזיר תוצאה.
במצבים כאלה היינו רוצים להעביר כחלק מהפרמטרים לפונקציית C גם דרך בה אותו קוד C יוכל להפעיל קטע מסוים מקוד ה Python, וכך אפילו לפני שהפונקציה חוזרת עם תוצאה היא תוכל להפעיל מדי פעם פונקציות פייתון שלנו ולדווח להן על התקדמות או לשאול שאלות.
נתבונן בקוד הבא בשפת C:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
void *print_message_function( void *ptr );
int call_some_threads(printer_t done)
{
time_t t;
srand((unsigned) time(&t));
pthread_t thread1, thread2;
int iret1, iret2;
iret1 = pthread_create( &thread1, NULL, print_message_function, (void*) done);
iret2 = pthread_create( &thread2, NULL, print_message_function, (void*) done);
pthread_join( thread1, NULL);
pthread_join( thread2, NULL);
printf("Thread 1 returns: %d\n",iret1);
printf("Thread 2 returns: %d\n",iret2);
return 0;
}
void *print_message_function( void *ptr )
{
int waittime = rand() % 10;
sleep(waittime);
printf("[%d]\n", waittime);
printer_t callback = (printer_t) ptr;
callback(waittime);
return NULL;
}
הקוד מפעיל שני Threads (כל אחד מהם יחכה מספר שניות אקראי בין 0 ל 9) ומדפיס למסך כמה זמן הוא חיכה. אבל, ופה הטריק, כל Thread כזה מקבל גם מצביע לפונקציה. אנחנו לא יודעים מה תהיה הפונקציה - זו מתקבלת כפרמטר done לפונקציה call_some_threads
- אבל בסוף הפונקציה print_message_function
ואחרי שהדפסנו הודעת דיווח על התקדמות משפת C אנחנו מפעילים את אותה פונקציה שקיבלנו כפרמטר.
אנחנו קוראים לפונקציה שהתקבלה לפרמטר done בשם Callback Function בגלל שזוהי פונקציה באמצעותה קוד C קורא בחזרה לקוד פייתון שהפעיל אותו.
אחרי שאקמפל את קובץ ה C ואהפוך אותו לספריה דינמית אוכל להפעיל את הקוד הבא מ Python כדי להעביר פונקציית Python בתור ערך לפרמטר done:
from ctypes import *
mylib = cdll.LoadLibrary("./libmydemo.so")
@CFUNCTYPE(None, c_int)
def print_done(n):
print(f"[Python] waited {n} seconds")
mylib.call_some_threads(print_done)
הדקורטור @CFUNCTYPE
אומר ל ctypes שיש פה פונקציה שאפשר להעביר ל C, שהיא מחזירה None ומקבלת פרמטר יחיד מסוג int. זה מספיק בשביל לשלוח אותה בתור פרמטר לפונקציה call_some_threads
שהוגדרה בקוד ה C. הרצת התוכנית תגרום ל:
יצירת שני Threads מתוך קוד C.
כל Thread מגריל מספר ומתחיל לחכות מספר שניות לפי הערך שהוגרל.
כש Thread כלשהו מסיים את העבודה הפונקציה המתאימה ב C תיקרא ותדפיס הודעה
אותה פונקציה לאחר מכן תפעיל את הפונקציה print_done
של פייתון שהעברנו כפרמטר (ומופיעה בקוד C בשם done ולאחר מכן בשם callback).
בעולם האמיתי אנחנו נראה הרבה פעמים קוד C שיוצא למשימה ארוכה ומתייעץ עם פייתון או מדווח לו על התקדמות, כאשר כל התקשורת נעשית במנגנון זה.