זיג (שפת תכנות)

זיג היא שפת תכנות אימפרטיבית לשימוש כללי. השפה מוכוונת לתכנות מערכות, והיא בעלת טיפוסים סטטיים ומקומפלת. השפה נוצרה על ידי אנדרו קלי ומיועדת לשמש תחליף קטן ופשוט יותר לשפת C, בתוספת יכולות מודרניות, אופטימיזציות חדשות ומגוון מנגנוני בטיחות (אם כי היא אינה מחייבת את המתכנת לשימוש בטוח בזיכרון). היא נבדלת משפות דומות לה כגון Go, ראסט וCarbon בכך שאינה מכוונת לשמש תחליף לC++.

השפה משפרת ומפשטת שימוש במבני בקרה, קריאה לפונקציות, ייבוא ספריות, הצהרה על משתנים ותמיכה ביוניקוד. בנוסף לכך השפה אינה כוללת מקרואים או הוראות לקדם-מעבד. זיג אימצה מתוך שפות מודרניות אחרות קונספטים כמו טיפוסים גנריים של זמן קומפילציה, מתן יכולת לפונקציות לעבוד על מגוון סוגי דאטה, וקומץ הוראות לקומפיילר שמאפשרות גישה למידע על הטיפוסים באמצעות רפלקשן.

תוספות אחרות לשפה נועדו לשפר בטיחות קוד. בדומה לC, זיג אינה כוללת איסוף זבל והטיפול בזיכרון נעשה ידנית. זיג ממזערת את הסיכון לשגיאות בעת הטיפול בזיכרון באמצעות תחביר פשוט לשימוש בטיפוסים אופציונליים. ישנו גם שלד (Framework) לבדיקות מובנה בשפה.

תיאור

מטרות 
המטרה המרכזית של זיג היא "להיות פרגמטית" ולשמש פתרון מוצלח יותר לסוג הבעיות שנפתרות כיום על ידי שפת C. לפיכך אחד השיקולים המרכזיים בשפה הוא יצירת קוד קריא. זיג מנסה להשתמש ככל האפשר בקונספטים ותחביר מוכרים, ולהימנע מתוספות של תחביר מיוחד לקונספטים שאין בהם חידוש משמעותי. זיג גם בנויה להיות "רובסטית, אופטימלית וניתנת לתחזוקה", והיא כוללת יכולות רבות לשיפור הבטיחות, האופטימיזציות ויכולת הבדיקה של קוד. התחביר הקטן והפשוט שלה משחק חלק חשוב ביכולת לתחזק קוד זיג, ואכן התפיסה של זיג היא שצריך לאפשר למתכנתים תחזוקה ותיקון של קוד מבלי שאלו יידרשו להיכנס לנבכי השפה ולהכיר אותה לעומק. ואפילו עם הייחודיות הזו, זיג עדיין מסוגלת להתקמפל בתוך ומול קוד C. ניתן לכלול headers של C בתוך קוד זיג ולקרוא לפונקציות שהם מייצאים, וניתן לקשר קוד זיג לפרויקטים שכתובים בC פשוט על ידי include של headers שיוצר הקומפיילר.

במסגרת התפיסה שקוד צריך להיות פשוט וקריא, זיג כוללת כמה שינויים סגנוניים ביחס לC ולשפות דמויות C. למשל, בשפות כמו C++ או ראסט יש overloading של אופרטורים, משמע שקוד כמו a = b+c יכול להוות בפועל קריאה לפונקציה שעושה גרסה כלשהי של חיבור שיכולה להפתיע את המתכנת שהתכוון לחיבור פשוט. בנוסף, קריאה לפונקציה שכזו יכולה לייצר exception שעלול למנוע ביצוע של החיבור כראוי. בזיג, לעומת זאת, אם ישנה קריאה לפונקציה אז היא תיראה כמו קריאה לפונקציה, ואם זה לא נראה כמו קריאה לפונקציה, אז אין קריאה לפונקציה. כמו כן, אם משהו עלול לייצר שגיאה, אז זה כתוב במפורש בקוד, וטיפול בשגיאות נעשה בעזרת טיפוסי שגיאה בהן יכולים לטפל catch או try.

