পাঠ ৫৬ · ৫৮-এর মধ্যে · মডিউল ১৩
Home / Courses / Concepts of Programming Languages & Compiler Design / Rust কেস স্টাডি

কেস স্টাডি: Rust-এর ওনারশিপ ও বরো চেকার

Case study: Rust's ownership & borrow checker
১০ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • Rust কীভাবে মেমরি সেফটি অর্জন করে গার্বেজ কালেকশনের রানটাইম খরচ ছাড়াই
  • ওনারশিপ রুল — একজন মালিক, স্কোপ-শেষে স্বয়ংক্রিয় ড্রপ, মুভ সিমান্টিক্স
  • বরোয়িং রুল — একাধিক ইমিউটেবল বনাম একটিমাত্র মিউটেবল বরো
  • একটি সরলীকৃত ওনারশিপ/বরো চেকার বাস্তবায়ন করে দুটো ভায়োলেশন ও একটি বৈধ সিকোয়েন্স যাচাই করা

১ · মেমরি সেফটি, কিন্তু GC ছাড়াই

M12/L54-এ আমরা দেখেছি বেশিরভাগ ভাষা (Python, Java, JavaScript) মেমরি সেফটি নিশ্চিত করে রানটাইমে গার্বেজ কালেকশন চালিয়ে। Rust সম্পূর্ণ ভিন্ন পথ নেয় — এটি M7/L36-এর টাইপ সেফটি/সাউন্ডনেস দর্শনকে মেমরি সেফটিতে প্রয়োগ করে: একটি ওনারশিপ সিস্টেম যা সম্পূর্ণভাবে কম্পাইল-টাইমে চেক হয় — M7/L32-এর স্ট্যাটিক-টাইপিং দর্শনেরই সম্প্রসারণ ("রানটাইমের আগেই সমস্যা ধরো"), এবার প্রয়োগ করা হচ্ছে মেমরি সমস্যার উপর, শুধু টাইপ সমস্যার উপর নয়।

২ · ওনারশিপ রুল

  • একজন মালিক: প্রতিটি মানের ঠিক একটি owner (ভ্যারিয়েবল) থাকে, একই সময়ে।
  • স্কোপ-শেষে স্বয়ংক্রিয় ড্রপ: মালিক ভ্যারিয়েবল স্কোপের (M8/L38) বাইরে গেলে, মানটি স্বয়ংক্রিয়ভাবে ড্রপ/ডিঅ্যালোকেট হয় — M12/L54-এর রানটাইম GC-এর একটি কম্পাইল-টাইম-গ্যারান্টিড, ডিটারমিনিস্টিক বিকল্প।
  • মুভ সিমান্টিক্স: ওনারশিপ অন্য ভ্যারিয়েবলে মুভ করা যায়; মুভের পর মূল ভ্যারিয়েবলটি আর ব্যবহারযোগ্য থাকে না — কম্পাইলার এটি জোর করে প্রয়োগ করে, M11/L49-এর স্বাভাবিক pass-by-value/reference সিমান্টিক্সের চেয়ে একটি জেনুইনভাবে কঠোর নিয়ম।

৩ · বরোয়িং

বরোয়িংBorrowingওনারশিপ না নিয়ে সাময়িকভাবে একটি মানে অ্যাক্সেস পাওয়ার Rust-এর মেকানিজম — M11/L49-এর pass-by-reference-এর সাথে সম্পর্কিত, কিন্তু অতিরিক্ত কম্পাইল-টাইম-চেকড নিয়মসহ। — একটি রেফারেন্স হয় একাধিক সিমাল্টেনিয়াস ইমিউটেবল (read-only) বরো হিসেবে, অথবা ঠিক একটি মাত্র মিউটেবল (read-write) বরো হিসেবে নেওয়া যায় — কখনোই দুটো একসাথে নয়। "বরো চেকার" এই নিয়ম সম্পূর্ণভাবে কম্পাইল-টাইমে প্রয়োগ করে, সরাসরি M12/L54-এর ড্যাংলিং-পয়েন্টার/use-after-free বাগ-শ্রেণি এবং নির্দিষ্ট কিছু ডেটা-রেস বাগ প্রতিরোধ করে, কোনো রানটাইম GC overhead ছাড়াই।

