1 पॉइंट द्वारा GN⁺ 4 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • std::pin::Pin यह type-level guarantee व्यक्त करता है कि pointer जिस value को point करता है, वह उस pointer के माध्यम से move नहीं होगी। इसकी ज़रूरत उन values के लिए होती है जिनका address stable रहना चाहिए, जैसे self-referential types.
  • async/await में .await के बाद भी जीवित रहने वाले local variables और references compiler द्वारा बनाए गए state machine के fields बन सकते हैं, इसलिए polling के बाद future को move होने से रोकने के लिए Future::poll Pin मांगता है.
  • Pin pinned value को safe code से move होने से रोकता है, लेकिन सामान्य mutation को पूरी तरह नहीं रोकता। अगर T: Unpin नहीं है, तो Pin से सुरक्षित रूप से &mut T नहीं निकाला जा सकता.
  • Rust के ज़्यादातर types डिफ़ॉल्ट रूप से Unpin होते हैं, इसलिए जो self-referential structs move नहीं होने चाहिए, उन्हें आमतौर पर PhantomPinned field जोड़कर !Unpin बनाया जाता है.
  • व्यवहार में, जब future को सीधे poll करना हो या किसी pinned future चाहने वाले API को देना हो, तब Box::pin या std::pin::pin! का उपयोग किया जाता है। लेकिन Future या low-level async primitives को सीधे implement करते समय unsafe invariants तक संभालने पड़ते हैं.

Pin की ज़रूरत क्यों है

  • std::pin::Pin एक pointer wrapper है, जो यह guarantee दर्शाता है कि pointer जिस value को point करता है, वह उस pointer के माध्यम से move नहीं होगी.
  • मुख्य समस्या self-referential types में आती है.
    • उदाहरण struct SelfRef में data: i32 और ptr: *const i32 होते हैं, जहाँ ptr, self.data को point करता है.
    • अगर struct instance को किसी दूसरे variable में move किया जाए या function से return किया जाए, तो memory address बदल सकता है.
    • raw pointer ptr पुरानी memory location को point करता रहेगा और dangling pointer बन जाएगा.
  • एक बार self-reference सेट हो जाने के बाद, ऐसी व्यवस्था चाहिए जो उस value को फिर move होने से रोके.

async/await और Future में आने वाली समस्या

  • async/await और Future वे प्रमुख क्षेत्र हैं जहाँ Pin अक्सर दिखाई देता है.
  • जो local variables .await point के बाद भी जीवित रहते हैं, वे compiler द्वारा बनाए गए state machine के fields बन जाते हैं.
  • अगर किसी local variable का reference भी उसी .await के बाद तक जीवित रहता है, तो generated future self-referential हो सकता है.
  • polling शुरू होने के बाद future अपने अंदर के दूसरे fields की ओर इशारा करने वाले references पर निर्भर हो सकता है.
    • इस स्थिति में अगर future move हो जाए, तो वे references invalid हो जाएंगे.
  • इसे रोकने के लिए Future::poll, &mut self की जगह Pin लेता है.
