সরাসরি পর্বে যান
পর্ব ২৮ · ৫৬-এর মধ্যে · মডিউল ৪

ফাংশন পয়েন্টার ও callback — ফাংশনের ঠিকানা দিয়ে কাজ পাঠানো

Function Pointers & Callbacks — Passing Behaviour as Data

Taught in Bangla, this lesson covers function pointers in C: reading the declaration syntax, qsort with comparator functions and their pitfalls, dispatch tables, callbacks, interface-style polymorphism in C, and the safety risks of bad function pointers.

এই পর্বে যা শিখবেন

  • ফাংশনের ঠিকানা কোথা থেকে আসে, আর সেটি একটি ভ্যারিয়েবলে কীভাবে রাখা যায়
  • ভয়ের মতো দেখতে ঘোষণা int (*cmp)(const void *, const void *) ধাপে ধাপে পড়া, আর typedef দিয়ে সহজ করা
  • qsort-কে নিজের নিয়ম দিয়ে সাজাতে বলা — GPA অনুযায়ী, সমান হলে নাম অনুযায়ী — আর comparator-এর লুকানো ফাঁদ
  • Dispatch table, callback আর “struct-ভর্তি ফাংশন পয়েন্টার” দিয়ে C-তে polymorphism
  • Function pointer কেন হ্যাকারদের প্রিয় নিশানা, আর আধুনিক সিস্টেম কীভাবে রক্ষা করে

১ · ফাংশনেরও একটি ঠিকানা আছে

পর্ব ১৭-এ দেখেছেন, প্রতিটি ভ্যারিয়েবল মেমরির কোনো একটি ঘরে থাকে, আর & দিয়ে সেই ঘরের ঠিকানা পাওয়া যায়। এবার একটি প্রশ্ন ভাবুন — আপনার লেখা ফাংশনগুলো কোথায় থাকে?

উত্তরটি পর্ব ২০-এর memory map-এ ছিল। প্রোগ্রাম চালানোর সময় operating system আপনার compile করা machine code মেমরির একটি অংশে বসায় — তার নাম text segment. প্রতিটি ফাংশন সেখানে একটি নির্দিষ্ট জায়গা থেকে শুরু হয়। অর্থাৎ ডেটার যেমন ঠিকানা আছে, কোডেরও তেমনি ঠিকানা আছে।

আপনি যখন add(2, 3) লেখেন, CPU আসলে করে: “add-এর ঠিকানায় লাফ দাও, কাজ শেষে ফিরে এসো।” সাধারণ ফাংশন কলে এই ঠিকানা compile বা link-এর সময়ই ঠিক হয়ে যায়। কিন্তু যদি ঠিকানাটি একটি ভ্যারিয়েবলে রাখা যায়? তাহলে কোন ফাংশন চলবে, সেটি প্রোগ্রাম চলার সময় বদলানো যাবে। এই ভ্যারিয়েবলের নামই function pointer.

ডেটার ঠিকানা আর কোডের ঠিকানা

A data pointer points at a value; a function pointer points at code

  1. Data pointerint *p = &marks;একটি মানের ঘর দেখায়
  2. Function pointerint (*f)(int, int) = add;কোডের শুরু দেখায়
  3. ডাকাf(2, 3)যে ফাংশনই রাখা থাকুক, সেটি চলে
ছবি ২৮.১ — function pointer একটি মান নয়, একটি “কাজ” দেখিয়ে দেয়।

অনলাইনে একটি জিনিস অর্ডার করলেন। কুরিয়ার কোম্পানি জানতে চাইল, “পার্সেল পৌঁছালে কাকে ফোন করব?” আপনি বাড়ির কেয়ারটেকারের নম্বর দিলেন। কোম্পানি এখনই ফোন করছে না — নম্বরটি খাতায় লিখে রাখল। তিন দিন পরে পার্সেল এলে তারা সেই নম্বরে ফোন করবে। কেয়ারটেকার কে, তিনি কী করবেন — কোম্পানির জানার দরকার নেই। তাদের কাছে শুধু একটি নম্বর আছে।

Function pointer হলো ঠিক সেই ফোন নম্বর. আর নম্বরটি আগে জমা দিয়ে রাখা, যাতে পরে প্রয়োজনমতো ডাকা হয় — এর নাম callback (“call back” = পরে ফিরতি ফোন)।

C-তে ফাংশনের নাম লিখে বন্ধনী না দিলে সেটি নিজেই ফাংশনের ঠিকানা হয়ে যায় — যেমন array-র নাম তার প্রথম ঘরের ঠিকানা হয়ে যায় (পর্ব ১৮). তাই f = add; আর f = &add; একই কথা। ডাকার সময়ও f(2, 3) আর (*f)(2, 3) একই কাজ করে; আজকাল প্রথমটিই বেশি লেখা হয়।

মূল ধারণা

সাধারণ ফাংশন কল: “এই কাজটি করো।” Function pointer দিয়ে কল: “এই খাতায় যে কাজের নাম লেখা আছে, সেটি করো।” দ্বিতীয়টিতে কোন কাজ হবে, তা ঠিক হয় প্রোগ্রাম চলার সময় — একে বলে indirect call. এই ছোট্ট নমনীয়তা থেকেই sorting library, মেনু, GUI, driver আর plugin — সবকিছুর নকশা জন্ম নেয়।

২ · ভয় পাওয়ানো syntax — ভেতর থেকে বাইরে পড়ুন

Function pointer-এর ধারণা সহজ, কিন্তু লেখার ধরন প্রথম দেখায় ভয় পাওয়ায়। qsort-এর নিয়মের ফাংশনের জন্য যে ঘোষণা লাগে, সেটি দেখতে এমন:

int (*cmp)(const void *, const void *);

এটি পড়ার একটি সহজ কৌশল আছে: নাম থেকে শুরু করুন, তারপর ভেতর থেকে বাইরে আসুন। বন্ধনী যেখানে আছে, আগে সেটির ভেতরটা পড়ুন। নিচের ডায়াগ্রামে “পরে” চেপে দেখুন কোন অংশ কোন ক্রমে পড়তে হয়:

int (* cmp ) (const void *, const void *) ;
  1. নাম দিয়ে শুরু — cmp. যা-ই হোক, এই জিনিসটির নাম cmp. প্রতিটি ঘোষণা পড়া শুরু হয় নাম থেকে।
  2. (*cmp) — “cmp একটি pointer”. বন্ধনীর ভেতরে নামের বাঁ পাশে *, তাই cmp একটি ঠিকানা রাখে। এই বন্ধনী খুব জরুরি — পরের ধাপে কারণ দেখবেন।
  3. ডান পাশের (const void *, const void *) — “একটি ফাংশনের pointer”. বন্ধনী থেকে বেরিয়ে ডানে তাকালে parameter-এর তালিকা। অর্থাৎ cmp যে জিনিসের ঠিকানা রাখে, সেটি একটি ফাংশন — যে দুটি void pointer নেয়।
  4. বাঁ পাশের int — “যে ফাংশন int ফেরত দেয়”. সবশেষে বাঁয়ে তাকান: সেই ফাংশন একটি int ফেরত দেয়।
  5. পুরো বাক্য: “cmp একটি pointer, যা এমন একটি ফাংশনকে দেখায় যেটি দুটি const void * নেয় এবং int ফেরত দেয়।” এবার বন্ধনী মুছে দেখুন: int *cmp(const void *, const void *); — এর মানে সম্পূর্ণ আলাদা! এটি একটি ফাংশনের ঘোষণা, যে int * ফেরত দেয়। দুটি বন্ধনীর জন্যই অর্থ পুরো বদলে যায়।

typedef — জটিল টাইপকে একটি সহজ নাম দেওয়া

পর্ব ২২-এ typedef দিয়ে struct-কে ছোট নাম দিয়েছিলেন। Function pointer-এর ক্ষেত্রে typedef আরও বেশি কাজের, কারণ একই লম্বা টাইপ বারবার লিখতে হয়। পেশাদার কোডে প্রায় সবসময় এভাবেই লেখা হয়:

কী বোঝাতে চাইtypedef ছাড়াtypedef দিয়ে
টাইপের একটি নাম—typedef int (*Compare)(const void *, const void *);
একটি ভ্যারিয়েবলint (*cmp)(const void *, const void *);Compare cmp;
ফাংশনের parametervoid sort_all(int (*cmp)(const void *, const void *));void sort_all(Compare cmp);
পাঁচটি ফাংশনের arrayvoid (*menu[5])(long *);Action menu[5];

