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

মডুলার প্রোগ্রাম — বড় প্রজেক্টকে ফাইলে ফাইলে সাজানো

Modular C — Headers, Information Hiding, Make & Libraries

Taught in Bangla, this lesson shows how to split a C program into modules: header and source files, what belongs in a header, information hiding with static and opaque pointers, building with make, creating libraries, and organising a project with git.

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

  • পর্ব ২৩-এর দোকানের খাতাকে ledger.h, ledger.c আর main.c-এ ভাগ করা — কোন জিনিস কোথায় যাবে
  • Header থেকে linker পর্যন্ত পুরো পথ — compiler কী দেখে, linker কী মেলায়
  • Opaque pointer দিয়ে information hiding — কেন FILE *-এর ভেতরটা আপনি কখনো দেখেন না
  • একটি ছোট কিন্তু আসল Makefile লেখা ও পড়া, আর Make কীভাবে ঠিক করে কী আবার বানাতে হবে
  • Static ও shared library বানানো, ABI কী, আর পেশাদার প্রজেক্টের ফোল্ডার সাজানো

১ · এক ফাইলে তিন হাজার লাইন — কেন আর চলে না

পর্ব ২৩-এ নাদিয়া রহিম চাচার দোকানের জন্য একটি খাতা-প্রোগ্রাম বানিয়েছিলেন — একটিমাত্র ফাইল, প্রায় ২২০ লাইন। চাচা খুশি। খবর ছড়াল, পাশের দুটি দোকানও চাইল। এবার নতুন চাহিদা এলো একের পর এক: মাসিক রিপোর্ট, বাকির তালিকা, ফাইলে সেভ (পর্ব ২৪), দিনের শেষে SMS. কাজ ভাগ করতে নাদিয়ার বন্ধু রাফি রিপোর্টের অংশটি লেখার দায়িত্ব নিলেন।

ছয় মাস পর shop.c ফাইলটি দাঁড়াল প্রায় তিন হাজার লাইনে। তখন যা যা ঘটতে লাগল:

  • খুঁজে পাওয়া যায় না। “মোট হিসাবের ফাংশনটা কোথায়?” — প্রতিবার স্ক্রল করে বা সার্চ করে খুঁজতে হয়।
  • দুজন একসাথে কাজ করতে পারেন না। নাদিয়া আর রাফি একই ফাইল বদলান, তাই git-এ প্রায় প্রতিদিন দুজনের পরিবর্তনে সংঘাত (conflict) হয়।
  • একটি লাইন বদলালেও পুরো ফাইল আবার compile হয়। পর্ব ০৩-এর separate compilation-এর সুবিধা পুরোপুরি হারিয়ে গেছে।
  • সবাই সবকিছু ছুঁতে পারে। রাফির রিপোর্ট-কোড সরাসরি lg.count বদলে দিল, আর নাদিয়ার হিসাব ভেঙে গেল — কেউ টেরও পেল না কোথা থেকে।
  • আবার ব্যবহার করা যায় না। খাতার যুক্তিটুকু একটি মোবাইল অ্যাপে লাগাতে চাইলে পুরো ফাইল থেকে কেটে আলাদা করতে হবে।

একটি বাড়ির কথা ভাবুন। রান্নাঘর, শোবার ঘর, বসার ঘর — প্রতিটি আলাদা ঘর, প্রতিটির একটি দরজা। মেহমান আসেন বসার ঘরে; রান্নাঘরের কোন তাকে লবণ আছে তা তাঁর জানার দরকার নেই। আপনি রান্নাঘরের তাক নতুন করে সাজালেও মেহমান টেরই পাবেন না। এক ফাইলের তিন হাজার লাইন যেন দেয়ালহীন একটি বাড়ি — চুলা, বিছানা, সোফা সব এক হলঘরে; পাঁচজন থাকতে শুরু করলেই বিশৃঙ্খলা।

সমাধান হলো প্রোগ্রামকে module-এ ভাগ করা। প্রতিটি মডিউল একটি নির্দিষ্ট দায়িত্ব নেয়, আর তার দুটি অংশ থাকে: বাইরের দরজা, অর্থাৎ interface — অন্যরা কী ব্যবহার করতে পারবে; আর ঘরের ভেতর, অর্থাৎ implementation — কাজটা আসলে কীভাবে হয়। পর্ব ২৩-এ শেখা separation of concerns এতদিন ছিল ফাংশনের স্তরে; এবার সেটি উঠে আসছে ফাইলের স্তরে।

মূল ধারণা

C-তে একটি মডিউল সাধারণত দুটি ফাইলের জোড়া: ledger.h = interface (দরজা — কোন টাইপ আর কোন ফাংশন পাওয়া যাবে) আর ledger.c = implementation (ঘরের ভেতর — ফাংশনগুলোর আসল কোড)। যে মডিউল খাতা ব্যবহার করবে, সে শুধু #include "ledger.h" লেখে; ledger.c সে কখনো দেখে না।

খুঁজে পাওয়া সহজ

মোটের হিসাবে ভুল? সোজা ledger.c খুলুন। ফাইলের নামই ঠিকানা।

নেভিগেশন

একসাথে কাজ

নাদিয়া ledger.c, রাফি report.c — আলাদা ফাইল, তাই সংঘাত কম।

টিমওয়ার্ক

দ্রুত build

একটি ফাইল বদলালে শুধু সেটিই আবার compile হয়, বাকিরা আগের .o নিয়ে বসে থাকে।

সময়

টেস্ট ও পুনর্ব্যবহার

খাতা-মডিউলকে আলাদা করে টেস্ট করা যায়, অন্য প্রোগ্রামেও হুবহু লাগানো যায়।

মান ও গতি

২ · খাতাকে তিন ফাইলে ভাগ — ledger.h, ledger.c, main.c

পর্ব ২৩-এ নাদিয়া প্রোগ্রামটিকে তিন স্তরে ভেবেছিলেন: UI (মেনু, প্রশ্ন, ছাপা), logic (ledger_ ফাংশনগুলো) আর ডেটা। সেই নকশাই এখন ফাইলের ভাগ ঠিক করে দেয় — আর logic স্তরে কোনো printf/scanf নেই বলেই ভাগটা এত পরিষ্কার।

প্রথম ফাইল — interface. এখানে আছে শুধু সেটুকু, যা অন্য ফাইলের জানা দরকার:

/* ledger.h - public interface of the ledger module */
#ifndef LEDGER_H
#define LEDGER_H

#include <stddef.h>                      /* for size_t */

typedef enum { PAY_CASH, PAY_BKASH, PAY_DUE } PayMethod;

typedef struct {
    char      item[32];
    int       qty;
    long      price;                       /* paisa: 72.50 Tk = 7250 */
    PayMethod pay;
} Sale;

typedef struct {
    Sale  *items;
    size_t count, capacity;
} Ledger;

void ledger_init(Ledger *lg);
int  ledger_add(Ledger *lg, Sale s);       /* 0 = ok, -1 = no memory */
long ledger_total(const Ledger *lg, int pay); /* pay = -1 means all */
void ledger_free(Ledger *lg);

#endif /* LEDGER_H */

দ্বিতীয় ফাইল — implementation. ফাংশনগুলোর আসল কোড, আর একটি নিজস্ব helper, যাকে বাইরের কেউ দেখতে পাবে না:

/* ledger.c - how the ledger actually works */
#include <stdlib.h>
#include "ledger.h"           /* own header: compiler checks they match */

static int grow(Ledger *lg)   /* private: invisible outside this file */
{
    size_t ncap = lg->capacity ? 2 * lg->capacity : 2;
    Sale *t = realloc(lg->items, ncap * sizeof *t);
    if (t == NULL) return -1;
    lg->items = t;
    lg->capacity = ncap;
    return 0;
}

void ledger_init(Ledger *lg) { lg->items = NULL; lg->count = lg->capacity = 0; }

int ledger_add(Ledger *lg, Sale s)
{
    if (lg->count == lg->capacity && grow(lg) != 0) return -1;
    lg->items[lg->count++] = s;
    return 0;
}

long ledger_total(const Ledger *lg, int pay)
{
    long sum = 0;
    for (size_t i = 0; i < lg->count; i++)
        if (pay < 0 || lg->items[i].pay == (PayMethod)pay)
            sum += lg->items[i].qty * lg->items[i].price;
    return sum;
}

void ledger_free(Ledger *lg) { free(lg->items); ledger_init(lg); }

তৃতীয় ফাইল — UI. এটি খাতা ব্যবহার করে, কিন্তু realloc বা grow সম্পর্কে কিছুই জানে না:

/* main.c - talks to the user, uses the ledger only through ledger.h */
#include <stdio.h>
#include "ledger.h"

static int  ui_menu(void);            /* private to main.c */
static void ui_add(Ledger *lg);
static void ui_totals(const Ledger *lg);

int main(void)
{
    Ledger lg;
    ledger_init(&lg);
    int choice;
    while ((choice = ui_menu()) != 0) {
        if (choice == 1) ui_add(&lg);
        else if (choice == 4) ui_totals(&lg);
        else puts("No such option.");
    }
    ledger_free(&lg);
    return 0;
}
/* ui_menu, ui_add, ui_totals: same as in lesson 23 */

খেয়াল করুন দুটি ছোট বিষয়। #include <stdio.h>-এ কোণ বন্ধনী মানে “system-এর ফোল্ডারে খোঁজো”, আর #include "ledger.h"-এ উদ্ধৃতিচিহ্ন মানে “আগে আমার প্রজেক্টের ফোল্ডারে খোঁজো”। আর ledger.c নিজের header নিজেই include করে — এতে header-এর prototype আর .c-এর সংজ্ঞায় কোনো অমিল থাকলে compiler সঙ্গে সঙ্গে ধরে ফেলে।

এবার build. পর্ব ০৩-এ দেখেছেন, -c মানে “শুধু .o পর্যন্ত যাও, link কোরো না”:

gcc -Wall -Wextra -g -c ledger.c     # ledger.c  -> ledger.o
gcc -Wall -Wextra -g -c main.c       # main.c    -> main.o
gcc main.o ledger.o -o shop          # linker joins them -> shop