M12/L54-এর সাথে সরাসরি বৈসাদৃশ্য

L54-এ Python-ধাঁচের রেফারেন্স কাউন্টিং + সাইকেল ডিটেক্টর দেখেছি — একটি রানটাইম সমাধান। Rust-এর ওনারশিপ সিস্টেম একই "মেমরি কখন মুক্ত করব" প্রশ্নের সম্পূর্ণ বিপরীত উত্তর দেয় — একটি কম্পাইল-টাইম সমাধান, কোনো রানটাইম ট্র্যাকিং ছাড়াই। দুটোই বৈধ, ভিন্ন ট্রেড-অফসহ ডিজাইন সিদ্ধান্ত — M13/L57-এ এই দুটোকে আরও ভাষার পাশাপাশি রেখে তুলনা করা হবে।

৪ · বাস্তবায়ন — একটি সরলীকৃত ওনারশিপ/বরো চেকার

Python নিজে এই নিয়ম প্রয়োগ করে না, তাই নিচের কোড সেলে একটি ছোট স্ট্যাটিক অ্যানালাইসিস টুল বানানো হয়েছে যা করত এই নিয়ম প্রয়োগ — একটি টয় অপারেশন-সিকোয়েন্স (init, move, use, borrow, borrow_mut, release) প্রসেস করে, use-after-move ও দুটো একসাথে মিউটেবল বরো — দুটো ভায়োলেশনই ধরে, এবং একটি সম্পূর্ণ বৈধ সিকোয়েন্স কোনো এরর ছাড়াই পাস করে।

Python
class OwnershipError(Exception):
    pass

def check_ownership(operations):
    """একটি টয় অপারেশন-সিকোয়েন্স প্রসেস করে Rust-এর ওনারশিপ/বরো রুল প্রয়োগ করে --
    ভায়োলেশন পেলে OwnershipError তোলে।"""
    state = {}      # var -> "valid" | "moved"
    borrows = {}     # var -> [(borrow_name, kind)]  kind: "shared" | "mut"
    log = []

    def var_status(v):
        return state.get(v, "undeclared")

    for op in operations:
        kind = op[0]

        if kind == "init":
            _, var = op
            state[var] = "valid"
            borrows[var] = []
            log.append(f"init {var}: OK")

        elif kind == "move":
            _, src, dst = op
            if var_status(src) == "moved":
                raise OwnershipError(f"use of moved value: '{src}' ইতিমধ্যে মুভ হয়ে গেছে")
            state[src] = "moved"
            state[dst] = "valid"
            borrows[dst] = []
            log.append(f"move {src} -> {dst}: OK ({src} এখন moved-from)")

        elif kind == "use":
            _, var = op
            if var_status(var) == "moved":
                raise OwnershipError(f"use of moved value: '{var}'")
            log.append(f"use {var}: OK")

        elif kind == "borrow":
            _, var, name = op
            if var_status(var) == "moved":
                raise OwnershipError(f"'{var}' বরো করা যাবে না: মান মুভ হয়ে গেছে")
            active_mut = [b for b in borrows[var] if b[1] == "mut"]
            if active_mut:
                raise OwnershipError(f"'{var}' ইমিউটেবল বরো করা যাবে না -- ইতিমধ্যে মিউটেবল বরো করা আছে ({active_mut[0][0]})")
            borrows[var].append((name, "shared"))
            log.append(f"borrow {var} as {name} (shared): OK")

        elif kind == "borrow_mut":
            _, var, name = op
            if var_status(var) == "moved":
                raise OwnershipError(f"'{var}' মিউটেবল বরো করা যাবে না: মান মুভ হয়ে গেছে")
            if borrows[var]:
                existing = borrows[var][0]
                raise OwnershipError(
                    f"'{var}' মিউটেবল বরো করা যাবে না -- ইতিমধ্যে বরো করা আছে ({existing[0]}, {existing[1]}) "
                    f"-- Rust একই সময়ে শুধু ১টি মিউটেবল বরো অনুমতি দেয়, অন্য কোনো বরোসহ নয়"
                )
            borrows[var].append((name, "mut"))
            log.append(f"borrow_mut {var} as {name}: OK")

        elif kind == "release":
            _, name = op
            for var, blist in borrows.items():
                borrows[var] = [b for b in blist if b[0] != name]
            log.append(f"release {name}: OK")

    return log