শেষ সারির typedef-ছাড়া রূপটি দেখুন — void (*menu[5])(long *). এটি পড়তে অভিজ্ঞ প্রোগ্রামারেরও এক মুহূর্ত লাগে। তাই নিয়ম সহজ: function pointer যেখানে একবারের বেশি লাগবে, সেখানে আগে typedef দিন। কোড ছোট হয়, ভুল কমে, আর পাঠক এক নজরে বোঝেন “এটি একটি কাজের ঠিকানা”.

৩ · qsort — সাজানোর কৌশল library-র, নিয়ম আপনার

ধরুন একটি কলেজের ভর্তি তালিকা বানাতে হবে। ছাত্রদের সাজাতে হবে GPA অনুযায়ী — বেশি GPA আগে। দুজনের GPA সমান হলে নামের বর্ণানুক্রমে। Sorting-এর algorithm নিজে লেখা ঝামেলার আর ভুলের ঝুঁকিও বেশি। C-এর standard library-তে আগে থেকেই একটি ফাংশন আছে — qsort.

কিন্তু একটা সমস্যা আছে। qsort লেখা হয়েছে কয়েক দশক আগে। সে আপনার Student struct চেনে না, GPA কী তা জানে না, “বেশি আগে” না “কম আগে” — তাও জানে না। তাহলে সে সাজাবে কীভাবে?

সমাধানটি সুন্দর: সাজানোর কৌশল (কোনটা কোথায় সরাতে হবে) qsort জানে; তুলনার নিয়ম (দুজনের মধ্যে কে আগে) আপনি একটি ফাংশন হিসেবে দেন। এই নিয়মের ফাংশনকে বলে comparator. qsort-এর ঘোষণা দেখুন:

void qsort(void *base, size_t count, size_t size,
           int (*compar)(const void *, const void *));

চারটি জিনিস লাগে: array কোথা থেকে শুরু, কয়টি উপাদান, প্রতিটি কত byte, আর তুলনার ফাংশনের ঠিকানা। qsort কাজের মাঝে বারবার আপনার ফাংশন ডাকে, প্রতিবার দুটি উপাদানের ঠিকানা দিয়ে। আপনার ফাংশন যা ফেরত দেয়, তা দেখে qsort সিদ্ধান্ত নেয়:

comparator(a, b) যা ফেরত দেয়qsort যা বোঝে
ঋণাত্মক সংখ্যা (যেমন -1)a আগে বসবে, b পরে
শূন্য (0)দুটি সমান — যেকোনো ক্রমে বসতে পারে
ধনাত্মক সংখ্যা (যেমন 1)a পরে বসবে, b আগে

লক্ষ করুন, মানটি ঠিক −১ বা ১ হতে হবে এমন নয় — শুধু চিহ্নটি (ঋণাত্মক, শূন্য, ধনাত্মক) গুরুত্বপূর্ণ। এবার দেখুন চার জন ছাত্রের তালিকা সাজানোর সময় qsort কীভাবে আপনার comparator-কে বারবার ডাকে। (আসল qsort কোন algorithm ব্যবহার করবে, C standard তা বলে দেয়নি — library ভেদে আলাদা। এখানে বোঝার সুবিধার জন্য সবচেয়ে সহজ পদ্ধতি, insertion sort-এর মতো করে দেখানো হলো।)

1int by_gpa_then_name(const void *a, const void *b) {2    const Student *x = a, *y = b;3    if (x->gpa != y->gpa)4        return (x->gpa < y->gpa) - (x->gpa > y->gpa);5    return strcmp(x->name, y->name);6}
এই ডাকে
x
y
ফেরত
qsort
  1. শুরু। Array-তে চার জন: Rafi (৩.৫০), Mitu (৩.৯০), Anik (৩.৫০), Tania (৩.৭৫). আপনি qsort-কে দিলেন array আর by_gpa_then_name-এর ঠিকানা। এখন থেকে ডাকাডাকির দায়িত্ব qsort-এর।
  2. প্রথম ডাক: Rafi বনাম Mitu. GPA আলাদা, তাই লাইন ৪ চলল। Rafi-র GPA কম, তাই (1) - (0) = ধনাত্মক — মানে “Rafi পরে বসবে”. qsort দুজনের জায়গা বদলাল।
  3. Rafi বনাম Anik. দুজনেরই ৩.৫০ — লাইন ৩-এর শর্ত মিথ্যা, তাই লাইন ৫: নাম তুলনা। strcmp("Rafi", "Anik") ধনাত্মক, কারণ R বর্ণমালায় A-এর পরে। আবার জায়গা বদল।
  4. Mitu বনাম Anik. Mitu-র GPA বেশি, তাই ফল ঋণাত্মক — “Mitu আগেই থাকুক”. কিছু সরানো হলো না। প্রথম তিন জন এখন ঠিক ক্রমে।
  5. Rafi বনাম Tania. ৩.৫০ < ৩.৭৫, ফল ধনাত্মক — Tania সামনে এগোল।
  6. Anik বনাম Tania. আবার ধনাত্মক — Tania আরও এক ঘর এগোল।
  7. Mitu বনাম Tania. ৩.৯০ > ৩.৭৫, ফল ঋণাত্মক — থামো। তালিকা তৈরি: Mitu, Tania, Anik, Rafi. খেয়াল করুন, qsort একবারও GPA বা নাম নিজে দেখেনি — শুধু আপনার ফাংশনের উত্তরের চিহ্ন দেখেছে।

এবার আসল প্রোগ্রামটি চালিয়ে দেখুন। Comparator-টি হুবহু ওপরের মতো:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct { char name[16]; double gpa; } Student;

static int by_gpa_then_name(const void *a, const void *b) {
    const Student *x = a, *y = b;                  /* void* back to Student* */
    if (x->gpa != y->gpa)
        return (x->gpa < y->gpa) - (x->gpa > y->gpa); /* higher GPA first */
    return strcmp(x->name, y->name);               /* tie: A to Z */
}

int main(void) {
    Student s[] = { {"Rafi", 3.50}, {"Mitu", 3.90}, {"Anik", 3.50}, {"Tania", 3.75} };
    size_t n = sizeof s / sizeof s[0];
    qsort(s, n, sizeof s[0], by_gpa_then_name);    /* pass the rule, not a result */
    for (size_t i = 0; i < n; i++)
        printf("%zu. %-6s %.2f\n", i + 1, s[i].name, s[i].gpa);
    return 0;
}

তিনটি জিনিস খেয়াল করুন। এক, qsort(..., by_gpa_then_name)-এ ফাংশনের নামের পরে বন্ধনী নেই — আমরা ফাংশনটি ডাকছি না, তার ঠিকানা পাঠাচ্ছি। দুই, comparator-এর parameter const void *, কারণ qsort যেকোনো টাইপের array সাজাতে পারে; ভেতরে প্রথম লাইনেই আমরা ঠিকানাটিকে আবার Student * হিসেবে ধরছি। তিন, (x < y) - (x > y) একটি পরিচিত কৌশল: প্রতিটি তুলনা 0 বা 1 দেয়, তাই ফল সবসময় −১, ০ বা ১ — কোনো বিয়োগে overflow-এর ভয় নেই। উল্টো ক্রম (কম GPA আগে) চাইলে দুই পাশ অদলবদল করলেই হবে।

qsort নামটি এসেছে Quicksort থেকে — ১৯৫৯-৬০ সালে ব্রিটিশ বিজ্ঞানী Tony Hoare এই algorithm উদ্ভাবন করেন। ফাংশনটি ১৯৭০-এর দশকে Unix-এর C library-তে আসে, পরে C standard-এর অংশ হয়। তবে নামে “q” থাকলেও standard কোথাও বলেনি যে ভেতরে quicksort-ই থাকতে হবে — অনেক library-তে ভেতরে অন্য algorithm চলে। এই দীর্ঘ জীবনের রহস্যও function pointer: ফাংশনটি কোনো নির্দিষ্ট টাইপের সাথে বাঁধা নয়, তাই কয়েক দশক পরের কোনো struct-ও সে সাজাতে পারে।

৪ · Comparator-এর তিনটি ফাঁদ

Comparator দেখতে ছোট্ট একটি ফাংশন, কিন্তু এখানে ভুল হলে প্রোগ্রাম crash করে না — চুপচাপ ভুল ক্রমে সাজায়। আর ভুল তালিকা দেখে বোঝাই কঠিন যে গোলমালটা কোথায়। তিনটি ফাঁদ প্রায় সবাই একবার না একবার পড়েন:

ফাঁদ ১ — বিয়োগ করে ফল ফেরত দেওয়া