pub trait Future {
    type Output;
    fn poll(self: Pin, cx: &mut Context Pin {
      pub const fn get_mut(self) -> &'a mut T
      where
          T: Unpin
      { ... }
  }
  • अगर type Unpin implement नहीं करता, यानी !Unpin है, तो केवल safe code से &mut T नहीं पाया जा सकता.
  • ऐसे में Pin::get_unchecked_mut जैसे unsafe methods का उपयोग करना पड़ता है, और code को यह वादा निभाना होता है कि value उस reference के बाहर move नहीं होगी.

Unpin और PhantomPinned

  • जो types Unpin implement करते हैं, वे memory safety के लिए pinning पर निर्भर नहीं होते.
// std::marker
pub auto trait Unpin {}
  • Rust के अधिकांश types move होने पर भी समस्या नहीं पैदा करते, इसलिए वे डिफ़ॉल्ट रूप से Unpin होते हैं.
    • उदाहरण: i32, String, Vec
  • Unpin हर type पर अपने-आप implement हो जाता है, जब तक कि उसे स्पष्ट रूप से !Unpin न बनाया जाए.
  • std::marker::PhantomPinned एक marker struct है जो स्पष्ट रूप से !Unpin है.
    • क्योंकि auto trait अपने-आप propagate होता है, इसलिए PhantomPinned field वाला struct भी अपने-आप !Unpin बन जाता है.
use std::marker::PhantomPinned;

struct SelfRef {
    data: i32,
    ptr: *const i32,
    _phantom: PhantomPinned, // makes the entire struct !Unpin
}
  • यह घोषित करने का standard तरीका है कि कोई custom struct pinned होने के बाद move हो जाए तो unsafe हो सकता है.
  • compiler आमतौर पर raw unsafe pointers से बनी self-references को अपने-आप detect नहीं कर सकता.
  • इसलिए developer को self-referential struct के लिए स्पष्ट रूप से Unpin छोड़ना पड़ता है.
    • आमतौर पर यह PhantomPinned field शामिल करके किया जाता है.
  • अगर self-referential type गलती से Unpin बना रहे, तो safe code Pin से mutable reference निकालकर value को move कर सकता है.
    • तब self-reference बनाने वाले unsafe code की assumptions टूट जाएंगी.

Pin कैसे बनाया जाता है

  • Pin अपने-आप value को pin नहीं करता.

  • Pin बनाने का मतलब यह साबित करना है कि उसका pointee, pin की lifetime तक stable memory location में बना रहेगा.

  • Pin::new

    • बनाने का सबसे सरल तरीका Pin::new है.
    let mut value = 42;
    let pinned = Pin::new(&mut value);
    
    • इस constructor का उपयोग केवल तब किया जा सकता है जब T: Unpin हो.
    • Unpin types pinning पर निर्भर नहीं करते, इसलिए उन्हें Pin में wrap करना हमेशा safe है.
    • इस मामले में pinning guarantee व्यावहारिक रूप से no-op होती है.
  • std::pin::pin!

    • जब heap allocation के बिना किसी value को local रूप से pin करना हो, तब pin! macro का उपयोग किया जा सकता है.
    use std::pin::pin;
    
    let future = pin!(async {
        println!("Hello");
    });
    
    • यह macro एक local variable बनाता है और उस variable को point करने वाला Pin return करता है.
    • compiler guarantee देता है कि वह local variable अपनी बाकी lifetime तक move नहीं होगा, इसलिए stack पर !Unpin value को सुरक्षित रूप से pin किया जा सकता है.
    • नाम के बावजूद pin! stack memory itself को pin नहीं करता.
    • यह सिर्फ local variable से बंधा हुआ pinned reference बनाता है, और variable के scope से बाहर जाते ही pinning guarantee भी समाप्त हो जाती है.
  • Box::pin

    • !Unpin types के लिए सबसे आम constructor Box::pin है.
    let pinned = Box::pin(SelfRef { ... });
    
    • pin! local variable से बंधा हुआ Pin बनाता है, जबकि Box::pin Box के स्वामित्व वाला Pin return करता है.
    • heap allocation स्वयं move नहीं होती, इसलिए Box की lifetime तक pointee का memory address stable रहता है.
    • Box को move करने पर भी उसके भीतर की value move नहीं होती, केवल Box के अंदर का pointer move होता है.
    • heap allocation उसी address पर बनी रहती है.
  • Pin::new_unchecked

    • जब safe constructor यह साबित नहीं कर पाते कि value अपनी जगह पर बनी रहेगी, तब unsafe code से सीधे Pin बनाया जा सकता है.
    let pinned = unsafe { Pin::new_unchecked(ptr) };
    
    • Pin::new_unchecked का caller यह वादा करता है कि returned Pin की lifetime तक pointee किसी भी pointer के माध्यम से फिर move नहीं होगी.
    • अगर यह वादा टूटता है, तो pinning guarantee पर निर्भर code में undefined behavior हो सकता है.
    • इसलिए इसका उपयोग आमतौर पर केवल तब किया जाता है जब कोई low-level abstraction implement की जा रही हो जो इस invariant को बनाए रख सके.

व्यवहार में कब ध्यान देना पड़ता है

  • अधिकांश Rust developers के लिए Pin और Unpin पृष्ठभूमि में चुपचाप काम करते हैं.
  • मुख्य रूप से दो स्थितियों में इन्हें सीधे संभालना पड़ता है.
    • async code का consumption: अगर future को सीधे poll करना हो या उसे किसी pinned future चाहने वाले API को देना हो, तो Box::pin(future) से heap पर pin किया जाता है या std::pin::pin!(future) से local stack पर pin किया जाता है.
    • Future का direct implementation: custom state machine या low-level async primitives लिखते समय Pin को संभालना पड़ता है, और pinning invariants बनाए रखने के लिए PhantomPinned तथा unsafe code की आवश्यकता पड़ सकती है.
  • Pin, address-sensitive types की समस्या के लिए Rust का zero-cost समाधान है.
  • इसकी मदद से Rust, garbage collector के बिना memory safety guarantees बनाए रखते हुए async/await और अन्य self-referential abstractions का उपयोग कर सकता है.

1 टिप्पणियां

 
GN⁺ 4 시간 전
Lobste.rs की रायें
  • std::pin::Pin Rust दुनिया का Monad जैसा लगता है। एक बार समझ आ जाए, तो ब्लॉग पोस्ट लिखे बिना रहा नहीं जाता

    • ऐसी पोस्ट अक्सर monad tutorial fallacy में फँस जाती हैं
    • क्या मतलब, Monad की तरह, ऐसी ब्लॉग पोस्ट असल में कुछ भी ठीक से समझा नहीं पातीं?
  • Pin को समझने की कोशिश करते समय मैं और दूसरे लोग जिन कुछ बातों में अटके थे, उन्हें कवर करना अच्छा रहेगा
    Unpin नाम बहुत अच्छा नहीं है। ज़्यादा सटीक लेकिन फिर भी खराब नाम MovableWhenPinned या PinIsNoOp हो सकते थे
    nightly में !Unpin वाला double negative अजीब दिखता है, लेकिन मौजूदा types को 99% वाले default case में रखने के लिए एक auto trait Unpin जोड़ना पड़ा, जिससे type बाहर निकल सके। इसे !MovableWhenPinned सोचें तो ज़्यादा समझ आता है
    stable version का विकल्प PhantomPinned भी अच्छा नाम नहीं है, क्योंकि pinned होना type की property नहीं बल्कि pinned reference होने से बनी एक अस्थायी स्थिति है। कोई वैकल्पिक नाम PhantomNotMovableWhenPinned जैसा होता
    इस तरह दिमाग में translate करना शुरू किया तो समझ बहुत बेहतर हो गई। हालांकि अब भी उलझन रहती है; शायद किस्मत अच्छी रही हो

    • पूरी तरह सहमत। पहले !Unpin से दिमाग खराब होता था, लेकिन Unpin को SafeToUnpin की तरह पढ़ना शुरू किया तो थोड़ी आसानी हुई
  • मैंने पहले यह सवाल पूछा था और लगता है किसी ने सोच-समझकर जवाब दिया था, लेकिन याद नहीं रहा। मेरी समझ में Pin async से आया था, और समस्या यह थी कि local variable references किसी खास function की state machine को दर्शाने वाले data blob के अंदर self-referential बन जाते थे
    अगर async state move हो जाए, तो वे local variable references पुराने, गलत location की ओर इशारा करने लगते हैं
    लेकिन क्या ऐसा सिर्फ इसलिए नहीं है क्योंकि reference एक real pointer होता है जिसके पास पूरा absolute address होता है? मैं सोचता हूँ कि समाधान reference को relative address बनाने के बजाय move करने की क्षमता हटाना क्यों था
    क्या जवाब मोटे तौर पर यह है कि “compiler, CPU और OS को pointers बहुत अच्छी तरह handle करने लायक बनाने में लाखों engineer-years लगे हैं, इसलिए pointers कई मायनों में बेहतर हैं, और इसलिए इधर-उधर Pin इस्तेमाल करना बेहतर है”; या फिर relative references सचमुच alternative के रूप में काम नहीं कर सकते, इसकी कोई ठोस वजह है?

    • समस्या सिर्फ यह नहीं है कि async state के अंदर कोई local variable उसी state के अंदर किसी दूसरे local variable को directly reference करता है। अगर बात सिर्फ इतनी होती, तो compiler सभी local variables जानता है, इसलिए access को relative बना सकता था। लेकिन अगर किसी type की गहराई में कोई reference किसी दूसरे type की गहराई में किसी value को point करे, तो बात कहीं ज़्यादा कठिन हो जाती है
      अगर references relative हों, तो उन types की memory representation इस पर निर्भर करते हुए बदलनी पड़ेगी कि वे async state के अंदर इस्तेमाल हो रहे हैं या नहीं, और relative reference से असली pointer restore करने के लिए साथ में pass किए जाने वाले base pointer की concept भी चाहिए होगी
      pinned reference के अंदर nested objects, root object pinned होने पर भी, अभी भी स्वतंत्र रूप से move हो सकते हैं, इसलिए यह भी नहीं कह सकते कि सभी काल्पनिक relative references एक ही base pointer के relative हैं
      अंत में absolute pointer की ज़रूरत पड़ती है और relative references फिट नहीं बैठते। तो क्या Rust compiler यहाँ मौजूद types को जानता है, इसलिए पूरे object graph को track करके moved object को point करने वाले references को नए location पर fix करके object को movable बना सकता है? ऐसा करने पर आप मूलतः tracing garbage collector बना रहे होंगे
      इसके अलावा Rust compiler object graph के सभी types नहीं जानता। references FFI के ज़रिए pass हो सकते हैं और external library उस reference को store कर सकती है। FFI boundary के पार moving references को fix करना practically बहुत कठिन समस्या है
      इसलिए यह सचमुच मुश्किल है। यह भी अहम है कि object movement खुद भी अपेक्षाकृत नई technique है। ज़्यादातर C/C++ programs में कहा जा सकता है कि सभी objects implicitly pinned हैं। वहाँ pinning पर कम चर्चा इसलिए होती है क्योंकि objects बस move नहीं होते, या अगर move होते भी हैं तो dangling references न बचें, इसकी जिम्मेदारी programmer की होती है
    • Pin उन दूसरी languages के साथ interoperability के लिए भी ज़रूरी है जहाँ Rust memory को opaque bits के ढेर की तरह मनमाने ढंग से move नहीं कर सकता
      मेरी समझ में C++ interoperability की समस्याओं में से एक यह है कि objects simple bit blobs नहीं हैं जिन्हें freely move किया जा सके, और आखिरकार काफी types को pinning चाहिए होती है, जिससे usability असुविधाजनक हो जाती है
      हालांकि यह कम-से-कम 6 महीने पहले इस पर काम कर रहे लोगों से हुई बातचीत पर आधारित है, इसलिए उसके बाद स्थिति कितनी बेहतर हुई है, पता नहीं
  • कुल मिलाकर official Rust docs के साथ पढ़ने लायक अच्छी व्याख्या लगती है। समस्या में जाने का तरीका थोड़ा ज़्यादा smooth है
    लेकिन self-referential structs से शुरू करना, मेरे हिसाब से, उसे छोड़ देने से भी ज़्यादा भ्रमित करता है। खासकर intro में “इसलिए ऐसा self-reference बनने के बाद SelfRef को move होने से रोकने का कोई तरीका चाहिए” वाला वाक्य, core point के बजाय “movement को पूरी तरह रोकने की समस्या” याद दिलाता है
    असली core point काफी बाद में आने वाली बात में है: “Pin value को physically move होने से नहीं रोकता। इसके बजाय यह type-level guarantee है कि उस pointer के through value move नहीं होगी”
    movement को खुद रोक नहीं सकते, इसलिए safe API में self-referential data को exclusive reference के पीछे ही expose करने के लिए Pin इस्तेमाल किया जाता है। हो सकता है मैं Pin को पहले से बहुत ज़्यादा समझ चुका हूँ, लेकिन explanation style थोड़ा polish किया जाए तो readers कम भटकेंगे

    • मैं पोस्ट को बदलकर इसे व्यक्त करने की कोशिश करूँगा
      यह पोस्ट pinning पर मेरे notes से ली गई है, और शुरुआत में मैंने भी इसे इसी तरह समझा था। “movement रोकना” जैसी problem को type-level guarantee से solve किया जा सकता है, यह बात सुंदर लगी थी
      बेशक Pin असल में यही नहीं करता, इसलिए उस बात को साफ़ दिखाने के लिए पोस्ट सुधारना सही होगा
  • इस पोस्ट में कहीं यह लिख देना ठीक रहेगा कि !UnPin केवल nightly Rust में express किया जा सकता है। यही PhantomPinned के मौजूद होने की मुख्य वजह है

  • इसे “pointer wrapper” कहा गया है, लेकिन Rust में भी pointers से निपटने की ज़रूरत शायद ही पड़ती है। समझ नहीं आता इसे क्यों इस्तेमाल करना चाहिए
    *const के लिए Google पर Rust docs ढूँढना मुश्किल है; सोच रहा हूँ क्या यह documented है
    “compiler द्वारा generated state machine का field बन जाता है” — क्या यह भी जानना ज़रूरी है? या कोई अजीब compiler error असल में यह बताने की कोशिश कर रहा है कि ऐसा हुआ था?
    “generated future self-referential हो जाता है” — क्या future इस्तेमाल करने पर यह implicitly होता है?
    Future::poll शायद मैंने कभी directly इस्तेमाल नहीं किया
    एक तरफ कहते हैं “safe code सामान्य &mut T recover नहीं कर सकता” और दूसरी तरफ “सामान्य mutation allow करता है”; तो फिर किया कैसे जाता है?
    ऐसी चीज़ों की वजह से मैंने Rust में और गहराई तक जाना छोड़ दिया

    • raw pointer Rust के primitive types में से एक है। documentation यहाँ और यहाँ है
      हालांकि यह भी सही है कि low-level में न जाएँ तो शायद ही इसका इस्तेमाल पड़े। मुझे भी C library call करने की ज़रूरत पड़ने पर ही इसके बारे में पता चला
      Future::poll Rust async code की बुनियाद है। आप इसे directly call नहीं करते; executor इसे call करता है। Rust में default executor नहीं है, इसलिए Tokio, smol, pollster जैसी चीज़ें जोड़नी पड़ती हैं, और ये Future trait में defined poll जैसे methods का इस्तेमाल करके काम संभालती हैं
    • मैं original post का author नहीं हूँ और ये अकेली वजहें भी नहीं हैं, लेकिन Rust में pointers से निपटने की मुझे जो वजहें पड़ीं, वे FFI और graph जैसी self-referential data structures थीं
      docs यहाँ सहित कई जगहों पर हैं
      यह उम्मीद करना कि दूसरे लोग सिर्फ वही explain करें जिसकी उन्हें खुद ज़रूरत पड़ी थी, थोड़ा ज़्यादा है
      “तो कैसे?” में आप क्या पूछ रहे हैं, मुझे ठीक से समझ नहीं आया