کامپایل متقابل (Cross Compilation) یعنی ساختن فایل اجرایی روی یک نوع دستگاه برای اجرا روی نوعی دیگر: یک .exe ویندوز که روی لینوکس ساخته شده، یک فایل ARM که روی لپتاپ x86-64 ساخته شده، یا برنامهای برای مک که در یک کار CI لینوکسی ساخته میشود. در زبان برنامهنویسی سلام این کار با یک گزینه انجام میشود: --target و پس از آن یک سهتایی هدف (Target Triple) مانند x86_64-w64-windows-gnu یا aarch64-linux-musl. کامپایلر تولیدکنندهی کد را انتخاب میکند، یک محیط اجرای C برای آن هدف پیدا میکند، نتیجه را پیوند میدهد و فایلی برای آن دستگاه دیگر به دستتان میدهد.
این نوشته توضیح میدهد پشت این گزینه چه میگذرد، کدام هدفها بی هیچ تنظیمی کار میکنند، کدامها پروندهی اضافه لازم دارند، چطور کدی بنویسید که برای همهی سکوها بیخطا کامپایل شود، و آزمونهای خود پروژه تا کجا پیش میروند.
کامپایل متقابل چیست و چرا اهمیت دارد؟
کامپایلری که فقط برای دستگاه خودش میسازد، شما را مجبور میکند هر سکویی را که برایش منتشر میکنید در اختیار داشته باشید یا برای هر کدام سرور CI جداگانه اجاره کنید. کامپایلر متقابل این محدودیت را برمیدارد. یک دستگاه توسعه و یک کار ساخت نگه میدارید و خروجی ویندوز، مک و لینوکس را از همان میگیرید.
برای اینکه کار درست پیش برود، سه چیز باید سر جایش باشد:
- تولید کد برای پردازندهی هدف: x86-64، x86 سیودوبیتی، AArch64، ARM سیودوبیتی، RISC-V یا WebAssembly.
- یک محیط اجرای C و کتابخانههای سیستمی برای سیستمعامل هدف که معمولاً sysroot نامیده میشود: musl یا glibc روی لینوکس، محیط اجرای mingw-w64 روی ویندوز و
libSystemروی مک. - یک پیونددهنده (Linker) که قالبهای پروندهی شیء و فایل اجرایی هدف را بشناسد: ELF، PE/COFF یا Mach-O.
Go با GOOS و GOARCH این کار را ساده کرده و به همین شناخته میشود، و Zig محیط اجرای C بسیاری از هدفها را درون ابزارش دارد. Rust تقریباً همهی سکوها را هدف میگیرد، ولی معمولاً به rustup target add بهعلاوهی یک پیونددهنده و sysroot برای سکوی دیگر نیاز دارد. سلام به سمت Go و Zig نزدیکتر است: نسخهی رسمی کامپایلر، پیونددهندهی خودش و رایجترین sysrootها را با خود دارد. (مقایسههای سلام با Go و Zig بقیهی جنبههای این زبانها را بررسی میکنند.)
گزینهی --target و سهتاییهای هدف
سهتایی هدف پردازنده، سازنده یا خانوادهی سیستمعامل و ABI را نام میبرد. اینها همانهایی هستند که با سلام بیشتر به کارشان میبرید:
| سهتایی | خروجی |
|---|---|
x86_64-linux-musl | فایل اجرایی ایستای لینوکس ۶۴ بیتی |
aarch64-linux-musl | فایل اجرایی ایستای لینوکس ARM ۶۴ بیتی (رزبری پای ۴ و ۵، سرورهای ARM) |
arm-linux-musleabihf | فایل اجرایی ایستای لینوکس ARM ۳۲ بیتی (ARMv7 با ممیز شناور سختافزاری) |
i686-linux-musl | فایل اجرایی ایستای لینوکس x86 ۳۲ بیتی |
x86_64-w64-windows-gnu | فایل .exe ویندوز ۶۴ بیتی |
i686-w64-windows-gnu | فایل .exe ویندوز ۳۲ بیتی |
aarch64-w64-windows-gnu | فایل .exe برای ویندوز روی ARM |
arm64-apple-macos، x86_64-apple-macos | فایل اجرایی مک برای تراشههای اپل یا اینتل |
universal-apple-macos | یک فایل مک که هر دو معماری را در خود دارد |
wasm32-wasi | ماژول WebAssembly برای یک محیط اجرای WASI (نوشتهی سلام و WebAssembly را ببینید) |
فرمان برای همهی آنها یکی است:
salam build app.salam --target=x86_64-w64-windows-gnu
salam build app.salam --target=aarch64-linux-musl --output=app-arm64وقتی هدف ویندوز باشد، نام پیشفرض خروجی پسوند .exe میگیرد. سهتاییهای بدون سیستمعامل (x86_64-unknown-none، i686-unknown-none، aarch64-unknown-none) برنامههای مستقل (Freestanding) بدون سیستمعامل میسازند و نوشتهی جداگانهای دارند: سلام مستقل و برنامهنویسی روی سختافزار خام.
کدام تولیدکنندهی کد کار را انجام میدهد؟
سلام سه پشتیبان (Backend) دارد و نوشتهی تولید کد در سلام آنها را به تفصیل بررسی میکند. در کامپایل متقابل، رفتارشان متفاوت است:
- LLVM (
--backend=llvm، و مسیری که--targetبرای بیشتر سهتاییها به طور پیشفرض میرود) هر سهتاییای را که LLVM بشناسد پشتیبانی میکند. پهنای اشارهگر را در نظر میگیرد: برای یک سهتایی ۳۲ بیتی، هر جایگاهsize_tدر IR تولیدشده ۳۲ بیتی میشود، همانطور کهdocs/PLATFORMS.mdتوضیح داده است. نسخههای رسمی برای لینوکس x86-64، مک و ویندوز ۶۴ بیتی، LLVM و پیونددهندهی LLD را درون خود دارند و آنها را درون همان فرایند اجرا میکنند. کامپایلری که بدون LLVM درونی از کد منبع ساخته شده باشد، بهclang،llcوlldموجود درPATHتکیه میکند. - بومی (
--backend=native) تولیدکنندهی کد ماشین خود سلام است. بنا برdocs/NATIVE_BACKEND.md، کد x86-64، x86 سیودوبیتی، AArch64، ARM سیودوبیتی (ARMv7-A با VFPv3-D16) و RISC-V 64 را در قالب پروندههای شیء ELF، COFF یا Mach-O مینویسد و هر هدف دیگری را با ارجاع به--backend=llvmرد میکند. نه به LLVM نیاز دارد و نه به کامپایلر C. - C (
--backend=c) کد C قابلحمل تولید میکند و تعیین همهی پهناها را به کامپایلر C میسپارد. این همان مسیری است کهdocs/PLATFORMS.mdبرای ساختهای ۳۲ بیتی در محیط عملیاتی پیشنهاد میکند، و--emit-cکد C تولیدشده را به شما میدهد تا با هر کامپایلر C متقابلی که میخواهید کامپایلش کنید. نسخهی مرورگری خود کامپایلر هم همینطور ساخته میشود: برایwasm32-unknown-emscriptenبه C تبدیل و با Emscripten کامپایل میشود.
چند خط قدیمی در docs/PLATFORMS.md و در salam --help هنوز پشتیبان بومی را فقط x86-64 معرفی میکنند. مرجع بهروز docs/NATIVE_BACKEND.md و docs/NATIVE_LINKER.md است: این پشتیبان از آن زمان تولیدکنندههای کد i686، armhf، RISC-V و AArch64 را هم به دست آورده است.
sysrootها و پیونددهندهی درون کامپایلر
بخش دردسرساز کامپایل متقابل معمولاً تولید کد نیست، پیوند دادن است: پیدا کردن crt1.o، libc.a و کتابخانههای واردات درست برای سکویی که در اختیار ندارید. نسخهی رسمی سلام برای لینوکس x86-64 با sysroot موسوم به musl برای لینوکس x86-64، sysrootهای mingw-w64 برای ویندوز ۶۴ و ۳۲ بیتی و sysroot مربوط به WASI ساخته میشود که همه درون خود فایل salam جاسازی شدهاند، و نخستین باری که ساختی به آنها نیاز داشته باشد در یک پوشهی نهانگاه (Cache) باز میشوند. گردشکار انتشار این را با یک آزمون ساده میسنجد: کامپایل متقابل به x86_64-linux-musl و x86_64-w64-windows-gnu در حالی که PATH به پوشهای ناموجود اشاره میکند تا هیچ ابزار بیرونیای پیدا نشود.
پیونددهنده به خود زبان سلام نوشته شده و درون کامپایلر است. سند طراحی آن خروجیهایی را که مینویسد اینطور فهرست میکند:
| هدف | خروجی | محیط اجرای C |
|---|---|---|
x86_64-linux-musl، aarch64-linux-musl، i686-linux-musl، arm-linux-musleabihf | ELF ایستا | musl همراه کامپایلر |
x86_64-linux-gnu، aarch64-linux-gnu | ELF پویای PIE | glibc |
x86_64-w64-windows-gnu، i686-w64-windows-gnu | PE | mingw-w64 |
aarch64-w64-windows-gnu | PE (ARM64) | llvm-mingw (UCRT) |
x86_64-apple-macos، arm64-apple-macos | Mach-O PIE با امضای ad-hoc | libSystem |
برای هدفهای glibc غیر از دستگاه میزبان، کامپایلر یک sysroot برای glibc را در /usr/aarch64-linux-gnu (و مسیرهای همارز) یا در SALAM_GLIBC_SYSROOT جستوجو میکند. دو هدف چیزی لازم دارند که خودتان باید نصب کنید: ویندوز روی ARM به محیط اجرای llvm-mingw نیاز دارد (که از راه aarch64-w64-mingw32-clang در PATH یا SALAM_LLVM_MINGW پیدا میشود)، و RISC-V به sysroot کتابخانهی C که با آن پیوند میدهید.
این خروجی یک دستگاه لینوکس x86-64 برای یک برنامهی کوچک است، با پشتیبان بومی و پیونددهندهی درونی:
salam build hello.salam --backend=native --target=x86_64-w64-windows-gnu --output=hello-win64
salam build hello.salam --backend=native --target=aarch64-linux-musl --output=hello-arm64
salam build hello.salam --backend=native --target=i686-linux-musl --output=hello-i686
salam build hello.salam --backend=native --target=arm64-apple-macos --output=hello-mac
salam build hello.salam --backend=native --target=universal-apple-macos --output=hello-mac-universal
file hello-*
# hello-arm64: ELF 64-bit LSB executable, ARM aarch64, statically linked
# hello-i686: ELF 32-bit LSB executable, Intel i386, statically linked
# hello-mac: Mach-O 64-bit arm64 executable
# hello-mac-universal: Mach-O universal binary with 2 architectures: x86_64, arm64
# hello-win64.exe: PE32+ executable for MS Windows (console), x86-64ساخت برنامه برای لینوکس، ویندوز و مک با زبان سلام
لینوکس: musl ایستا یا glibc پویا
روی لینوکس میان دو کتابخانهی C انتخاب میکنید. سهتایی *-linux-musl یک فایل اجرایی با پیوند ایستا و بدون هیچ وابستگی زمان اجرا میدهد: آن را روی هر دستگاه لینوکسی با همان معماری پردازنده کپی کنید و اجرا میشود، چه آن دستگاه glibc داشته باشد، چه musl (مانند Alpine) و چه چیز دیگری. برای ابزارهای خط فرمان، کانتینرهایی که FROM scratch ساخته میشوند و سرورهایی که با کپی یک فایل مستقر میکنید، معمولاً همین را میخواهید.
سهتایی *-linux-gnu به شکل پویا با glibc پیوند میخورد، و وقتی لازمش دارید که برنامه یک کتابخانهی اشتراکی سیستمی را بار کند که فقط به شکل .so وجود دارد، مانند یک جعبهابزار رابط گرافیکی یا کتابخانهی کلاینت یک پایگاه داده. کد i686، ARM و RISC-V پشتیبان بومی از نشانیهای مطلق استفاده میکند، پس فایلهای اجرایی پویا برای این هدفها در نشانی ثابتی بار میشوند و مستقل از مکان نیستند؛ ساختهای x86-64 و AArch64 از نوع PIE هستند.
ویندوز: x64، ۳۲ بیتی و ویندوز روی ARM
x86_64-w64-windows-gnu و i686-w64-windows-gnu فایلهای اجرایی PE معمولی با محیط اجرای mingw-w64 میسازند. چند گزینه فقط برای هدفهای ویندوزی وجود دارند و به --backend=native یا --linker=builtin نیاز دارند:
--subsystem=windowsبرنامهای گرافیکی میسازد که پنجرهی کنسول باز نمیکند؛--icon=app.icoنماد برنامه را درون فایل میگذارد؛--manifest=FILEیک مانیفست برنامه را درون فایل میگذارد؛--version-info=KEY=VALUEیک منبع نسخه اضافه میکند (نسخه، شرکت، توضیح، محصول، حق نشر).
یک پروژه میتواند همین مقدارها را در جدول [windows] پروندهی salam.toml تعیین کند، و salam bundle یک برنامهی رومیزی را در قالبهای مختلف، از جمله فایل ZIP یا نصبکنندهی NSIS، بستهبندی میکند؛ نوشتهی برنامههای رومیزی با سلام این روند را شرح میدهد. روی ویندوز ۳۲ بیتی، تابعهای Win32 از قرارداد فراخوانی stdcall پیروی میکنند؛ کامپایلر این را برای تابعهایی که میشناسد خودش رعایت میکند و گونهی extern stdcall func تابعهایی را پوشش میدهد که از راه اشارهگر به آنها میرسید.
روی یک دستگاه ویندوز ARM، اگر --backend داده نشود، کامپایلر به طور پیشفرض پشتیبان بومی را به کار میبرد، چون نه LLVM و نه tcc برای این میزبان عرضه نشده است.
مک و فایلهای اجرایی universal
پیونددهندهی درونی، فایلهای اجرایی Mach-O برای مکهای دارای تراشهی اپل و مکهای اینتلی مینویسد، همراه با امضای کد ad-hoc که مک arm64 پیش از اجرای هر برنامهای میخواهد. --target=universal-apple-macos برنامه را برای هر معماری یک بار کامپایل و پیوند میکند و این دو را در یک فایل universal کنار هم میگذارد. نسخهی هدف مک از خود سهتایی (arm64-apple-macos13) یا از MACOSX_DEPLOYMENT_TARGET گرفته میشود و پیشفرض آن macOS 12 است.
ساختن برای مک روی لینوکس بدون SDK اپل هم کار میکند. بدون SDK، کتابخانه یا چارچوبی که درخواست میکنید باز هم فرمان بارگذاریاش را با مسیر استاندارد سیستم میگیرد و وارداتها با جستوجوی تخت (Flat Lookup) بسته میشوند، پس بارگذار پویا آنها را در هر تصویر بارشدهای که صادرشان کند پیدا میکند. اگر SDK در دسترس باشد (SDKROOT، یا ابزارهای خط فرمان Xcode روی خود مک)، پیونددهنده پروندههای .tbd را میخواند و هر وارداتی را به همان کتابخانهای میبندد که صادرش میکند.
هدفهای ARM و ۳۲ بیتی
«۳۲ بیتی» به پهنای اشارهگرها و size_t اشاره دارد، نه به بزرگترین عددی که میتوانید با آن حساب کنید. روی یک هدف ۳۲ بیتی، صحیح۶۴ و طبیعی۶۴ همچنان کار میکنند؛ جمع و تفریق روی جفت ثباتها انجام میشود و تقسیم تابعهای کمکی زمان اجرا را صدا میزند. صحیح۱۲۸ و طبیعی۱۲۸ هم کار میکنند و تقسیمهایشان با تابعهای کمکی خود سلام انجام میشود، چون libgcc سیودوبیتی آنها را ندارد. اعشار۶۴ روی همهی این هدفها با واحد ممیز شناور سختافزاری اجرا میشود. آنچه تغییر میکند این است که اشارهگرها و اندازه مثبت (همان usize) چهار بایتیاند، و پشتیبانها دقیقاً به همین حساساند.
این برنامه چه برای x86-64 ساخته شود و چه برای i686-linux-musl، همین چهار خط را چاپ میکند:
تابع ریشه:
بزرگ := ۹۰۰۰۰۰۰۰۰۰ برگردان صحیح۶۴
سرچاپ "صحیح۶۴ روی هر هدفی"، اندازهگونه(صحیح۶۴)، "بایت است"
سرچاپ "9e9 * 3 ="، بزرگ * ۳
سرچاپ "9e9 / 7 ="، بزرگ / ۷
پهن := (بزرگ برگردان طبیعی۱۲۸) * (بزرگ برگردان طبیعی۱۲۸)
سرچاپ "مربع در طبیعی۱۲۸:"، پهن
پایانروی ARM دو نکته اهمیت دارد. سختافزار AArch64 و RISC-V در تقسیم صحیح بر صفر به جای توقف، مقداری برمیگرداند، پس پشتیبان بومی روی این معماریها پیش از هر تقسیم بر مقدار غیرثابت یک بررسی صریح نگه میدارد، حتی در ساخت انتشار. و هدف armhf همان خط پایهی armhf دبیان است (ARMv7-A، VFPv3-D16، بدون NEON)، پس برداریسازهای پشتیبان بومی روی آن خاموش میمانند.
کدی بنویسید که همهجا ساخته شود
بیشتر کدهای سلام بدون زحمت قابلحملاند، چون کتابخانهی استاندارد سیستمعامل را پنهان میکند. هر جا که واقعاً تفاوتی میان سکوها لازم باشد، ثابتهای زمان کامپایل را به کار ببرید: در کد انگلیسی SALAM_OS_WINDOWS، SALAM_OS_MAC، SALAM_OS_LINUX و SALAM_ARCH_ARM64 و شکلهای رشتهای SALAM_OS و SALAM_ARCH، و در کد فارسی همین پرچمها با نام فارسی، مانند سلام سیستم ویندوز، سلام سیستم مک، سلام سیستم و سلام معماری. یک اگر در بالاترین سطح پرونده روی این ثابتها تعریفها را انتخاب میکند؛ شاخههایی که به کار نمیآیند پیش از بررسی گونهها حذف میشوند.
اگر سلام سیستم ویندوز:
پایا پوشهتنظیمات := "C:\\ProgramData\\notes"
وگرنه سلام سیستم مک:
پایا پوشهتنظیمات := "/Library/Application Support/notes"
وگرنه:
پایا پوشهتنظیمات := "/etc/notes"
پایان
تابع ریشه:
سرچاپ "ساختهشده برای"، سلام سیستم، "روی"، سلام معماری
سرچاپ "تنظیمات در"، پوشهتنظیمات
پایاناگر برای x86_64-w64-windows-gnu و i686-w64-windows-gnu ساخته و زیر Wine اجرا شود، به ترتیب ساختهشده برای windows روی x64 و ساختهشده برای windows روی x86 را چاپ میکند و سپس مسیر ویندوزی را.
حذف شاخههای مرده پیش از بررسی گونهها پیامدی دارد که باید بدانید: اشتباهی درون شاخهی ویندوز، وقتی روی لینوکس بررسی میکنید دیده نمیشود. salam check برای فراخوانی تابعی ناموجود درون اگر سلام سیستم ویندوز: هیچ خطایی گزارش نمیکند، ولی salam obj app.salam --target=x86_64-w64-windows-gnu بیدرنگ آن را نشان میدهد. salam obj در مرحلهی پروندهی شیء متوقف میشود، پس به پیونددهنده یا sysroot سکوی دیگر نیازی ندارد. آن را در CI برای هر هدفی که منتشر میکنید اجرا کنید تا یک بررسی گونهی ارزان برای هر سکو داشته باشید. درس کامپایل شرطی فهرست کامل این پرچمها را آورده است.
هدفها چگونه آزموده میشوند؟
یک کامپایلر متقابل به اندازهی آزمونهای خروجیاش قابل اعتماد است، و یک پروژهی جوان در این باره باید دقیق باشد، نه اطمینانبخش.
- دروازهی پشتیبان بومی در CI، برنامههای آزمون و خود کامپایلر را روی لینوکس برای مک arm64، مک x64، ویندوز x64 و لینوکس arm64 میسازد و سپس آنها را روی دستگاههای واقعی هر نوع در GitHub اجرا میکند؛ کامپایلری که اینطور ساخته شده باید روی هر میزبان همان IR مرجع LLVM را دوباره تولید کند. کار جداگانهای هم آزمونها را روی یک دستگاه
windows-11-armمیسازد و اجرا میکند. - ساختهای لینوکس i686 و armhf با اجرای گروههای اصلی آزمون، به شکل بومی (i686) و زیر
qemu-arm(armhf)، همراه با فهرستی مستند از استثناها بررسی میشوند؛ ساخت ویندوز i686 زیر Wine اجرا میشود. - خود کامپایلر برای لینوکس x86-64، x86-64 با musl، AArch64 و i686، مک arm64، و ویندوز x86-64، i686 و ARM64 منتشر میشود.
- پروندهی
docs/PLATFORM_NOTES.mdدر پشتهی شبکه، آنچه اجرا شده را از آنچه فقط خوانده شده جدا میکند: AArch64 زیر qemu و ویندوز زیر Wine اجرا شدهاند؛ پروندههای شیء مک در آن دور ساخته شدند ولی اجرا نشدند؛ پشتیبانی FreeBSD، OpenBSD و NetBSD فقط با خواندن سرآیندها بررسی شده است.
سیستمهای BSD ثابتهای زمان کامپایل دارند ولی هدفی ندارند که در CI ساخته و اجرا شود، پس فعلاً آنها را پشتیبانینشده در نظر بگیرید.
محدودیتهایی که باید بدانید
- کتابخانههای اشتراکی سیستم. یک ساخت ایستای musl نمیتواند کتابخانهای را بار کند که روی هدف فقط به شکل شیء اشتراکی وجود دارد. برنامههایی که کتابخانههای C را صدا میزنند، مانند GTK، WebKit یا کلاینت یک پایگاه داده، به کتابخانههای خود هدف نیاز دارند، و این معمولاً یعنی ساختن روی همان سکو یا فراهم کردن یک sysroot کامل.
- در دسترس بودن ابزارها. کامپایلری که از کد منبع ساخته شده ممکن است LLVM یا sysrootها را درون خود نداشته باشد، پس کامپایل متقابل با آن به نصب
clang،lldو sysrootها نیاز دارد. ویندوز روی ARM و RISC-V حتی در نسخههای رسمی هم پروندههای زمان اجرای اضافه میخواهند. - همهی ترکیبها اجرا نمیشوند. ساختن با آزمودن یکی نیست. اگر سکویی برایتان مهم است، آزمونهای خودتان را روی آن اجرا کنید، زیر qemu، زیر Wine یا روی سختافزار واقعی.
پرسشهای پرتکرار
آیا با زبان سلام میشود روی لینوکس فایل exe ویندوز ساخت؟
بله. salam build app.salam --target=x86_64-w64-windows-gnu یک فایل اجرایی ویندوز ۶۴ بیتی میسازد و i686-w64-windows-gnu یک فایل ۳۲ بیتی. نسخههای رسمی کامپایلر (صفحهی نصب را ببینید) محیط اجرای mingw-w64 و پیونددهنده را با خود دارند، پس نصب چیز دیگری لازم نیست.
چطور یک فایل اجرایی ایستا برای لینوکس بسازم؟
یک سهتایی musl مانند x86_64-linux-musl یا aarch64-linux-musl به کار ببرید. نتیجه با پیوند ایستا ساخته میشود و روی هر دستگاه لینوکسی با همان معماری پردازنده، صرفنظر از کتابخانهی C آن، اجرا میشود.
آیا زبان سلام از سیستمهای ۳۲ بیتی پشتیبانی میکند؟
بله. لینوکس و ویندوز i686 و لینوکس ARM سیودوبیتی جزو هدفهای پشتیبانیشدهاند و صحیح۶۴، طبیعی۶۴، صحیح۱۲۸ و اعشار۶۴ همه روی آنها کار میکنند. اندازه مثبت و اشارهگرها روی این هدفها ۳۲ بیتیاند.
آیا سلام بدون مک میتواند برنامهی مک بسازد؟
سلام میتواند روی لینوکس فایلهای اجرایی مک، از جمله فایلهای universal، بسازد و امضا کند. برای پیوند دقیق با چارچوبهای خاص و برای آزمودن آنچه ساختهاید، هنوز یک مک (یا یک دستگاه CI با macOS) ابزار درست است.