המטרות של זיג שונות מאלו של שפות רבות שנוצרו בתחילת המאה ה-21 כמו Go, ראסט, Carbon, Nim ואחרות. ככלל, השפות הללו מורכבות יותר וכוללות יכולות כמו operator overloading, או פונקציות שמתחזות לערכים (properties), טיפוסים גנריים ועוד יכולות שנועדו לעזור בבניית פרויקטי תוכנה גדולים. יכולות כאלו מקושרות לתפיסות מהסוג של של C++, ובהתאם השפות האלו מכווננות לתחומי העיסוק של C++. זיג שמרנית יותר בכל מה שנודע להרחבה של מערכת הטיפוסים, הGenerics שהיא תומכת בהם הם של זמן קומפילציה וסוג של Duck typing מתאפשר אצלה בעזרת הפקודה comptime.

טיפול בזיכרון 
אחת מהסיבות העיקריות לבאגים בתכנות בשפת C היא מערכת ניהול הזיכרון שמתבססת על malloc. בעת השימוש בmalloc, מוקצים בלוקים של זיכרון ומוחזרים פויינטרים לאותו מיקום בזיכרון. אין שום מנגנון שיבטיח שהזיכרון ישוחרר כשהתוכנה סיימה להשתמש בו, והדבר יכול להוביל לדליפת זיכרון.

בעיה נפוצה אף יותר, ויש שיקראו לה השגיאה הגרועה ביותר בתכנות, היא שגיאת null pointer, בה מצביע אינו מצביע לכתובת חוקית בזיכרון בגלל כשל של הmalloc בהקצאת הזיכרון, או בגלל בעיה אחרת.

בשפות תכנות רבות בעיות אלו בניהול זיכרון נפתרות באמצעות מנגנון איסוף זבל (GC) שבודק אם יש בתוכנה פויינטרים לזיכרון שהוקצה, ומסיר בלוקים של זיכרון שנותרו ללא פויינטרים שמצביעים אליהם. למרות שהמנגנון הזה מפחית מאוד, ואפילו יכול למנוע לחלוטין שגיאות זיכרון, מנגנוני איסוף זבל הם בדרך כלל איטיים יחסית לניהול זיכרון ידני, והביצועים שלהם לא צפויים. לפיכך הם אינם מתאימים לתכנות מערכות.

פתרון מוכר אחר לבעיות זיכרון הוא ספירה אוטומטית של התייחסויות (Automatic Reference Counting, ARC) שמממשת את אותו עיקרון של בדיקת פויינטרים והסרת בלוקים של זיכרון, אך עושה זאת בזמן הקצאת הזיכרון על ידי מעקב אחרי מספר הפויינטרים המצביעים לאותו חלק בזיכרון. בטכניקה הזאת אין צורך בסריקות הפויינטרים שגוזלות זמן, אך במקומן נוסף זמן ריצה לפעולת הקצאת הזיכרון ושיחרורו.

זיג נועדה לספק ביצועים זהים או טובים יותר משפת C, כך ששימוש באיסוף זבל או בARC לא ישרת אותה היטב. לכן, נכון לשנת 2022, היא משתמשת בפתרון שידוע בשם "טיפוסים אופציונליים" (Optional types) או פויינטרים חכמים. במנגנון הזה, במקום לאפשר לפויינטר להצביע לכלום או לאפס, טיפוס מצביע רגיל יכול להצביע רק למקום חוקי בזיכרון, ובמקביל קיים טיפוס מיוחד שיכול להצביע גם הצבעה חוקית וגם הצבעה לא חוקית (הצבעה לאפס בשפת C). מבנה פויינטר כזה דומה לstruct שמכיל מצביע ובנוסף לו אינדיקטור שמציין אם המצביע חוקי, כך שהאי-חוקיות היא מפורשת ואינה פרשנות של כתובת ההצבעה. בטכניקה של טיפוסים אופציונליים ניהול החוקיות של הפויינטר נעשית אוטומטית על ידי השפה ללא התערבות של המתכנת, כך שפויינטר שהקצאת הזיכרון אליו נכשלה יהיה לא חוקי, ולפיכך לא ניתן יהיה להשתמש בו בטעות כאילו הוא מחזיק זיכרון חוקי.

