Rustling Up Cross-Platform Development – انجمن DEV

تجربه من با توسعه موبایل چند پلتفرمی فاقد برخی عناصر مهم مانند فلاتر یا زامارین است. بنابراین، این مقاله تحلیلی جامع از ابزارهای این فضا نیست.
در طول سالها، چند ابزار مختلف را برای توسعه بین پلتفرمها امتحان کردهام، از جمله PhoneGap (که احتمالاً باید از آن اجتناب میکردم)، React Native، Qt و کمی Kotlin Native. به طور کلی، من کاملاً معتقدم که رابط کاربری باید بومی باشد و ابزارهایی مانند PhoneGap آن را برای چیزی بیشتر از یک برنامه ساده کاهش نمی دهند. در حالی که React Native جوانب مثبت و منفی خود را دارد، اما من را به عنوان یک توسعه دهنده جلب نکرده است. در عوض، من ایده داشتن یک هسته متقابل پلتفرم و رابط کاربری بومی را ترجیح می دهم. به عنوان فردی که در حول و حوش کیت کت (4.4؟ 🤔) از Android به iOS تغییر مکان دادم، از آنجایی که طبیعتاً تمایل بیشتری به زبانهای مبتنی بر llvm دارم، C++ اولین انتخاب من برای کد بین پلتفرمی بود. در iOS، پل زدن C++ و Objective-C از طریق ترکیبی از هر دو به نام Objective-C++ نسبتاً آسان است. من روی چند پروژه بزرگ کار کرده ام که به شدت بر اساس این ایده بود و می توانم تأیید کنم که این یک راه حل کارآمد است. با این حال، Objective-C هر روز کمتر محبوب می شود و Objective-C++ جانور ترسناک تری برای کار با آن است. نمی توانم بگویم نوشتن آن برایم لذت بخش بود. علاوه بر این، هیچ دلیل قانعکنندهای برای نوشتن کد در سطح برنامه در C++ نمیبینم. شاید برای کدهای سطح سیستم عامل، اما این موضوع برای بحث دیگری است. پس از چند بار تلاش با C++، Kotlin Native (KN) را امتحان کردم، که ابزار و پشتیبانی IDE بسیار بهتری داشت، حتی در نسخه های اولیه. کاتلین یک زبان سرگرم کننده برای خواندن و نوشتن است و با قسمت “بومی”، حتی می توانیم خود را از شر JVM خلاص کنیم. بنابراین اگر قبلاً در اکوسیستم اندروید غوطه ور شده اید، کاتلین را دوست دارید و از کار در اندروید استودیو لذت می برید، پس KN باید انتخاب خوبی برای شما باشد. با این حال، در این مقاله، من می خواهم دیدگاه “زنگ زده” تری را بررسی کنم. بیایید شیرجه بزنیم
من چند بار با Rust در iOS صحبت کردم و به نظر بسیار شبیه C++ بود. شما یک کتابخانه استاتیک می سازید، از هدرهای C به عنوان چسب استفاده می کنید و در نهایت با اشکال زدایی دست و پنجه نرم می کنید. این رویکرد زمانی ساده است که شما فقط یک بخش کوچک از منطق را در یک کتابخانه مشترک استخراج کرده و از طریق یک رابط باریک با آن تعامل دارید. اما اگر بخواهید بیشتر منطق برنامه را در فهرست به اشتراک گذاشته شده قرار دهید، چه؟ آن وقت است که همه چیز پیچیده می شود.
اخیراً در کنفرانس Rust London به طور تصادفی با پروژه ای برخورد کردم که نظرم را جلب کرد. Crux نام دارد و کتابخانهای است که به شما کمک میکند یک هسته کاربردی و پارادایم پوسته ضروری را پیادهسازی کنید. به عبارت دیگر، به شما امکان می دهد منطق برنامه خود را از کد UI خود جدا کنید و آن را بین پلتفرم ها به اشتراک بگذارید.
اگرچه ایده یک هسته کاربردی و پوسته ضروری ممکن است ساده به نظر برسد، اجرای واقعی می تواند مشکل باشد. با شروع کار بر روی آن، به طور اجتناب ناپذیری با موانع و چالش هایی مواجه خواهید شد، به خصوص زمانی که صحبت از جداسازی منطق اصلی از رابط کاربری باشد.
دومین چالش بزرگ بعد از “نامگذاری متغیر” یافتن معماری مناسب برای استفاده است. معماریهای سنتی MVC/MVP ممکن است همیشه بهترین گزینه نباشند، و پیگیری تمام جریانهای داده در برنامههایی که قبلاً با آنها کار میکردم دشوار بود. علاوه بر این، رابطهای کاربری دنیای واقعی میتوانند پیچیده و پویا باشند، که وضعیتها و تعاملات بیشتری را به لایه UI اضافه میکند.
اینجاست که مفهوم کاربردی Core بدون عوارض جانبی وارد می شود. Crux به ساخت پایه کمک می کند. برای من، در فهمیدن اینکه چگونه کد خود را ساختار دهم و چگونه منطق اصلی را به روشی ارگونومیک و خواندن آسان جدا کنم، واقعاً مفید بوده است. در عرض چند ساعت یک برنامه کوچک ایجاد کردم که با API های DALL-E تعامل دارد (بسیار واضح است، درست است؟) و روی 3 پلتفرم کار می کند (در واقع 2.5 چون وب را تمام نکرده ام 😅). در بخش بعدی، برداشت های اولیه ام را به اشتراک خواهم گذاشت.
برپایی
از آنجایی که این پروژه در مراحل اولیه خود است، راه اندازی آن به اندازه React Native یکپارچه نیست. با این حال، اگر تصمیم دارید با این پشته برای یک پروژه واقعی بروید، مشارکت در ابزارسازی داخلی کار چندان مهمی نیست. در واقع، اکثر پروژه های بزرگ، حتی پروژه های تک پلتفرمی، شامل باغ وحشی از اسکریپت های مختلف bash هستند و به هر حال فایل می سازند. این کتاب توضیح بسیار خوبی در مورد نحوه عملکرد آن دارد و حتی برنامه های نمونه را ارائه می دهد.
من شخصاً بهتر دیدم که پروژه را از ابتدا با استفاده از کتاب تنظیم کنم. به این ترتیب، میتوانستم همه مکانها را ببینم تا اگر مشکلی پیش آمد، جستجو کنم. کمتر از یک ساعت طول کشید تا پروژه هسته و iOS را راه اندازی کنم و این روند ساده بود. خوشبختانه، پیکربندی هسته در فایلهای .rs و toml است که پیگیری آنها بسیار آسان است.
برای iOS، به چند اسکریپت bash نیاز دارید (اوه، من از نوشتن bash متنفرم). اما در مورد من، کپی پیست کافی بود، و ChatGPT زندگی را حتی در صورت نیاز به شخصی سازی در bash قابل تحمل کرد. به طور خلاصه، شما باید هسته را به عنوان یک کتابخانه استاتیک کامپایل کنید، پیوندهای زبان های UI را با استفاده از uniffi crate ایجاد کنید، و این مراحل را به پروژه Xcode اضافه کنید تا نیازی به بازسازی و پیوند مجدد هسته به صورت دستی نداشته باشید. Uniffi نیاز به نوشتن یک فایل IDL Interface Definition Language دارد که روشها و ساختارهای دادهای موجود برای زبانهای مورد نظر را توصیف میکند. من Swift/Kotlin و TS را به ترتیب برای iOS/Android و Web تولید کردم.
UDL به شکل زیر است:
namespace core {
sequence<u8> handle_event([ByRef] sequence<u8> msg);
sequence<u8> view();
};
در پایان، ساختار پروژه به این صورت است (بدون اندروید و وب در تصویر):
توسعه
وقتی نوبت به توسعه میرسد، احتمالاً زمان خود را بین Xcode/Android Studio و هر چیزی که برای Rust و توسعه وب ترجیح میدهید تقسیم میکنید. من برخی از روحهای شجاع را دیدهام که تلاش میکنند توسعه موبایل را در Emacs انجام دهند، اما در پایان روز، آنها به طور قابل توجهی کندتر از هم تیمیهای خود بودند.
خبر خوب این است که کار بر روی هسته، ایجاد رابط کاربری و نوشتن تست، و سپس تغییر به Xcode/Studio برای صیقل دادن قطعات هسته به صورت موازی، بسیار راحت است. من شخصاً از CLion برای Rust استفاده می کنم و جرات نمی کنم بیش از 2 مورد از 3 (CLion/Xcode/Android Studio) را همزمان باز کنم. Rust بسیار آهسته کامپایل می شود، که برای من مشکلی نیست، زیرا پروژه Swift/ObjC من در محل کار حدود 50 دقیقه طول کشید تا یک ساخت تمیز در MacPro با پیکربندی برتر (نه MacBook 🐌). با این حال، برای توسعه دهندگان وب، این ممکن است کمی سخت باشد. اما ماژولارسازی مناسب پروژه می تواند به این امر کمک کند.
نوشتن کد در Rust در ابتدا می تواند کمی چالش برانگیز باشد، اما من متوجه شدم که بسیاری از ایده ها شبیه به Swift هستند، بنابراین مانند یک تجربه کاملا متفاوت نیست. Enums مانند سوئیفت، اینطور نیست؟ 😁
#[derive(Serialize, Deserialize)]
pub enum Event {
Reset,
Ask(String),
Gen(String),
#[serde(skip)]
Set(Result<Response<gpt::ChatCompletion>>),
#[serde(skip)]
SetImage(Result<Response<gpt::PictureMetadata>>),
}
وقتی صحبت از اشکال زدایی می شود، می توانید از نقاط شکست از طریق آن استفاده کنید lldb “مجموعه نقطه شکست” دستور اشکال زدایی کد Swift و Rust را در کتابخانه ایستا پیوند داده شده شما. این به اندازه اشکال زدایی یک پروژه Kotlin خالص در اندروید استودیو راحت نیست، اما همچنان کار را انجام می دهد.
به عنوان مثال خطای متغیر .env از دست رفته حتی از داخل Xcode به راحتی قابل شناسایی است.
خط دقیق در لاگ ها:
با این حال، من نتوانستم هیچ مشکلی در مورد اشکال زدایی هسته و پوسته به طور جداگانه ببینم. در واقع، این میتواند بسیار مفید باشد که بتوان هر مؤلفه را بهطور مستقل اشکال زدایی کرد، زیرا میتواند شناسایی منبع هر گونه اشکال یا مشکل را آسانتر کند.
چه در مورد interop… من قرار نیست دروغ بگویم، ایده آل نیست. به طور خاص، تعامل بین Rust و Swift به همان اندازه بین Swift/Objective-C و Kotlin/Java یکپارچه نیست. مثلا، f64 نمی توان آنطور که هست از مرز عبور کرد ( که منطقی است، اما همچنان). با این حال، چند برگه تقلب برای کمک به درک قوانین interop در دسترس است. برای سوئیفت، قوانین زیر اعمال می شود:
- اولیه ها به همتای واضح Swift خود نگاشت می شوند (به عنوان مثال
u32
تبدیل می شودUInt32
،string
تبدیل می شودString
، و غیره.). - یک رابط شی اعلام شده به عنوان
interface T
به عنوان یک پروتکل سوئیفت نشان داده می شودTProtocol
و یک کلاس Swift بتنیT
که با آن مطابقت دارد. - یک عدد اعلام شد
enum T
یا[Enum] interface T
به عنوان یک عدد سوئیفت نشان داده می شودT
با انواع مناسب - انواع اختیاری با استفاده از سینتکس نوع اختیاری داخلی سوئیفت نمایش داده می شوند
T?
. - دنباله ها به عنوان آرایه های Swift و نقشه ها به عنوان فرهنگ لغت Swift نشان داده می شوند.
- خطاها به صورت شماره های سوئیفت که مطابق با
Error
پروتکل - فراخوانی های تابعی که دارای نوع خطای مرتبط هستند با علامت گذاری می شوند
throws
در سوئیفت
من قوانین مشابهی را برای Kotlin Native به یاد دارم. در واقع، رابط بین هسته و پوسته باید لاکونیک باشد. به نظر من این محدودیت ها خوب نیستند، اما خیلی هم ضرری ندارند.
معماری
صحبت در مورد الگوهای معماری. آیا مهندس موبایلی را دیده اید که در مورد الگوها صحبت نمی کنند؟ Crux از Elm الهام گرفته شده است، صفحه بسیار خوبی در کتاب وجود دارد و همچنین اسناد Elm ارزش خواندن دارد، بنابراین از توضیحات صرف نظر می کنیم. به طور کلی من حرکت به سمت معماری های یک طرفه و پیام رسان را می بینم. آنها تمیز و کاملاً دقیق هستند، که باعث می شود به روز رسانی کد و عدم ایجاد ناهماهنگی زمانی که یک فیلد متنی دارای سه حالت مختلف در سراسر لایه ها باشد، آسان تر می شود. درست است که کتابخانههای اندروید UIKit یا Vanila بهترین مناسب نیستند (اگرچه هنوز امکان استفاده مجدد از برخی ایدهها وجود دارد)، اما SwiftUI و Jetpack Compose بسیار مناسب هستند. اگر تعامل ژستها و رابطهای کاربری سنگین انیمیشن بنویسید – چالش برانگیز خواهد بود. مثلاً اگر تغییر حرکتی انجام دهید، آیا باید وضعیت فعلی را در UI حفظ کنید یا آن را به هسته منتقل کنید؟ یا UITableView (iOS) و RecyclerView (اندروید) چرخه زندگی کمی متفاوت برای سلولها دارند، بنابراین برای مدلهای سلولی، هسته چگونه با آن برخورد خواهد کرد. کمی چالش برانگیز، اما همچنان ممکن است، مثل همیشه بدون گلوله نقره ای.
با این حال، بخشی که بیشتر از همه دوست داشتم، ویژگی قابلیت ها بود. قابلیت ها یک راه خوب و واضح برای مقابله با عوارض جانبی مانند شبکه، پایگاه های داده و چارچوب های سیستم ارائه می دهند. مطمئناً، میتوانید یک کتابخانه HTTP به زبان C بنویسید و از آن در همه جا استفاده کنید، و شاید حتی بتوانید پایداری را برای استفاده فقط از SQLite استاندارد کنید. اما موارد مختلفی مانند صدا/تصویر، سیستمهای فایل، اعلانها، بیومتریک یا حتی لوازم جانبی مانند Apple Pencil باید در نظر گرفته شود. و سیستم شما در حال حاضر کتابخانه های خوبی برای مقابله با این موارد دارد، که حتی ممکن است بهینه سازی شوند (کیفیت سرویس یا پیکربندی URLSession در iOS) تا موثرتر باشند. اینجاست که قابلیتها به وجود میآیند – آنها به شما اجازه میدهند آنچه را که نیاز دارید را اعلام کنید، در حالی که مشخصات پیادهسازی را برای کد پلتفرم حفظ میکنید. این یک راه عالی برای نگه داشتن کد ماژولار و قابل نگهداری است.
وقتی هسته مرکزی رویدادی را که نیاز به برقراری تماس HTTP دارد مدیریت میکند، در واقع به شل دستور میدهد تا تماس را انجام دهد.
fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
match event {
Event::Ask(question) => {
model.questions_number += 1;
gpt::API::new().make_request(&question, &caps.http).send(Event::Set);
},
...
و شل در حال ارسال درخواست است
switch req.effect {
...
case .http(let hr):
// create and start URLSession task
}
همین منطق را می توان برای پایگاه های داده (فقط KV-storage و رابطه ای جدا)، بیومتریک، هر چیز دیگری اعمال کرد.
افکار نهایی
علیرغم این واقعیت که من در Crux تازه کار هستم و هنوز به Rust مسلط نیستم، توانستم یک برنامه ساده بسازم که بر روی iOS، Android و وب (تقریباً) در مدت زمان کمتری نسبت به ساخت هر سه مورد نیاز داشته باشد. خراش.
Crux هنوز در مراحل اولیه خود است، به عنوان مثال در زمان یادداشت من، قابلیت HTTP از هدر و بدنه پشتیبانی نمی کرد. اما من امیدوارم که این پروژه به رشد خود ادامه دهد و مشارکت کنندگان بیشتری را جذب کند، زیرا ایده پشت آن واقعا جالب است.
حتی اگر نمیخواهید از Rust برای توسعه پلتفرم متقابل استفاده کنید، فکر میکنم ارزش این را دارد که به این پروژه نگاهی بیندازید تا ببینید چگونه ممکن است بتوانید از برخی از ایدههای موجود در پشته مورد علاقه خود دوباره استفاده کنید. در پایان، هر چیزی که به ما کمک کند کدهای بهتر، ماژولارتر و قابل نگهداری تر بنویسیم، برنده است.