অনেক বই আর AI এভাবে লেখে: return *(const int *)a - *(const int *)b;. দেখতে চালাক — a বড় হলে ফল ধনাত্মক, ছোট হলে ঋণাত্মক। সাধারণ ছোট সংখ্যায় ঠিকই চলে। কিন্তু পর্ব ০৫-এর overflow মনে করুন। a = 2000000000 আর b = -2000000000 হলে আসল পার্থক্য চার শত কোটি, যা int-এ ধরে না। Signed int-এর overflow C-তে undefined behavior — বাস্তবে সাধারণত ফল উল্টো চিহ্নের হয়ে যায়, আর comparator বলে “বড়টি ছোট”.

return a - b;                 /* ঝুঁকিপূর্ণ: বড় মানে overflow */
return (a > b) - (a < b);     /* নিরাপদ: সবসময় -1, 0 বা 1 */

ফাঁদ ২ — ভুল cast বা ভুল টাইপ

  • দশমিক কেটে যাওয়া: GPA-র জন্য কেউ লিখলেন return (int)(x->gpa - y->gpa);. ৩.৯০ − ৩.৫০ = ০.৪০, আর (int) করলে হয় 0 — অর্থাৎ comparator বলছে “দুজন সমান”! পুরো তালিকা প্রায় না-সাজানো থেকে যায়।
  • ঠিকানার স্তর গুলিয়ে ফেলা: যদি array-তে struct না রেখে struct-এর pointer রাখেন (Student *list[]), তাহলে qsort comparator-কে দেবে “pointer-এর ঠিকানা”. তখন const Student *x = a; লিখলে ভুল — লাগবে const Student *x = *(const Student * const *)a;. কোনো warning আসবে না, কারণ void * থেকে যেকোনো ঠিকানায় রূপান্তর C নীরবে মেনে নেয়।
  • Comparator-এর টাইপই বদলে দেওয়া: int cmp(const Student *, const Student *) লিখে cast করে qsort-কে দেওয়া। অনেক মেশিনে কাজ করে যায়, কিন্তু ভুল টাইপের ফাংশন পয়েন্টার দিয়ে ডাকা C standard অনুযায়ী undefined behavior. নিয়ম: parameter সবসময় const void *, cast ভেতরে।

ফাঁদ ৩ — অসংগত নিয়ম

Comparator-কে একটি সংগত নিয়ম মানতে হয়: a < b আর b < c হলে a < c হতেই হবে; আর একই জোড়া বারবার জিজ্ঞেস করলে একই উত্তর দিতে হবে। এলোমেলো সংখ্যা ফেরত দেওয়া, বা “সমান হলে কখনো −১ কখনো ১” — এমন নিয়ম qsort-কে বিভ্রান্ত করে। ফল হতে পারে ভুল ক্রম, এমনকি কিছু library-তে array-র সীমার বাইরে পড়া।

আরও একটি সূক্ষ্ম বিষয়: qsort stable হওয়ার নিশ্চয়তা দেয় না। অর্থাৎ দুজনের comparator-ফল 0 হলে, আগের তালিকায় যে আগে ছিল সে এখনো আগে থাকবে — এর কোনো গ্যারান্টি নেই। তাই “সমান GPA হলে নামের ক্রম” চাইলে সেটি comparator-এর ভেতরেই লিখতে হবে, যেমন আমরা লিখেছি। “আগের ক্রম তো এমনিতেই থাকবে” — এই ভরসায় থাকবেন না।

৫ · Dispatch table — লম্বা switch-এর বদলে ফাংশনের তালিকা

পর্ব ২৩-এর রহিম চাচার খাতার কথা মনে আছে? মেনু থেকে একটি নম্বর পড়ে switch দিয়ে ঠিক করা হতো কোন ফাংশন চলবে। তিন-চারটি অপশনে সেটি দিব্যি চলে। কিন্তু চাচা যদি বিশটি অপশন চান? তখন switch হয়ে যায় পাতা-ভর্তি case, আর প্রতিটি নতুন অপশনে দুই জায়গায় বদলাতে হয় — মেনুর লেখা আর switch.

Function pointer দিয়ে একটি পরিষ্কার বিকল্প আছে: সব কাজের ফাংশনের ঠিকানা একটি array-তে রাখুন। মেনুর নম্বর থেকে index বের করুন, তারপর table[i](...) — এক লাইনেই সঠিক কাজ। এই array-কে বলে dispatch table. পর্ব ১১-এ শুনেছিলেন, compiler নিজেও বড় switch-কে ভেতরে ভেতরে প্রায়ই একটি jump table-এ বদলে দেয় — এটি তারই হাতে লেখা রূপ।

ইনপুট: 2চাচা চাপলেন “Expense”
যাচাই: 1 ≤ 2 ≤ 3 ✓index = 2 − 1 = 1
ইনপুট: 7ভুল বোতাম
যাচাই: 7 > 3 ✗table ছোঁয়াই হবে না
table[0] → sale0x401136
table[1] → expense0x401172
table[2] → report0x4011ae
expense(&cash) চললCash: 380 Tk
কোনো ফাংশন ডাকা হয়নিNo such option.
  1. ইনপুট এলো। চাচা মেনু থেকে 2 চাপলেন — খরচ লিখবেন। প্রোগ্রামের হাতে এখন শুধু একটি সংখ্যা।
  2. আগে যাচাই, পরে ব্যবহার। মেনুতে অপশন ১ থেকে ৩. সংখ্যাটি সেই সীমার ভেতরে কি না দেখা হলো। তারপর মেনুর নম্বরকে array-র index-এ বদলানো হলো: array শুরু হয় 0 থেকে, তাই 2 − 1 = 1.
  3. Table-এ খোঁজা। table একটি array, যার প্রতিটি ঘরে একটি ফাংশনের ঠিকানা (ডান পাশের সংখ্যাগুলো উদাহরণ মাত্র — আসল ঠিকানা প্রতিবার আলাদা হতে পারে)। table[1]-এ আছে expense-এর ঠিকানা।
  4. এক লাইনে ডাকা। table[i](&cash) — CPU সেই ঠিকানায় লাফ দিল, expense চলল, খরচ বাদ দিয়ে নতুন হিসাব ছাপল। কোনো if নেই, কোনো case নেই।
  5. ভুল ইনপুট — 7. যাচাই না থাকলে table[6] পড়া হতো — array-র সীমার বাইরে, যেখানে কী আছে কেউ জানে না। সেই এলোমেলো byte-গুলোকে “ঠিকানা” ধরে CPU লাফ দিত — ফল crash, অথবা হ্যাকারের বসানো কোড চালানো। এজন্যই dispatch table-এ bounds checking ঐচ্ছিক নয়, বাধ্যতামূলক।

এবার চালানো যায় এমন একটি ছোট সংস্করণ — চাচার ক্যাশবাক্সের হিসাব। ইনপুট আগে থেকেই দেওয়া, তাতে একটি ভুল অপশনও (7) আছে। “Run” চাপুন:

#include <stdio.h>
typedef void (*Action)(long *cash);

static void sale(long *c)    { long x; if (scanf("%ld", &x) == 1 && x > 0) *c += x; printf("Cash: %ld Tk\n", *c); }
static void expense(long *c) { long x; if (scanf("%ld", &x) == 1 && x > 0) *c -= x; printf("Cash: %ld Tk\n", *c); }
static void report(long *c)  { printf("Report: %ld Tk in the box\n", *c); }

static const Action table[] = { sale, expense, report };    /* menu 1, 2, 3 */
static const size_t N = sizeof table / sizeof table[0];
int main(void) {
    long cash = 0; int choice;
    for (;;) {
        printf("\n1) Sale  2) Expense  3) Report  0) Exit\n> ");
        if (scanf("%d", &choice) != 1 || choice == 0) break;
        if (choice < 1 || (size_t)choice > N) { puts("No such option."); continue; }
        table[choice - 1](&cash);           /* one line replaces the whole switch */
    }
    puts("Bye!");
    return 0;
}

Action typedef বলছে: “এমন ফাংশন, যে একটি long * নেয় আর কিছু ফেরত দেয় না।” তিনটি কাজের ফাংশনই এই আকারের, তাই তারা একই array-তে বসতে পারে। নতুন অপশন যোগ করতে এখন শুধু একটি ফাংশন লিখে table-এ তার নাম বসালেই হবে — main ছুঁতেই হবে না, আর N নিজে থেকে বেড়ে যাবে। আউটপুটে দেখুন, 7 এলে “No such option.” এসেছে, প্রোগ্রাম ভাঙেনি। table-কে const রাখার কারণও আছে: চলার সময় কেউ ভুল করে বা ইচ্ছা করে এর ঠিকানা বদলাতে পারবে না, আর compiler একে read-only মেমরিতে রাখতে পারে।

