مقایسهی سلام و Zig مقایسهی دو زبان سیستمی جوان است که در نگاه اول شبیه هماند. Zig یک زبان سطح پایین است که بهعنوان «C بهتر» طراحی شده: بدون جریان کنترل پنهان، بدون تخصیص پنهان، با تخصیصدهندههای (allocator) صریحی که به هر چیزی که حافظه لازم دارد داده میشوند، و با comptime که کد معمولی Zig را در زمان کامپایل اجرا میکند. زبان برنامهنویسی سلام یک زبان سیستمی با گونههای ایستاست، با نحو بلوکی نزدیکتر به پایتون یا Lua، یک کتابخانهی استاندارد بزرگ در سطح برنامههای کاربردی، و کدی که میشود آن را به انگلیسی یا فارسی نوشت. هر دو به کد بومی کامپایل میشوند، هیچکدام زبالهروب (Garbage Collector) ندارد، و هر دو هنوز به نسخهی ۱٫۰ نرسیدهاند.
این نوشته شیوهی برخورد دو زبان با خطاها، حافظه، کد زمان کامپایل، کتابخانههای C و کامپایل متقابل را با برنامههای کوچک یکسان در هر دو زبان مقایسه میکند. برنامههای سلام هنگام ساخت این سایت کامپایل و اجرا میشوند. برنامههای Zig از کتابخانهی استاندارد نسخهی ۰٫۱۵ پیروی میکنند و چون Zig پیش از ۱٫۰ است، جزئیات ممکن است در نسخههای بعد تغییر کند.
دو زبان جوان با شباهتهای زیاد
Zig نخستین بار در ۲۰۱۶ به دست Andrew Kelley پدید آمد و بنیاد نرمافزار Zig (Zig Software Foundation) آن را توسعه میدهد. همین حالا کاربران شناختهشدهای دارد، از جمله محیط اجرای جاوااسکریپت Bun، پایگاه دادهی TigerBeetle و پایانهی Ghostty. سلام جوانتر است و کامپایلرش از وقتی پیادهسازی اولیهی C در اوت ۲۰۲۶ کنار گذاشته شد، به خود سلام نوشته شده است. هر دو متنباز هستند.
| Zig | سلام | |
|---|---|---|
| هدف طراحی | جایگزینی سادهتر و امنتر برای C | زبان سیستمی خوانا برای برنامهها، سرورها و کد سطح پایین |
| نحو | شبیه C، با آکولاد و نقطهویرگول | بلوک با : و پایان، بدون نقطهویرگول، واژههای کلیدی انگلیسی یا فارسی |
| حافظه | تخصیصدهندههای صریح که بهصورت آرگومان داده میشوند | یک تخصیصدهندهی سراسری برای ظرفها؛ قلمرو (arena) و استخر حافظه در صورت نیاز |
| خطاها | اجتماع خطا (!T)، try، catch، errdefer | خطادار، تلاش، گرفتن خطا؛ خطا نوع، پیام و موقعیت دارد |
| زمان کامپایل | comptime: اجرای هر کد Zig در حین کامپایل | ثابتها، کامپایل شرطی، پرچمهای -D، گونههای عامِ تکریختشده (monomorphized) |
| تعامل با C | سرآیندهای C را مستقیم وارد میکند؛ zig cc کد C و C++ را کامپایل میکند | اعلانهای درونداد:؛ فراخوانیها در همهی پشتوانهها از ABI زبان C پیروی میکنند |
| کتابخانهی استاندارد | سطح پایینتر: ساختمان داده، ورودی و خروجی، HTTP، JSON، رمزنگاری | سطح برنامه: مسیریاب HTTP، پایگاه داده، JSON، TLS، تصویر، یادگیری ماشین |
| وضعیت | پیش از ۱٫۰، تغییرات ناسازگار میان نسخهها | پیش از ۱٫۰، تغییرات ناسازگار میان نسخهها |
مدیریت خطا: اجتماع خطا و خطادار
مدیریت خطا در این دو زبان بهطرز شگفتآوری شبیه از آب درآمده است. این تابعی است که یک درگاه TCP را از متن میخواند، در هر دو زبان، همراه حلقهای که ورودیهای درست و نادرست را گزارش میکند. در Zig، تابعی که ممکن است شکست بخورد یک اجتماع خطا (error union) برمیگرداند، try خطا را به بالا میفرستد و if/else با capture هر دو نتیجه را مدیریت میکند:
const std = @import("std");
fn parsePort(text: []const u8) !u16 {
const n = try std.fmt.parseInt(u32, text, 10);
if (n < 1 or n > 65535) return error.OutOfRange;
return @intCast(n);
}
pub fn main() void {
const inputs = [_][]const u8{ "8080", "70000", "http" };
for (inputs) |text| {
if (parsePort(text)) |port| {
std.debug.print("port {d}\n", .{port});
} else |err| {
std.debug.print("bad: {s}\n", .{@errorName(err)});
}
}
}در سلام، تابعی که ممکن است شکست بخورد با خطادار (throws) علامت میخورد، هر فراخوانی آن یا با تلاش (try) علامت میخورد یا درون یک بلوک تلاش قرار میگیرد، و گرفتن خطا (catch) خطا را مدیریت میکند:
واردسازی تبدیل
تابع خواندن درگاه(متن: رشته): صحیح خطادار:
ن := تلاش تبدیل.رشته به عدد(متن)
اگر ن < ۱ یا ن > ۶۵۵۳۵:
پرتاب خطا خطا.جدید(نوع خطا.خارج از بازه، "درگاه خارج از بازه: " + متن)
پایان
برگشت ن
پایان
تابع ریشه:
هر متن در ["8080"، "70000"، "http"]:
تلاش:
سرچاپ "درگاه"، خواندن درگاه(متن)
گرفتن خطا خ:
سرچاپ "نامعتبر:"، خ.پیام
پایان
پایان
پایانشکل کلی یکی است: نه استثنایی در کار است، نه باز کردن پشته، هر نقطهی شکست در محل فراخوانی دیده میشود و دستورهای دیرکن (defer) همچنان اجرا میشوند. هر دو یک جایگزین یکخطی هم دارند: Zig مینویسد parsePort(text) catch 8080 و سلام تلاش خواندن درگاه(متن) گرفتن خطا ۸۰۸۰.
تفاوت در چیزی است که خطا با خود دارد. خطای Zig فقط نامی از یک مجموعهی خطاست و بس؛ جزئیات بیشتر، هر وقت لازم باشد، از راه یک مقدار تشخیصی جداگانه منتقل میشود، و ردگیری بازگشت خطا (error return trace) در ساختهای اشکالزدایی نشان میدهد خطا از کجا آمده است. خطای سلام مقداری از گونهی داخلی خطا (Error) است با یک نوع، یک پیام، یک کد اختیاری و برای تجزیهگرها شمارهی خط و ستون، پس پیامی مانند json: missing key 'age' (line 1, column 1) مستقیم از کتابخانهی استاندارد بیرون میآید. Zig errdefer دارد که فقط وقتی تابع خطا برمیگرداند اجرا میشود؛ سلام معادلی برای آن ندارد و از دیرکن یا یک گرفتن خطای صریح استفاده میکند.
حافظه: تخصیصدهندههای صریح در برابر دیرکن و آزادسازی
تخصیصدهندهها امضای Zigاند. هر تابع یا ظرفی که حافظه میگیرد یک std.mem.Allocator دریافت میکند، پس فراخواننده تصمیم میگیرد حافظه از یک هیپ همهمنظوره بیاید، از یک قلمرو (arena)، از یک بافر ثابت یا از یک تخصیصدهندهی آزمایشی که نشتها را گزارش میکند. از Zig ۰٫۱۵ به بعد، ArrayList استاندارد حتی تخصیصدهندهاش را نگه نمیدارد و هر فراخوانیای که ممکن است حافظه بگیرد آن را دریافت میکند:
const std = @import("std");
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var squares: std.ArrayList(usize) = .empty;
defer squares.deinit(allocator);
for (1..6) |i| {
try squares.append(allocator, i * i);
}
std.debug.print("{any}\n", .{squares.items});
}سلام معاملهی برعکس را انتخاب کرده است. ظرفهایی مانند وکتور و نگاشت از یک تخصیصدهندهی سراسری استفاده میکنند، پس هیچجا آرگومان تخصیصدهندهای نیست، اما آزاد کردن حافظه همچنان صریح است، با همان الگوی دیرکن:
تابع ریشه:
ناپایا مربعها := وکتور {} برگردان وکتور<صحیح۶۴>
دیرکن مربعها.آزادکن()
تکرار ۱ تا ۵ در ای:
مربعها.بیفزا((ای * ای) برگردان صحیح۶۴)
پایان
سرچاپ مربعها
پایاننسخهی سلام کوتاهتر است چون به جای شما تصمیم میگیرد. وقتی قلمرو یا استخرهای اندازهثابت لازم دارید، بستهی حافظه تابعهای قلمرو جدید، تخصیص در قلمرو، بازنشانی قلمرو و استخر جدید را دارد (NewArena، ArenaAlloc، ArenaReset و NewPool)، اما اینها حافظهی خام تحویل میدهند و مثل تخصیصدهندهی Zig به ظرفها وصل نمیشوند. اگر برنامهتان باید بر هر تخصیص کنترل داشته باشد، مثلاً در یک سیستم نهفته یا یک پایگاه داده با بودجهی حافظهی سختگیرانه، مدل Zig انعطاف بیشتری دارد.
برای پیدا کردن اشتباهها، DebugAllocator در Zig هنگام deinit نشتها را گزارش میکند، و ساختهای Debug و ReleaseSafe مرز آرایه و سرریز عدد صحیح را در زمان اجرا بررسی میکنند. سلام اندیس آرایه و وکتور را در ساختهای معمولی (نه با --release) در زمان اجرا بررسی میکند، خارج از بازه بودن برخی اندیسها را هنگام کامپایل اثبات میکند، برخی الگوهای استفاده پس از آزادسازی و آزادسازی دوباره را پیش از اجرای برنامه گزارش میکند، و salam memcheck برنامه را زیر Valgrind یا AddressSanitizer اجرا میکند. هیچکدام از دو زبان ایمنی حافظه را آنطور که بررسیکنندهی امانت (Borrow Checker) در Rust اثبات میکند، اثبات نمیکند. نوشتهی مدیریت حافظه در سلام سمت سلام را با جزئیات پوشش میدهد.
comptime در برابر ابزارهای زمان کامپایل سلام
comptime جایی است که Zig در لیگ دیگری بازی میکند. یک تابع Zig وقتی آرگومانهایش معلوم باشد میتواند در زمان کامپایل اجرا شود، گونهها مقدارهاییاند که تابعها میتوانند بگیرند و برگردانند، و Zig گونههای عام را هم همینطور پیاده میکند: ArrayList(u64) یک فراخوانی تابع است که یک گونه برمیگرداند. انتخابهای وابسته به سکو هم از همین سازوکار استفاده میکنند:
const builtin = @import("builtin");
const config_dir = if (builtin.os.tag == .windows) "AppData" else ".config";سلام به جای یک ابزار عمومی، چند ابزار محدودتر دارد. یک اگر در سطح بالای فایل روی یک پرچم زمان کامپایل فقط شاخهی سکوی هدف را نگه میدارد، -DNAME پرچمهای خودتان را اضافه میکند، -dNAME=value ثابت اضافه میکند و گونههای عام در زمان کامپایل برای هر گونه جداگانه ساخته میشوند. در فایل فارسی، ثابتهای خود زبان هم نام فارسی دارند (سلام سیستم ویندوز به جای SALAM_OS_WINDOWS):
اگر سلام سیستم ویندوز:
پایا پوشه_تنظیمات := "AppData"
وگرنه:
پایا پوشه_تنظیمات := ".config"
پایان
تابع ریشه:
سرچاپ "پوشهی تنظیمات:"، پوشه_تنظیمات
سرچاپ "ساخته شده برای"، سلام سیستم، "روی"، سلام معماری
پایانخروجی به ماشینی بستگی دارد که برنامه برایش ساخته میشود، پس اینجا فقط بررسی میشود؛ روی یک ماشین لینوکس x86-64، .config و ساخته شده برای linux روی x64 چاپ میکند. عبارتهای ثابت در زمان کامپایل محاسبه میشوند و کامپایلر در همین مرحله بررسیهای زیادی انجام میدهد (جامع بودن همخوان، بازهی اندیسها، نامهای استفادهنشده، HTML و CSS زبان صفحه). اما هیچ راه عمومیای برای اجرای تابعهای خودتان در حین کامپایل وجود ندارد و کد عام نمیتواند گونهها را آنطور که @typeInfo به Zig اجازه میدهد وارسی کند. اگر میخواهید سریالساز، تجزیهگر یا جدول جستجو را درون خود زبان از روی گونهها تولید کنید، Zig میتواند و سلام نمیتواند. رمزگذار و رمزگشاهای JSON سلام را خود کامپایلر میسازد، نه کد کتابخانه. درس کامپایل شرطی پرچمهای سلام را فهرست میکند.
تعامل با C
هر دو زبان با C مثل یک همسایهی درجهیک رفتار میکنند. Zig میتواند یک سرآیند C را مستقیم وارد کند و تابعهایش را بدون هیچ پوششی صدا بزند، و زنجیرهی ابزارش کامپایلر C و C++ هم هست (zig cc):
const std = @import("std");
const c = @cImport(@cInclude("stdlib.h"));
pub fn main() void {
std.debug.print("{d} {d}\n", .{ c.abs(-7), c.atoi("42") });
}سلام سرآیند نمیخواند. تابعهای C مورد نیازتان را در یک بلوک درونداد: (extern:) با گونههای سلامی متناظر با گونههای C اعلان میکنید و مثل هر تابع دیگری صدایشان میزنید. نامهای C، حتی در فایل فارسی، همان نامهای انگلیسی میمانند:
درونداد:
تابع abs(x: صحیح): صحیح
تابع atoi(s: رشته): صحیح
پایان
تابع ریشه:
سرچاپ abs(-۷)، atoi("42")
پایانوارد کردن سرآیند در Zig برای یک API بزرگ C راحتتر است، چون اعلانها را دوباره تایپ نمیکنید. اعلانهای صریح سلام کار بیشتری میخواهند اما مرز را در یک جا قابل دیدن میکنند. در جهت مخالف، بلوک برونداد: (export:) در سلام تابعها را با نامهای سادهی C منتشر میکند و salam build --lib یک کتابخانهی اشتراکی یا ایستا همراه یک سرآیند تولیدشده میسازد. ساختارها با ABI زبان C روی همان سکو بهصورت مقداری منتقل میشوند. درس تعامل با C و صدا زدن C از سلام جزئیات را پوشش میدهند، از جمله پیوند پویا (link dynamic) برای کتابخانهها.
کامپایل متقابل و زنجیرهی ابزار
کامپایل متقابل (cross-compilation) یکی از بهترین ویژگیهای Zig است. zig build و zig cc از هر میزبانی میتوانند سیستمعاملها و معماریهای زیادی را هدف بگیرند و Zig سرآیندها و کدهای libc مورد نیاز را همراه خود دارد؛ به همین دلیل برخی پروژهها در زبانهای دیگر از Zig فقط بهعنوان کامپایلر متقابل C استفاده میکنند.
سلام با یک سهتایی --target کامپایل متقابل میکند، مثلاً salam build app.salam --target=x86_64-w64-windows-gnu. ساختهای انتشاری کامپایلر LLVM و پیونددهندهاش را درون خود دارند، و پشتوانهی بومی برای x86-64، AArch64، x86 سیودو بیتی و ARM و RISC-V 64 خودش فایل شیء مینویسد، با یک پیونددهندهی داخلی برای برخی هدفها. WebAssembly (wasm32-wasi) و هستههای مستقل (*-unknown-none) هم جزو هدفها هستند. راهاندازی Zig پختهتر است و هدفهای بیشتری را با پیکربندی کمتر پوشش میدهد. کامپایل متقابل سلام نشان میدهد امروز چه چیزی کار میکند و هر هدف چه لازم دارد.
کارایی
سنجش پروژه سلام را با C، C++، Rust، Go، پایتون و PHP مقایسه میکند، اما نه با Zig، پس عدد اندازهگیریشدهای برای گزارش در اینجا وجود ندارد. آنچه میشود گفت: Zig برای ساختهای بهینه از راه LLVM کامپایل میکند، همانطور که مسیر انتشاری سلام، و هر دو در دستهی زبانهای کامپایلی بدون زبالهروب قرار میگیرند. روی ۳۹ برنامهی همالگوریتم، میانگین سلام ۱۰٫۶ میلیثانیه بود در برابر ۱۱٫۷ میلیثانیهی C و ۱۱٫۳ میلیثانیهی Rust، که صفحهی سرعت آن را «یک رده» میخواند؛ سلام در ۶ برنامه جلوتر، در ۲۷ برنامه برابر و در ۶ برنامه عقبتر بود. برنامهها کوتاهاند، راهاندازی جزو هر زمان است و فاصلههای کمتر از حدود ۵٪ نوسان به حساب میآیند. انتظار داشته باشید Zig و سلام روی کد مشابه نزدیک هم باشند، و اگر برایتان مهم است، اندازه بگیرید.
اکوسیستم و پایداری
هر دو زبان میان نسخهها تغییر میکنند و هر دو این را صریح میگویند. برای نمونه، نسخهی ۰٫۱۵ Zig رابطهای خواننده و نویسندهی کتابخانهی استاندارد را از نو طراحی کرد و کدهای نوشتهشده برای نسخههای قبل باید بهروز میشدند. سلام در ۲۰۲۶ اعلان متغیر محلی با گونهی صریح را حذف کرد و قاعدههایش هم از نسخهای به نسخهی دیگر تغییر میکند.
اکوسیستمها از جنس متفاوتیاند. Zig جامعهای رو به رشد دارد، یک مدیر بستهی درون سیستم ساختش، و چند پروژهی متنباز بزرگ که آن را در محیط عملیاتی ثابت کردهاند. جامعهی سلام بسیار کوچکتر است و مخزن بستههای شخص ثالثش تازه و تقریباً خالی، اما کتابخانهی استانداردش در سطح برنامههای کاربردی بسیار بزرگتر است: سرورهای HTTP با مسیریابی و میانافزار، درایورهای پایگاه داده، JSON، YAML و TOML، TLS نوشتهشده با خود سلام، کدکهای تصویر، ماتریسها و لایههای شبکهی عصبی، و زبان صفحهای که HTML و CSS میسازد. Zig قطعههای ساختمانی تیز به شما میدهد و انتظار دارد خودتان سرهمشان کنید؛ سلام قطعههای آمادهتری میدهد.
سلام یا Zig: کدام را انتخاب کنیم؟
Zig را انتخاب کنید وقتی:
- به کنترل دقیق بر هر تخصیص نیاز دارید، با تخصیصدهندهای که برای هر فراخوانی انتخاب میشود؛
- تولید کد در زمان کامپایل و بازتاب گونه (type reflection) را درون خود زبان میخواهید؛
- یک پایگاه کد C را جایگزین یا گسترش میدهید و میخواهید سرآیندهایش را مستقیم وارد کنید؛
- کامپایل متقابل به هدفهای زیاد با کمترین راهاندازی، یا یک کامپایلر C جایگزین، لازم دارید.
سلام را انتخاب کنید وقتی:
- سرویس، ابزار خط فرمان یا بکاند وب مینویسید و میخواهید کتابخانهی استاندارد بیشتر کار را انجام دهد؛
- نحو سطح بالاتر و خواناتر، جداشمارهایی که داده دارند،
همخوانجامع و میانجیها را ترجیح میدهید؛ - یک زنجیرهی ابزار با آزمون، قالببندی، بستهها، سرور زبان (LSP) و زبان صفحه میخواهید؛
- شما یا تیمتان میخواهید کد را به فارسی هم بنویسید، نه فقط انگلیسی.
پرسشهای پرتکرار
آیا زبان سلام شبیه Zig است؟
هدفهای مشترکی دارند: کد بومی، نبود زبالهروب، خطاهای صریح با try (در سلام تلاش) و defer (در سلام دیرکن) برای پاکسازی. در فلسفه فرق دارند: Zig حافظه و کد زمان کامپایل را صریح و برنامهپذیر میکند، در حالی که سلام تخصیصدهنده را پنهان میکند و کتابخانهی استاندارد بزرگتر و نحو سطح بالاتری دارد.
آیا سلام comptime دارد؟
نه به معنای Zig. سلام عبارتهای ثابت را محاسبه میکند، با پرچمهای زمان کامپایل کد را نگه میدارد یا حذف میکند و گونههای عام را برای هر گونه جداگانه میسازد، اما نمیتواند تابعهای دلخواه را در حین کامپایل اجرا کند.
آیا سلام مثل Zig از تخصیصدهنده استفاده میکند؟
نه. ظرفها از یک تخصیصدهندهی سراسری استفاده میکنند و با آزادکن() آزاد میشوند، معمولاً از راه دیرکن. بستهی حافظه (mem) هر وقت لازم باشد قلمرو و استخر برای حافظهی خام فراهم میکند.
سلام سریعتر است یا Zig؟
هیچ سنجش منتشرشدهای این دو را مقایسه نکرده است. هر دو برای ساختهای بهینه از راه LLVM کامپایل میکنند و زبالهروب ندارند، پس باید روی کد مشابه در یک رده باشند.