print("== সিকোয়েন্স ১: বৈধ (কোনো এরর ছাড়াই পাস করা উচিত) ==")
valid_ops = [
    ("init", "a"),
    ("borrow", "a", "r1"),
    ("borrow", "a", "r2"),    # দুটো shared বরো একসাথে: বৈধ
    ("release", "r1"),
    ("release", "r2"),
    ("borrow_mut", "a", "m1"),
    ("release", "m1"),
    ("move", "a", "b"),
    ("use", "b"),
]
try:
    for line in check_ownership(valid_ops):
        print(" ", line)
    print("ফলাফল: কোনো এরর নেই (সঠিক)")
except OwnershipError as e:
    print("অপ্রত্যাশিত এরর:", e)

print("\n== সিকোয়েন্স ২: USE AFTER MOVE (ধরা পড়া উচিত) ==")
bad_ops_1 = [
    ("init", "a"),
    ("move", "a", "b"),
    ("use", "a"),    # a ইতিমধ্যে b-তে মুভ হয়ে গেছে -- অবৈধ
]
try:
    check_ownership(bad_ops_1)
    print("বাগ: কোনো এরর তোলা হয়নি!")
except OwnershipError as e:
    print("সঠিকভাবে ধরা পড়েছে:", e)

print("\n== সিকোয়েন্স ৩: দুটো একসাথে মিউটেবল বরো (ধরা পড়া উচিত) ==")
bad_ops_2 = [
    ("init", "a"),
    ("borrow_mut", "a", "m1"),
    ("borrow_mut", "a", "m2"),   # m1 তখনো সক্রিয় -- অবৈধ
]
try:
    check_ownership(bad_ops_2)
    print("বাগ: কোনো এরর তোলা হয়নি!")
except OwnershipError as e:
    print("সঠিকভাবে ধরা পড়েছে:", e)

    
লক্ষ্য করুন — সিকোয়েন্স ১-এ দুটো shared বরো (r1, r2) একসাথে সমস্যা ছাড়াই কাজ করে (ইমিউটেবল বরো একাধিক হতে পারে), কিন্তু সিকোয়েন্স ৩-এ দ্বিতীয় borrow_mut ঠিক তখনই ব্যর্থ হয় যখন প্রথমটি (m1) এখনো রিলিজ হয়নি — check_ownership সঠিকভাবে OwnershipError তোলে। সিকোয়েন্স ২-এ a-কে b-তে মুভ করার পর a ব্যবহারের চেষ্টা করলেই সাথে সাথে এরর ধরা পড়ে — ঠিক যেভাবে বাস্তব Rust কম্পাইলার একই ভুল কম্পাইল-টাইমেই রিপোর্ট করত।
মূল কথা · Key takeaway

Rust প্রমাণ করে মেমরি সেফটি ও রানটাইম গার্বেজ কালেকশন একে অপরের অনিবার্য শর্ত নয় — একটি ভালো-ডিজাইনড, কম্পাইল-টাইম-চেকড ওনারশিপ সিস্টেমও একই লক্ষ্য (ড্যাংলিং পয়েন্টার প্রতিরোধ, M12/L54) অর্জন করতে পারে, তবে একটি ভিন্ন খরচে — প্রোগ্রামারকে বরো চেকারের কড়া নিয়ম মেনে কোড লিখতে হয়।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ সিকোয়েন্স ৩-এ যদি borrow_mut-এর বদলে দ্বিতীয়টিও borrow (shared) হতো, তাহলে কি এরর আসত?

না — উপরের কোডে দেখুন borrow শুধুমাত্র তখনই এরর তোলে যদি একটি সক্রিয় মিউটেবল বরো থাকে (active_mut চেক)। একাধিক shared/ইমিউটেবল বরো একসাথে সম্পূর্ণ বৈধ — কারণ একাধিক "শুধু-পড়া" অ্যাক্সেস কখনো একে অপরের সাথে সংঘর্ষে (ডেটা রেস) জড়ায় না। সমস্যা তৈরি হয় শুধুমাত্র যখন কমপক্ষে একটি বরো লেখার (মিউটেবল) ক্ষমতা রাখে।