তাহলে কি switch আর লিখবেন না? না, তা নয়। তিন-চারটি অপশন, প্রতিটির কাজ দুই লাইনের — সেখানে switch-ই বেশি পরিষ্কার। Dispatch table জ্বলে ওঠে যখন অপশন অনেক, প্রতিটির কাজ বড়, অথবা অপশনের তালিকা চলার সময় বদলায় (যেমন plugin যোগ হয়)। Network protocol-এর message টাইপ, CPU emulator-এর opcode, বা কমান্ড-লাইন টুলের subcommand — এগুলোতে প্রায়ই dispatch table দেখবেন।

৬ · Callback — “ঘটনা ঘটলে এই নম্বরে ফোন করবেন”

qsort-এ আপনার ফাংশন ডাকা হয়েছিল সঙ্গে সঙ্গে — qsort চলার মধ্যেই। Callback-এর আরেকটি, আরও শক্তিশালী রূপ আছে: আপনি আজ ঠিকানা জমা দেন, আর ডাকা হয় পরে — যখন কোনো ঘটনা ঘটে। কুরিয়ার কোম্পানির খাতায় ফোন নম্বর রেখে আসার মতো।

নিচে একটি ছোট্ট “কুরিয়ার library” কল্পনা করুন। Library-র কাজ পার্সেল আসার অপেক্ষা করা; পার্সেল এলে কী করতে হবে, সেটি আপনি জানেন। দেখুন নিয়ন্ত্রণ কীভাবে আপনার কোড থেকে library-তে যায়, আবার ফিরে আসে:

1/* আপনার কোড */2void on_arrive(const char *parcel) {3    printf("Got %s!\n", parcel);4}5int main(void) {6    courier_register(on_arrive);7    courier_run();8}9/* কুরিয়ার library-র ভেতরে */10static void (*saved)(const char *);11void courier_register(void (*cb)(const char *)) { saved = cb; }12void courier_run(void) {13    /* ... অপেক্ষা ... পার্সেল এলো ... */14    if (saved != NULL) saved("Parcel #42");15}
Library-র খাতা
saved
কার হাতে নিয়ন্ত্রণ
  1. নম্বর জমা দেওয়া। main library-কে on_arrive-এর ঠিকানা দিল — বন্ধনী ছাড়া, অর্থাৎ ডাকা নয়, শুধু ঠিকানা। এই মুহূর্তে কিছুই ছাপা হচ্ছে না।
  2. Library খাতায় লিখে রাখল। saved = cb; — ঠিকানাটি একটি ভ্যারিয়েবলে জমা হলো। Library জানে না on_arrive কী করে; জানার দরকারও নেই। এই কাজকে বলে callback register করা।
  3. আপনার কোড অন্য কাজে। main এবার courier_run ডাকল এবং নিয়ন্ত্রণ library-র হাতে দিয়ে দিল। বাস্তবে এখানে সেকেন্ড, মিনিট, এমনকি ঘণ্টা কেটে যেতে পারে।
  4. ঘটনা ঘটল। পার্সেল এলো। GUI-তে এটি হতো বাটন ক্লিক, সার্ভারে নতুন request, টাইমারে নির্ধারিত সময় শেষ হওয়া।
  5. আগে যাচাই, তারপর ফোন। Library দেখল খাতায় সত্যিই কোনো নম্বর আছে কি না (saved != NULL). কেউ register না করলে NULL থাকত, আর NULL ঠিকানায় লাফ দিলে প্রোগ্রাম crash করত।
  6. আপনার ফাংশন চলল। Library-র ভেতর থেকে আপনার on_arrive ডাকা হলো, সাথে পার্সেলের তথ্য। আপনি কখনো সরাসরি on_arrive ডাকেননি — ডেকেছে library. এটাই callback.
  7. নিয়ন্ত্রণ ফিরে এলো। on_arrive শেষ হলে library-তে, তারপর main-এ ফেরা। এই উল্টো দিকের ডাককে বলে inversion of control — “আপনি আমাকে ডাকবেন না, আমিই আপনাকে ডাকব।”

বাস্তবের সিস্টেমে callback কোথায় কোথায়

Event handler

ওয়েবপেজে button.addEventListener("click", onClick) — ক্লিক হলে ব্রাউজার আপনার ফাংশন ডাকে। Windows বা Android অ্যাপের প্রতিটি বাটনের পেছনেও একই ধারণা: একটি event handler.

JavaScript, GUI library

Signal handler

Terminal-এ Ctrl+C চাপলে OS প্রোগ্রামকে SIGINT নামের signal পাঠায়। signal(SIGINT, on_ctrl_c) দিয়ে রাখা signal handler তখন ডাকা হয় — যেমন খাতা সেভ করে তারপর বন্ধ হওয়া।

POSIX, <signal.h>

Timer

“৩০ সেকেন্ড পরে এটি করো” — JavaScript-এর setTimeout(fn, 30000), বা embedded সিস্টেমে টাইমারের interrupt. সময় পূর্ণ হলে নির্ধারিত ফাংশন ডাকা হয়।

ব্রাউজার, microcontroller

প্রোগ্রাম শেষের কাজ

C standard library-তেই একটি সহজ callback আছে: atexit(cleanup). প্রোগ্রাম স্বাভাবিকভাবে শেষ হওয়ার ঠিক আগে cleanup ডাকা হয় — ফাইল বন্ধ করা, লগ লেখা।

<stdlib.h>

Signal handler-এর একটি বিশেষ নিয়ম আছে। Signal যেকোনো মুহূর্তে আসতে পারে — এমনকি প্রোগ্রাম যখন printf-এর মাঝখানে। তাই handler-এর ভেতরে কেবল অল্প কিছু “নিরাপদ” কাজ করা যায়; printf বা malloc সেই তালিকায় নেই। প্রচলিত কৌশল: handler শুধু একটি flag (volatile sig_atomic_t) সেট করে, আর মূল লুপ সেই flag দেখে ধীরেসুস্থে কাজ করে।

Callback-এর সাথে নিজের তথ্য পাঠানো — context pointer

একটি বাস্তব সমস্যা আছে। ধরুন একই callback দশটি আলাদা বাটনের জন্য, আর জানতে হবে কোন বাটন চাপা হলো। এজন্য ভালো C library-গুলো register করার সময় ফাংশনের সাথে একটি বাড়তি void * নেয়, আর callback ডাকার সময় সেটি হুবহু ফেরত দেয়। একে বলে context pointer (অনেক library-তে নাম user_data বা arg). POSIX thread তৈরির pthread_create-এ এই নকশাই দেখবেন: একটি ফাংশন, আর তার জন্য একটি void * তথ্য।

৭ · একই interface, ভিন্ন ভিন্ন driver — C-তে polymorphism

এবার সবচেয়ে বড় ধারণাটি। পর্ব ২২-এর শেষে Linux kernel-এর struct file_operations-এর নাম শুনেছিলেন। প্রশ্নটি হলো: Linux-এ “ফাইল পড়া” একটিই কাজ — read(). অথচ সেই “ফাইল” হতে পারে হার্ডডিস্কের একটি ডকুমেন্ট, একটি USB ডিভাইস, একটি serial port, বা কিবোর্ড। প্রতিটির ভেতরের কাজ সম্পূর্ণ আলাদা। Kernel একটি লাইনে সবাইকে সামলায় কীভাবে?

উত্তর: একটি struct, যার ভেতরে শুধু function pointer. Kernel ঠিক করে দেয় struct-এর আকার — কোন কোন কাজ থাকবে (read, write, open…). প্রতিটি driver সেই struct-এর একটি কপি বানিয়ে নিজের ফাংশনগুলোর ঠিকানা বসায়। Kernel শুধু জানে “এই ডিভাইসের struct-এর read ঘরে যে ঠিকানা আছে, সেটি ডাকো।” এই চুক্তিকেই বলে interface.

