מהן Callback Functions ב CTypes

14/11/2020

ספריית 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. הרצת התוכנית תגרום ל:

  1. יצירת שני Threads מתוך קוד C.

  2. כל Thread מגריל מספר ומתחיל לחכות מספר שניות לפי הערך שהוגרל.

  3. כש Thread כלשהו מסיים את העבודה הפונקציה המתאימה ב C תיקרא ותדפיס הודעה

  4. אותה פונקציה לאחר מכן תפעיל את הפונקציה print_done של פייתון שהעברנו כפרמטר (ומופיעה בקוד C בשם done ולאחר מכן בשם callback).

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