זיג
Zig |
פרדיגמות |
Multi paradigm, תכנות אימפרטיבי, תכנות פרוצדורלי, תכנות פונקציונלי, Concurrent |
---|
תאריך השקה |
2016[1] |
---|
מתכנן |
אנדרו קלי |
---|
מפתח |
אנדרו קלי |
---|
גרסה אחרונה |
0.13.0 (6 ביוני 2024) |
---|
טיפוסיות |
סטטית, חזקה, מובלעת, מבנית, גנרית |
---|
הושפעה על ידי |
C, Rust, Go, C++, JavaScript |
---|
רישיון |
MIT |
---|
סיומת |
zig.,.zir |
---|
ziglang.org |
|
זיג היא שפת תכנות אימפרטיבית לשימוש כללי. השפה מוכוונת לתכנות מערכות, והיא בעלת טיפוסים סטטיים ומקומפלת. השפה נוצרה על ידי אנדרו קלי ומיועדת לשמש תחליף קטן ופשוט יותר לשפת C, בתוספת יכולות מודרניות, אופטימיזציות חדשות ומגוון מנגנוני בטיחות (אם כי היא אינה מחייבת את המתכנת לשימוש בטוח בזיכרון). היא נבדלת משפות דומות לה כגון Go, ראסט וCarbon בכך שאינה מכוונת לשמש תחליף לC++[2].[3][4]
השפה משפרת ומפשטת שימוש במבני בקרה, קריאה לפונקציות, ייבוא ספריות, הצהרה על משתנים ותמיכה ביוניקוד. בנוסף לכך השפה אינה כוללת מאקרואים או הוראות לקדם-מעבד. זיג אימצה מתוך שפות מודרניות אחרות קונספטים כמו טיפוסים גנריים של זמן הידור, מתן יכולת לפונקציות לעבוד על מגוון סוגי נתונים, וקומץ הוראות למהדר שמאפשרות גישה למידע על הטיפוסים באמצעות רפלקשן.
תוספות אחרות לשפה נועדו לשפר בטיחות קוד. בדומה ל־C, זיג אינה כוללת איסוף זבל והטיפול בזיכרון נעשה ידנית. זיג ממזערת את הסיכון לשגיאות בעת הטיפול בזיכרון באמצעות תחביר פשוט לשימוש בטיפוסים אופציונליים. ישנו גם שלד (Framework) לבדיקות מובנה בשפה.
תיאור
מטרות
המטרה המרכזית של זיג היא "להיות פרגמטית" ולשמש פתרון מוצלח יותר לסוג הבעיות שנפתרות כיום על ידי שפת C. לפיכך אחד השיקולים המרכזיים בשפה הוא יצירת קוד קריא. זיג מנסה להשתמש ככל האפשר בקונספטים ותחביר מוכרים, ולהימנע מתוספות של תחביר מיוחד לקונספטים שאין בהם חידוש משמעותי. זיג גם בנויה להיות "רובסטית, אופטימלית וניתנת לתחזוקה", והיא כוללת יכולות רבות לשיפור הבטיחות, האופטימיזציות ויכולת הבדיקה של קוד. התחביר הקטן והפשוט שלה משחק חלק חשוב ביכולת לתחזק קוד זיג, ואכן התפיסה של זיג היא שצריך לאפשר למתכנתים תחזוקה ותיקון של קוד מבלי שאלו יידרשו להיכנס לנבכי השפה ולהכיר אותה לעומק. ואפילו עם הייחודיות הזו, זיג עדיין מסוגלת להתקמפל בתוך ומול קוד C. ניתן לכלול headers של C בתוך קוד זיג ולקרוא לפונקציות שהם מייצאים, וניתן לקשר קוד זיג לפרויקטים שכתובים ב־C פשוט על ידי include של headers שיוצר הקומפיילר.
במסגרת התפיסה שקוד צריך להיות פשוט וקריא, זיג כוללת כמה שינויים סגנוניים ביחס ל־C ולשפות דמויות C. למשל, בשפות כמו C++ או ראסט קיימת העמסה של אופרטורים, משמע שקוד כמו a = b+c יכול להוות בפועל קריאה לפונקציה שמבצעת גרסה שונה של האופרטור "+" שאיננה חיבור פשוט ויכולה להפתיע את המתכנת שהתכוון לחיבור פשוט. בנוסף, קריאה לפונקציה שכזו יכולה לייצר חריגה שעלולה למנוע ביצוע של החיבור כראוי. בזיג, לעומת זאת, אם ישנה קריאה לפונקציה אז היא תיראה כמו קריאה לפונקציה, ואם זה לא נראה כמו קריאה לפונקציה, אז אין קריאה לפונקציה. כמו כן, אם קוד עלול לייצר שגיאה, הקוד יצהיר על כך מפורשות. טיפול בשגיאות נעשה בעזרת טיפוסי שגיאה בהן יכולים לטפל catch או try.
המטרות של זיג שונות מאלו של שפות רבות שנוצרו בתחילת המאה ה-21 כמו Go, ראסט, Carbon, Nim ואחרות. ככלל, השפות הללו מורכבות יותר וכוללות יכולות כמו העמסת אופרטורים, או פונקציות שמתחזות לערכים (properties), טיפוסים גנריים ועוד יכולות שנועדו לעזור בבניית פרויקטי תוכנה גדולים. יכולות כאלו מקושרות לתפיסות מהסוג של C++, ובהתאם השפות האלו מכווננות לתחומי העיסוק של C++. זיג שמרנית יותר בכל מה שנודע להרחבה של מערכת הטיפוסים, ה־Generics שהיא תומכת בהם הם של זמן קומפילציה וסוג של Duck typing מתאפשר אצלה בעזרת הפקודה comptime.
טיפול בזיכרון
אחת מהסיבות העיקריות לבאגים בתכנות בשפת C היא מערכת ניהול הזיכרון שמתבססת על malloc. בעת השימוש ב־malloc, מוקצים בלוקים של זיכרון ומוחזרים פויינטרים לאותו מיקום בזיכרון. אין שום מנגנון שיבטיח שהזיכרון ישוחרר כשהתוכנה סיימה להשתמש בו, והדבר יכול להוביל לדליפת זיכרון.
בעיה נפוצה אף יותר, ויש שיקראו לה השגיאה הגרועה ביותר בתכנות[6][7], היא שגיאת null pointer, בה מצביע אינו מצביע לכתובת חוקית בזיכרון בגלל כשל של ה־malloc בהקצאת הזיכרון, או בגלל בעיה אחרת.[8]
בשפות תכנות רבות בעיות אלו בניהול זיכרון נפתרות באמצעות מנגנון איסוף זבל (GC) שבודק אם יש בתוכנה מצביעים לזיכרון שהוקצה, ומסיר בלוקים של זיכרון שנותרו ללא מצביעים שמצביעים אליהם. למרות שהמנגנון הזה מפחית מאוד, ואפילו יכול למנוע לחלוטין שגיאות זיכרון, מנגנוני איסוף זבל הם בדרך כלל איטיים יחסית לניהול זיכרון ידני, והביצועים שלהם לא צפויים. לפיכך הם אינם מתאימים לתכנות מערכות.
פתרון מוכר אחר לבעיות זיכרון הוא ספירה אוטומטית של התייחסויות (Automatic Reference Counting, ARC) שמממשת את אותו עיקרון של בדיקת מצביעים והסרת בלוקים של זיכרון, אך עושה זאת בזמן הקצאת הזיכרון על ידי מעקב אחרי מספר המצביעים המצביעים לאותו חלק בזיכרון. בטכניקה הזאת אין צורך בסריקות המצביעים שגוזלות זמן, אך במקומן נוסף זמן ריצה לפעולת הקצאת הזיכרון ושחרורו[8].
זיג נועדה לספק ביצועים זהים או טובים יותר משפת C, כך ששימוש באיסוף זבל או ב־ARC לא ישרת אותה היטב. לכן, נכון לשנת 2022, היא משתמשת בפתרון שידוע בשם "טיפוסים אופציונליים" (Optional types) או פויינטרים חכמים. במנגנון הזה, במקום לאפשר למצביע להצביע לכלום או לאפס, טיפוס מצביע רגיל יכול להצביע רק למקום חוקי בזיכרון, ובמקביל קיים טיפוס מיוחד שיכול להצביע גם הצבעה חוקית וגם הצבעה לא חוקית (הצבעה לאפס בשפת C). מבנה פויינטר כזה דומה ל־struct שמכיל מצביע ובנוסף לו אינדיקטור שמציין אם המצביע חוקי, כך שהאי-חוקיות היא מפורשת ואינה פרשנות של כתובת ההצבעה. בטכניקה של טיפוסים אופציונליים ניהול החוקיות של הפויינטר נעשית אוטומטית על ידי השפה ללא התערבות של המתכנת, כך שפויינטר שהקצאת הזיכרון אליו נכשלה יהיה לא חוקי, ולפיכך לא ניתן יהיה להשתמש בו בטעות כאילו הוא מחזיק זיכרון חוקי[9].
היתרונות במנגנון שכזה הוא שאין כל תוספת עומס על החישוב, או שהתוספת זניחה. אמנם המהדר צריך לייצר את הקוד שמעביר את הטיפוסים האופציונליים למקומות בהם מתבצע שינוי בערכי המצביעים, מה שלא קורה עם מצביעים פשוטים, אבל קוד שכזה הוא בדיוק מה שיאפשר להגדיר כבר בזמן ההידור היכן יכולות להתרחש בעיות זיכרון ללא צורך לעסוק בעניין בזמן ריצה. למשל ב־C ניתן ליצור ולהשתמש במצביע שמצביע לכתובת 0, ואז לקבל שגיאות בזמן ריצה, אולם שפה עם טיפוסים אופציונליים יכולה לבדוק בזמן הידור שכל הניסיונות להשתמש במצביעים נעשים כשהמצביע במצב תקין ולמנוע את בעיות הזיכרון עוד לפני שמגיע שלב הריצה. הטכניקה אמנם לא פותרת את כל בעיות ניהול הזיכרון, אבל אם אכן מתרחשת שגיאת זיכרון בזמן ריצה, הטכניקה מאפשרת לזהות בקלות רבה יותר היכן הן קרו ולמה.[10]
בנוסף, ניהול הזיכרון בזיג נעשה באמצעות 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, ובכך לשנות את דרך ההקצאה, ואפילו להגדיר שיטת הקצאה חדשה. גמישות כזו מאפשרת, למשל, הגדרת שיטות הקצאה שונות לאובייקטים שונים, מבלי לעבור דרך מנגנוני ההקצאה של מערכת ההפעלה שמשתמשים בדפים שלמים מן הזיכרון[11].
הטיפוסים אופציונליים הם דוגמה טובה למנגנונים פשוטים וגנריים בשפה שהם גם שימושיים מאוד. ואכן, אם נסתכל על הטיפוסים האופציונליים, נראה שהם נותנים יכולות שהן הרבה מעבר לטיפול ב־null pointers. הם יכולים לשמש במקרים בהם נדרש טיפוס שמסוגל להיות במצב "אין ערך". דוגמה: נניח שיש לנו פונקציה בשם countNumberOfUsers שמחזירה מספר שלם, ויש לנו משתנה מסוג integer בשם theCountedUsers שמקבל את התוצאה של הפונקציה. בשפות רבות יוגדר מספר קסם שכאשר הוא מופיע כערך ב־theCountedUsers משמעו שעדיין לא קראו לפונקציה, ובמקרים רבים מספר הקסם הזה יהיה 0. בזיג, לעומת זאת, המקרה הזה ימומש על ידי
var theCountedUsers: ?i32 = null
שמכניס למשתנה ערך שחד משמעית אומר "לא קראו עדיין לפונקציה"[11].
לזיג יש גם יכולת כללית יותר שמסייעת בניהול בעיות זיכרון: הקונספט של defer, שמאפשר לסמן קוד ככזה שיתבצע בסוף פונקציה ללא קשר למה שהתרחש בתוכה, וללא קשר לשאלה אם בזמן ריצה היא הסתיימה בצורה תקינה או הופסקה בשל שגיאה. דוגמה ידועה יחסית לשימוש ביכולת הזו היא כאשר פונקציה מקצה זיכרון שאינו נדרש עם סיום פעולתה. אם נשחרר את הזיכרון בסוף הפונקציה, ייתכן שהפונקציה תיכשל בשלב כלשהו בריצה שלה והזיכרון לא ישתחרר ויישאר תפוס ללא סיבה, וכך תתרחש דליפת זיכרון. בזיג ניתן במקרה כזה להעביר לבלוק של ה־defer את השחרור של אותו זיכרון. כך הזיכרון ישתחרר גם כאשר הפונקציה לא הצליחה לסיים את פעולתה[11].
מאחר שניהול הזיכרון בזיג נמנע מהקצאות נסתרות, ההקצאות לא מנוהלות על ידי השפה ישירות. הגישה ל־heap בזיג מתבצעת בעזרת הספרייה הסטנדרטית בצורה מפורשת[12].
אינטראקציה ישירה מול 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[11].
קימפול מוצלב
מבחינת זיג הידור מוצלב (cross compilation) הוא תרחיש נפוץ לשימוש בשפה, לפיכך כל מהדר זיג יכול להדר קובצי הרצה בינאריים לכלל פלטפורמות המטרה שלו, ומדובר על עשרות פלטפורמות שכאלו. פלטפורמות המטרה כוללות ארכיטקטורות נפוצות כגון ARM, x86-64 אך גם מערכות פחות נפוצות, כגון PowerPC, SPARC, MIPS, RISC-V ואפילו z/Architecture (S390) של IBM. מערכת הכלים של זיג יכולה לקמפל לכל אחת מהפלטפורמות הללו ללא צורך בהתקנת תוכנה נוספת, מאחר שכל התמיכה הנדרשת כלולה במערכת הבסיסית[11].
Comptime
בזיג ניתן להשתמש בפקודה comptime כדי שהקומפיילר ישערך את אותו חלק של הקוד בזמן קומפילציה, ולא בזמן ריצה. כך משיגה זיג יכולות מקבילות להרצת מאקרו ולקומפילציה מותנית ללא צורך בהוספה של שפת קדם-מעבד.[13]
בזמן קומפלציה טיפוסים הופכים ל"אזרחים ממדרגה ראשונה", כך מתאפשרת בשפה Duck typing בזמן קומפילציה, שהיא הטכניקה בה זיג מממשת טיפוסים גנריים.
מימוש של טיפוס גנרי של רשימה מקושרת, למשל, יכול להיראות כך בזיג:
fn LinkedList(comptime T: type) type;
הפונקציה מקבלת טיפוס T ומחזירה מבנה שמגדיר רשימה מקושרת של אותו טיפוס.
יכולות נוספות
זיג תומכת בתכנות גנרי לזמן קימפול, ב־reflection, ב־evaluation, בקימפול מוצלב ובניהול זיכרון ידני[14]. על אף שהשפה מכוונת לתת תחליף משופר לשפת C, היא גם שואבת השראה מראסט ומשפות אחרות[15]. לזיג יש יכולות רבות בתחומי ה־Low-level, ובכללם packed structs (סטראקטים ללא ריפוד לגודל בין השדות), מספרים בכל גודל שהוא[16] וכמה סוגי פויינטרים[17].
זיג אינה רק שפה חדשה, אלא גם סט כלים, כאשר הקומפיילר שלה הוא גם קומפיילר ל־C ולC++, ויכול אף להתמודד עם שתיהן ביחד באותו פרויקט.
השם
נראה שהשם "זיג" נבחר תוך שימוש בסקריפט פייתון שכתב אנדרו קלי, ששילב אותיות לכדי מילים בעלות ארבע אותיות, כשבסוף נבחרה דווקא מילה בעלת שלוש אותיות[18].
גרסאות
עד גרסה 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[19] כ־Back-end[20][21], ולפיכך גם הוא בעל פלטפורמות יעד רבות[22]. הקומפיילר הוא תוכנה חופשית וכתוב בקוד פתוח תחת רישיון MIT[23]. לקומפיילר של זיג יש יכולת לקמפל C ו־C++ בדומה ליכולות של Clang, תוך מתן גישה לספריה הסטנדרטית של C ולזו של C++ (הספריות libc ו־libcxx בהתאמה). הדבר מתאפשר דרך הפקודות zig cc או zig c++[24]. עקב ההסתמכות על LLVM גם הפקודות האלו יכולות לשמש לקימפול מוצלב[25][26].
ידוע שנכתבו בזיג על אפליקציות למערכות הפעלה שולחניות, אפליקציות בסיסיות לאנרדואיד (בעזרת Android NDK) וישנה יכולת לכתוב גם ל־iOS.
לזיג אין מנהל חבילות רשמי משל עצמה (ישנו מנהל חבילות לא רשמי), אך יש תכנון למנהל חבילות בגרסה 0.12.
במקור לא הייתה לזיג מערכת ניהול חבילות, אך בגרסה 0.11.0 שוחררה גרסה נסיונית של מנהל חבילות שהורחבה בגרסה 0.12.0. אין לשפה מאגר חבילות רשמי, וחבילות מוגדרות במערכת הניהול כקישורים לקבצים מכווצים בהם נמצא קובץ בשם build.zig סטנדרטי שמשמש את הקומפיילר בקומפילציה, וככל האפשר גם קובץ בשם build.zig.zon בו מוגדרים השם והגרסה של החבילה.
הפיתוח של זיג ממומן על ידי קרן שהוקמה לצורך העניין: Zig Software Foundation (ZSF), מלכ"ר שנשיאו הוא אנדרו קלי. הקרן משמשת לקבלת תרומות ולתשלום לעובדים שנשכרים על מנת לעבוד על זיג במשרה מלאה[27][28][29].
דוגמאות
שלום עולם
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 של הדפדפן ספארי.
ראו גם
לקריאה נוספת
קישורים חיצוניים
הערות שוליים