היתרונות במנגנון שכזה הוא שאין כל תוספת עומס על החישוב, או שהתוספת זניחה. אמנם הקומפיילר צריך לייצר את הקוד שמעביר את הטיפוסים האופציונליים למקומות בהם מתבצע שינוי בערכי הפויינטרים, מה שלא קורה עם פויינטרים פשוטים, אבל קוד שכזה הוא בדיוק מה שיאפשר להגדיר כבר בזמן הקומפילציה היכן יכולות להתרחש בעיות זיכרון ללא צורך לעסוק בעניין בזמן ריצה. למשל בC ניתן ליצור ולהשתמש בפוינטר שמצביע לכתובת 0, ואז לקבל שגיאות בזמן ריצה, אולם שפה עם טיפוסים אופציונליים יכולה לבדוק בזמן קומפילציה שכל הניסיונות להשתמש בפוינטרים נעשים כשהפוינטר במצב תקין ולמנוע את בעיות הזיכרון עוד לפני שמגיע שלב הריצה. הטכניקה אמנם לא פותרת את כל בעיות ניהול הזיכרון, אבל אם אכן מתרחשת שגיאת זיכרון בזמן ריצה, הטכניקה מאפשרת לזהות בקלות רבה יותר היכן הן קרו ולמה.

בנוסף, ניהול הזיכרון בזיג נעשה באמצעות structs שמשמשים בביצוע ההקצאה ומתארים את פעולת ההקצאה, ולא באמצעות קריאה לlibc. למשל בC פונקציה שמייצרת מחרוזת שמכילה עותקים של מחרוזת אחרת כנראה תיראה כך:const char* repeat(const char* original, size_t times);בקוד הזה הפונקציה תבדוק את הגודל של original ואז תקצה את הגודל הזה times פעמים בשביל המחרוזת שהיא צפויה לבנות. מי שקורא לפונקציה הזאת לא יראה את הmalloc שהיא מבצעת, ואם הוא לא ישחרר את הזיכרון לאחר שיסיים להשתמש בו, תהיה לו דליפת זיכרון. בזיג ניתן להשתמש בפונקציה שנראית כך בשביל בניית המחרוזת:fn repeat(allocator: *std.mem.Allocator, original: []const u8, times: usize) std.mem.Allocator.Error![]const u8;בקוד הזה המשתנה allocator מקבל struct שמתאר איזה קוד אמור לבצע את ההקצאה, והפונקציה repeat יכולה להחזיר את המחרוזת, או לחלופין שגיאת הקצאה, כפי שמאפשר לה סימן הקריאה שמסמן טיפוס אופציונלי. העברת הallocator לתוך הפונקציה משמעה שהקצאת הזיכרון אינה נסתרת. מי שקרא לפונקציה repeat יודע שהייתה הקצאת זיכרון וגם יודע מי מבצע אותה ואיך. הספרייה הסטנדרטית של זיג לא מבצעת הקצאות, ולמתכנת יש גם יכולת לשנות את הstruct שמועבר לallocator, ובכך לשנות את דרך ההקצאה, ואפילו להגדיר שיטת הקצאה חדשה. גמישות כזו מאפשרת, למשל, הגדרת שיטות הקצאה שונות לאובייקטים שונים, מבלי לעבור דרך מנגנוני ההקצאה של מערכת ההפעלה שמשתמשים בדפים שלמים מן הזיכרון.

הטיפוסים אופציונליים הם דוגמה טובה למנגנונים פשוטים וגנריים בשפה שהם גם שימושיים מאוד. ואכן, אם נסתכל על הטיפוסים האופציונליים, נראה שהם נותנים יכולות שהן הרבה מעבר לטיפול בnull pointers. הם יכולים לשמש במקרים בהם נדרש טיפוס שמסוגל להיות במצב "אין ערך". דוגמה: נניח שיש לנו פונקציה בשם countNumberOfUsers שמחזירה מספר שלם, ויש לנו משתנה מסוג integer בשם theCountedUsers שמקבל את התוצאה של הפונקציה. בשפות רבות יוגדר מספר קסם שכאשר הוא מופיע כערך בtheCountedUsers משמעו שעדיין לא קראו לפונקציה, ובמקרים רבים מספר הקסם הזה יהיה 0. בזיג, לעומת זאת, המקרה הזה ימומש על ידי
var theCountedUsers: ?i32 = null
שמכניס למשתנה ערך שחד משמעית אומר "לא קראו עדיין לפונקציה".

