FFI ও ABI — ভিন্ন ভাষারা একে অপরকে ডাকে যেভাবে
FFI and ABI
দুই ভাষার function যেন একে অপরকে ডাকতে পারে — এই চুক্তির নাম ABI; চুক্তির একটা ধারাও ভাঙলে compiler চিৎকার করে না, ডেটা নীরবে নষ্ট হয়।
আগে এটা বুঝি
Python-এ তিন লাইনে SQLite database খুলেন:
import sqlite3
db = sqlite3.connect("app.db")থামুন — sqlite3 module আসলে Python না। ভেতরে C-তে লেখা SQLite engine,
সঙ্গে হাজার হাজার লাইনের parsing, B-tree, locking code। আপনার Python string
ওই C জগতে ঢুকেছে, C-র memory-তে B-tree হাঁটেছে, ফল ফিরে এসে Python object
হয়েছে — প্রতিটা query-তে, আপনার অজান্তে।
এই সেতুটা কীভাবে দাঁড়ায়? Assembly module-এর registers-and-calling-conventions লেসনে দেখেছেন — একই compiler-এর দুই C function-এর মধ্যে call-ও একটা কঠোর চুক্তি মেনে চলে: argument কোন register-এ, return কোথায়, কোন register caller বাঁচাবে। ভিন্ন compiler, ভিন্ন ভাষার মধ্যে সেই চুক্তিটাই ABI — Application Binary Interface — আর FFI (Foreign Function Interface) হলো ভাষার সেই সুবিধা যা আপনাকে চুক্তিটা সরাসরি লেখার দরজা খুলে দেয়।
কঠিন অংশটা এখানেই: চুক্তি ভাঙলে কেউ চিৎকার করে না। Type system নেই সেখানে, compiler warning নেই — শুধু ভুল offset-এ পড়া ডেটা, আর production-এ ছয় মাস পরে ফাটা bug। আজকের experiment-এ আমরা ইচ্ছা করে চুক্তি ভেঙে corruption-টা নিজের চোখে দেখব।
আজকের প্রশ্ন তিনটা: (১) দুই binary জগতের মধ্যে চুক্তিপত্রে ঠিক কী কী লেখা থাকে? (২) boundary পার হতে কী খরচ — আর সেই খরচ কখন গুরুত্বপূর্ণ? (৩) GC-চালিত জগৎ থেকে যে object C-কে ধার দিলেন, সেটা কে-ই বা ফেরত দেবে?
মূল ধারণা
ABI — চুক্তিপত্রের inventory
Source level-এ ভাষারা এক হলেও binary level-এ প্রতিটা function শুধু address, প্রতিটা ডেটা শুধু bytes। ওই bytes-গুলোর অর্থ ঠিক রাখতে ABI-তে যা যা লেখা থাকে:
| উপাদান | প্রশ্ন | ভাঙলে যা হয় |
|---|---|---|
| Calling convention | Argument কোন register/stack slot-এ? Return কোথায়? | আবর্জনা argument, instant crash |
| Register preservation | Caller-saved না callee-saved? | ডাকার পরে loop variable রহস্যময় বদলে যায় |
| Type size ও alignment | long কত byte? Struct-এর field কোন offset-এ? | Field-গুলো সিলেন্টলি ভুল মান পড়ে |
| Struct-by-value return | Register-এ (≤16B, SysV) না hidden pointer দিয়ে? | Restype ভুল হলে RAX-এর garbage |
| Name mangling | Symbol-এর নাম binary-তে ঠিক কী? | undefined symbol link error |
| Symbol visibility | কোন symbol export হবে? | dlsym NULL ফেরত |
runtime-systems লেসনের startup-chain আর assembly module-এর stack-frame আলোচনা এই চুক্তির দুই প্রান্ত ছিল — process যে state-এ জন্মায়, frame যেভাবে সাজে। আজ সেই চুক্তিকে ভাষার মধ্যে টেনে আনছি।
Struct layout — সবচেয়ে ঘন ঘন ভাঙা ধারা
struct Sample {
char tag; /* offset 0, জায়গা ১ বাইট */
/* ৩ বাইট padding — int-এর alignment ৪ */
int value; /* offset 4 */
char flag; /* offset 8 */
/* ৩ বাইট tail padding — struct-এর alignment ৪ */
};
/* sizeof(struct Sample) == 12 */নিয়মটা দুই লাইনে: প্রতিটা field তার alignment-এর multiple offset-এ বসে;
পুরো struct-ের size তার সর্বোচ্চ alignment-এর multiple। Field-এর ক্রম
বদলালে size বদলায় — {int, char, char} হলে ৮, {char, int, char} হলে ১২।
এই padding নিজে থেকেই প্রথম ফাঁদ: দুই ভাষা ভিন্ন ধারায় layout লিখলে
byte-stream-টা এক থাকে কিন্তু অর্থ সরে যায়।
Name mangling — overloading-এর দাম
C++ overload দেয় — scale(double,double) আর scale(int,int) দুটোই আছে
পারে। Binary-তে দুটোর নাম আলাদা হতে হবে, তাই compiler নামে signature গেঁথে
দেয় (Itanium mangling):
C++ source: double scale(double, double)
Binary symbol: _Z5scaledd ← name + param-types encoded
extern "C" double c_scale(double, double);
Binary symbol: c_scale ← mangling বন্ধ — C-র সাথে কথা বলা যায়তাই FFI-র প্রথম নিয়ম: C++ library-কে ডাকতে হলে header-এ extern "C" —
অথবা mangled নামটাই ডাকতে হবে। মনে রাখুন: extern "C" শুধু linkage
বদলায়, C++ semantics (class, exception, template) যায় না — আর যাওয়াটাও
দরকার, কারণ C জগতে তার কোনো ভাষাই নেই।
FFI-র তিন রাস্তা
১. Runtime loading + introspection
dlopen("libfoo.so") → dlsym("foo") → signature হাতে ঘোষণা
Python ctypes / CFFI, Java JNA — ভেতরে libffi যা runtime-এ
calling convention execute করে (trampoline machine-code বানায়)
২. Codegen bindings (compile-time)
SWIG / Rust bindgen / cbindgen / JNI headers — C header থেকে
host-language declaration generate করে; type-check compile-এই
৩. C ABI as lingua franca
সবাই C-র চুক্তিতে কথা বলে: Go(cgo), Rust(extern "C"), JVM(JNI),
Python(refcount API), Node(NAPI) — এক ভাষা আরেক ভাষাকে সরাসরি না
বুঝেও C-কে common tongue বানিয়ে কথা বলেতৃতীয় পয়েন্টটাই industry-র আসল ঐকমত্য: কোনো ভাষা-জোড়ার জন্য dedicated
bridge বানানো N² সমস্যা; সবাই C ABI-তে converge করলে সমস্যা N-এ নেমে আসে।
এজন্যই Rust-এর #[no_mangle] pub extern "C", Go-র //export, Python-এর
CPython API — সবার সবাই C-র ছদ্মবেশে।
ভেতরে কী ঘটছে
ctypes-এর একটা call-এর ভেতরটা
lib.add2(3, 4) লিখলে ঘটে:
- Your Pythonlib.add2(3, 4) — দেখতে ordinary call
- ctypes stubargtypes মেনে প্রতিটা argument C-representable করে marshal: int → c_long, str → UTF-8 bytes copy
- libffi trampolinesignature অনুযায়ী runtime-এ machine-code বসায় — registers ঠিক করে call instruction
- C ABI boundaryএখান থেকে সবাই সমান: SysV convention, callee চলে, GIL released (hand-written extension-এ explicit)
- Back againRAX → restype অনুযায়ী Python object বানিয়ে ফেরত — boxing খরচ এখানেই
দুইটা observation: প্রতিটা crossing-এ marshal-in + call + box-out — তিন খরচ; আর signature ভুল ঘোষণা করলে (restype/argtypes) কোনো স্তরই আটকাবে না — machine সত্যিই চুক্তির মতোই চলবে, ভুল চুক্তিতেও।
Marshalling-এর আসল বিল
Boundary পার হওয়া ফ্রি না, আর খরচগুলো asymmetric:
| যা পার হয় | খরচ | লুকানো বিষয় |
|---|---|---|
| int/float scalar | প্রায় শূন্য (register-এই) | তবু Python int → C long-এ unbox লাগে |
| string | UTF-8 encode + copy + NUL terminate | বড় string = বড় memcpy, প্রতি call-এ |
| array/buffer | copy, বা zero-copy view (ঝুঁকিপূর্ণ) | C-র হাতে থাকতে Python side mutate করলে? |
| struct by value | SysV classification: ≤16B register pair, বড়টা memory | restype এক অক্ষর ভুল = RAX garbage |
| object pointer | সরাসরি দেওয়া যায় না | GC জগতের object non-GC জগতে রাখা — পরের অংশ |
আর একটা লুকানো খরচ: GIL। Hand-written extension Py_BEGIN_ALLOW_THREADS
না দিলে C কোড চলার পুরো সময় Python-এর অন্য সব thread আটকে থাকে — ctypes
call-এ এটা automatic, কিন্তু আপনি জানা দরকার কারণ concurrency-models লেসনের
event-loop blocking-সমস্যার এটাই Python রূপ।
GC interplay — কার হাতে object, কে ফেরত দেবে
garbage-collection আর reference-counting লেসনে দেখেছেন: collector বাঁচায় যা-কে reach করা যায়। C-র ভেতরে থাকা raw pointer collector-এর নজরের বাইরে — CPython-এর refcount দেখে, JVM-এর tracing দেখে না। তাহলে তিনটা protocol ছাড়া উপায় নেই:
borrowed : C শুধু call-এর সময় ব্যবহার করে, সংরক্ষণ করে না — সবচেয়ে নিরাপদ
owned : C-র হাতে মালিকানা — সে-ই free করবে; Python আর touch করবে না
hand-off : মালিকানা দুইদিকে চলাচল করে — তখন explicit retain/release API
লাগে, আর wrapper-কে __del__/finalizer-এ release বসাতে হয়Moving collector-ওয়ালা ভাষায় (JVM, .NET) আরও এক স্তর: GC object সরাতে
পারে, তাই C-কে raw address দেওয়াই অবৈধ — pinned handle বা copy বাধ্যতামূলক
(JNI-র GetByteArrayElements ঠিক এটাই করে)। আজকের build-এ hand-off protocol-
টা হাতে-কলমে লিখবেন।
উদাহরণ
Padding walkthrough — compiler-এর চোখে
/* layout.c — gcc layout.c && ./a.out */
#include <stdio.h>
#include <stddef.h>
struct A { char t; int v; char f; }; /* খারাপ ক্রম */
struct B { int v; char t; char f; }; /* ভালো ক্রম — একই fields */
int main(void) {
printf("A: size=%zu tag@%zu value@%zu flag@%zu\n",
sizeof(struct A),
offsetof(struct A, t), offsetof(struct A, v), offsetof(struct A, f));
printf("B: size=%zu value@%zu tag@%zu flag@%zu\n",
sizeof(struct B),
offsetof(struct B, v), offsetof(struct B, t), offsetof(struct B, f));
return 0;
}A: size=12 tag@0 value@4 flag@8 ← ৬ বাইট ডেটা, ৬ বাইট ফাঁকা!
B: size=8 value@0 tag@4 flag@5 ← একই ডেটা, ৩৩% ছোটFFI-র দৃষ্টিতে এখানেই প্রথম বিপদ: Python-এর ctypes struct যদি A-র জায়গায়
B-র ক্রম ঘোষণা করে, প্রতিটা field silently ভুল byte পড়বে — কোনো error
নেই, শুধু ভুল সংখ্যা। দ্বিতীয় বিপদ ভিন্ন platform: long Windows-এ ৪,
Linux-এ ৮ বাইট — একই header, ভিন্ন layout।
ভুল চুক্তির ফল — একটা সংখ্যা-trace
C জানে struct A (tag@0, value@4), Python ভেবেছে B (value@0):
Python লিখেছে (B-layout মনে করে):
offset 0: E8 03 00 00 ← value = 1000
offset 4: 41 ← tag = 'A'
C পড়ছে (A-layout জেনে):
tag@0 = 0xE8 = 232 ← value-র নিচের byte!
value@4 = 0x00000041 = 65 ← 'A'-ই, int ভেবে
flag@8 = ??? = garbage ← object-এর বাইরেই পড়েছে
ফল: কোনো error নেই। শুধু প্রতিটা মান ভুল, একটা read out-of-bounds।এই নীরবতাই FFI debugging-কে কঠিন করে — আর আজকের experiment-এ এটা reproduce করবেন।
ছবিতে
Boundary-র মানচিত্র
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ PYTHON জগৎ │ │ C জগৎ │
│ │ │ │
│ objects, GC heap, GIL, │ │ raw memory, manual lifetime│
│ dynamic types │ │ fixed layouts │
│ │ │ │
│ lib.add2(3, 4) ───────────┼───► │ long add2(long a, long b) │
│ ▲ marshal-in │ ABI │ │ │
│ ▼ box-out │◄──── │ ▼ │
│ int result ◄──────────────┼──── │ RAX │
│ │ │ │
└─────────────────────────────┘ └─────────────────────────────┘
মাঝখানে একটাই common ভাষা: SysV x86-64 ABIএকই আট বাইট, দুই অর্থ:
PYTHON-এর চোখে: C-এর চোখে:
┌─────────────────┐ ┌─────────────────┐
│ PyObject*: │ │ char buf[8]: │
│ refcnt=1 │ ──same──►│ যা খুশি তা-ই │
│ type=str │ bytes │ যতক্ষণ খুশি │
│ "hello" │ │ │
└─────────────────┘ └─────────────────┘
GC এটাকে সরাতে/মুছতে পারে C জানেই না কে মালিকনিজে চালিয়ে দেখুন
চুক্তি মানুন, তারপর ভেঙে ফেলুন — ctypes forensics
ধাপ ১ — C লাইব্রেরি:
/* libtiny.c */
#include <stdlib.h>
#include <string.h>
long add2(long a, long b) {
return a + b;
}
typedef struct {
char tag; /* @0 */
/* pad 3 */
int value; /* @4 */
char flag; /* @8 */
/* pad 3 — size 12 */
} Point;
Point make_point(char tag, int value, char flag) {
Point p;
p.tag = tag;
p.value = value;
p.flag = flag;
return p;
}
int sum_point(const Point *p) {
return p->value + p->tag + p->flag;
}gcc -shared -fPIC -o libtiny.so libtiny.c
nm -D libtiny.so | grep -E " T (add2|make_point|sum_point)"
# দেখবেন: তিনটাই T (exported text symbol) — dlopen/dlsym-এর দরজা খোলাধাপ ২ — সঠিক wrapper:
# tiny_ok.py
import ctypes
lib = ctypes.CDLL("./libtiny.so")
class Point(ctypes.Structure):
_fields_ = [("tag", ctypes.c_char), # C-র layout-এর হুবহু প্রতিলিপি
("value", ctypes.c_int),
("flag", ctypes.c_char)]
lib.add2.argtypes = [ctypes.c_long, ctypes.c_long]
lib.add2.restype = ctypes.c_long
lib.make_point.argtypes = [ctypes.c_char, ctypes.c_int, ctypes.c_char]
lib.make_point.restype = Point # struct by value!
lib.sum_point.argtypes = [ctypes.POINTER(Point)]
lib.sum_point.restype = ctypes.c_int
print("add2:", lib.add2(20, 22)) # 42
p = lib.make_point(b"A", 1000, b"B")
print("fields:", p.tag, p.value, p.flag) # b'A' 1000 b'B'
print("sizeof match:", ctypes.sizeof(Point)) # 12 — C-র সাথে মিল
print("sum:", lib.sum_point(ctypes.byref(p))) # 1000+65+66=1131ধাপ ৩ — চুক্তি ভাঙুন (ইচ্ছা করে):
# tiny_broken.py — একই library, ভুল layout ঘোষণা
import ctypes
lib = ctypes.CDLL("./libtiny.so")
class WrongOrder(ctypes.Structure):
_fields_ = [("value", ctypes.c_int), # C জানে ওখানে tag!
("tag", ctypes.c_char),
("flag", ctypes.c_char)]
# size 8 — C-র Point size 12: চুক্তি দুই জায়গায় ভাঙা
w = WrongOrder(value=1000, tag=b"A", flag=b"B")
lib.sum_point.argtypes = [ctypes.POINTER(WrongOrder)] # ctypes থামাবে না!
print("sizeof:", ctypes.sizeof(w)) # 8 ≠ 12
print("sum:", lib.sum_point(ctypes.byref(w)))
# C পড়ছে: tag@0=0xE8(232), value@4=('A'..'B' bytes)=65-ish, flag@8=OOB garbage
# ফল: কোনো exception নেই — প্রতিবার ভিন্ন ভুল সংখ্যা। এটাই silent corruption।python3 tiny_ok.py
python3 tiny_broken.py # আবার চালান — একই ভুল সংখ্যা, প্রতিবারtiny_broken-এর sum এই machine-এ 16937 — deterministic কিন্তু সম্পূর্ণ
অর্থহীন: C-র চোখে tag@0 = 0xE8 signed char = −24, value@4 = 'A','B' bytes
মিলে 16961, flag@8 object-এর বাইরে থেকে 0। কোনো exception নেই, কোনো
warning নেই — শুধু অর্থহীন সংখ্যা। ভিন্ন platform/build-এ সংখ্যাটা ভিন্ন হবে;
অর্থহীনতা থাকবে একই।
ধাপ ৪ — crossing-এর দাম:
# cost.py
import ctypes
import time
lib = ctypes.CDLL("./libtiny.so")
lib.add2.argtypes = [ctypes.c_long, ctypes.c_long]
lib.add2.restype = ctypes.c_long
def bench(label, fn, n=5_000_000):
acc = 0
t0 = time.perf_counter()
for i in range(n):
acc = fn(acc, i)
dt = time.perf_counter() - t0
print(f"{label:<14} {dt/n*1e9:6.0f} ns/call (acc={acc})")
bench("ctypes add2", lambda a, i: lib.add2(a, 1))
bench("pure python", lambda a, i: a + 1)Typical (এই লেখকের মেশিনে; আপনারটায় ভিন্ন হবে, অনুপাত থাকবে):
ctypes add2 770 ns/call
pure python 108 ns/callযা প্রমাণিত: সঠিক চুক্তিতে সেতু স্বচ্ছ; ভুল চুক্তিতে কোনো স্তরই বাধা দেয় না — এমনকি object-এর বাইরে পড়েও; আর ছোট কাজে crossing-নিজেই দ্বিগুণ খরচ — FFI-র golden rule: boundary কমাও, এক call-এ বেশি কাজ পাঠাও (batch API), প্রতি element-এ এক call না।
সঠিক argtypes/restype-এ ctypes-C সেতু নিখুঁত কাজ করে; layout একটু সরালেই কোনো error ছাড়াই ভুল মান আর out-of-bounds read হয়; nm -D-তে export-এর তালিকা দৃশ্যমান; আর ছোট call-এ crossing-cost নিজেই মূল খরচ হয়ে দাঁড়ায়।
নিজে বানান
Refcounted handle wrapper — ownership handoff-এর পূর্ণ চুক্তি
- C পাশে handle_create/retain/release API লিখুন — refcount + stderr log, জীবন-মৃত্যুর হিসাব দৃশ্যমান
- ctypes Structure দিয়ে C layout-এর প্রতিলিপি বানান, restype/argtypes সব ঘোষণা করুন
- Python wrapper class — single owner, __del__-ই release-এর একমাত্র দরজা
- share() দিয়ে hand-off protocol দেখান — retain ছাড়া share মানে future double-free
- del করে প্রমাণ করুন: refs 1→0 হলে C-side free, আর কোনো leak বা double-free নেই
লক্ষ্য: hood-এর ownership matrix-এর তৃতীয় সারি (hand-off) পুরোপুরি বাস্তবে — দুই জগতের কেউ কারো মেমরি না চুরি করে একই object share করবে।
/* libhandle.c — refcounted handle-এর C পাশ */
/* compile: gcc -shared -fPIC -o libhandle.so libhandle.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
int refs;
char tag[24];
} Handle;
static int next_id = 1;
Handle *handle_new(const char *tag) {
Handle *h = malloc(sizeof(Handle));
h->id = next_id++;
h->refs = 1; /* caller-এর হাতে ঠিক ১টা reference */
snprintf(h->tag, sizeof(h->tag), "%s", tag);
fprintf(stderr, "[C] new id=%d refs=%d (%s)\n", h->id, h->refs, h->tag);
return h;
}
void handle_retain(Handle *h) {
h->refs++;
fprintf(stderr, "[C] retain id=%d refs=%d\n", h->id, h->refs);
}
void handle_release(Handle *h) {
h->refs--;
fprintf(stderr, "[C] release id=%d refs=%d\n", h->id, h->refs);
if (h->refs == 0) {
fprintf(stderr, "[C] FREE id=%d — হিসাব শেষ\n", h->id);
free(h);
}
}
int handle_id(const Handle *h) { return h->id; }"""handle.py — ownership handoff মেনে চলা ctypes wrapper"""
import ctypes
import os
_lib = ctypes.CDLL(os.path.join(os.path.dirname(os.path.abspath(__file__)),
"libhandle.so"))
class CHandle(ctypes.Structure):
_fields_ = [("id", ctypes.c_int),
("refs", ctypes.c_int),
("tag", ctypes.c_char * 24)]
_lib.handle_new.restype = ctypes.POINTER(CHandle)
_lib.handle_new.argtypes = [ctypes.c_char_p]
_lib.handle_retain.argtypes = [ctypes.POINTER(CHandle)]
_lib.handle_release.argtypes = [ctypes.POINTER(CHandle)]
_lib.handle_id.argtypes = [ctypes.POINTER(CHandle)]
_lib.handle_id.restype = ctypes.c_int
class Handle:
"""Python-পাশের owner: release-এর একমাত্র দরজা __del__"""
def __init__(self, tag):
self._ptr = _lib.handle_new(tag.encode())
def share(self):
"""hand-off: নতুন owner — তাই retain আবশ্যক, নইলে ভবিষ্যতে
দুইজন release করবে = double-free"""
_lib.handle_retain(self._ptr)
twin = Handle.__new__(Handle)
twin._ptr = self._ptr
return twin
@property
def id(self):
return _lib.handle_id(self._ptr)
def __del__(self):
ptr = getattr(self, "_ptr", None)
if ptr is not None:
_lib.handle_release(ptr)
self._ptr = None # resurrection-এর বিরুদ্ধে বীমা
if __name__ == "__main__":
a = Handle("database") # refs 1
b = a.share() # refs 2 — এখন দুই owner
print("ids:", a.id, b.id)
del b # refs 2 → 1 — a এখনো বাঁচিয়ে আছে
print("a alive after del b:", a.id)
del a # refs 1 → 0 — C-side FREE log দেখুন
print("end — কোনো leak না, কোনো double-free না")gcc -shared -fPIC -o libhandle.so libhandle.c
python3 handle.pyOutput (pipe-এ: stderr unbuffered, stdout block-buffered — তাই [C] log আগে; terminal-এ interleave হবে):
[C] new id=1 refs=1 (database)
[C] retain id=1 refs=2
[C] release id=1 refs=1
[C] release id=1 refs=0
[C] FREE id=1 — হিসাব শেষ
ids: 1 1
a alive after del b: 1
end — কোনো leak না, কোনো double-free নাশেষের release refs=0 + FREE টা interpreter shutdown-এ a-র __del__ থেকে —
refcount-এর deterministic finalization, GC লেসনের স্মৃতি। দুইবার release
হয়নি, leak হয়নি — protocol কাজ করেছে।
নিজে বাড়ান:
share()-এ retain লাইনটা মুছে দিন আর দুইdelকরুন — double-free-এর crash/glibc abort message-টা পড়ুন। এটাই protocol-ভাঙার দাম।- Context manager (
__enter__/__exit__) যোগ করুন — scope-borrowed pattern-এর Python রূপ। - Weakref registry যোগ করুন: একই C pointer-এ দ্বিতীয় Python wrapper বানানো
ঠেকান (identity map) — নইলে দুই wrapper-ই
__del__-এ release করবে। - valgrind (
valgrind --leak-check=full python3 handle.py) চালিয়ে প্রমাণ করুন zero leaks — তারপর একটা release comment করে আবার চালিয়ে leak report পড়ুন।
বাস্তব সিস্টেমে
FFI/ABI যেখানে প্রতিদিন কাজ করে
NumPy, PyTorch, pandas। Python-এর numeric ecosystem-এর গতির রহস্য FFI design: এক boundary crossing-এ পুরো array-র operation (vectorized kernel C/C++/CUDA-তে), element-প্রতি crossing নয়। আজকের cost measurement-এর golden-rule-টাই এদের architecture।
SQLite। সম্ভবত পৃথিবীর সবচেয়ে বেশি deployed C library — প্রতিটা phone, browser, OS-এ। এর C API-টাই এত শুদ্ধ যে প্রায় প্রতিটা ভাষা সরাসরি bind করেছে — C-as-lingua-franca-র শ্রেষ্ঠ উদাহরণ।
Android JNI। Java/Kotlin app-এর প্রতিটা native feature JNI-র মধ্য দিয়ে — reference management (LocalRef/GlobalRef!), pinned arrays, thread attach — আজকের ownership protocol-গুলোর industrial-scale রূপ।
Go আর cgo। Go-র documentation সরাসরি সতর্ক করে: cgo call দামি (scheduler hand-off + stack switch), goroutine-দের মধ্যে C callback বিপজ্জনক — concurrency-models লেসনের M:N model-এর সীমানা এখানেই।
WASM। WebAssembly-র উচ্চাকাঙ্ক্ষা: C ABI-র বাইরে একটা portable ABI — ভিন্ন ভাষা ভিন্ন runtime নিয়েও এক sandbox-এ কথা বলবে। WASI আর component model এই চেষ্টারই নাম।
Linux kernel-এর প্রতিশ্রুতি। “We do not break userspace” — kernel-এর syscall ABI freeze মানেই ১৯৯১-এর binary আজও চলে। ABI stability-র সবচেয়ে সফল রাজনীতি; virtual-memory/system-call লেসনের জগতটা এই প্রতিশ্রুতির উপরেই দাঁড়িয়ে।
যে ভুলগুলো সবাই করে
“সব ভাষা শেষপর্যন্ত machine code-এ যায়, তাই সরাসরি একে অপরকে ডাকা উচিত।”
Machine code শুধু instruction sequence — চুক্তি না। Call সফল হতে হলে argument-এর register, struct-এর offset, symbol-এর নাম, lifetime-এর নিয়ম — সব এক হতে হবে; এটাই ABI, আর এটা স্বয়ংক্রিয়ভাবে মেলে না। C++ আর Rust-এর মতো দুই modern ভাষাও পরস্পরকে সরাসরি ডাকতে পারে না — দুজনেই C ABI-তে নেমে কথা বয়।
“FFI call প্রায় ফ্রি — তো একটা function call-ই তো।”
আজকের measurement: ছোট কাজে crossing নিজেই মূল খরচ (marshal + box + কখনো GIL dance)। ভারী কাজে (১০০ms image op) crossing negligible — তাই rule-টা relative: crossing সংখ্যা কমাও, প্রতি crossing-এর কাজ বাড়াও। Element- প্রতি FFI call লেখা numeric কোড সহজেই pure-Python-এর চেয়ে ধীর হয়ে যায় — ironic কিন্তু সাধারণ।
“Struct layout universal — একই C source সব জায়গায় একই bytes।”
Layout compiler, platform আর flags-এর ফাংশন: long Windows-LLP64-তে ৪ বাইট,
Linux-LP64-তে ৮; #pragma pack/__attribute__((packed)) alignment বদলায়;
field-order-ই size বদলায় (আজকের example)। Cross-platform FFI-তে layout
assumption-এর বদলে generator (bindgen) বা explicit fixed-width types
(int32_t) + static_assert(sizeof) ব্যবহার করুন।
“`extern "C"` বসালেই C++ object C-কে দেওয়া যাবে।”
extern "C" শুধু name mangling বন্ধ করে — linkage চুক্তি। Class instance
pass করা মানে vtable pointer, exception machinery, constructor invariant —
C-র জগতে যার কোনো অর্থ নেই। যা দেওয়া যায়: POD data, opaque pointer (handle),
আর flat functions। এজন্যই প্রতিটা mature C++ library-র C API layer আলাদা
থাকে — opaque struct pointer + create/destroy/free function trio।
বুঝেছেন কি না দেখুন
1struct S { char a; double b; short c; }; — x86-64-তে sizeof আর প্রতিটা
field-এর offset হিসাব করুন। Field-ক্রম বদলে double, short, char করলে
size কত?
প্রয়োগ
struct S { char a; double b; short c; }; — x86-64-তে sizeof আর প্রতিটা
field-এর offset হিসাব করুন। Field-ক্রম বদলে double, short, char করলে
size কত?প্রথম ক্রমে: a@0 (১ বাইট), b-র alignment ৮ → padding ৭ → b@8, c@16 (২ বাইট), tail padding ৬ → sizeof = 24। দ্বিতীয় ক্রমে: b@0, c@8, a@10, tail padding ৫ → sizeof = 16। একই fields, ৮ বাইট পার্থক্য — আর FFI-তে এই offset-গুলোই চুক্তি। Alignment-এর সর্বোচ্চ member = ৮, তাই size সবসময় ৮-এর multiple হতে বাধ্য।
2C++-এ function overloading সম্ভব, C-তে না। Name mangling এই পার্থক্যটা
binary level-এ কীভাবে solve করে, আর extern "C" কেন FFI-র প্রবেশদ্বার?
যুক্তি
extern "C" কেন FFI-র প্রবেশদ্বার?Binary-তে symbol = নাম, তাই overload-দুই function-কে ভিন্ন symbol দরকার।
Mangler signature encode করে নামে (scale(double,double) → _Z5scaledd),
ফলে দুই overload দুই distinct symbol। C-তে overloading নেই — নামই যথেষ্ট,
mangling নেই। extern "C" সেই unmangled চুক্তিতে ফেরত দেয়, ফলে C linker,
dlsym, ctypes — সবাই symbol খুঁজে পায়। মনে রাখতে হবে এটা শুধু naming চুক্তি:
C++ semantics বাইরে থেকে ডাকা বেআইনিই থাকে।
3ctypes wrapper-এ কেউ লিখল: lib.make_point.restype = ctypes.c_void_p
(সঠিক: Point)। কী ঘটবে, আর কোন লক্ষণে বুঝবেন ভুলটা?
প্রয়োগ
lib.make_point.restype = ctypes.c_void_p
(সঠিক: Point)। কী ঘটবে, আর কোন লক্ষণে বুঝবেন ভুলটা?SysV ABI-তে ১২-বাইট struct memory-তে ফেরত আসে (hidden destination pointer) —
RAX-এ pointer-টা থাকে, কিন্তু c_void_p ঘোষণায় ctypes RAX-কেই ফল ভাববে।
সেটা আসলে destination-buffer-এর address বা তার অংশ — একটা অস্থায়ী stack
address, যা call শেষে অবৈধ। লক্ষণ: ফেরত “pointer”-টা কখনো valid দেখায়
(dangling), dereference-এ রহস্যজনক garbage বা crash, আর প্রতি run-এ ভিন্ন
মান। শিক্ষা: restype শুধু convenience না — struct-by-value return-এর
calling-convention branch এটাই নির্ধারণ করে।
4আপনার C library তৈরি করবে এমন session object, যা Python থেকে দীর্ঘদিন
ধরে রাখা হবে, মাঝে মাঝে একাধিক Python object একসাথে ব্যবহার করবে। Ownership
protocol design করুন — কে কখন free করবে, race/leak/double-free কোথায় আটকাবেন?
ডিজাইন
Hand-off + refcount protocol: (১) session_create → refs=1, Python wrapper
single-owner; (২) প্রতি নতুন holder-এ session_retain, holder মরলে (wrapper-এর
__del__) session_release; refs=0-তেই C-side free — মালিকানার হিসাব এক
জায়গায়, atomic counter দিলে multi-thread-safe। Leak-আটকাতে: weakref identity
map — এক pointer-এ এক wrapper, duplicate __del__ অসম্ভব। Race-আটকাতে:
release-এর আগে ptr None-set (resurrection guard), C-side counter atomic।
Double-free structurally অসম্ভব হয় কারণ wrapper-এর release-দরজা একটাই আর
identity map নকল wrapper আটকায়। Bonus: context manager দিয়ে scoped borrow
API — long-lived hold আর transient use আলাদা করে।
5Linux kernel প্রতি release-এ ভেতরে বদলায় — scheduler, allocator, page table
layout। তবু ১৯৯০-এর compiled binary আজও চলে। কোন সীমানার কোন স্থায়িত্ব
এটা সম্ভব করল, আর কোন সীমানা ইচ্ছামতো বদলানো যায়?
যুক্তি
স্থির: syscall ABI — syscall number, argument register, errno semantics, procfs format, ELF/process-startup contract। Kernel-user boundary-টা distribution/vendor/app — সবার common floor, ভাঙলে পুরো ecosystem ভাঙে (“we do not break userspace”)। বদলানো যায়: kernel-এর সব internal interface — scheduler algorithm, memory allocator, driver model, file-system internals — কারণ ওদের client শুধু kernel নিজে, একই release-এ recompile হয়। শিক্ষাটা FFI-design-এও এক: stable contract রাখুন শুধু সবচেয়ে বাইরের সীমানায়, ভেতরটা মুক্ত রাখুন — আর stable surface যত ছোট, maintenance তত সস্তা (symbol visibility নীতির সাথে একই সূত্র)।
এরপর কী
এরপর কী
এই module-এর একেকটা লেসন ভাষার একেকটা যন্ত্র খুলে দেখিয়েছে — আর আজ দেখলাম ভাষারাও পরস্পরের সাথে যন্ত্রের ভাষায়ই কথা বলে: ABI-র নীরব চুক্তিতে। এবার module-এর শেষ লেসনে ফেরা যাক সবচেয়ে বড় প্রশ্নে, যেটার উত্তর কোনো যন্ত্র না — চিন্তার ধরন: imperative, functional, OO, logic — paradigm আসলে কী দেয়? কোন restriction কোন guarantee কেনে? একটাই সমস্যা চার ফ্রেমে সমাধান করে দেখব আর জিজ্ঞেস করব: কোন ফ্রেমে bug করা সবচেয়ে কঠিন?
পরের লেসন: Paradigm — imperative, functional, logic, OO: চিন্তার চার ফ্রেম।
আরও পড়ুন
- System V Application Binary Interface — x86-64 supplement — Michael Matz, Jan Hubička, Andreas Jaeger, Mark Mitchell · Calling convention, struct classification (INTEGER/SSE), register preservation-এর primary spec
- Linkers and Loaders — John R. Levine · Symbol export/import, visibility আর dynamic linking-এর ভিত্তি — PIC লেসনের ধারাবাহিকতা
- A Portable Foreign Function Interface Library (libffi) — Anthony Green · Runtime-এ calling convention implement করার যন্ত্র — ctypes-এর ইঞ্জিন
- Python ctypes documentation — Python Software Foundation · restype/argtypes contract আর struct mapping-এর reference