প্র ০২ উপরের চেকারে move অপারেশন কেন destination ভ্যারিয়েবলের জন্য একটি নতুন খালি borrows এন্ট্রি তৈরি করে?

কারণ মুভের পর মানটি একটি নতুন, সম্পূর্ণ পৃথক ভ্যারিয়েবলের (destination) মালিকানায় চলে যায় — তার নিজস্ব বরো-স্টেট শূন্য থেকে শুরু হওয়া উচিত, উৎস ভ্যারিয়েবলের কোনো পুরনো বরো-হিস্টোরি বহন করা উচিত নয় (উৎস ভ্যারিয়েবল নিজেই এখন "moved" স্ট্যাটাসে চলে গেছে এবং আর ব্যবহারযোগ্য নয়)। এটি বাস্তব Rust-এর সাথে সামঞ্জস্যপূর্ণ — মুভের পর নতুন ভ্যারিয়েবলটিই একমাত্র বৈধ হ্যান্ডেল।

প্র ০৩ Rust-এর ওনারশিপ সিস্টেম M7/L36-এর "টাইপ সাউন্ডনেস" ধারণার সাথে কীভাবে সম্পর্কিত?

M7/L36-এ টাইপ সাউন্ডনেস মানে ছিল — যদি একটি প্রোগ্রাম টাইপ-চেক পাস করে, তাহলে রানটাইমে কোনো "টাইপ-সম্পর্কিত" সমস্যা ঘটবে না, এটি একটি কম্পাইল-টাইম গ্যারান্টি। Rust-এর ওনারশিপ/বরো চেকার ঠিক একই দর্শন প্রয়োগ করে, কিন্তু মেমরি-সেফটি সমস্যার (ড্যাংলিং পয়েন্টার, ডেটা রেস) উপর — যদি একটি Rust প্রোগ্রাম বরো-চেকার পাস করে, রানটাইমে সেই নির্দিষ্ট মেমরি-সেফটি সমস্যাগুলো ঘটবে না, এই গ্যারান্টিও সম্পূর্ণ কম্পাইল-টাইমেই প্রতিষ্ঠিত হয়।

অনুশীলন

  1. চিন্তা করুন: যদি সিকোয়েন্স ১-এ ("borrow_mut", "a", "m1")-এর আগে ("release", "m1") কল না করে সরাসরি ("move", "a", "b") চেষ্টা করা হয় (m1 তখনো সক্রিয়), তাহলে কী হবে?

    উপরের সরলীকৃত চেকারে move অপারেশন বর্তমানে সক্রিয় বরো আছে কিনা তা পরীক্ষা করে না (শুধু var_status(src) == "moved" চেক করে) — তাই এই সরলীকৃত সংস্করণে এটি ভুলভাবে পাস করে যাবে। বাস্তব Rust কম্পাইলার আসলে এটি ধরত ("cannot move out of 'a' because it is borrowed") — এটি এই পাঠের চেকারের একটি ইচ্ছাকৃত সরলীকরণ, সম্পূর্ণ বরো-চেকার বাস্তবায়ন এই কোর্সের পরিসরের বাইরে।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি নতুন সিকোয়েন্স [("init", "x"), ("borrow", "x", "r1"), ("borrow_mut", "x", "m1")] চালিয়ে দেখুন কী এরর আসে (একটি সক্রিয় shared বরো থাকা অবস্থায় মিউটেবল বরো চেষ্টা)।

    OwnershipError আসবে — borrow_mut-এর শর্ত হলো borrows[var] সম্পূর্ণ খালি থাকতে হবে (কোনো ধরনের বরো, shared বা mut, একদমই না থাকতে হবে)। যেহেতু r1 (shared) তখনো সক্রিয়, borrows["x"] খালি নয়, তাই মিউটেবল বরো ব্যর্থ হবে — এটাই Rust-এর নিয়মের পূর্ণ রূপ: "একাধিক ইমিউটেবল, অথবা একটিমাত্র মিউটেবল, কখনো মিশ্রিত নয়।"

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

আগের পাঠ
কেস স্টাডি: Python-এর ডিজাইন সিদ্ধান্ত