কিন্তু main.c compile করার সময় compiler তো ledger.c দেখেইনি — তাহলে ledger_add ডাকাটা সে মানল কীভাবে? আর শেষে linker কীভাবে ঠিক ফাংশনটি খুঁজে পেল? নিচের ডায়াগ্রামে পুরো পথটি ধাপে ধাপে দেখুন:

  1. main.c#include "ledger.h"ডাকে ledger_add
  2. Translation unitmain.c + ledger.hpreprocessor জুড়েছে
  3. main.oU ledger_add“আমার দরকার”
  4. ledger.oT ledger_add“আমার কাছে আছে”
  5. Linkershopদুটি মিলিয়ে দিল
main.cযা আপনি লিখেছেন
#include <stdio.h>
#include "ledger.h"

int main(void) {
    Ledger lg;
    ledger_init(&lg);
    Sale s = { "Rice 1kg", 2, 7250, PAY_CASH };
    ledger_add(&lg, s);
    ...
gcc -E main.ccompiler আসলে এটুকুই দেখে
/* stdio.h থেকে আনা কয়েকশো লাইন ... */
typedef enum { PAY_CASH, PAY_BKASH, PAY_DUE } PayMethod;
typedef struct { ... } Sale;
typedef struct { ... } Ledger;
void ledger_init(Ledger *lg);
int  ledger_add(Ledger *lg, Sale s);   /* শুধু ঘোষণা, শরীর নেই */
...
int main(void) {
    ...
    ledger_add(&lg, s);
nm main.o (সংক্ষেপে)main.o-এর ভেতরের নামের তালিকা
U ledger_add     U = undefined: দরকার, কিন্তু আমার কাছে নেই
U ledger_free
U ledger_init
T main           T = text: এই ফাংশনের কোড আমার ভেতরে আছে
t ui_menu        ছোট হাতের t = static, শুধু এই ফাইলের
nm ledger.o (সংক্ষেপে)আলাদাভাবে compile হওয়া আরেকটি টুকরো
t grow           static: linker-এর কাছে অদৃশ্য
T ledger_add     এই সেই কোড, যা main.o খুঁজছে
T ledger_free
T ledger_init
T ledger_total
U free
U realloc        এগুলো আসবে C library থেকে
Terminalসব “দরকার” মিলে গেছে
$ gcc main.o ledger.o -o shop
main.o: U ledger_add  ⟷  ledger.o: T ledger_add  ✓
ledger.o: U realloc   ⟷  libc: realloc           ✓
$ ./shop
1) Add  2) List  4) Totals  0) Exit
Terminalledger.o দিতে ভুলে গেলে
$ gcc main.o -o shop
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x2b): undefined reference to `ledger_init'
main.c:(.text+0x5e): undefined reference to `ledger_add'
collect2: error: ld returned 1 exit status
  1. ধাপ ১ — ডাক দেওয়া। main.c খাতায় একটি বিক্রি যোগ করতে ledger_add ডাকে। কিন্তু এই ফাংশনের কোড অন্য ফাইলে। main.c-এর হাতে আছে শুধু একটি লাইন: #include "ledger.h".
  2. ধাপ ২ — Header জুড়ে দেওয়া। Preprocessor ledger.h-এর পুরো লেখা হুবহু main.c-এর ভেতরে বসিয়ে দেয়। এই জোড়া লাগানো পূর্ণ লেখার নাম translation unit — compiler একবারে ঠিক এটুকুই দেখে, এর বাইরে কিছু নয়। সেখানে ledger_add-এর prototype আছে, কিন্তু শরীর নেই।
  3. ধাপ ৩ — Compiler-এর বিশ্বাস। Prototype দেখে compiler যাচাই করে ডাকটি ঠিক আছে কি না — দুটি argument, প্রথমটি Ledger *, দ্বিতীয়টি Sale. তারপর সে main.o-তে একটি চিরকুট রেখে দেয়: “ledger_add দরকার, পরে কেউ জোগাড় করে দেবে।” Object file-এর এই নামগুলোকে বলে symbol.
  4. ধাপ ৪ — অন্য টুকরো। একেবারে আলাদাভাবে ledger.c compile হয়ে ledger.o হলো। এর ভেতরে ledger_add-এর আসল কোড আছে (বড় হাতের T). grow আছে ছোট হাতের t নিয়ে — static বলে সে শুধু এই ফাইলের, বাইরের কেউ তাকে জুড়তে পারবে না।
  5. ধাপ ৫ — Linker মেলায়। Linker সব .o-র “দরকার” তালিকা আর “আছে” তালিকা মিলিয়ে দেখে। main.o-এর U ledger_add মিলল ledger.o-এর T ledger_add-এর সাথে। সব মিলে গেলে তৈরি হয় shop.
  6. ধাপ ৬ — না মিললে। Link কমান্ডে ledger.o দিতে ভুলে গেলে “দরকার” তালিকার নামগুলোর জোড়া পাওয়া যায় না — আর আসে পর্ব ০৩-এর পরিচিত undefined reference. Compile ধাপ কিন্তু সফল হয়েছিল, কারণ prototype দেখে compiler সন্তুষ্ট ছিল। Header হলো প্রতিশ্রুতি; প্রতিশ্রুতি রক্ষা হলো কি না, যাচাই করে linker.
Header = প্রতিশ্রুতি (compiler যাচাই করে)
.c ফাইল = প্রতিশ্রুতি রক্ষা (linker যাচাই করে)

৩ · Header-এর নিয়ম — কী থাকবে, কী কখনো থাকবে না

একটি header অনেকগুলো .c ফাইলে include হয়। খাতার header ঢুকবে main.c-এ, রাফির report.c-এ, পরে হয়তো sms.c-তেও। তাই নিয়ম একটাই: header-এ এমন কিছু রাখা যাবে না, যা প্রতিটি ফাইলে কপি হলে সমস্যা করে। Declaration (“এই নামে একটি জিনিস আছে”) যত জায়গায় খুশি থাকতে পারে; কিন্তু definition (“এই সেই জিনিস, এর জন্য মেমরি নাও”) পুরো প্রোগ্রামে থাকবে ঠিক একবার।

জিনিসকোথায়কেন
ফাংশনের prototype — int ledger_add(Ledger *lg, Sale s);✅ headerশুধু ঘোষণা; বারবার থাকলেও ক্ষতি নেই
typedef, struct, enum-এর গঠন✅ headerটাইপ মেমরি নেয় না, শুধু নকশা
#define ধ্রুবক — #define MAX_ITEM 32✅ headerলেখা বদলানোর নির্দেশ মাত্র
extern int ledger_debug;✅ header“আছে, অন্য কোথাও” — ঘোষণা
ফাংশনের পূর্ণ definition (শরীরসহ)❌ শুধু .cদুটি ফাইলে ঢুকলে একই ফাংশনের দুটি কপি
ভ্যারিয়েবল — int ledger_debug = 0;❌ শুধু .cপ্রতিটি ফাইলে আলাদা ভ্যারিয়েবল তৈরি হতে চায়
static helper ও শুধু-নিজের ভ্যারিয়েবল❌ শুধু .cএগুলো ইচ্ছা করেই গোপন — দরজায় টাঙানোর কিছু নয়

ভুলটি করলে কী হয়? ধরুন রাফি সুবিধার জন্য header-এ লিখে দিলেন int ledger_debug = 0;. main.c আর report.c — দুটিই header include করে, তাই দুটি .o-তেই একটি করে ledger_debug তৈরি হলো। প্রতিটি ফাইল আলাদাভাবে compile হয়, তাই compiler কিছুই টের পায় না। ধরা পড়ে link করার সময়:

/usr/bin/ld: report.o:(.bss+0x0): multiple definition of `ledger_debug'; main.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status

ইচ্ছা করে ভাগ করা global — extern

পর্ব ১৩-এ দেখেছেন, global ভ্যারিয়েবল যত কম, তত ভালো। তবু কখনো সত্যিই একটি মান সব মডিউলের দেখা দরকার — যেমন “debug বার্তা ছাপব কি না”। সঠিক নিয়ম: header-এ extern দিয়ে ঘোষণা, আর ঠিক একটি .c ফাইলে আসল সংজ্ঞা।

/* in ledger.h : a promise, no memory */
extern int ledger_debug;

/* in ledger.c : the one and only real variable */
int ledger_debug = 0;

ফাইলের নিজস্ব জিনিস — static

উল্টো দিকে, grow-এর মতো helper বা ui_menu-এর মতো ফাংশন অন্য কারও জানার দরকার নেই। পর্ব ১৩-এর static ও extern অংশে দেখেছেন, ফাংশনের বাইরে static লিখলে নামটি শুধু সেই ফাইলের হয়ে যায় — একে বলে internal linkage. এতে দুটি লাভ: অন্য ফাইল ভুল করে এটিকে ডাকতে পারে না, আর দুটি মডিউলে একই নামের helper (যেমন দুই ফাইলেই static int grow) থাকলেও কোনো সংঘাত হয় না।

অভ্যাস করে নিন: .c ফাইলের প্রতিটি ফাংশন আর ফাইল-স্তরের ভ্যারিয়েবল ডিফল্টভাবে static লিখুন। Header-এ যেটি দেওয়ার সিদ্ধান্ত নেবেন, শুধু সেটি থেকে static সরান। দরজা খোলা রাখা একটি সচেতন সিদ্ধান্ত হওয়া উচিত, ভুলবশত নয়।

একই header দুবার ঢুকলে — include guard

রাফির report.h-ও ledger.h include করে (রিপোর্টে Ledger টাইপ লাগে)। এখন main.c যদি দুটিই include করে, তাহলে ledger.h-এর লেখা ঢুকবে দুবার — আর একই typedef struct {…} Sale; দুবার দেখে compiler বলবে redefinition. এর সমাধান পর্ব ২৭-এর preprocessor অংশ থেকে আপনার চেনা: include guard. ledger.h-এর প্রথম দুই লাইন আর শেষ লাইন আবার দেখুন:

#ifndef LEDGER_H      /* first time: LEDGER_H is not defined, so enter */
#define LEDGER_H      /* now it is defined */
/* ... types and prototypes ... */
#endif                /* second time: the whole body is skipped */

অনেক compiler #pragma once-ও বোঝে — এক লাইনেই একই কাজ। প্রধান compiler-গুলো মানে, তবে এটি C standard-এর অংশ নয়। যেটিই নিন, প্রতিটি header-এ একটি guard থাকা বাধ্যতামূলক অভ্যাস।

বৃত্তাকার include থেকে সাবধান। ledger.h যদি report.h include করে, আর report.h include করে ledger.h — guard থাকায় অসীম লুপ হবে না, কিন্তু একটি ফাইল অন্যটির টাইপ দেখার আগেই ব্যবহার করতে চাইবে, আর আসবে unknown type name. সমাধান: নির্ভরতা এক দিকে রাখুন (রিপোর্ট খাতার ওপর নির্ভর করে, খাতা রিপোর্টের ওপর নয়)। শুধু পয়েন্টার লাগলে পুরো header না টেনে একটি forward declaration লিখুন: typedef struct Ledger Ledger;

৪ · Information hiding — ভেতরটা লুকিয়ে রাখা

অংশ ১-এর সেই সমস্যাটি মনে করুন: রাফির রিপোর্ট-কোড সরাসরি lg.count বদলে দিয়েছিল। ফাইল ভাগ করার পরও এটি সম্ভব! কারণ Ledger struct-এর পুরো গঠন ledger.h-এ লেখা আছে — যে-ই header include করে, সে-ই items, count, capacity দেখতে ও বদলাতে পারে। দরজা আছে, কিন্তু দেয়ালে কাচ লাগানো আর জানালা খোলা।

আরও বড় সমস্যা: নাদিয়া খাতাকে অ্যারে থেকে linked list-এ (পর্ব ৩৩) বদলাতে চাইলে, যে দশটি ফাইল সরাসরি lg.items[i] পড়ে, দশটিই বদলাতে হবে।

১৯৭২ সালে David Parnas একটি প্রবন্ধ লেখেন — “On the Criteria To Be Used in Decomposing Systems into Modules”. তাঁর যুক্তি ছিল: প্রোগ্রামকে কাজের ধাপ অনুযায়ী ভাগ করবেন না; ভাগ করুন সিদ্ধান্ত অনুযায়ী — যে সিদ্ধান্ত ভবিষ্যতে বদলাতে পারে (যেমন “ডেটা কীভাবে সাজানো থাকবে”), সেটি একটি মডিউলের ভেতরে লুকিয়ে রাখুন। এই নীতির নাম information hiding. পঞ্চাশ বছর পরও সফটওয়্যার নকশার এটি অন্যতম ভিত্তি।

C-তে এই লুকানোর কৌশলের নাম opaque pointer (অস্বচ্ছ পয়েন্টার)। Header-এ struct-এর শুধু নাম থাকে, গঠন থাকে .c ফাইলে। বদলটা দেখুন:

/* BEFORE: everyone sees and can touch the fields
typedef struct { Sale *items; size_t count, capacity; } Ledger; */

/* AFTER: only the name is public */
typedef struct Ledger Ledger;

Ledger *ledger_new(void);                   /* NULL if no memory   */
void    ledger_delete(Ledger *lg);
int     ledger_add(Ledger *lg, Sale s);
long    ledger_total(const Ledger *lg, int pay);
size_t  ledger_count(const Ledger *lg);     /* read-only access    */
const Sale *ledger_get(const Ledger *lg, size_t i);
#include <stdlib.h>
#include "ledger.h"

struct Ledger {                /* the full shape lives ONLY here */
    Sale  *items;
    size_t count, capacity;
};

Ledger *ledger_new(void)    { return calloc(1, sizeof(Ledger)); }
void    ledger_delete(Ledger *lg) { if (lg) free(lg->items); free(lg); }
size_t  ledger_count(const Ledger *lg) { return lg->count; }

typedef struct Ledger Ledger; লাইনটি compiler-কে বলে: “struct Ledger নামে একটি টাইপ আছে।” কিন্তু তার ভেতরে কী আছে বা আকার কত, তা বলে না। এমন টাইপকে বলে incomplete type. এর পয়েন্টার রাখা যায় (পয়েন্টারের আকার সবসময় জানা), কিন্তু এর field পড়া বা এর ভ্যারিয়েবল বানানো যায় না। ধাপে ধাপে দেখুন main.c কী পারে আর কী পারে না:

lg->count = 0;field-এ সরাসরি হাত
Ledger lg;stack-এ নিজের খাতা
Ledger *lg = ledger_new();দরজা দিয়ে চাওয়া
typedef struct Ledger Ledger;শুধু নাম
ledger_new / add / count / deleteদরজার হাতল
struct Ledger { … };পূর্ণ গঠন — শুধু ledger.c জানে
+ long cash_total;নতুন field, বাইরে কেউ টের পায় না
কে কী দেখেmain.c ↔ ledger.h ↔ ledger.c
main.c   →  দেখে: struct Ledger নামে কিছু একটা আছে
            জানে না: আকার কত, ভেতরে কী
ledger.c →  দেখে: struct Ledger { Sale *items; size_t count, capacity; }
gcc -c main.cfield-এ হাত দেওয়ার চেষ্টা
main.c:14:7: error: invalid use of incomplete typedef 'Ledger'
   14 |     lg->count = 0;
      |       ^~
(পুরোনো GCC-তে: dereferencing pointer to incomplete type)
gcc -c main.cনিজে একটি খাতা বানানোর চেষ্টা
main.c:9:12: error: storage size of 'lg' isn't known
    9 |     Ledger lg;
      |            ^~
আকার না জানলে কত byte জায়গা নেবে, compiler বুঝবে কীভাবে?
main.cসঠিক পথ — শুধু দরজা দিয়ে
Ledger *lg = ledger_new();          /* খাতা বানায় ledger.c */
if (lg == NULL) return 1;
ledger_add(lg, s);
printf("%zu sales\n", ledger_count(lg));
ledger_delete(lg);                  /* ফেরতও দেয় ledger.c */
ledger.c বদলালmain.c-এর একটি অক্ষরও বদলায়নি
struct Ledger {
    Sale  *items;
    size_t count, capacity;
    long   cash_total;            /* নতুন: মোট আগেই হিসাব করা */
};
$ make
gcc -Wall -Wextra -g -c ledger.c   ← শুধু এটি
gcc main.o ledger.o report.o -o shop
  1. দরজায় শুধু নাম। ledger.h-এ আছে typedef struct Ledger Ledger; — একটি নাম, গঠন নয়। main.c জানে “খাতা” নামে কিছু আছে; কয়টি field, কোন ক্রমে, কত byte — কিছুই জানে না। পূর্ণ গঠন আছে শুধু ledger.c-তে।
  2. Field-এ হাত দেওয়া — বন্ধ। রাফির পুরোনো অভ্যাস: lg->count = 0;. এখন compiler থামিয়ে দেয়, কারণ count নামে কোনো field আছে কি না তা সে জানেই না। আগে যে ভুল নীরবে হিসাব নষ্ট করত, এখন সেটি compile-এর সময়ই ধরা পড়ে।
  3. নিজে খাতা বানানো — বন্ধ। Ledger lg; লিখলে compiler-কে stack-এ জায়গা নিতে হবে, কিন্তু আকারই অজানা। তাই খাতা বানানোর দায়িত্বও মডিউলের: ledger_new() heap-এ জায়গা নিয়ে পয়েন্টার দেয়। এটি opaque pointer-এর একটি দাম — প্রতিটি খাতার জন্য একটি malloc/calloc.
  4. সঠিক পথ — দরজার হাতল। পয়েন্টার রাখা যায়, কারণ পয়েন্টারের আকার সবসময় জানা। সব কাজ হয় interface-এর ফাংশন দিয়ে: যোগ, গোনা, পড়া, মুছে ফেলা। পড়ার জন্য ledger_count আছে, কিন্তু count সরাসরি বদলানোর কোনো ফাংশন নেই — তাই সংখ্যাটি কখনো ভুল হতে পারে না।
  5. পুরস্কার — ভেতরটা স্বাধীন। নাদিয়া struct-এ cash_total যোগ করলেন। main.c বা report.c-এর কোনো লাইন বদলাতে হলো না, কারণ তারা কখনো field দেখেইনি। শুধু ledger.c আবার compile আর link — ঠিক এটাই Parnas-এর প্রতিশ্রুতি।

এবার নিজে চালিয়ে দেখুন। অনলাইন compiler একটিমাত্র ফাইল নেয়, তাই নিচে তিনটি “ফাইল” একটি ফাইলে পরপর রাখা হয়েছে — আর কৌশলটি হলো main-কে রাখা হয়েছে struct-এর গঠনের আগে. ফলে main লেখার মুহূর্তে compiler সত্যিই শুধু নামটি জানে, ঠিক আলাদা main.c-এর মতো:

#include <stdio.h>
#include <stdlib.h>
/* ---- ledger.h: the only part main() can see ---- */
typedef struct Ledger Ledger;               /* opaque: name only, no fields */
Ledger *ledger_new(void);
void    ledger_add(Ledger *lg, long paisa);
long    ledger_total(const Ledger *lg);
void    ledger_delete(Ledger *lg);
int main(void) {                            /* ---- main.c ---- */
    Ledger *lg = ledger_new();
    if (lg == NULL) return 1;
    ledger_add(lg, 14500); ledger_add(lg, 15600);
    printf("Total: %ld paisa\n", ledger_total(lg));
    /* printf("%d\n", lg->count); */          /* try: remove the comment marks */
    ledger_delete(lg);
    return 0;
}
struct Ledger { long sum; int count; };     /* ---- ledger.c: hidden ---- */
Ledger *ledger_new(void) { return calloc(1, sizeof(Ledger)); }
void ledger_add(Ledger *lg, long p) { lg->sum += p; lg->count++; }
long ledger_total(const Ledger *lg) { return lg->sum; }
void ledger_delete(Ledger *lg) { free(lg); }

আউটপুট আসবে Total: 30100 paisa — অর্থাৎ ৩০১ টাকা, পর্ব ২৩-এর সেই দুটি বিক্রির যোগফল। এবার ১৪ নম্বর লাইনের /* আর */ মুছে আবার চালান: compiler বলবে invalid use of incomplete typedef 'Ledger'. main struct-এর গঠন দেখার আগেই লেখা, তাই count বলে কিছু আছে কি না সে জানে না। calloc ব্যবহার করা হয়েছে কারণ সে মেমরি শূন্য দিয়ে ভরে দেয় — তাই sum আর count শুরুতেই ০, আলাদা init লাগে না। আর ledger_new NULL দিলে main থেমে যায় — মেমরি না পাওয়ার সম্ভাবনা interface-এই লেখা আছে।

এই কৌশল আপনি প্রথম দিন থেকেই ব্যবহার করছেন, হয়তো খেয়াল করেননি। পর্ব ২৪-এর FILE *fp = fopen(...) — FILE-এর ভেতরে কী আছে, C standard কখনো বলেনি। প্রতিটি অপারেটিং সিস্টেমের library নিজের মতো করে সাজায়, আর আপনি শুধু fopen, fgets, fclose ব্যবহার করেন। তাই একই কোড Windows আর Linux দুই জায়গাতেই চলে।

৫ · Make — শুধু যা বদলেছে, তা-ই আবার বানানো

অংশ ২-এ তিনটি কমান্ড হাতে লিখে build করেছিলেন। রাফির report.c যোগ হলে চারটি, SMS যোগ হলে পাঁচটি। আর প্রতিবার ভাবতে হবে: “কোনটি বদলেছি? কোনটি আবার compile করতে হবে?” একটি ভুলে যাওয়া মানে পুরোনো .o দিয়ে তৈরি প্রোগ্রাম — যেখানে আপনার নতুন সংশোধনটাই নেই। এই হিসাবের কাজটি মানুষের নয়, একটি build system-এর। সবচেয়ে পুরোনো আর এখনো বহুল ব্যবহৃত build system হলো Make.

Make পড়ে একটি ফাইল — Makefile. এতে প্রতিটি নিয়মের তিনটি অংশ:

🎯 Target

Target — যা বানাতে চান। কোলনের বাঁ পাশে: ledger.o:

কী বানাব

🧱 Prerequisite

Prerequisite — যা থেকে বানানো হয়। কোলনের ডান পাশে: ledger.c ledger.h

কী লাগবে

🛠️ Recipe

Recipe — বানানোর কমান্ড। পরের লাইনে, TAB দিয়ে শুরু: gcc -c ledger.c

কীভাবে বানাব

দোকানের খাতা প্রজেক্টের একটি ছোট কিন্তু আসল Makefile দেখুন (রাফির report.c আর report.h-সহ):

# Makefile - shop ledger  (every recipe line starts with a TAB)
CC     = gcc
CFLAGS = -Wall -Wextra -g
OBJS   = main.o ledger.o report.o

shop: $(OBJS)
	$(CC) $(OBJS) -o shop

main.o: main.c ledger.h report.h
	$(CC) $(CFLAGS) -c main.c

ledger.o: ledger.c ledger.h
	$(CC) $(CFLAGS) -c ledger.c

report.o: report.c report.h ledger.h
	$(CC) $(CFLAGS) -c report.c

clean:
	rm -f shop $(OBJS)

.PHONY: clean

লাইন ধরে পড়ুন। উপরের তিনটি লাইন ভ্যারিয়েবল — $(CFLAGS) লিখলে সেখানে বসে যায় -Wall -Wextra -g, পর্ব ০৩-এর সেই প্রতিদিনের flag. পরের নিয়মগুলো বলছে: shop তৈরি হয় তিনটি .o থেকে; প্রতিটি .o তৈরি হয় তার .c আর যে header-গুলো সে include করে, সেগুলো থেকে। clean কোনো ফাইল নয়, একটি কাজের নাম — তাই .PHONY দিয়ে Make-কে জানিয়ে রাখা হলো। টার্মিনালে শুধু make লিখলে Make প্রথম target (shop) বানাতে চেষ্টা করে।

TAB, space নয়! Recipe লাইনের শুরুতে অবশ্যই একটি TAB অক্ষর থাকতে হবে। চারটি space দেখতে হুবহু এক, কিন্তু Make মানবে না:

Makefile:7: *** missing separator.  Stop.

অনেক editor TAB চাপলে নিজে থেকেই space বসিয়ে দেয়, আর AI বা ওয়েবসাইট থেকে কপি করলেও প্রায়ই TAB হারিয়ে যায়। VS Code-এ নিচের status bar-এ “Spaces: 4” লেখা থাকলে Makefile-এর জন্য সেটি বদলে নিন। ওপরের “কপি” বোতাম দিয়ে নিলে আসল TAB-ই পাবেন।

Make কীভাবে ঠিক করে কী আবার বানাতে হবে

Make-এর পুরো বুদ্ধি একটি সহজ প্রশ্নে: “target কি তার কোনো prerequisite-এর চেয়ে পুরোনো?” প্রতিটি ফাইলের সাথে অপারেটিং সিস্টেম শেষ বদলানোর সময় (timestamp) রাখে। Prerequisite target-এর চেয়ে নতুন হলে বুঝতে হবে target বানানোর পর সেটি বদলেছে — তাই আবার বানাও। না হলে বাদ। ধরুন সকাল ১০টায় সব build করা ছিল, তারপর ১০:৩০-এ আপনি শুধু ledger.c-তে একটি সংশোধন করে সেভ করলেন। এবার make:

main.c
ledger.c
report.c
ledger.h · report.h
main.o
ledger.o
report.o
shop
Terminal + make-এর হিসাবধূসর লাইন = make মনে মনে যা ভাবছে

            
  1. শুরুর অবস্থা। সকাল ১০টায় সব source লেখা, ১০:০৫-এ সব .o, ১০:০৬-এ shop. প্রতিটি target তার prerequisite-এর চেয়ে নতুন। এখন make চালালে সে বলে “up to date” — কিছুই করার নেই।
  2. আপনি শুধু ledger.c বদলালেন। ১০:৩০-এ সেভ করলেন। অপারেটিং সিস্টেম ফাইলটির timestamp বদলে দিল। এবার make লিখলেন। Make-এর লক্ষ্য প্রথম target shop; কিন্তু তার আগে সে shop-এর প্রতিটি prerequisite একে একে পরীক্ষা করবে।
  3. main.o — বাদ। main.o (১০:০৫) তার তিনটি prerequisite — main.c, ledger.h, report.h (সব ১০:০০) — এর চেয়ে নতুন। মানে main.o বানানোর পর এগুলোর কোনোটি বদলায়নি। Compile বাদ। প্রথম ধাপটি বাঁচল।
  4. ledger.o — আবার বানাও। ledger.c (১০:৩০) এখন ledger.o (১০:০৫)-এর চেয়ে নতুন। Make recipe চালাল: gcc -Wall -Wextra -g -c ledger.c. নতুন ledger.o-এর সময় হলো ১০:৩১.
  5. report.o — বাদ। report.c, report.h, ledger.h — কোনোটিই বদলায়নি। দ্বিতীয় ধাপটিও বাঁচল। খেয়াল করুন, আমরা বদলেছি ledger.c, header নয় — তাই header-এর ওপর নির্ভরশীল ফাইলগুলোও নিরাপদে বাদ পড়ল।
  6. shop — আবার link. ledger.o (১০:৩১) এখন shop (১০:০৬)-এর চেয়ে নতুন, তাই link করতেই হবে। মোট চলল দুটি কমান্ড — একটি compile আর একটি link; দুটি compile বাদ পড়ল। তিন ফাইলে লাভ সামান্য, কিন্তু হাজার ফাইলের প্রজেক্টে এটাই ঘণ্টাকে সেকেন্ডে নামিয়ে আনে। এর নাম incremental build.

Prerequisite ভুলে গেলে Make চুপচাপ ভুল করে। ধরুন main.o-এর লাইনে ledger.h লিখতে ভুলে গেলেন। এরপর Sale struct-এ একটি field যোগ করলেন। Make দেখবে main.c বদলায়নি, তাই main.o পুরোনো আকারের Sale নিয়েই থেকে যাবে, আর ledger.o জানবে নতুন আকার। কোনো error নেই — শুধু অদ্ভুত, ভুল ফল। এমন সন্দেহ হলে make clean তারপর make দিয়ে সব নতুন করে বানান। বড় প্রজেক্টে GCC-র -MMD -MP flag দিয়ে compiler নিজেই প্রতিটি ফাইলের header-তালিকা লিখে দেয়, যাতে হাতে লিখতে না হয়।

প্রতিদিনের Make কমান্ড

কমান্ডকী করে
makeপ্রথম target (shop) বানায় — শুধু যা দরকার
make ledger.oশুধু একটি নির্দিষ্ট target বানায়
make cleanতৈরি করা সব ফাইল মুছে দেয় — পরের make সব নতুন করে বানাবে
make -j4একসাথে সর্বোচ্চ ৪টি recipe চালায়। main.o, ledger.o, report.o একে অপরের ওপর নির্ভর করে না, তাই তিনটি একসাথে compile হতে পারে
make -nকী কী চালাবে শুধু দেখায়, আসলে চালায় না — শেখার জন্য চমৎকার

বড় প্রজেক্টে প্রতিটি .o-এর আলাদা নিয়মের বদলে একটি pattern rule লেখা হয়: %.o: %.c, যার recipe-তে $< মানে প্রথম prerequisite আর $@ মানে target. AI-এর Makefile-এ দেখলে চিনতে পারলেই যথেষ্ট।

Make লিখেছিলেন Stuart Feldman, ১৯৭৬ সালে Bell Labs-এ — যেখানে C আর Unix-এরও জন্ম। আর TAB-এর গল্প? Feldman পরে বলেছিলেন, শুরুতে সহজ কিছু একটা করেছিলেন বলে TAB বেছে নেন; কয়েক সপ্তাহের মধ্যে প্রায় এক ডজন ব্যবহারকারী হয়ে যায়, তাদের Makefile ভাঙতে চাননি — আর সেই সিদ্ধান্ত আজও বহাল। একটি ছোট সিদ্ধান্ত কীভাবে পঞ্চাশ বছরের interface হয়ে যায়, এটি তার সেরা উদাহরণ।

CMake — সব প্ল্যাটফর্মের জন্য একটি বর্ণনা

Makefile মূলত Unix-ধাঁচের পরিবেশের জন্য; Windows-এর Visual Studio নিজের ধরনের প্রজেক্ট ফাইল চায়। বড় C ও C++ প্রজেক্ট তাই প্রায়ই CMake ব্যবহার করে। CMake নিজে build করে না — আপনি একবার প্রজেক্টের বর্ণনা লেখেন, আর CMake প্রতিটি প্ল্যাটফর্মের জন্য উপযুক্ত build ফাইল (Makefile, Ninja ফাইল বা Visual Studio প্রজেক্ট) তৈরি করে দেয়। একে বলে build generator:

cmake_minimum_required(VERSION 3.13)
project(shop_ledger C)

add_library(ledger STATIC src/ledger.c src/report.c)
target_include_directories(ledger PUBLIC include)

add_executable(shop src/main.c)
target_link_libraries(shop PRIVATE ledger)
cmake -S . -B build        # read CMakeLists.txt, generate build files in build/
cmake --build build        # run the real build (make, ninja, or MSVC)

CMake header-এর নির্ভরতা নিজেই হিসাব রাখে। তবু শুরুতে Make শিখুন — ভেতরের ধারণাটি সেখানেই সবচেয়ে পরিষ্কার।

৬ · Library — মডিউলকে প্যাকেট করে বিলি করা

পাশের দোকানের জন্য আরেকজন প্রোগ্রামার একটি SMS-রিপোর্ট টুল বানাতে চান, যেটিও খাতা ব্যবহার করবে। সোর্স কোড কপি করে দিলে আপনার পরের সংশোধন তাঁর কাছে পৌঁছাবে না। সাধারণ পথ হলো মডিউলকে একটি library বানিয়ে দেওয়া — compile করা কোড আর সাথে header. পর্ব ০৩-এ দেখেছেন library দুইভাবে জোড়া যায়: static আর dynamic. এবার নিজে বানিয়ে দেখি:

  1. arlibledger.a.o-গুলো এক বান্ডিলে
  2. Static link-lledgerকোড কপি হয় shop-এ
  3. Sharedlibledger.soশুধু নাম লেখা থাকে
  4. চালানোর সময়loaderখুঁজে এনে জোড়ে
  5. আপডেটv1 → v2সুবিধা আর ঝুঁকি
Terminalstatic library বানানো
$ gcc -Wall -Wextra -g -c ledger.c report.c
$ ar rcs libledger.a ledger.o report.o
$ ar t libledger.a          # ভেতরে কী আছে?
ledger.o
report.o
Terminal-L কোথায় খুঁজবে, -l কী খুঁজবে
$ gcc main.c -L. -lledger -o shop
  -L.       → library খোঁজার তালিকায় এই ফোল্ডারও যোগ করো
  -lledger  → "lib" + ledger + ".so" বা ".a" নামের ফাইল খোঁজো
$ rm libledger.a
$ ./shop                    # তবু চলে — কোড shop-এর ভেতরে ঢুকে গেছে
Terminalshared library বানানো
$ gcc -fPIC -Wall -Wextra -g -c ledger.c report.c
$ gcc -shared -o libledger.so ledger.o report.o
$ gcc main.c -L. -lledger -o shop
$ ldd ./shop
    libledger.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
Terminalপ্রোগ্রাম খোলার মুহূর্তে
$ ./shop
./shop: error while loading shared libraries: libledger.so:
cannot open shared object file: No such file or directory
$ LD_LIBRARY_PATH=. ./shop   # loader-কে বলা হলো এই ফোল্ডারেও খোঁজো
1) Add  2) List  4) Totals  0) Exit
libledger.so v2এক ফাইল বদলালে সবাই বদলায়
✓ বাগ ঠিক করা ledger_total  → shop, sms_report … সবাই পরের বার খুললেই পায়
✗ Sale-এ নতুন field যোগ       → পুরোনো shop ধরে নেয় Sale 56 byte,
                               নতুন library ধরে নেয় 64 byte → ভুল হিসাব
সমাধান: ভেতরটা opaque রাখা, নইলে নতুন version নম্বর (libledger.so.2)
  1. ar দিয়ে বান্ডিল। Static library আসলে কয়েকটি .o ফাইলের একটি আর্কাইভ — অনেকটা zip-এর মতো। ar rcs মানে: ফাইলগুলো ঢোকাও বা বদলাও (r), আর্কাইভ না থাকলে বানাও (c), আর linker-এর জন্য সূচি লেখো (s). নামের নিয়ম: শুরুতে lib, শেষে .a (Windows-এর MSVC-তে .lib).
  2. Static link. -L. বলে “library এই ফোল্ডারেও খোঁজো”, আর -lledger বলে “libledger নামের library জোড়ো” — lib আর শেষাংশ linker নিজেই যোগ করে। -l সবসময় সেই ফাইলগুলোর পরে লিখুন, যাদের এটি লাগে। Linker শুধু দরকারি অংশ shop-এর ভেতরে কপি করে; এরপর .a মুছে ফেললেও প্রোগ্রাম চলে।
  3. Shared library. -fPIC দিয়ে এমন কোড বানানো হয় যা মেমরির যেকোনো ঠিকানায় বসে চলতে পারে, তারপর -shared দিয়ে তৈরি হয় shared library — Linux-এ .so, Windows-এ .dll, Mac-এ .dylib. এবার link করার সময় shop-এ কোড কপি হয় না, শুধু লেখা থাকে “আমার libledger.so লাগবে”। একই ফোল্ডারে .a আর .so দুটিই থাকলে GCC সাধারণত .so-কেই বেছে নেয়।
  4. চালানোর মুহূর্তে জোড়া। প্রোগ্রাম খোলার সময় অপারেটিং সিস্টেমের loader library-টি খুঁজে এনে জোড়ে। সে খোঁজে নির্দিষ্ট system ফোল্ডারে; আপনার প্রজেক্ট-ফোল্ডারে নয়। তাই পর্ব ০৩-এর “.dll not found”-এর Linux সংস্করণ দেখা গেল। পরীক্ষার জন্য LD_LIBRARY_PATH দিয়ে ফোল্ডার জানানো যায়; আসল ইনস্টলে library যায় system-এর library ফোল্ডারে।
  5. আপডেট — সুবিধা ও ঝুঁকি। ledger_total-এ একটি বাগ ঠিক করে নতুন .so দিলেন — যত প্রোগ্রাম এটি ব্যবহার করে, সবাই নতুন করে compile ছাড়াই সংশোধন পেয়ে গেল। কিন্তু Sale struct-এ একটি field যোগ করলে পুরোনো shop এখনো পুরোনো আকার ধরে নিয়ে মেমরি পড়বে — নতুন library-র সাথে তার হিসাব মিলবে না। এটাই ABI ভাঙা।
প্রশ্নStatic (.a)Shared (.so / .dll)
কোড কোথায় থাকেপ্রতিটি প্রোগ্রামের ভেতরে কপিএকটি আলাদা ফাইলে, সবাই ভাগ করে
প্রোগ্রাম অন্য কম্পিউটারে নিলেএকা চলেlibrary-ও সাথে লাগবে
বাগ ঠিক করলেপ্রতিটি প্রোগ্রাম আবার link করতে হয়শুধু library ফাইল বদলালেই হয়
বড় ঝুঁকিপুরোনো, অনিরাপদ কপি বহু প্রোগ্রামে থেকে যাওয়াABI ভেঙে পুরোনো প্রোগ্রাম নষ্ট হওয়া

ABI — compile হয়ে যাওয়া কোডের চুক্তি

Header হলো সোর্স কোডের স্তরে চুক্তি (API): কোন নাম, কোন টাইপ। কিন্তু compile হওয়ার পর আরেকটি চুক্তি তৈরি হয়, যাকে বলে ABI — struct-এর আকার কত byte, কোন field কত নম্বর byte থেকে শুরু, ফাংশনে argument কোন register-এ যায়, symbol-এর নাম কী। ধরুন ৬৪-bit Linux-এ পুরোনো shop compile হয়েছিল এই ধারণায় যে Sale ৫৬ byte, আর pay field শুরু হয় ৪৮ নম্বর byte থেকে। নতুন library-তে price-এর পরে একটি long discount যোগ হলো — এখন Sale ৬৪ byte, আর pay সরে গেছে ৫৬ নম্বর byte-এ। পুরোনো প্রোগ্রাম সেটা জানে না, তাই ভুল জায়গা থেকে ভুল মান পড়ে — কোনো error ছাড়াই। তাই shared library-র লেখকেরা খুব সাবধানে ABI রক্ষা করেন: struct-কে opaque রাখেন (তখন আকার শুধু library জানে), পুরোনো ফাংশন না বদলে নতুন ফাংশন যোগ করেন, আর ABI ভাঙতেই হলে library-র নামে নতুন version নম্বর দেন (যেমন libledger.so.1 থেকে libledger.so.2), যাতে পুরোনো প্রোগ্রাম পুরোনো version-ই খোঁজে।

৭ · প্রজেক্টের ফোল্ডার, README আর git

ফাইল বাড়তে থাকলে একটি ফোল্ডারে সব ফেলে রাখাও আরেক রকম এলোমেলো। পেশাদার C প্রজেক্টে একটি প্রচলিত সাজানো আছে; যেকোনো GitHub প্রজেক্ট খুললে প্রায় এরকমই দেখবেন:

shop-ledger/
├── README.md          কী, কীভাবে build, কীভাবে চালাবেন
├── Makefile
├── .gitignore         build/ যেন git-এ না যায়
├── include/
│   ├── ledger.h       public interface — বাইরের জগৎ শুধু এটুকু দেখে
│   └── report.h
├── src/
│   ├── ledger.c
│   ├── report.c
│   └── main.c
├── tests/
│   └── test_ledger.c  ledger মডিউলের নিজস্ব টেস্ট
└── build/             .o, libledger.a, shop — তৈরি হওয়া জিনিস

📂 include/ ও src/

Header আলাদা থাকলে বোঝা যায় কোনটি দরজা আর কোনটি ভেতর। compile-এর সময় -Iinclude flag বলে দেয় header কোথায় খুঁজতে হবে।

interface বনাম implementation

🏗️ build/

যা কিছু তৈরি হয়, সব এক জায়গায়। পরিষ্কার করতে পুরো ফোল্ডার মুছলেই হয়, আর সোর্সের সাথে মিশে যায় না।

তৈরি হওয়া জিনিস

🧪 tests/

খাতা-মডিউলে কোনো printf/scanf নেই, তাই কীবোর্ড ছাড়াই একটি ছোট প্রোগ্রাম দিয়ে unit test করা যায়। মডিউল ভাগের এটি বড় পুরস্কার।

মান যাচাই

📝 README.md

প্রজেক্টের প্রথম পাতা: এটি কী করে, কী লাগবে, কীভাবে build ও চালাতে হয়, ফোল্ডারগুলো কী। ছয় মাস পরের আপনিও এর পাঠক।

দলিল

এবার পুরো প্রজেক্টকে git-এর অধীনে আনুন। মডিউল ভাগের পর git আরও কাজের — নাদিয়া src/ledger.c আর রাফি src/report.c বদলালে তাঁদের পরিবর্তন আলাদা ফাইলে, তাই merge প্রায় সবসময় নিজে থেকেই হয়ে যায়। একটি নিয়ম মনে রাখুন: git-এ রাখুন যা মানুষ লেখে (source, header, Makefile, README), রাখবেন না যা মেশিন বানায় (.o, .a, shop).

git init
echo "build/" > .gitignore
git add README.md Makefile .gitignore include src tests
git commit -m "Split ledger into modules: ledger, report, main"

Branch, merge, pull request আর দলগতভাবে কাজের পুরো পদ্ধতি শিখতে দেখুন ABCL TECH-এর Software Engineering & Git কোর্স। Module ভাগ আর git — দুটি একসাথেই দলগত কাজের ভিত্তি।

৮ · AI-এর যুগে মডুলার প্রোগ্রাম

AI-কে “একটি দোকানের হিসাবের প্রোগ্রাম লেখো” বললে প্রায় সবসময় একটি লম্বা ফাইল আসে — সব ফাংশন, সব global, একটি বিশাল main. একবার চালানোর জন্য ঠিক আছে; কিন্তু সেটিই যদি প্রজেক্টের ভিত হয়, ছয় মাস পর আপনি সেই দেয়ালহীন বাড়িতে আটকে যাবেন। আর “ফাইলে ভাগ করো” বললে সে প্রায়ই এমন ভুল করে, যা ধরা পড়ে শুধু link বা build-এর সময়।

AI সাধারণত যেসব মডিউল-ভুল করে

AI-এর ভুলআপনি যা দেখবেনঠিক উপায়
সব কোড একটি ফাইলে, অনেক globalচলে, কিন্তু বদলানো ও টেস্ট করা কঠিনদায়িত্ব অনুযায়ী মডিউল আর পরিষ্কার interface চান
Header-এ ভ্যারিয়েবল বা ফাংশনের সংজ্ঞাmultiple definition of (link-এর সময়)Header-এ extern বা prototype, সংজ্ঞা একটি .c-তে
Include guard নেইredefinition of struct বা typedefপ্রতিটি header-এ #ifndef guard
দুটি header একে অপরকে include করেunknown type nameনির্ভরতা এক দিকে; দরকারে forward declaration
Makefile-এ TAB-এর বদলে spacemissing separator. Stop.Recipe লাইনের শুরুতে আসল TAB
Makefile-এ header prerequisite নেইheader বদলালেও পুরোনো .o থেকে যায় — অদ্ভুত বাগপ্রতিটি .o-এর পাশে তার header, অথবা -MMD -MP
Helper ফাংশনে static নেইদুই মডিউলে একই নাম হলে multiple definitionpublic নয় এমন সবকিছু static

AI-কে মডিউল ভাগ করতে বলার সবচেয়ে ভালো উপায়: আগে interface, পরে implementation. কোড দেওয়ার আগে AI-কে শুধু header-এর নকশা দিতে বলুন — সেটি আপনি পড়ে অনুমোদন দেবেন। কোনো ফাংশন public হবে কি না, সেই সিদ্ধান্ত আপনার, AI-এর নয়:

আমার একটি C প্রোগ্রাম আছে (নিচে পেস্ট করছি) — একটি দোকানের বিক্রির খাতা, এখন সব এক ফাইলে। এটিকে মডিউলে ভাগ করতে চাই। ১. প্রথমে শুধু পরিকল্পনা দাও: কয়টি মডিউল, প্রতিটির দায়িত্ব এক লাইনে, কে কার ওপর নির্ভর করে (নির্ভরতা যেন এক দিকে থাকে, বৃত্তাকার নয়)। ২. প্রতিটি মডিউলের .h ফাইল দাও — শুধু টাইপ, prototype আর প্রতিটি ফাংশনের পাশে এক লাইনের মন্তব্য (কী নেয়, কী ফেরত দেয়, ভুল হলে কী করে)। Ledger struct-কে opaque pointer হিসেবে রাখো। ৩. নিয়ম: header-এ কোনো ভ্যারিয়েবল বা ফাংশনের সংজ্ঞা নয়; প্রতিটি header-এ include guard; public নয় এমন সব ফাংশন static; ledger মডিউলে কোনো printf/scanf নয়। ৪. আমি header অনুমোদন দেওয়ার পরেই .c ফাইল আর একটি Makefile দাও (CFLAGS = -Wall -Wextra -g, clean target, প্রতিটি .o-এর পাশে তার header prerequisite). Recipe লাইনে TAB ব্যবহার করবে। [এখানে আপনার কোড পেস্ট করুন]

AI-এর মডিউল ভাগ হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১C-তে header আর source আলাদা কেন? নতুন ভাষাগুলো (Go, Rust, Python) এই ব্যবস্থা বাদ দিল কেন, আর কী দিয়ে বদলাল?

C-এর header ব্যবস্থা কোনো সুচিন্তিত “module system” নয় — এটি ১৯৭০-এর দশকের সীমাবদ্ধতার ফল। তখনকার কম্পিউটারে মেমরি এত কম ছিল যে compiler একবারে একটিমাত্র ফাইল দেখতে পারত। অন্য ফাইলের ফাংশন কেমন, তা জানানোর সবচেয়ে সহজ উপায় ছিল: ঘোষণাগুলো একটি আলাদা ফাইলে লিখে preprocessor দিয়ে লেখা হুবহু কপি করে বসানো। #include আসলে একটি “কপি-পেস্ট” নির্দেশ, এর বেশি কিছু নয়।

এই সরল সমাধানের দাম আমরা আজও দিই:

  • একই কথা দুবার লেখা: prototype header-এ, সংজ্ঞা .c-তে — দুটি মিলিয়ে রাখার দায়িত্ব প্রোগ্রামারের।
  • ধীর build: একটি বড় header একশো .c ফাইলে include হলে compiler তাকে একশো বার পড়ে। C++-এ এই সমস্যা আরও তীব্র।
  • ভঙ্গুরতা: include-এর ক্রম, guard ভুলে যাওয়া, একটি header-এর macro অন্য header-এর অর্থ বদলে দেওয়া।

নতুন ভাষাগুলো compiler-কে নিজেই মডিউল বুঝতে শিখিয়েছে। Go-তে একটি ফোল্ডার একটি package, আর বড় হাতের অক্ষরে শুরু হওয়া নামই বাইরে দৃশ্যমান — Total public, total private. Rust-এ pub শব্দ দিয়ে ঠিক করা হয় কী বাইরে যাবে, আর compiler নিজেই ঘোষণা খুঁজে নেয়। Python-এ প্রতিটি ফাইলই একটি module. এমনকি C++ ২০২০ সালের standard-এ নিজস্ব modules যোগ করেছে। কোনো ক্ষেত্রেই আলাদা header হাতে লিখতে হয় না।

তবু C-এর ব্যবস্থা টিকে আছে, কারণ এর একটি অপ্রত্যাশিত গুণ আছে: header একটি সাধারণ টেক্সট ফাইল, যা যেকোনো ভাষা পড়তে পারে। Python, Rust বা Go থেকে C library ডাকার সময় এই header-ই হয় যোগাযোগের সেতু। C-এর ABI আজ কার্যত সব ভাষার মধ্যে “সাধারণ ভাষা”।

মূল উপলব্ধি: ভাষা বদলায়, কিন্তু ধারণা একই থাকে — interface আর implementation আলাদা রাখা, আর কী বাইরে দেখা যাবে তা সচেতনভাবে ঠিক করা। C-তে এটি আপনি হাতে করেন, তাই ধারণাটি সবচেয়ে পরিষ্কার দেখা যায়।

প্র ০২FILE * কেন opaque pointer-এর সবচেয়ে বিখ্যাত উদাহরণ? এই কৌশল থেকে “স্থায়ী API” তৈরির কী শিক্ষা পাওয়া যায়?

C standard বলে FILE একটি object type, যা একটি stream নিয়ন্ত্রণের তথ্য রাখে — কিন্তু তার ভেতরে কোন field আছে, কখনো বলে না। ফলে Windows, Linux, macOS আর ছোট embedded সিস্টেমের library প্রত্যেকে নিজের মতো করে FILE সাজিয়েছে: কোথাও buffer-এর আকার আলাদা, কোথাও thread-এর তালা (lock) আছে, কোথাও নেই। (কিছু library-তে struct-টি header-এ দেখা গেলেও কোনো portable প্রোগ্রাম তার field ছোঁয় না।) অথচ পঞ্চাশ বছর আগে লেখা fopen/fgets/fclose-এর কোড আজও সব জায়গায় compile হয়।

কেন এটি সম্ভব হলো? কারণ ব্যবহারকারী কখনো ভেতরের ওপর নির্ভর করেননি। এখান থেকে স্থায়ী API তৈরির কয়েকটি শিক্ষা:

  • যা দেখাবেন, তা চিরকালের জন্য: একটি field public করলে কেউ না কেউ সেটি ব্যবহার করবে, আর তখন সেটি বদলানো মানে তাদের কোড ভাঙা। তাই শুরুতে যত কম দেখানো যায়।
  • তৈরি ও ধ্বংস মডিউলের হাতে: fopen তৈরি করে, fclose মুছে দেয়। ব্যবহারকারী কখনো sizeof(FILE) জানতে চায় না — তাই library আকার বাড়ালেও কেউ ভাঙে না।
  • পড়ার জন্য ফাংশন: ftell, feof, ferror — field না দেখিয়ে প্রশ্নের উত্তর দেওয়া।

একই কৌশল বড় সিস্টেমেও। Windows API-তে প্রায় সবকিছু একটি HANDLE; Unix-এ খোলা ফাইল মানে শুধু একটি সংখ্যা (file descriptor), আর আসল তথ্য থাকে kernel-এর ভেতরে। SQLite-এর sqlite3 *, OpenSSL-এর নতুন সংস্করণগুলোর বহু struct — সবই opaque. OpenSSL আগে অনেক struct খোলা রেখেছিল; পরে সেগুলো লুকাতে গিয়ে বহু প্রোগ্রামকে কোড বদলাতে হয়েছিল — শুরুতে লুকানো কত সস্তা, তার একটি শিক্ষা।

দামও আছে: প্রতিটি অবজেক্টের জন্য heap allocation, প্রতিটি field পড়তে একটি ফাংশন ডাক (যা compiler সাধারণত inline করতে পারে না, কারণ কোড অন্য ফাইলে)। খুব দ্রুত চলা দরকার এমন ছোট struct-এ (যেমন একটি বিন্দুর x, y) এই দাম অপ্রয়োজনীয়।

মূল উপলব্ধি: একটি API-র আয়ু নির্ভর করে সে কতটা কম প্রতিশ্রুতি দেয় তার ওপর। যা লুকানো, তা স্বাধীনভাবে বদলানো যায়; যা দেখানো, তা চিরকালের দায়।

প্র ০৩Make-এর timestamp ধারণা প্রায় পঞ্চাশ বছরের পুরোনো। এর সীমাবদ্ধতা কোথায়, আর আধুনিক build tool সেগুলো কীভাবে সামলায়?

Make-এর মূল ধারণা অসাধারণ সরল: “target কি prerequisite-এর চেয়ে পুরোনো?” এই একটি প্রশ্ন দিয়ে সে ১৯৭৬ সাল থেকে অসংখ্য প্রজেক্ট build করছে। কিন্তু সরলতার কিছু সীমা আছে:

  • সময় মানেই পরিবর্তন নয়: একটি ফাইল খুলে কিছু না বদলে আবার সেভ করলেন — timestamp বদলাল, কিন্তু বিষয়বস্তু একই। Make তবু সব নির্ভরশীল ফাইল আবার বানাবে। উল্টোটাও হয়: git দিয়ে একটি পুরোনো সংস্করণে ফিরলে ফাইলের সময় এমন হতে পারে যে Make ভুল করে কিছু বাদ দেয়।
  • ঘড়ির গরমিল: নেটওয়ার্ক ড্রাইভে বা দুটি মেশিনের মধ্যে ঘড়ি না মিললে “কোনটি নতুন” প্রশ্নের উত্তরই ভুল হয়। Make তখন “clock skew detected” সতর্কবার্তা দেয়।
  • নির্ভরতা হাতে লেখা: অংশ ৫-এ দেখেছেন, একটি header লিখতে ভুলে গেলে Make নীরবে ভুল করে। আর recipe বদলালে (যেমন CFLAGS-এ -O2 যোগ করলেন) Make সেটা ধরে না, কারণ Makefile নিজে prerequisite নয়।
  • বড় প্রজেক্টে ধীরতা: লাখ লাখ ফাইলের প্রজেক্টে শুধু “কী বদলেছে” খুঁজতেই Make অনেক সময় নেয়।

আধুনিক tool-গুলো এসব সামলায় ভিন্ন উপায়ে। Ninja (Google Chrome-এর build দ্রুত করতে তৈরি) নিজে খুব কম সিদ্ধান্ত নেয় — CMake-এর মতো generator তার ফাইল বানায়, আর Ninja শুধু দ্রুত চালায়; সে কোন কমান্ড দিয়ে কোন ফাইল বানানো হয়েছিল তাও মনে রাখে। Bazel-এর মতো tool সময়ের বদলে ফাইলের বিষয়বস্তুর hash (একটি ছোট আঙুলের ছাপ) তুলনা করে — বিষয়বস্তু না বদলালে কিছুই আবার বানায় না, আর একই hash-এর ফল অন্য মেশিন থেকেও ধার নিতে পারে। ccache একই ধারণায় compile-এর ফল জমা রাখে।

মূল উপলব্ধি: “কী বদলেছে” প্রশ্নটি সহজ শোনালেও এর উত্তর কতটা নির্ভুল, তার ওপর নির্ভর করে build কতটা দ্রুত আর কতটা বিশ্বাসযোগ্য। সময় হলো সস্তা কিন্তু অনুমান-নির্ভর উত্তর; hash হলো দামি কিন্তু নিশ্চিত উত্তর — আবার সেই পরিচিত trade-off.

প্র ০৪ABI স্থিতিশীলতা নিয়ে Linux distribution-গুলো এত চিন্তিত কেন? একটি library-র struct বদলানো কেন পুরো অপারেটিং সিস্টেমের সমস্যা হতে পারে?

Ubuntu বা Debian-এর মতো একটি Linux distribution হাজার হাজার প্রোগ্রাম আর library একসাথে সাজিয়ে দেয়। এর বেশিরভাগই shared library ব্যবহার করে। ধরুন একটি জনপ্রিয় image library শত শত প্রোগ্রামে ব্যবহৃত হয়। তার নতুন সংস্করণে একটি public struct-এ একটি field যোগ হলো। এখন:

  • নতুন library-র সাথে compile হওয়া প্রোগ্রাম ঠিক চলবে।
  • পুরোনো library-র সাথে compile হওয়া প্রোগ্রাম চলবে ঠিকই, কিন্তু ভুল offset থেকে মেমরি পড়বে — হয় crash, নয়তো নীরব ভুল ফল, এমনকি নিরাপত্তা-ত্রুটি।

তাই ABI ভাঙা মানে সেই library ব্যবহারকারী প্রতিটি প্রোগ্রাম আবার build করতে হবে। Debian-এর মতো distribution-এ এর একটি নামই আছে — “library transition”, যেখানে নির্ভরশীল সব প্যাকেজ নতুন করে build করে একসাথে ছাড়া হয়। এটি সময়সাপেক্ষ, আর এর মধ্যে কিছু প্যাকেজ আটকে যায়।

সমস্যা সামলানোর কৌশলগুলো:

  • soname: Library-র নামে major version থাকে (যেমন libssl.so.3). ABI ভাঙলে নম্বর বাড়ে, আর পুরোনো ও নতুন সংস্করণ পাশাপাশি থাকতে পারে।
  • Symbol versioning: GNU C library (glibc) একই ফাংশনের একাধিক সংস্করণ ভেতরে রেখে দেয়, যাতে বহু বছর আগে compile হওয়া প্রোগ্রাম পুরোনো আচরণটাই পায়। এজন্যই পুরোনো Linux প্রোগ্রাম নতুন সিস্টেমেও সাধারণত চলে।
  • Opaque struct: আকার লুকানো থাকলে field যোগ করা ABI ভাঙে না।

Linux kernel-এর ক্ষেত্রেও একই চিন্তা। Linus Torvalds-এর বহুল উদ্ধৃত নিয়ম — “we don't break userspace”: kernel ব্যবহারকারী প্রোগ্রামের জন্য যে interface দেয় (system call), তা বদলে পুরোনো প্রোগ্রাম ভাঙা চলবে না। মজার ব্যাপার, kernel-এর ভেতরের driver-দের জন্য কোনো স্থির ABI নেই — সেটি ইচ্ছা করেই, যাতে ভেতরটা স্বাধীনভাবে উন্নত করা যায়। বাইরে কঠোর, ভেতরে স্বাধীন — ঠিক information hiding.

মূল উপলব্ধি: compile হয়ে বিলি হওয়া কোড আর ফেরত আনা যায় না। তাই যে interface অন্যের মেশিনে চলা প্রোগ্রাম ব্যবহার করে, তার প্রতিটি পরিবর্তন একটি প্রতিশ্রুতি ভাঙার প্রশ্ন — আর বড় সিস্টেমের সুনাম তৈরি হয় এই প্রতিশ্রুতি রক্ষা করেই।

প্র ০৫ভালো মডিউল-সীমানা কীভাবে কয়েকজন মানুষ (আর AI) কে একসাথে, সংঘাত ছাড়া কাজ করতে দেয়?

১৯৬৮ সালে Melvin Conway একটি পর্যবেক্ষণ লেখেন, যা এখন “Conway's law” নামে পরিচিত: একটি প্রতিষ্ঠান যে সিস্টেম বানায়, তার গঠন হয় প্রতিষ্ঠানের যোগাযোগের কাঠামোর প্রতিচ্ছবি। উল্টো দিক থেকে দেখলে: আপনি যদি চান কয়েকজন স্বাধীনভাবে কাজ করুক, তাহলে কোডের সীমানা এমনভাবে টানুন যাতে প্রত্যেকের কাজ একটি মডিউলের ভেতরে থাকে, আর মডিউলগুলোর মধ্যে কথা হয় শুধু interface দিয়ে।

দোকানের খাতায় এটি কীভাবে কাজ করে দেখুন। প্রথমে নাদিয়া আর রাফি মিলে ledger.h চূড়ান্ত করলেন — এটিই তাঁদের চুক্তি। তারপর:

  • নাদিয়া ledger.c-এর ভেতরটা যতবার খুশি বদলান; চুক্তি না ভাঙলে রাফির কিছু যায় আসে না।
  • রাফি report.c লিখতে পারেন ledger.c শেষ হওয়ার আগেই — শুধু header দেখে, আর দরকারে একটি নকল (stub) খাতা বানিয়ে টেস্ট করে।
  • git-এ তাঁদের পরিবর্তন আলাদা ফাইলে, তাই merge-সংঘাত প্রায় হয় না। Code review-ও সহজ: header বদলালে সবাই দেখবেন, .c বদলালে শুধু মালিক।

AI-এর ক্ষেত্রে এই নীতি আরও শক্তিশালী। AI একবারে সীমিত পরিমাণ লেখা “মাথায় রাখতে” পারে (context window). তিন হাজার লাইনের একটি ফাইলে সে প্রায়ই দূরের কোনো অংশ ভুলে যায় আর এমন পরিবর্তন করে যা অন্যত্র কিছু ভেঙে দেয়। কিন্তু আপনি যদি বলেন “এই header মেনে শুধু report.c লেখো”, তাহলে তার কাজের সীমা ছোট আর পরিষ্কার — ভুলের সুযোগ কম, আর যাচাই করাও সহজ, কারণ interface আগে থেকেই আপনার অনুমোদিত। একাধিক AI সহকারী, বা আপনি ও AI, আলাদা মডিউলে একসাথে কাজ করতে পারেন।

উল্টো চিত্রও সত্যি: সীমানা ভুল জায়গায় টানলে (যেমন দুটি মডিউল একে অপরের ভেতরের তথ্য সারাক্ষণ চায়) প্রতিটি পরিবর্তনে দুজনকে বসে আলোচনা করতে হয় — তখন দুজন মিলে একজনের চেয়েও ধীরে কাজ হয়।

মূল উপলব্ধি: মডিউল-সীমানা শুধু কোডের নকশা নয়, কাজ ভাগের নকশাও। ভালো interface হলো একটি চুক্তি, যা মানুষ আর মেশিন দুজনকেই স্বাধীনভাবে কিন্তু নিরাপদে কাজ করতে দেয়।

অনুশীলন

বেশিরভাগ কাজ ভাবা আর পর্যবেক্ষণের — শেষটি ছাড়া কোড লিখতে হবে না।

  1. Header না source? নিচের প্রতিটি লাইন কোথায় যাবে — ledger.h নাকি ledger.c? (ক) long ledger_total(const Ledger *lg, int pay); (খ) static int grow(Ledger *lg) { … } (গ) int ledger_debug = 0; (ঘ) typedef struct Ledger Ledger; (ঙ) extern int ledger_debug;

    (ক) header — prototype, শুধু ঘোষণা; অন্যদের ডাকার জন্য দরকার।

    (খ) .c — শরীরসহ সংজ্ঞা, আর static মানেই “শুধু এই ফাইলের”। Header-এ রাখলে প্রতিটি ফাইলে একটি করে আলাদা কপি তৈরি হতো।

    (গ) .c — মান বসানো ভ্যারিয়েবল মানে সংজ্ঞা; header-এ গেলে multiple definition.

    (ঘ) header — opaque pointer-এর জন্য নামটুকু public. পূর্ণ struct Ledger { … } যাবে .c-তে।

    (ঙ) header — extern শুধু প্রতিশ্রুতি দেয়, মেমরি নেয় না; (গ)-এর সাথে জোড়া বাঁধে।

  2. Make-এর মতো ভাবুন: অংশ ৫-এর Makefile ধরে উত্তর দিন — (ক) শুধু ledger.h বদলালে make কোন কোন কমান্ড চালাবে? (খ) শুধু main.c বদলালে? (গ) কিছু না বদলে make clean তারপর make করলে?

    (ক) ledger.h তিনটি .o-রই prerequisite, তাই তিনটিই আবার compile হবে, তারপর link — মোট চারটি কমান্ড। এজন্যই header কম বদলানো ভালো, আর header-এ শুধু দরকারি জিনিস রাখা উচিত।

    (খ) শুধু main.o আবার compile, তারপর link — দুটি কমান্ড।

    (গ) clean সব .o আর shop মুছে দেয়, তাই পরের make তিনটি compile আর একটি link চালাবে — পুরো build. নিজে মেলাতে চাইলে make -n দিয়ে আগে থেকে দেখে নিন।

  3. Opaque pointer নিয়ে পরীক্ষা: অংশ ৪-এর opaque_demo.c-তে (ক) ১৪ নম্বর লাইনের মন্তব্যচিহ্ন মুছে চালান। (খ) এবার সেটি আবার মন্তব্য করে, main-এর ভেতরে Ledger x; লিখে চালান। (গ) শেষে struct Ledger { … }; লাইনটি কেটে main-এর উপরে বসান, আর (ক) আবার চেষ্টা করুন। তিনবার কী দেখলেন, আর কেন?

    (ক) invalid use of incomplete typedef 'Ledger' — main struct-এর গঠন জানে না, তাই count পড়তে পারে না।

    (খ) storage size of 'x' isn't known — আকার অজানা, তাই ভ্যারিয়েবল বানানো যায় না। এজন্যই ledger_new() লাগে।

    (গ) এবার কোনো error নেই, count ছাপা হয় (2). গঠন দেখা গেলেই লুকানো শেষ। আসল প্রজেক্টে এটাই হয় যখন কেউ struct-কে .c থেকে header-এ সরিয়ে দেয় — information hiding এক লাইনেই হারিয়ে যায়। তাই opaque struct কখনো header-এ নয়।

  4. নিজের একটি মডিউল (কোড লেখার কাজ): পর্ব ১৩-এর টোকেন মেশিনটি (next_token) একটি মডিউলে রূপ দিন: token.h, token.c, main.c আর একটি Makefile. শর্ত: শেষ নম্বরটি বাইরে থেকে বদলানো যাবে না, আর একটি token_reset() ফাংশন থাকবে।

    একটি সম্ভাব্য নকশা:

    • token.h: include guard, আর দুটি prototype — int token_next(void); ও void token_reset(int start);. কোনো ভ্যারিয়েবল নেই।
    • token.c: #include "token.h", তারপর ফাইল-স্তরে static int last = 100; — static বলে অন্য কোনো ফাইল একে ছুঁতে পারবে না; দুটি ফাংশনই একে ব্যবহার করে।
    • main.c: token_next() তিনবার ডেকে ছাপায়, তারপর token_reset(500) করে আবার ডাকে।
    • Makefile: tokens: main.o token.o, আর main.o ও token.o দুটিরই prerequisite-এ token.h; recipe-তে TAB; একটি clean.

    যাচাই: main.c-তে last = 0; লিখে দেখুন — compiler বলবে 'last' undeclared. আর extern int last; লিখে চেষ্টা করলে compile হবে, কিন্তু link-এর সময় undefined reference to `last' — কারণ static নামটি linker-এর কাছেই পৌঁছায় না।

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

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