usb_ops.read → usb_readUSB driver-এর নিজের কোড
usb_ops.write → usb_write
serial_ops.read → serial_readSerial driver-এর নিজের কোড
serial_ops.write → serial_write
bt_ops.read → bt_readনতুন Bluetooth driver
struct file_ops {
read; write; }kernel-এর ঠিক করা interface
dev->ops->read(dev, buf)kernel-এর একটিই লাইন
usb_read চললপেনড্রাইভ থেকে ডেটা
serial_read চললserial port থেকে ডেটা
bt_read চললkernel-এর লাইন একটুও বদলায়নি
  1. Interface ঠিক করা। Kernel বলে দিল: “যে-ই ডিভাইস হোক, আমাকে একটি struct দিতে হবে যার read আর write ঘরে দুটি ফাংশনের ঠিকানা থাকবে।” এটুকুই চুক্তি — কোনো ডিভাইসের নাম এখানে নেই।
  2. USB driver নিজের ঠিকানা বসাল। USB driver-এর লেখকেরা usb_read আর usb_write লিখলেন — ভেতরে USB-এর নিজস্ব সব জটিলতা — আর usb_ops struct-এ সেগুলোর ঠিকানা বসালেন।
  3. Serial driver-ও একই কাজ করল। সম্পূর্ণ আলাদা দল, আলাদা হার্ডওয়্যার, আলাদা কোড। কিন্তু struct-এর আকার হুবহু এক।
  4. Kernel একটি USB ডিভাইস থেকে পড়ল। dev->ops এখানে usb_ops-কে দেখাচ্ছে, তাই read ঘর থেকে পাওয়া গেল usb_read-এর ঠিকানা। Kernel জানেই না ভেতরে USB আছে।
  5. একই লাইন, অন্য ডিভাইস। এবার dev->ops দেখাচ্ছে serial_ops-কে। Kernel-এর লাইন হুবহু এক, কিন্তু চলল সম্পূর্ণ ভিন্ন কোড। একই ডাক, ভিন্ন আচরণ — এর নাম polymorphism.
  6. নতুন driver, পুরোনো kernel কোড। পরের বছর কেউ Bluetooth driver লিখলেন। Kernel-এর ওই লাইন বদলাতে হলো না — নতুন struct বসালেই হলো। এভাবেই হাজার হাজার driver একই kernel-এ জুড়ে যায়। এটাই plugin নকশার মূল সূত্র।

C-তে এর কোড দেখতে মোটামুটি এমন (সরল করা; আসল file_operations-এ আরও অনেক ঘর আছে):

struct file_ops {
    long (*read)(struct device *dev, char *buf, long n);
    long (*write)(struct device *dev, const char *buf, long n);
};

static const struct file_ops usb_ops = { .read = usb_read, .write = usb_write };

/* kernel-এর ভেতরে — কোন driver, তা জানার দরকার নেই */
long n = dev->ops->read(dev, buf, 512);
মূল ধারণা

C++, Java বা Python-এ আপনি পরে “object”, “class” আর “virtual method” শিখবেন। ভেতরে ঢুকলে দেখবেন প্রায় একই জিনিস: প্রতিটি class-এর জন্য compiler ফাংশনের ঠিকানার একটি টেবিল বানায় — C++-এ যাকে সাধারণত বলে vtable — আর প্রতিটি object-এর ভেতরে থাকে সেই টেবিলের একটি pointer. অর্থাৎ object-oriented programming-এর হৃদয়ে আছে আজকের এই function pointer-ভর্তি struct. C-তে আপনি সেটি নিজের হাতে বানান, অন্য ভাষায় compiler বানিয়ে দেয়।

৮ · নিরাপত্তা — ভুল ঠিকানায় লাফ দিলে কী হয়

Function pointer-এর ক্ষমতা যেখানে, বিপদও সেখানে। একটি সাধারণ data pointer নষ্ট হলে আপনি ভুল মান পড়েন। কিন্তু function pointer নষ্ট হলে CPU ভুল কোড চালায় — আর সেটি অনেক বড় বিপদ।

পরিস্থিতিযা ঘটেপ্রতিরোধ
Callback register হয়নি, ঠিকানা NULLডাকলেই segmentation fault, প্রোগ্রাম বন্ধডাকার আগে if (cb != NULL)
Pointer-এ শুরুর মান দেওয়া হয়নিআবর্জনা মানকে ঠিকানা ধরে লাফ — কখনো crash, কখনো অদ্ভুত আচরণঘোষণার সময়ই = NULL বা আসল ফাংশন
যাচাই ছাড়া table indexArray-র বাইরের byte-কে ঠিকানা ধরে লাফIndex-এর সীমা যাচাই
Struct-এর পাশের buffer উপচে পড়লপাশে রাখা function pointer বদলে গেল — হ্যাকারের পছন্দের কোড চললBounds checking, const table, CFI

শেষ সারিটি বিশেষভাবে দেখুন। পর্ব ১৯-তে buffer overflow দেখেছেন। ধরুন একটি struct-এ প্রথমে একটি নামের buffer, তার ঠিক পরে একটি function pointer. কেউ নামের জায়গায় অনেক লম্বা লেখা দিল, আর বাড়তি byte গিয়ে বসল function pointer-এর ঘরে। পরে প্রোগ্রাম যখন সেই pointer দিয়ে ডাকবে, CPU লাফ দেবে আক্রমণকারীর বেছে নেওয়া ঠিকানায়। কয়েক দশক ধরে বহু আক্রমণ ঠিক এই পথেই হয়েছে — function pointer, return address বা C++ object-এর vtable pointer বদলে দিয়ে।

আধুনিক প্রতিরক্ষা — Control-Flow Integrity

এই বিপদের বিরুদ্ধে কয়েক স্তরের পাহারা তৈরি হয়েছে। পর্ব ১৭-এর ASLR ঠিকানা এলোমেলো করে, যাতে আক্রমণকারী সহজে আন্দাজ করতে না পারে কোথায় লাফ দিতে হবে। আরেকটি শক্তিশালী ধারণা হলো Control-Flow Integrity (CFI). এর মূল চিন্তা সহজ:

মেট্রো রেলের কথা ভাবুন। ট্রেন শুধু নির্দিষ্ট স্টেশনেই থামতে পারে — লাইনের মাঝখানে যেকোনো জায়গায় দরজা খোলে না। CFI প্রোগ্রামের জন্য এমন “বৈধ স্টেশনের তালিকা” বানায়। কোনো function pointer দিয়ে লাফ দেওয়ার ঠিক আগে যাচাই হয়: “এই ঠিকানা কি বৈধ ফাংশনের শুরু, আর টাইপ কি মেলে?” না মিললে প্রোগ্রাম সঙ্গে সঙ্গে থামিয়ে দেওয়া হয় — হ্যাকারের কোড চালানোর চেয়ে বন্ধ হয়ে যাওয়া অনেক ভালো।

বাস্তবে এটি কয়েকভাবে আসে: Clang compiler-এর CFI অপশন, Windows-এর Control Flow Guard, আর আধুনিক CPU-তে হার্ডওয়্যার সহায়তা — Intel-এর CET আর ARM-এর BTI, যেগুলো যাচাই করে যে indirect call গিয়ে পড়েছে একটি চিহ্নিত বৈধ শুরুর জায়গায়। এগুলোর বিস্তারিত এই কোর্সের বাইরে; মনে রাখার কথা হলো, function pointer এত গুরুত্বপূর্ণ নিশানা যে CPU নির্মাতারাও এর পাহারায় নতুন নির্দেশ যোগ করেছেন।

আপনার নিজের কোডে তিনটি অভ্যাসই যথেষ্ট সুরক্ষা দেয়: (১) dispatch table আর driver-এর ops struct static const রাখুন, যাতে চলার সময় বদলানো না যায়; (২) বাইরে থেকে আসা কোনো সংখ্যা দিয়ে table-এ ঢোকার আগে সীমা যাচাই করুন; (৩) ঐচ্ছিক callback ডাকার আগে NULL কি না দেখুন।

৯ · AI-এর যুগে — function pointer কোডে AI যেসব ভুল করে

“qsort দিয়ে সাজাও” বা “মেনুটা function pointer দিয়ে করো” — AI এগুলো কয়েক সেকেন্ডে লিখে দেয়, আর কোড compile-ও হয়। সমস্যা হলো, এই বিষয়ের ভুলগুলো প্রায় কখনো compile-এর সময় ধরা পড়ে না। চলার সময়েও সাধারণ ইনপুটে চুপ থাকে, ধরা পড়ে শুধু বিশেষ ইনপুটে।

AI-এর ভুলকখন ধরা পড়েঠিক রূপ
Comparator-এ return a - b;খুব বড় বা খুব ছোট সংখ্যায় ভুল ক্রম(a > b) - (a < b)
double-এর পার্থক্যকে (int) করে ফেরতকাছাকাছি মানগুলো “সমান” ধরে, সাজানো হয় নাতুলনা করে −১/০/১ ফেরত
Pointer-এর array সাজাতে struct-এর মতো castআবর্জনা মান তুলনা, warning নেই*(const T * const *)a
table[choice] — কোনো যাচাই নেইভুল বোতাম চাপলেই crashআগে 1 ≤ choice ≤ N
ঐচ্ছিক callback সরাসরি ডাকাকেউ register না করলে crashif (cb) cb(...)
Signal handler-এর ভেতরে printfকালেভদ্রে, অনিশ্চিতভাবে — খুঁজে পাওয়া কঠিনশুধু flag সেট করা

