اصل ماجرا
پست به بررسی فاز پیش از main در برنامههای رست میپردازد و نشان میدهد چگونه میتوان با استفاده از کرِیتورهای ctor و لینک‑سِکشنها دادهها را قبل از ورود به تابع main جمعآوری، مرتبسازی و ثبتنام کرد. این تکنیکها امکان حذف تخصیص حافظه، حذف قفلها و بهبود زمان شروع برنامه را فراهم میآورند.
متن کامل ترجمهشده
اینجا زندگی قبل از اصلی در Rust permalinkDisclosures ð§ این پست 100٪ انسان نوشته شده است. Claude برای بازخورد و برای کمک به دیگراف نماد لینکر استفاده می شود. Cursor برای بازخورد و برای اطمینان از مثال ها تشکیل شده بود. نویسنده این پست به شدت علاقه مند به موضوع زندگی قبل از اصلی است: او نویسنده ctor crate است، و خالق پروژه linktime که ما در نمونه های زیر استفاده خواهد کرد. هر Rust باینری یک چیز مشترک دارد: fn main(). اگر شما از جهان C آمده اید، که ممکن است بیشتر آشنا به عنوان int main(argc, argv). برخی از پلتفرم ها ممکن است آن را کمی بیشتر، اما زیر کلاه، هر باینری دارای یک نقطه ورود است. ما می خواهیم بحث در مورد آنچه قبل از اصلی اتفاق می افتد و چه چیزهای جالب ما می توانیم در آنجابرخی از دانش پس زمینه ممکن است برای خواننده مفید باشد، از جمله: قبل از اصلی چیزی که ممکن است به اکثر توسعه دهندگان آشنا نیست این است که چگونه شما وارد عملکرد اصلی می شوید. شما می بینید، زیر کلاه برای هر زبان زمان اجرا است. C دارای یک: زمان اجرا C که شما ممکن است به عنوان libc شناخته شود. Rust همچنین زمان اجرا خود را دارد: کتابخانه استاندارد Rust. و از آنجا که C زبان انگلیسی زمان اجرا برای اکثر کد اجرا می شود 1, Rust زمان اجرا خود را در بالای Câs، به طور موثر ساختن خود را سطح بالاتر است Câs. یک زمان اجرا کمی فاسد برای تعریف است. این هر دو کد اجرا می شود که در دیسک زندگی می کند و سرپایه های کامپیوتری و کتابخانه های مورد استفاده در زمان کامپیوتری است. اما هدف از زمان اجرا همیشه یکسان است: یکپارچه کردن کد توسعهRust از این زمان برای تنظیم بخشی از زبان و زمان اجرا خود استفاده می کند. به طور خاص، Rust دارای زیرساخت برای مقابله با ترس و ناخوشایند است. Rust همچنین نیاز به ترجمه استراتژی برنامه سبک C 2 را به رابط خود std::env::args. ماشین برای همه این در پروژه کمپین Rust قابل مشاهده است. Runtimes از این مرحله پیش از اصلی استفاده می کنند زیرا آن را تضمین می کند (1) قبل از استفاده از کد کاربر، و (2) یک محیط واحد، بسیار مداوم و قابل پیش بینی، که اجازه می دهد برای شروع قابل اعتماد و deterministic. با عدم استفاده از این محیط، شما در یک مرحله بسیار مفید bootstrapping از دست می گیرید. ما بعدا در این پست می بینیم که چگونه می توانیم برخی از اولویت های مفید ایجاد استفاده از زندگی قبل از دست اصلی. Points Entry یک باینری شروع می کند هنگامی کهدر لینوکس، نقطه ورود در زمینه e_entry از عنوان ELF ذخیره می شود، و به طور پیش فرض، لینکر آدرس یک نماد به نام _start در آنجا قرار می دهد. یک نماد مشابه در ویندوز وجود دارد، و اجراگر را در یک عملکرد به نام _WinMainCRTStartup راه اندازی می کند. در این نقطه زمان اجرا C فرصت دارد که خود را تنظیم کند، و راهی که تمام زمان اجرا این کار را از طریق فعالیت های ابتدایی سازی انجام می دهد. در تکرار های اولیه زمان اجرا، bootstrapping یک درخت استاتیک از ویژگی ها بود: فایل I/O را آغاز کنید، Allocator را آغاز کنید، و غیره. به عنوان زمان اجرا پیچیده تر شد، این درخت فعالیت ها پیچیده تر شد، و اندازه های باینری برای جذب بیشتر عملکرد زمان اجرا C است که ممکن است نیاز داشته باشند.با گذشت زمان، لینکداران توانایی حذف کد غیر مورد استفاده را پیش از نوشتن کد باینری به دیسک (از جمله قسمت های غیر مورد استفاده از زمان اجرا C) توسعه دادند و با این وجود نیاز به جایگزینی برای درختان تماس init استاتیک وجود داشت. بهترین روش 4 برای اعلام کد init از GCC آمده بود: attribute((constructor)). راه این کار بود تا یک لیست از ویژگی های init را به یک قطعه متصل از زمان اجرا C در دیسک قرار دهد. هنگامی که زمان اجرا C شروع شد، می توانست از هر یک از این ویژگی ها عبور کند و آنها را بخواند، اجازه می دهد که بیتی های مختلف از زمان اجرا C به درخواست ابتدایی سازی بدون به طور قوی متصل کردن زیرسیستم ها، و اجازه می دهد لینکدار به حذف زیرسیستم های غیر مورد استفاده، کد init و همه چیز. در نهایت، سفارشدر اکثر پلتفرم های 5، لینکر برای انجام کار اولویت قرار گرفت: هر پلتفرم با یک راه برای اولویت گذاری ترتیب که داده ها به بخش ها نوشته می شود، که اجازه می دهد زمان اجرا C به پایان می رسد با یک لیست به طور کامل از نشانگرهای عملکرد 6. ما حتی می توانیم نمونه ای از این را با دست در Rust با استفاده از #[unsafe(link_section = ”…”)] attribute (برای امتحان آن در Rust Playground): /// مثال لینوکس: زمان اجرا glibc مدرن استفاده از .init_array برای نگه داشتن عملکرد /// نشانگرها، و یک فرمت شماری اجازه می دهد که آنها را به ترتیب. توجه داشته باشید که اولویت ها /// کمتر از یا برابر 100 برای زمان اجرا خود، بنابراین هر کد که /// می خواهد برای استفاده از زمان اجرا C باید یک اولویت 101 یا// ما می توانیم یک عملکرد را به یک نشانگر عملکرد با یکی از بلوک های زیر تبدیل کنیم: // #[ استفاده شده] // <— بدون این، Rust ممکن است تصمیم بگیرد که عملکرد init مورد استفاده قرار نمی گیرد و آن را حذف کند // #[unsafe(link_section = “.init_array.NNNNN”)] // <— بخش که در آن ما نشانگر عملکرد را قرار می دهیم // INIT_ARRAY_FN_PTR استاتیک INIT_FN_FIRST: “C” fn() // = عملکرد خارجی; // <— داده های نشانگر عملکرد: ما این کار را به آن اختصاص می دهیم // // // بیرونی “C” fn function() { … } // <— عملکرد خود #[ استفاده] #[unsafe(link_section = ”._initarray.101”) استاتیک INIT_secF”) } linktime: ctor، link-section و بیشتر مثال های در این پست در لینوکس و BSD های مختلف کار خواهد کرد، اما طراحی شده نیست برای مثال های cross-platform باشد. به عنوان مثال، macOS دارای نماد های شروع و پایان، اما آنها به صورت متفاوت نامیده می شوند 7. ویندوز نماد های شروع و پایان را پشتیبانی نمی کند، اما مجموعه ای از قوانین برای طبقه بندی بخش هایی است که در واقع مساوی است. زیرا پلتفرم ها به طور گسترده متغیر هستند، ما ctor و link-section crates (از پروژه linktime) را به عنوان یک راه برای حذف تفاوت های خاص پلتفرم و پنهان کردن پیچیدگی کلی از کار linker است. ذخیره سازی عالی و linkme دو خط بسیار محبوب دیگری است که بر اساس همان اصول ساخته شده است، اما دارای محدودیت های 8 است که آنها را کمتر مناسب برای مثال ها دراین به ما اجازه می دهد برای ساده سازی مثال های ما در بالا به: استفاده از ctor::ctor; #[ctor(Unsafe, Priority = 101)] fn init1() { println!(“Initializing (first)!”); } #[ctor(unsafe, priority = 201)] fn init2() { println!(“Initializing (second)!”); } fn main() { println!(“Main!”) } توجه داشته باشید که هر یک از مثال ها به طور صریح به عملکرد های init می گویند. لینکر آنها را به طوری سازمانده است که زمان اجرا C آنها را برای ما خواند! بخش ها و لینکر اسکرپتها فرآیند که در آن سازندگان در ارتباط هستند پنهان نیست، با این حال.اکثر لینکرها اجازه می دهند توسعه دهندگان به ارائه لینکر اسکرپت ها - فایل های متن که در کنار کد منبع شما زندگی می کنند (که به فایل های اشیاء جمع آوری می شود) و به لینکر در مورد چگونگی جمع آوری این فایل های اشیاء راهنمایی می کنند. با استفاده از یک لینکر اسکرپت، یک فایل C تنها می تواند تبدیل به یک Linux اجرا می شود، یا یک بلوک از جمع آوری خام که در بخش راه اندازی یک دیسک سخت زندگی می کند. لینکر اسکرپت ها همچنین اجازه می دهد تا نماد های مجازی را تعریف کنید - یعنی نماد هایی که در هر فایل منبع وجود ندارد اما می تواند توسط کد C برای دسترسی به نشانگرها به داده های زیرساخت در باینری بارگذاری شده استفاده شود. لینکر اسکرپت ها موضوعی پیچیده و فراتر از مقیاس این پست هستند، اما ما می توانیمدر مثال بالا، نماد های مجازی TEXT_START و TEXT_END به طور واضح تعریف شده اند تا به آغاز و پایان بخش .text اشاره کنند. دوره در TEXT_START = .؛ یک سنتاکس ویژه است که به یک شمارش مکان اشاره می کند که به طور حدود به آدرس اتمام فعلی در باینری حل می شود. نماد های Linker این بسیاری از توسعه دهندگان را که برای اولین بار با آن روبرو می شوند، اما linker آدرس نماد های آغاز و پایان را تنظیم می کند، و بنابراین جایی که ثابت با همان نام قرار داده می شود، و ارزش نماد هایی را که نشان می دهند را تنظیم نمی کند. به این معنی: شروع و متوقف کردن نماد ها یک *const هستند. نماد های آغاز و متوقف به خودشان داده ای ندارند و تنها برای آدرس هایشان استفاده می شوند! قسمت از طیف داده هامشخص کردن علائم آغاز و پایان برای هر بخش می تواند پیچیده و خسته کننده در اسکرپت های لینکر باشد، بنابراین بسیاری از لینکرها 9 در نهایت یک ویژگی را دریافتند که می توانند به طور خودکار علائم را تعیین کنند که تمام قسمت ها را در اجرا می کنند. به عنوان مثال، برای زنجیره های ابزار GNU، یک بخش به نام MY_SECTION به طور خودکار علائم __start_MY_SECTION و __stop_MY_SECTION را تعریف می کند. macOS دارای یک مدل مشابه است که برای هر بخش علائم section$start و section$end را ترکیب می کند. در لینکر GNU، آن قسمت هایی که در اسکرپت لینکر مشخص نشده اند به طور مشخص به عنوان âorphanâ sections 10 نامیده می شوند.در مثال زیر می بینید که نماد های آغاز و پایان MaybeUninit<()> هستند. نماد های مرز هیچ اطلاعاتی ندارند و تنها آدرس آنها قابل توجه است. نوع Rust ایده آل برای این ها یک typâ خارجی âopaque خواهد بود (این توسط ویژگی extern_types اجرا می شود). از آنجا که این نماد ها در حال حاضر در Stable Rust اجرا نمی شوند، MaybeUninit یک Stand-in است. این به این معنی است که سازنده داده ها نامحدود هستند و به طور کلی برای خواندن از طریق اشاره ای امن نیست. از آنجایی که استفاده از یک نشانگر ثابت &raw به یک ماده استاتیک همیشه معتبر است، با این حال، ما هنوز می توانیم آدرس آن را بدون خواندن ارزش آن را حفظ کنیم. سعی کنید آن را در Rust Playground استفاده کنید: std::mem::MaybeUninit; #[[us] #به جای آن، لینکر می بایست // علائم مرز STATIC_STRING_START و STATIC_STRING_END را در // آغاز و پایان بخش قرار دهد! غیر امن خارجی “C” { #[link_name = “__start_our_strings”] استاتیک STATIC_STRING_START: MaybeUninit<()>; #[link_name = “__stop_our_strings”] استاتیک STATIC_STRING_END: MaybeUninit<()>; } fn main() { let strings: &‘static [&‘static string] = unsafe { // SAFETY: get the addresses of the start and symbol ends without reading them // let start = STATIC_STRING_END as *const &‘static string>; } fn main() { letما می توانیم آن را برای ساده سازی مثال بالا به: استفاده از link_section::{in_section, section}; #[section(typed)] استاتیک OUR_STRINGS: link_section::TypedSection<&‘static str>; #[in_section(OUR_STRINGS)] استاتیک FIRST_STRING: &‘static str = “سلام، ”; #[in_section(OUR_STRINGS)] استاتیک SECOND_STRING: &‘static str = “world!”; fn main() {println!(“String: {}”, OUR_STRINGS.join.join(”)); } در این مثال ها ما ارسال اشیاء به بخش لینک در یک ماژول تنها در یک کمد، اما این یک نیاز نیست. در واقع،این یک مدل شناخته شده است: فرایند مانند Dagger و Spring بر اساس همان اصل ساخته شده است که مصرف کنندگان داده های ثبت نام نباید با ارائه دهندگان آن داده ها متصل شوند. یک ارائه دهندگان داده ها را در سایت تعریف خود ثبت می کند، یک مصرف کننده به سادگی ثبت می کند. چیزی که با قسمت های لینکر نسبت به DI سنتی متفاوت است این است که در DI فرایند اغلب نیاز به پیاده روی گرافیک ماژول یا اسکن کلاس های بارگذاری شده در راه اندازی برای کشف هر دو ارائه دهندگان و سایت های مصرف کننده است. با قسمت های لینکر، این جادو در هنگام نوشتن باینری پردازش می شود. لینکر آن است که تمام داده های ارائه دهندگان را جمع آوری می کند و آن را به طور نادرست برای مصرف کننده در دسترس می سازد. مثال زیر از یک لینک_section::s ثبت نام می کند تایک سرور وب hypothetical می تواند از این مدل برای ثبت راه ها و middleware که به طور خودکار در زمان ساخت جمع آوری می شود استفاده کند.
[…]
چرا مهمه؟
این روش بهینهسازی زمان اجرا و کاهش وابستگیهای زمان اجرا در برنامههای رست، بهویژه در ابزارهای بزرگ و کتابخانههای زیرساختی، را امکانپذیر میسازد.
به درد کی میخوره؟
developers, tech_leads
تو عمل چی کار کنیم؟
توسعهدهندگان میتوانند با استفاده از کرِیتورهای پیشمقدمه و لینک‑سِکشنها، ثبتنام خودکار ماژولها و دادهها را بدون وابستگی به زمان اجرا انجام دهند و از هزینههای تخصیص حافظه و قفلها دوری کنند.
نظر Blue IT News
استفاده از فاز پیش از main در رست میتواند بهصورت بیقفل و بدون هزینههای زمان اجرا، دادهها را جمعآوری و آمادهسازی کند؛ این روش برای کتابخانههای بزرگ و ابزارهای کامپایلزمانی بسیار مؤثر است.
<div class=“disclosure”> این صفحه ترجمه و تفسیر کاملی از گزارش اصلی Grack است که توسط تیم تحریریه بلو آی تی نیوز به فارسی ترجمه و تحلیل شده. برای مشاهده نسخه اصلی، به منبع مراجعه کنید. </div>