לזיג יש גם יכולת כללית יותר שמסייעת בניהול בעיות זיכרון: הקונספט של defer, שמאפשר לסמן קוד ככזה שיתבצע בסוף פונקציה ללא קשר למה שהתרחש בתוכה, וללא קשר לשאלה אם בזמן ריצה היא הסתיימה בצורה תקינה או הופסקה בשל שגיאה. דוגמה ידועה יחסית לשימוש ביכולת הזו היא כאשר פונקציה מקצה זיכרון שאינו נדרש עם סיום פעולתה. אם נשחרר את הזיכרון בסוף הפונקציה, ייתכן שהפונקציה תיכשל בשלב כלשהו בריצה שלה והזיכרון לא ישתחרר ויישאר תפוס ללא סיבה, וכך תתרחש דליפת זיכרון. בזיג ניתן במקרה כזה להעביר לבלוק של הdefer את השחרור של אותו זיכרון. כך הזיכרון ישתחרר גם כאשר הפונקציה לא הצליחה לסיים את פעולתה.

מאחר שניהול הזיכרון בזיג נמנע מהקצאות נסתרות, ההקצאות לא מנוהלות על ידי השפה ישירות. הגישה לheap בזיג מתבצעת בעזרת הספרייה הסטנדרטית בצורה מפורשת.

אינטראקציה ישירה מול C 
זיג דוגלת בגישה מתפתחת לשימוש בשפה, ולפיכך מאפשרת שילוב קוד זיג חדש לתוך קוד C קיים. לשם כך היא מוכוונת לאינטראקציה חלקה ככל האפשר של קוד זיג עם ספריות C קיימות. האינטגרציה פשוטה, כפי שיראה הקוד בהמשך, אולם כדי להשתמש בה יש להכיר את הדרך בה זיג מייבאת ספריות.

זיג מייבאת את הספריות שלה בעזרת הפקודה @import, בקוד שבדרך כלל נראה כך:const std = @import("std");מיד לאחר מכן יכול קוד זיג באותו הקובץ לפונקציות מתוך הספרייה std, לדוגמה:std.debug.print("Hello, world!\n", .{});אם רוצים לעבוד עם קוד C, צריך רק להחליף את ה@import ב@cimport.const c = @cImport(@cInclude("soundio/soundio.h"));עכשיו יכול הקוד בזיג לקרוא לפונקציות מתוך ספריית soundio כאילו הספרייה הייתה כתובה בעצמה בזיג. מאחר שזיג משתמשת במבני נתונים חדשים שמוגדרים בצורה מפורשת, בניגוד לint ולfloat הגנריים של C, ישנה קבוצה מצומצמת של פקודות שמשמשות להעביר מידע בין הטיפוסים של C לאלו של זיג, ביניהם @intCast ו@ptrCast.

קימפול מוצלב 
מבחינת זיג קימפול מוצלב (cross compilation) הוא תרחיש נפוץ לשימוש בשפה, לפיכך כל קומפיילר זיג יכול לקמפל קובצי הרצה בינאריים לכלל פלטפורמות המטרה שלו, ומדובר על עשרות פלטפורמות שכאלו. פלטפורמות המטרה כוללות ארכיטקטורות נפוצות כגון ARM, x86-64 אך גם מערכות פחות נפוצות, כגון PowerPC, SPARC, MIPS, RISC-V ואפילו z/Architecture (S390) של IBM. מערכת הכלים של זיג יכולה לקמפל לכל אחת מהפלטפורמות הללו ללא צורך בהתקנת תוכנה נוספת, מאחר שכל התמיכה הנדרשת כלולה במערכת הבסיסית.

יכולות נוספות 
זיג תומכת בתכנות גנרי לזמן קימפול, בreflection, בevaluation, בקימפול מוצלב ובניהול זיכרון ידני. על אף שהשפה מכוונת לתת תחליף משופר לשפת C, היא גם שואבת השראה מראסט ומשפות אחרות. לזיג יש יכולות רבות בתחומי הLow-level, ובכללם packed structs (סטראקטים ללא ריפוד לגודל בין השדות), מספרים בכל גודל שהוא וכמה סוגי פויינטרים.