AI-কে কাজ দেওয়ার সময় এই ফাঁদগুলো আগেই প্রম্পটে বলে দিন। এতে সে সাবধানে লেখে, আর আপনি যাচাইয়ের একটি তালিকা হাতে পান:

C-তে একটি comparator লেখো যাতে qsort দিয়ে Student struct-এর array সাজানো যায় (struct-এ char name[16] আর double gpa আছে)। নিয়ম: বেশি GPA আগে, GPA সমান হলে নামের বর্ণানুক্রমে। শর্তগুলো মানবে: ১. parameter হবে const void *, cast করবে ফাংশনের ভেতরে। ২. বিয়োগ করে ফল ফেরত দেবে না; (a > b) - (a < b) ধরনের তুলনা ব্যবহার করবে। ৩. double-কে int-এ cast করবে না। ৪. gcc -Wall -Wextra-তে কোনো warning আসবে না। ৫. শেষে ৫টি পরীক্ষার ইনপুট দাও — সমান GPA, খুব কাছাকাছি GPA (৩.৯৯ আর ৩.৯৮), আর একই নাম — আর প্রতিটির প্রত্যাশিত ক্রম লেখো।

AI-এর লেখা function pointer কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১C++-এর virtual function আর আজকের function pointer-ভর্তি struct — এরা কি আসলে একই জিনিস? ভেতরে কী ঘটে?

ধারণার দিক থেকে প্রায় একই। C++ standard নিজে “vtable” শব্দটি বলে না — শুধু বলে virtual function-এর আচরণ কেমন হবে। কিন্তু GCC, Clang, MSVC — প্রচলিত সব বড় compiler এটি একইভাবে বানায়:

  • প্রতিটি class-এর একটি টেবিল: যে class-এ virtual function আছে, compiler তার জন্য একটি read-only array বানায়, যার প্রতিটি ঘরে একটি ফাংশনের ঠিকানা। এটিই vtable — আমাদের usb_ops-এর মতো।
  • প্রতিটি object-এ একটি লুকানো pointer: object তৈরির সময় compiler তার ভেতরে নিজে থেকে একটি pointer বসায়, যা নিজের class-এর vtable দেখায় — আমাদের dev->ops-এর মতো।
  • ডাকা: shape->area() লিখলে compiler আসলে বানায় “object-এর vtable pointer পড়ো → নির্দিষ্ট ঘর থেকে ঠিকানা পড়ো → সেখানে লাফ দাও”. হুবহু dev->ops->read(dev, ...), শুধু dev-কে লুকিয়ে this হিসেবে পাঠানো হয়।

তাহলে C++ কী বাড়তি দিল? নিরাপত্তা আর সুবিধা। C-তে আপনি ভুল করে একটি ঘর ফাঁকা রাখতে পারেন, বা ভুল টাইপের ফাংশন বসাতে পারেন; C++ compiler এগুলো ধরে। Inheritance-এ বাবা-class-এর টেবিল থেকে ঘর কপি করা, নতুন ঘর যোগ করা — এই হিসাবও compiler রাখে। বিনিময়ে আপনি নিয়ন্ত্রণ কিছুটা হারান। Linux kernel C-তে লেখা হয়েও যে নকশা ব্যবহার করে, সেটি আসলে হাতে বানানো object-oriented নকশা।

খরচ: প্রতিটি virtual call-এ দুটি বাড়তি মেমরি-পড়া আর একটি indirect লাফ। আরও বড় খরচ হলো, compiler সাধারণত আগে থেকে জানে না কোন ফাংশন চলবে, তাই ফাংশনটি জায়গায় বসিয়ে দিতে (inlining) পারে না। গেম ইঞ্জিন বা high-frequency trading-এর মতো জায়গায় এজন্য প্রোগ্রামাররা কখনো কখনো virtual call এড়িয়ে চলেন।

মূল উপলব্ধি: নতুন ভাষার “জাদুকরী” ফিচারগুলোর অনেকটাই পুরোনো, সরল ধারণার ওপর একটি সুবিধাজনক আবরণ। নিচের স্তরটি বুঝলে ওপরের যেকোনো ভাষার আচরণ, খরচ আর দুর্বলতা আপনি নিজেই অনুমান করতে পারবেন।

প্র ০২“Callback hell” কী? JavaScript-এর মতো ভাষা কেন callback থেকে promise আর async/await-এর দিকে গেল?

ধরুন একটি কাজ চারটি ধাপে করতে হবে, আর প্রতিটি ধাপ শেষ হলে পরেরটি শুরু হবে — ইউজারের তথ্য আনো, তারপর তার অর্ডার আনো, তারপর পেমেন্ট যাচাই করো, তারপর রসিদ পাঠাও। প্রতিটি ধাপ নেটওয়ার্কে সময় নেয়, তাই প্রতিটির জন্য একটি callback. ফল: callback-এর ভেতরে callback, তার ভেতরে আরেকটি — কোড ডান দিকে সরতে সরতে একটি পিরামিডের মতো হয়ে যায়। এটিই “callback hell”.

  • পড়া কঠিন: কাজের ক্রম ওপর থেকে নিচে নয়, ভেতর থেকে আরও ভেতরে।
  • Error সামলানো কঠিন: প্রতিটি স্তরে আলাদা করে ভুল যাচাই করতে হয়; একটি ভুলে গেলে ভুলটি চুপচাপ হারিয়ে যায়।
  • নিয়ন্ত্রণ অন্যের হাতে: আপনার callback library কতবার ডাকবে — একবার, দুবার, নাকি একবারও না — তা আপনার হাতে নেই। একে বলে inversion of control-এর ঝুঁকি।

সমাধান কীভাবে এলো: JavaScript-এ ২০১৫ সালের ES2015 (ES6) সংস্করণে Promise ভাষার অংশ হলো — callback দেওয়ার বদলে ফাংশন একটি “ভবিষ্যতে ফল দেওয়ার প্রতিশ্রুতি” ফেরত দেয়, যেগুলো .then() দিয়ে শিকলের মতো জোড়া যায়। তারপর ES2017-এ এলো async/await, যেখানে কোড দেখতে আবার সাধারণ ওপর-থেকে-নিচে কোডের মতো, অথচ ভেতরে এখনো সেই callback-ই চলছে। C#, Python, Rust-সহ অনেক ভাষা একই পথ নিয়েছে।

লক্ষ করুন, callback কিন্তু হারিয়ে যায়নি — promise আর async/await-এর ভেতরেও ইঞ্জিন শেষ পর্যন্ত একটি ফাংশনই ডাকে। ভাষা শুধু সেটিকে মানুষের পড়ার মতো রূপ দিয়েছে। C-তে এই সুবিধা নেই, তাই বড় event-driven C প্রোগ্রাম (যেমন নেটওয়ার্ক সার্ভার) প্রায়ই state machine আর dispatch table দিয়ে সাজানো হয়।

মূল উপলব্ধি: একটি কৌশল শক্তিশালী হলেই যথেষ্ট নয়; মানুষ সেটি সহজে পড়তে ও ঠিক রাখতে পারছে কি না, সেটিও নকশার অংশ। ভাষার বিবর্তনের বড় অংশই এই দ্বিতীয় প্রশ্নের উত্তর খোঁজা।

প্র ০৩qsort void * দিয়ে যেকোনো টাইপ সাজাতে পারে। এই নমনীয়তার দাম কী, আর C++-এর template কীভাবে আলাদা?

qsort একটি generic ফাংশন — একটি কোড দিয়ে int, double, struct, সব সাজায়। C-তে এটি সম্ভব হয়েছে দুটি কৌশলে: ডেটা আসে void * আর byte-এর মাপ হিসেবে, আর তুলনার নিয়ম আসে function pointer হিসেবে। এর তিনটি দাম আছে:

  • টাইপ-নিরাপত্তা হারানো: void *-এর পরে compiler আর জানে না ভেতরে কী আছে। ভুল size দিলে, বা comparator-এ ভুল টাইপে cast করলে কোনো warning আসে না — প্রোগ্রাম চুপচাপ ভুল করে।
  • প্রতিটি তুলনায় indirect call: দশ লক্ষ উপাদান সাজাতে কয়েক কোটি তুলনা হতে পারে, আর প্রতিটি একটি function pointer দিয়ে ডাকা। Compiler সাধারণত comparator-কে qsort-এর ভেতরে বসিয়ে দিতে পারে না, কারণ qsort আগেই আলাদাভাবে compile হয়ে library-তে বসে আছে।
  • উপাদান সরানো byte ধরে: qsort জানে না উপাদানটি কী, তাই অদলবদল করে নির্দিষ্ট সংখ্যক byte কপি করে — টাইপ জানা থাকলে যা আরও দক্ষভাবে করা যেত।