ক্রমএকটি নতুন মডিউল যোগ করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📝 ledger.h-এ interface ঠিক করা — টাইপ আর prototype
  2. 🔧 ledger.c-এ implementation লেখা, helper-গুলো static
  3. 📐 প্রতিটি .c আলাদাভাবে compile করে .o বানানো
  4. 🧵 Linker দিয়ে সব .o জুড়ে shop তৈরি
  5. 🧪 চালিয়ে টেস্ট করা, তারপর git-এ commit

নিচের কোনটি header ফাইলে রাখা উচিত নয়?

  • int ledger_add(Ledger *lg, Sale s);
  • int ledger_debug = 0;
  • typedef enum { PAY_CASH, PAY_BKASH } PayMethod;
  • extern int ledger_debug;

main.c সফলভাবে compile হলো, কিন্তু gcc main.o -o shop দিতেই এলো undefined reference to `ledger_add'. সবচেয়ে সম্ভাব্য কারণ কী?

  • ledger.h খুঁজে পাওয়া যায়নি
  • Header-এ prototype নেই
  • Link কমান্ডে ledger.o দেওয়া হয়নি
  • Header-এ include guard নেই

Header-এ শুধু typedef struct Ledger Ledger; আছে, struct-এর গঠন ledger.c-তে। main.c-তে কোনটি করা যাবে?

  • Ledger *lg = ledger_new();
  • Ledger lg;
  • lg->count++;
  • malloc(sizeof(Ledger))

Makefile চালাতেই এলো Makefile:7: *** missing separator. Stop. — কী ঠিক করবেন?

  • CFLAGS লাইন যোগ করা
  • .PHONY যোগ করা
  • Target-এর নাম বদলানো
  • ৭ নম্বর লাইনের শুরুর space মুছে একটি TAB বসানো

শুধু ledger.c বদলে make চালালেন (অংশ ৫-এর Makefile). কী ঘটবে?

  • তিনটি .c ফাইলই আবার compile, তারপর link
  • শুধু ledger.c compile, তারপর link
  • শুধু ledger.c compile, link লাগবে না
  • কিছুই না — “up to date”

একটি shared library-র নতুন সংস্করণে public struct Sale-এ একটি field যোগ হলো। পুরোনো, আবার compile না করা প্রোগ্রামের কী হতে পারে?

  • কোনো সমস্যা নেই, নতুন field স্বয়ংক্রিয়ভাবে সামলানো হয়
  • প্রোগ্রাম খোলার সময় loader পরিষ্কার error দেখাবে
  • পুরোনো আকার ধরে মেমরি পড়বে — crash বা নীরব ভুল ফল (ABI ভাঙা)
  • প্রোগ্রাম নিজের ভেতরে রাখা পুরোনো কপিটিই চালাবে

AI একটি Makefile দিল, যেখানে main.o: main.c লেখা — কিন্তু main.c তো ledger.h include করে। সমস্যা কী?

  • ledger.h বদলালে main.o আবার compile হবে না — নীরব, অদ্ভুত বাগ
  • Make সঙ্গে সঙ্গে error দেবে
  • Compiler ledger.h খুঁজে পাবে না
  • কোনো সমস্যা নেই, header লেখা ঐচ্ছিক

শব্দকোষ

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

মনে রাখুন

  1. মডিউল = interface (.h) + implementation (.c); অন্যরা শুধু header include করে।
  2. Header-এ শুধু ঘোষণা আর include guard; সংজ্ঞা একটিই, কোনো একটি .c-তে। নিজস্ব জিনিস static, ইচ্ছা করে ভাগ করা global extern.
  3. Opaque pointer struct-এর ভেতরটা লুকায় — বাইরে কেউ নির্ভর না করলে ভেতরটা নিশ্চিন্তে বদলানো যায়।
  4. Make timestamp মিলিয়ে শুধু বদলানো অংশ আবার বানায়; recipe-তে TAB, আর প্রতিটি .o-এর পাশে তার header.
  5. Library: .a কপি হয়, .so/.dll খোলার সময় জোড়ে; shared library-তে public struct বদলানো মানে ABI ভাঙা।

পরের পর্বে: মডিউলের প্রতিটি ফাংশন এখন একটি চুক্তি। কিন্তু চুক্তি রক্ষা করা না গেলে — মেমরি নেই, ফাইল খোলা যায় না, ইনপুট ভুল — ফাংশন কীভাবে জানাবে, আর ডাকনেওয়ালা কী করবে? পর্ব ৩০-এ এরর হ্যান্ডলিং: ভুলের জন্য প্রস্তুত থাকার পেশাদার কৌশল।

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

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