זיג אינה רק שפה חדשה, אלא גם סט כלים, כאשר הקומפיילר שלה הוא גם קומפיילר לC ולC++, ויכול אף להתמודד עם שתיהן ביחד באותו פרויקט.

גרסאות 
עד גרסה 0.9 שימש קומפיילר bootstraping כתוב בC++ בשביל להריץ את הקומפיילר של זיג, אך מאז גרסה 0.10 הקומפיילר של זיג כתוב בזיג, משמע: הקומפיילר הוא Self-hosting. זהו הקומפיילר שמשמש כיום ברירת מחדל, והקומפיילר הישן בC++ צפוי להפסיק להיות זמין אפילו כאופציה החל מגרסה 0.11. הבק-אנד (האופטימייזר) של זיג ממשיך להיות LLVM, שבעצמו כתוב בC++. גרסה 0.10 של זיג משתמשת בLLVM15. הקומפיילר של זיג קטן יחסית לLLVM: גודלו 4.4 מגה-בייטים, וכשהוא נארז ביחד עם LLVM, הוא מהווה רק 2.5% מגודל החבילה. המעבר לקומפיילר ולינקר כתובים בזיג הקטין את השימוש בזיכרון לפחות משליש ביחס לקומפיילר הקודם, האיץ מעט את זמן הקומפילציה ופתר כמה באגים. עם זאת, גרסה 0.10 כוללת גם כמה שיפורים לקומפיילר הישן שנותר כאופציונלי בה, אם כי לא ניתן להשתמש בו עם הלינקר שכתוב בzig. בגרסה 0.10 נוספה כיכולת נסיונית תמיכה בGPU של AMD, ובמידה פחותה גם תמיכה בGPU של NVidia ובפלייסטיישן 4 ו-5.

הקומפיילר הישן שעושה bootstrap נכתב בזיג ובC++, ומשתמש בLLVM 13 כBack-end, ולפיכך גם הוא בעל פלטפורמות יעד רבות. הקומפיילר הוא תוכנה חופשית וכתוב בקוד פתוח תחת רישיון MIT. לקומפיילר של זיג יש יכולת לקמפל C וC++ בדומה ליכולות של Clang, תוך מתן גישה לספריה הסטנדרטית של C ולזו של C++ (הספריות libc וlibcxx בהתאמה). הדבר מתאפשר דרך הפקודות zig cc או zig c++. עקב ההסתמכות על LLVM גם הפקודות האלו יכולות לשמש לקימפול מוצלב.

ידוע שנכתבו בזיג על אפליקציות למערכות הפעלה שולחניות, אפליקציות בסיסיות לאנרדואיד (בעזרת Android NDK) וישנה יכולת לכתוב גם לiOS.

לזיג אין מנהל חבילות רשמי משל עצמה (ישנו מנהל חבילות לא רשמי), אך יש תכנון למנהל חבילות בגרסה 0.12.

הפיתוח של זיג ממומן על ידי קרן שהוקמה לצורך העניין: Zig Software Foundation (ZSF), מלכ"ר שנשיאו הוא אנדרו קלי. הקרן משמשת לקבלת תרומות ולתשלום לעובדים שנשכרים על מנת לעבוד על זיג במשרה מלאה.

דוגמאות

שלום עולם 
const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hello, {s}!\n", .{"world"});
}

רשימה מקושרת רגילה 
pub fn main() void {
    var node = LinkedList(i32).Node {
        .prev = null,
        .next = null,
        .data = 1234,
    };

    var list = LinkedList(i32) {
        .first = &node,
        .last = &node,
        .len = 1,
    };
}

fn LinkedList(comptime T: type) type {
    return struct {
        pub const Node = struct {
            prev: ?*Node,
            next: ?*Node,
            data: T,
        };

        first: ?*Node,
        last:  ?*Node,
        len:   usize,
    };
}

פרויקטים 
Bun הוא מנוע JavaScript וTypeScript שכתוב בזיג, ומשתמש מכונה הווירטואלית JavaScriptCore של הדפדפן ספארי.

ראו גם 
 C
 C++
 Nim
 Rust
 D
 Carbon

הערות שוליים

ביבליוגרפיה

קישורים חיצוניים 

 
 Zig באתר GitHub
 סרטון: Introducing Zig
 סרטון: The Road to 1.0
 Zig Weekly

תוכנה חוצת פלטפורמות
שפות תכנות