C++-এর পথ: std::sort একটি template. আপনি যে টাইপ আর যে তুলনার নিয়ম দেন, compiler সেই জোড়ার জন্য একটি আলাদা sort ফাংশন বানিয়ে ফেলে। ফলে টাইপ পুরো জানা থাকে, ভুল cast ধরা পড়ে, আর তুলনার কোড সরাসরি ভেতরে বসে যায় — তাই std::sort প্রায়ই qsort-এর চেয়ে লক্ষণীয়ভাবে দ্রুত। দাম: প্রতিটি টাইপের জন্য আলাদা কোড, তাই প্রোগ্রাম বড় হয় আর compile বেশি সময় নেয়। Rust-এর generic-ও মূলত এই পথে চলে।

C নিজেও কিছুটা এগিয়েছে — C11-এ _Generic এসেছে, আর ম্যাক্রো দিয়ে (পর্ব ২৭) টাইপ-অনুযায়ী কোড বানানোর কৌশল বহু পুরোনো। তবু qsort-এর নকশা আজও টিকে আছে, কারণ এটি সরল, ছোট, আর কোনো বিশেষ ভাষা-সুবিধা ছাড়াই কাজ করে।

মূল উপলব্ধি: “একটি কোড সবার জন্য” (ছোট, নমনীয়, কিন্তু ধীর ও কম নিরাপদ) বনাম “প্রতিটির জন্য আলাদা কোড” (দ্রুত, নিরাপদ, কিন্তু বড়) — generic programming-এর এই trade-off প্রায় প্রতিটি ভাষায় কোনো না কোনো রূপে দেখবেন।

প্র ০৪আক্রমণকারীরা কেন function pointer-কে এত পছন্দ করে? শুধু “ভালো করে কোড লেখো” বলা কেন যথেষ্ট নয়?

একজন আক্রমণকারীর চূড়ান্ত লক্ষ্য সাধারণত একটাই: প্রোগ্রামকে দিয়ে নিজের পছন্দের কাজ করানো। এজন্য CPU-কে কোনোভাবে নিজের বেছে নেওয়া ঠিকানায় লাফ দেওয়াতে হবে। প্রোগ্রামে এমন জায়গা খুব বেশি নেই, যেখানে লাফের ঠিকানা মেমরি থেকে পড়া হয় — ফাংশনের return address (পর্ব ১৩), আর function pointer. তাই এ দুটিই সবচেয়ে মূল্যবান নিশানা।

  • প্রথম যুগ: buffer উপচে নিজের কোড stack-এ বসানো, তারপর return address বা function pointer সেদিকে ঘোরানো। জবাবে এলো non-executable stack — data-র মেমরিতে কোড চালানো নিষেধ।
  • দ্বিতীয় যুগ: নিজের কোড বসানো না গেলে প্রোগ্রামের নিজের কোডের টুকরো জুড়ে জুড়ে কাজ করানো — return-oriented programming. জবাবে এলো ASLR আর stack canary.
  • তৃতীয় যুগ: তথ্য ফাঁস করে ASLR পেরোনো, তারপর vtable বা function pointer বদলানো। জবাবে CFI আর হার্ডওয়্যার সহায়তা (Intel CET, ARM BTI ও pointer authentication).

“ভালো কোড লেখা” কেন যথেষ্ট নয়? কারণ একটি ব্রাউজার বা অপারেটিং সিস্টেমে কয়েক কোটি লাইন কোড, হাজারো লেখক, কয়েক দশকের ইতিহাস। কোথাও একটি ছোট ভুল থাকবেই — এটাই বাস্তবতা। তাই নিরাপত্তা প্রকৌশল ধরে নেয় “ভুল থাকবে”, আর এমন স্তর বসায় যাতে একটি ভুল থেকে পুরো নিয়ন্ত্রণ চলে না যায়। একে বলে defense in depth — বহুস্তরের প্রতিরক্ষা।

অন্যদিকে ভাষা-নকশাও বদলাচ্ছে। Rust-এর মতো ভাষা নিরাপদ কোডে মেমরির বাইরে লেখাকেই প্রায় অসম্ভব করে দেয়, ফলে function pointer বদলানোর পথই বন্ধ। এজন্যই বিভিন্ন সরকারি সাইবার-নিরাপত্তা সংস্থা আর বড় প্রতিষ্ঠান memory-safe ভাষার দিকে যাওয়ার পরামর্শ দিচ্ছে।

মূল উপলব্ধি: নিরাপত্তা কোনো একটি কৌশল নয়, একটি চলমান প্রতিযোগিতা। Function pointer কেন বিপজ্জনক, সেটি বুঝলে আপনি বুঝবেন কেন const table, bounds checking আর NULL যাচাই — এই ছোট অভ্যাসগুলো আসলে বড় প্রতিরক্ষার প্রথম ইট।

প্র ০৫আপনি একটি plugin interface বানাচ্ছেন, যা বহু বছর অন্যরা ব্যবহার করবে। কীভাবে নকশা করবেন যাতে পরে বদলালেও পুরোনো plugin না ভাঙে?

Plugin interface হলো একটি চুক্তি — আপনার প্রোগ্রাম আর অন্যের লেখা কোডের মধ্যে। চুক্তি একবার ছড়িয়ে পড়লে সেটি বদলানো খুব কঠিন, কারণ যাঁরা plugin লিখেছেন তাঁরা হয়তো আর কোড আপডেট করবেন না। তাই শুরুতেই কয়েকটি সতর্কতা নেওয়া হয়:

  • Version বা আকারের ঘর রাখা: struct-এর প্রথম ঘরে একটি version নম্বর, বা struct-এর নিজের আকার (sizeof) রাখা। প্রোগ্রাম আগে সেটি পড়ে বোঝে plugin কোন সংস্করণের চুক্তি মেনে লেখা। Windows-এর অনেক পুরোনো API-তে struct-এর প্রথম ঘরে তার আকার দেওয়ার নিয়ম ঠিক এই কারণেই।
  • শুধু শেষে যোগ করা: নতুন ফাংশন পয়েন্টার সবসময় struct-এর শেষে যোগ করা হয়, মাঝখানে কখনো নয় — যাতে পুরোনো ঘরগুলোর অবস্থান না বদলায় (পর্ব ২২-এর alignment আর offset মনে করুন)।
  • ঐচ্ছিক ঘর NULL হতে পারে: পুরোনো plugin নতুন ঘর চেনে না, তাই সেখানে NULL থাকবে। প্রোগ্রাম ডাকার আগে NULL যাচাই করে, না থাকলে একটি সাধারণ আচরণ ধরে নেয়।
  • Context pointer সবখানে: প্রতিটি callback-এ একটি void * দিলে plugin নিজের তথ্য রাখতে পারে, global ভ্যারিয়েবল লাগে না।

বাস্তবের দুই উদাহরণ, দুই দর্শন: Linux kernel ইচ্ছা করে নিজের ভেতরের driver interface স্থির রাখে না — kernel-এর ডকুমেন্টেশনে এর পক্ষে যুক্তি লেখা আছে: driver-এর কোড kernel-এর সাথেই রাখা হলে interface বদলানোর সময় সব driver একসাথে ঠিক করে দেওয়া যায়, ফলে নকশা পুরোনো ভুলে আটকে থাকে না। কিন্তু ব্যবহারকারীর প্রোগ্রাম যে system call interface দেখে, সেটি ভাঙা প্রায় নিষিদ্ধ — “userspace ভাঙা যাবে না” kernel-এর একটি কঠোর নীতি। অর্থাৎ কার সাথে চুক্তি, তার ওপর নির্ভর করে কতটা স্থির রাখতে হবে।

মূল উপলব্ধি: ভালো interface শুধু আজকের কাজ নয়, ভবিষ্যতের পরিবর্তনের জায়গাও রেখে দেয়। Function pointer-এর struct বানানোর সময় নিজেকে জিজ্ঞেস করুন: “পাঁচ বছর পরে এখানে একটি নতুন কাজ যোগ করতে হলে কি পুরোনো সবাই ভেঙে পড়বে?”

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — পড়া, অনুমান করা আর চালিয়ে মিলিয়ে দেখাই আসল কাজ।

  1. ঘোষণা পড়ুন: ভেতর থেকে বাইরে পড়ার কৌশলে নিচের তিনটির অর্থ বাংলায় লিখুন — (ক) double (*f)(double); (খ) double *g(double); (গ) void (*handlers[4])(int);

    (ক) f একটি pointer, যা এমন একটি ফাংশন দেখায় যেটি একটি double নেয় আর double ফেরত দেয় — যেমন f = sqrt; লেখা যায়।

    (খ) এখানে নামের চারপাশে বন্ধনী নেই, তাই g একটি ফাংশন, যে double নেয় আর double * (একটি ঠিকানা) ফেরত দেয়। কোনো function pointer নেই।

    (গ) handlers একটি array, যাতে ৪টি ঘর; প্রতিটি ঘর একটি pointer, যা int নেওয়া আর কিছু ফেরত না দেওয়া ফাংশন দেখায়। অর্থাৎ ৪টি ফাংশনের একটি dispatch table. typedef দিয়ে: typedef void (*Handler)(int); Handler handlers[4];

  2. উল্টো ক্রম: rank.c-তে তালিকাটি কম GPA আগে, আর সমান হলে নাম Z থেকে A — এভাবে সাজাতে হলে comparator-এর কোন দুটি জায়গা বদলাবেন? চালিয়ে মিলিয়ে দেখুন।

    GPA-র লাইনে দুই পাশ অদলবদল করুন: return (x->gpa > y->gpa) - (x->gpa < y->gpa); — এখন x-এর GPA বেশি হলে ফল ধনাত্মক, অর্থাৎ x পরে বসবে। নামের লাইনে strcmp(y->name, x->name) লিখুন — x আর y-এর জায়গা বদলালেই ক্রম উল্টে যায়। (-strcmp(...) লেখাও চলে, কারণ strcmp-এর ফল কখনো INT_MIN হয় না, তবে প্যারামিটার অদলবদলই বেশি পরিষ্কার।) ফল হবে: Rafi ৩.৫০, Anik ৩.৫০, Tania ৩.৭৫, Mitu ৩.৯০.

  3. বাগ শিকার: একজন লিখলেন int cmp(const void *a, const void *b) { return *(const int *)a - *(const int *)b; } আর array দিলেন { 5, -2147483647, 2147483647, 0 }. কোথায় সমস্যা হতে পারে, আর ঠিক রূপ কী?

    ৫ − (−২১৪৭৪৮৩৬৪৭) বা ২১৪৭৪৮৩৬৪৭ − (−২১৪৭৪৮৩৬৪৭)-এর মতো বিয়োগের ফল int-এর সীমা (প্রায় ২১৪ কোটি) ছাড়িয়ে যায় — signed overflow, যা undefined behavior. বাস্তবে সাধারণত ফল ঋণাত্মক হয়ে যায়, ফলে comparator বলে “বড়টি ছোট”, আর তালিকা ভুল ক্রমে সাজে। ঠিক রূপ: int x = *(const int *)a, y = *(const int *)b; return (x > y) - (x < y);. এখানে কোনো বিয়োগ নেই সংখ্যাগুলোর মধ্যে — শুধু দুটি তুলনার ফল (0 বা 1) বিয়োগ হচ্ছে।

  4. Dispatch table বাড়ান: cashbox.c-তে চতুর্থ একটি অপশন “4) Reset” যোগ করুন, যা ক্যাশ 0 করে দেয়। কয়টি জায়গায় কোড বদলাতে হলো? switch দিয়ে লেখা থাকলে কয়টি লাগত?

    তিনটি ছোট পরিবর্তন: (১) একটি নতুন ফাংশন — static void reset(long *c) { *c = 0; puts("Cash reset."); }; (২) table-এ reset যোগ — { sale, expense, report, reset }; (৩) মেনুর লেখায় “4) Reset”. সীমা যাচাইয়ের লাইন একটুও বদলাতে হয়নি, কারণ N নিজে থেকেই 4 হয়ে গেছে। Switch হলে নতুন case 4: লিখতে হতো, আর “কোন সংখ্যা বৈধ” — সেই হিসাবও আলাদা করে মেলাতে হতো। দুই ক্ষেত্রেই মেনুর লেখা আলাদা থেকে যায়; পরের ধাপে চাইলে প্রতিটি অপশনের নাম আর ফাংশন একটি struct-এ একসাথে রেখে মেনুও table থেকে ছাপানো যায়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি callback-এর জীবনের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✍️ আপনি on_arrive ফাংশনটি লেখেন
  2. 📇 Library-কে ঠিকানা দেন — courier_register(on_arrive)
  3. 📒 Library ঠিকানাটি একটি pointer-এ জমা রাখে
  4. 📦 পরে ঘটনা ঘটে — পার্সেল আসে
  5. 📞 Library NULL যাচাই করে আপনার ফাংশন ডাকে

Function pointer আসলে কী রাখে?

  • ফাংশনের ফেরত দেওয়া মান
  • মেমরিতে ফাংশনের কোড যেখান থেকে শুরু, সেই ঠিকানা
  • ফাংশনের নাম, একটি string হিসেবে
  • ফাংশনের পুরো কোডের একটি কপি

int *f(int); আর int (*f)(int); — পার্থক্য কী?

  • কোনো পার্থক্য নেই, দুটিই একই
  • প্রথমটি function pointer, দ্বিতীয়টি ফাংশন
  • প্রথমটি int-এর ঠিকানা ফেরত দেওয়া একটি ফাংশন; দ্বিতীয়টি int ফেরত দেওয়া ফাংশনের pointer
  • দ্বিতীয়টি ফাংশনের একটি array

qsort-এর comparator cmp(a, b) একটি ধনাত্মক সংখ্যা ফেরত দিলে qsort কী বোঝে?

  • a আর b সমান
  • a আগে বসবে
  • তুলনায় কোনো error হয়েছে
  • a পরে বসবে, b আগে

Comparator-এ return a - b; (দুটি int) লেখা কেন ঝুঁকিপূর্ণ?

  • খুব বড় বা খুব ছোট মানে বিয়োগফল int-এ ধরে না, ফলে চিহ্ন উল্টে যেতে পারে
  • qsort শুধু −১, ০ আর ১ মেনে নেয়
  • বিয়োগ করা তুলনার চেয়ে অনেক ধীর
  • এটি compile-ই হয় না

মেনুর জন্য table[choice - 1](&cash); লেখার আগে কী করা সবচেয়ে জরুরি?

  • Table-কে qsort দিয়ে সাজানো
  • choice ১ থেকে N-এর মধ্যে আছে কি না যাচাই করা
  • প্রতিটি ফাংশনের ঠিকানা printf দিয়ে ছাপানো
  • switch দিয়ে আবার লিখে ফেলা

Linux-এর file_operations-এর মতো “function pointer-ভর্তি struct” কোন ধারণার C-রূপ?

  • Recursion
  • Padding ও alignment
  • Polymorphism — একই interface, ভিন্ন ভিন্ন আচরণ
  • Macro দিয়ে কোড বসানো

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. কোডও মেমরিতে থাকে, তাই প্রতিটি ফাংশনের ঠিকানা আছে; সেটি রাখে function pointer. বন্ধনী ছাড়া ফাংশনের নাম = তার ঠিকানা।
  2. ঘোষণা পড়ুন নাম থেকে, ভেতর থেকে বাইরে; একবারের বেশি লাগলে typedef.
  3. qsort জানে কীভাবে সাজাতে হয়, comparator বলে কে আগে — ঋণাত্মক/শূন্য/ধনাত্মক। বিয়োগ নয়, (a > b) - (a < b).
  4. Dispatch table লম্বা switch-এর জায়গা নেয় — কিন্তু index যাচাই আর static const বাধ্যতামূলক।
  5. Callback = আজ ঠিকানা জমা, পরে ডাক; ফাংশন পয়েন্টারের struct = interface ও polymorphism. ডাকার আগে NULL যাচাই — আর মনে রাখুন, এটি আক্রমণকারীর প্রিয় নিশানা।

পরের পর্বে: আজ usb_ops-এর মতো interface দেখলেন — চুক্তি এক জায়গায়, কাজ অন্য জায়গায়। পরের পর্বে সেই চিন্তা নিয়ে যাব পুরো প্রোগ্রামে: কোড কীভাবে আলাদা আলাদা ফাইলে ভাগ করতে হয়, header ফাইল কেন চুক্তির কাজ করে, আর Makefile ও library দিয়ে বড় প্রজেক্ট কীভাবে সাজানো হয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান