סוגי הנתונים

בקטע הזה מתוארים סוגי הנתונים של HIDL. פרטים על ההטמעה זמינים במאמרים HIDL C++‎ (להטמעות ב-C++) או HIDL Java (להטמעות ב-Java).

הדמיון ל-C++‎ כולל:

  • structs משתמשים בתחביר של C++‎, ו-unions תומכים בתחביר של C++‎ כברירת מחדל. צריך לתת שם לשניהם. לא ניתן להשתמש במבנים ובאיחודים אנונימיים.
  • מותר להשתמש ב-Typedef ב-HIDL (כמו ב-C++).
  • מותר להוסיף תגובות בסגנון C++‎, והן מועתקות לקובץ הכותרת שנוצר.

הדמיון ל-Java כולל:

  • לכל קובץ, HIDL מגדיר מרחב שמות בסגנון Java שחייב להתחיל ב-android.hardware.. מרחב השמות שנוצר ב-C++‎ הוא ::android::hardware::….
  • כל ההגדרות של הקובץ נכללות בתוך מעטפת interface בסגנון Java.
  • הצהרות על מערכי HIDL פועלות לפי הסגנון של Java, ולא לפי הסגנון של C++‎. דוגמה:
    struct Point {
        int32_t x;
        int32_t y;
    };
    Point[3] triangle;   // sized array
  • התגובות דומות לפורמט של javadoc.

ייצוג נתונים

למשתנה struct או union שמורכב מ-Standard-Layout (קבוצת משנה של הדרישה לסוגים של נתונים רגילים) יש פריסה עקבית של זיכרון בקוד C++ שנוצר, שמופעלת באמצעות מאפייני התאמה מפורשים במשתני struct ו-union.

סוגי HIDL פרימיטיביים, וגם סוגי enum ו-bitfield (שתמיד נגזרים מסוגי פרימיטיבים), ממופה לסוגים רגילים של C++‎, כמו std::uint32_t מ-cstdint.

מכיוון ש-Java לא תומכת בסוגי נתונים ללא חתימה, סוגי HIDL ללא חתימה ממופה לסוג Java החתום התואם. Structs ממופה לכיתות Java,‏ מערכי נתונים ממופה למערכי נתונים ב-Java, ואיחודים לא נתמכים כרגע ב-Java. מחרוזות מאוחסנות באופן פנימי כ-UTF8. מאחר ש-Java תומכת רק במחרוזות UTF16, ערכים של מחרוזות שנשלחים אל הטמעת Java או ממנה מתורגמים, ויכול להיות שהם לא יהיו זהים בתרגום חוזר כי קבוצות התווים לא תמיד ממפות בצורה חלקה.

נתונים שמתקבלים דרך IPC ב-C++ מסומנים ב-const ונשמרים בזיכרון לקריאה בלבד שנשאר רק למשך זמן הקריאה של הפונקציה. נתונים שמתקבלים דרך IPC ב-Java כבר הועתקו לאובייקטים של Java, כך שאפשר לשמור אותם בלי העתקה נוספת (וניתן לשנות אותם).

הערות

אפשר להוסיף הערות בסגנון Java להצהרות על סוגים. הערות מנותחות על ידי הקצה העורפי של ערכת בדיקות הספק (VTS) של המהדר של HIDL, אבל אף אחת מהערות המנותחות האלה לא מובנת למהדר של HIDL. במקום זאת, התכונה VTS Compiler‏ (VTSC) מטפלת בהערות VTS שעברו ניתוח.

בהערות נעשה שימוש בתחביר Java: @annotation או @annotation(value) או @annotation(id=value, id=value…) כאשר הערך יכול להיות ביטוי קבוע, מחרוזת או רשימה של ערכים בתוך {}, בדיוק כמו ב-Java. אפשר לצרף כמה הערות באותו שם לאותו פריט.

הצהרות קודמות

ב-HIDL, לא ניתן להצהיר מראש על מבני struct, ולכן אי אפשר להגדיר משתני נתונים שמפנים לעצמם (לדוגמה, אי אפשר לתאר רשימה מקושרת או עץ ב-HIDL). ברוב ה-HALs הקיימים (לפני Android 8.x) יש שימוש מוגבל בהצהרות עתידיות, שאפשר להסיר אותן על ידי סידור מחדש של ההצהרות על מבני הנתונים.

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

הצהרות בתצוגת עץ

ב-HIDL יש תמיכה בהצהרות בתצוגת עץ בכמה רמות שרוצים (למעט חריג אחד שמתואר בהמשך). לדוגמה:

interface IFoo {
    uint32_t[3][4][5][6] multidimArray;

    vec<vec<vec<int8_t>>> multidimVector;

    vec<bool[4]> arrayVec;

    struct foo {
        struct bar {
            uint32_t val;
        };
        bar b;
    }
    struct baz {
        foo f;
        foo.bar fb; // HIDL uses dots to access nested type names
    }
    

היוצא מן הכלל הוא שאפשר להטמיע סוגים של ממשקים רק ב-vec<T>, ורמה אחת בלבד (ללא vec<vec<IFoo>>).

תחביר של מצביע גולמי

בשפת HIDL לא נעשה שימוש ב-*, והיא לא תומכת בגמישות המלאה של מצביעים גולמיים ב-C/C++. מידע נוסף על האופן שבו HIDL עוטף את ההפניות ואת המערכים או הוקטורים זמין במאמר תבנית vec<T>.

ממשקים

למילת המפתח interface יש שני שימושים.

  • הפקודה פותחת את ההגדרה של ממשק בקובץ ‎ .hal.
  • אפשר להשתמש בו כסוג מיוחד בשדות של מבנה/איחוד, בפרמטרים של שיטות ובחזרות. הוא נחשב לממשק כללי ולת synonym של android.hidl.base@1.0::IBase.

לדוגמה, ל-IServiceManager יש את השיטה הבאה:

get(string fqName, string name) generates (interface service);

השיטה מבטיחה לחפש ממשק כלשהו לפי שם. היא זהה גם להחלפת הממשק ב-android.hidl.base@1.0::IBase.

אפשר להעביר ממשקים רק בשתי דרכים: כפרמטרים ברמה העליונה או כחברים ב-vec<IMyInterface>. הם לא יכולים להיות חברים במערך של מערכי וקטורים, מבנים, מערכי מחרוזות או יוניונים.

MQDescriptorSync ו-MQDescriptorUnsync

הסוגים MQDescriptorSync ו-MQDescriptorUnsync מעבירים מתארי תור של הודעות מהירות (FMQ) מסונכרנים או לא מסונכרנים דרך ממשק HIDL. פרטים נוספים זמינים במאמר HIDL C++‎ (אין תמיכה ב-FMQ ב-Java).

סוג הזיכרון

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

אזהרה: נתונים מובְנים שמאוחסנים בזיכרון משותף חייבים להיות מסוג שהפורמט שלו לא משתנה במהלך כל תקופת החיים של גרסת הממשק שמעבירה את memory. אחרת, יכולות להיות ל-HAL בעיות תאימות קטלניות.

סוג מצביע

הסוג pointer מיועד לשימוש פנימי ב-HIDL בלבד.

תבנית מסוג bitfield<T>

bitfield<T>, כאשר T הוא enum שהוגדר על ידי משתמש, מציין שהערך הוא אופרטור OR בייטבי של ערכי ה-enum שהוגדרו ב-T. בקוד שנוצר, הערך bitfield<T> מופיע כסוג הבסיסי של T. לדוגמה:

enum Flag : uint8_t {
    HAS_FOO = 1 << 0,
    HAS_BAR = 1 << 1,
    HAS_BAZ = 1 << 2
};
typedef bitfield<Flag> Flags;
setFlags(Flags flags) generates (bool success);

המהדר מטפל בסוג Flags באותו אופן שבו הוא מטפל ב-uint8_t.

למה לא להשתמש ב-(u)int8_t/(u)int16_t/(u)int32_t/(u)int64_t? השימוש ב-bitfield מספק לקורא מידע נוסף על HAL, וכעת הוא יודע ש-setFlags מקבל ערך OR בייטים של Flag (כלומר, הוא יודע שהקריאה ל-setFlags עם 16 לא חוקית). ללא bitfield, המידע הזה מועבר רק באמצעות מסמכים. בנוסף, VTS יכול לבדוק אם הערך של הדגלים הוא אופרטור OR בינארי של Flag.

ידיות של סוגים פרימיטיביים

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

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

native_handle_t

Android תומך ב-native_handle_t, מושג כללי של כתובת מפורטת שמוגדר ב-libcutils.

typedef struct native_handle
{
  int version;        /* sizeof(native_handle_t) */
  int numFds;         /* number of file-descriptors at &data[0] */
  int numInts;        /* number of ints at &data[numFds] */
  int data[0];        /* numFds + numInts ints */
} native_handle_t;

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

מאחר של-native_handle_t יש גודל משתנה, אי אפשר לכלול אותו ישירות ב-struct. שדה ה-handle יוצר הפניה ל-native_handle_t שהוקצה בנפרד.

בגרסאות קודמות של Android, הידיים (handles) הילידיות נוצרו באמצעות אותן הפונקציות שקיימות ב-libcutils. ב-Android מגרסה 8.0 ואילך, הפונקציות האלה מועתקות למרחב השמות android::hardware::hidl או מועברות ל-NDK. קוד שנוצר באופן אוטומטי ב-HIDL מבצע סריאליזציה ופענוח של הפונקציות האלה באופן אוטומטי, ללא מעורבות של קוד שנכתב על ידי משתמשים.

בעלות על התיאור של הטיפול בקובץ ועל מתאר הקובץ

כשקוראים לשיטת ממשק HIDL שמעבירה (או מחזירה) אובייקט hidl_handle (ברמה העליונה או כחלק מסוג מורכב), הבעלות על מתארי הקבצים שמכיל אותו היא:

  • מבצע הקריאה החוזרת שמעביר אובייקט hidl_handle כארגומנטים שומר על הבעלות על מתארי הקבצים שמכילים את native_handle_t שהוא עוטף. מבצע הקריאה החוזרת צריך לסגור את מתארי הקבצים האלה כשהוא מסיים להשתמש בהם.
  • התהליך שמחזיר אובייקט hidl_handle (על ידי העברה שלו לפונקציה _cb) שומר על הבעלות על מתארי הקבצים שמכילים את ה-native_handle_t שמאוחזק על ידי האובייקט. התהליך צריך לסגור את מתארי הקבצים האלה כשהוא מסיים להשתמש בהם.
  • ל-transport שמקבל hidl_handle יש בעלות על מתארי הקבצים בתוך ה-native_handle_t שמקובץ באובייקט. הנמען יכול להשתמש במתארי הקבצים האלה כפי שהם במהלך הקריאה החוזרת (callback) של העסקה, אבל צריך לשכפל את ה-handle המקורי כדי להשתמש במתארי הקבצים מעבר לקריאה החוזרת. כשהעסקה מסתיימת, התעבורה קוראת באופן אוטומטי ל-close() עבור מתארי הקבצים.

HIDL לא תומך במזהים ב-Java (כי Java לא תומכת כלל במזהים).

מערכים בגודל מוגדר

במערכים בגודל מוגדר במבנים של HIDL, הרכיבים יכולים להיות מכל סוג שאפשר לכלול במבנה:

struct foo {
uint32_t[3] x; // array is contained in foo
};

מיתרים

מחרוזות מופיעות בצורה שונה ב-C++ וב-Java, אבל סוג האחסון הבסיסי של התעבורה הוא מבנה של C++. פרטים נוספים זמינים במאמרים סוגים של נתונים ב-HIDL C++‎ וסוגים של נתונים ב-HIDL Java.

הערה: העברה של מחרוזת אל Java או מ-Java דרך ממשק HIDL (כולל מ-Java ל-Java) גורמת להמרות של קבוצות תווים, שעשויות לא לשמור על הקידוד המקורי.

תבנית מסוג vec<T>

התבנית vec<T> מייצגת מאגר בגודל משתנה שמכיל מופעים של T.

הערך של T יכול להיות אחד מהערכים הבאים:

  • סוגי נתונים בסיסיים (למשל uint32_t)
  • מיתרים
  • מערכות ערכים מוגדרות על ידי משתמש
  • מבני נתונים מוגדרים על ידי משתמש
  • ממשקים, או מילת המפתח interface (vec<IFoo>, vec<interface> נתמכת רק כפרמטר ברמה העליונה)
  • כינויים
  • bitfield<U>
  • vec<U>, כאשר U נמצא ברשימה הזו מלבד interface (למשל, vec<vec<IFoo>> לא נתמך)
  • U[] (מערך בגודל U), כאשר U נמצא ברשימה הזו מלבד ממשק

סוגים מוגדרים על ידי משתמש

בקטע הזה מתוארים סוגים מוגדרים על ידי משתמשים.

טיפוסים בני מנייה (enum)

ב-HIDL אין תמיכה ב-enums אנונימיים. אחרת, enums ב-HIDL דומים ל-C++11:

enum name : type { enumerator , enumerator = constexpr ,   }

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

// RED == 0
// BLUE == 4 (GREEN + 1)
enum Color : uint32_t { RED, GREEN = 3, BLUE }

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

// ULTRAVIOLET == 5 (Color:BLUE + 1)
enum FullSpectrumColor : Color { ULTRAVIOLET }

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

הערכים של enums מתייחסים לסינטקס של הנקודתיים (לא לסינטקס של הנקודה כמו בסוגים בתצוגת עץ). התחביר הוא Type:VALUE_NAME. אין צורך לציין את הסוג אם יש ערך שמפנה לאותו סוג enum או לסוגים צאצאים. דוגמה:

enum Grayscale : uint32_t { BLACK = 0, WHITE = BLACK + 1 };
enum Color : Grayscale { RED = WHITE + 1 };
enum Unrelated : uint32_t { FOO = Color:RED + 1 };

החל מ-Android 10, למאפייני enum יש מאפיין len שאפשר להשתמש בו בביטויים קבועים. MyEnum::len הוא המספר הכולל של הרשומות בספירה. הערך הזה שונה מהמספר הכולל של הערכים, שעשוי להיות קטן יותר אם יש ערכים כפולים.

Struct

ב-HIDL אין תמיכה במבנים אנונימיים. אחרת, המבנה של structs ב-HIDL דומה מאוד ל-C.

ב-HIDL אין תמיכה במבנים של נתונים באורך משתנה שמכילים רק struct. הרשימה הזו כוללת את המערך ללא אורך מוגדר, שמשמשים לפעמים בתור השדה האחרון של מבנה ב-C/C++‎ (לפעמים מופיע עם גודל של [0]). HIDL vec<T> מייצג מערכי גודל דינמי עם הנתונים שמאוחסנים במאגר נתונים זמני נפרד. מופעים כאלה מיוצגים על ידי מופע של vec<T> ב-struct.

באופן דומה, string יכול להיכלל ב-struct (מאגרי ה-buffer המשויכים הם נפרדים). בקוד ה-C++ שנוצר, המופעים של סוג ה-handle של HIDL מיוצגים באמצעות הפניה ל-handle המקורי, כי המופעים של טיפוס הנתונים הבסיסי הם באורך משתנה.

Union

HIDL לא תומך באיחודים אנונימיים. אחרת, איחודים דומים ל-C.

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

union UnionType {
uint32_t a;
//  vec<uint32_t> r;  // Error: can't contain a vec<T>
uint8_t b;1
};
fun8(UnionType info); // Legal

אפשר להצהיר על איחודים גם בתוך מבני struct. לדוגמה:

struct MyStruct {
    union MyUnion {
      uint32_t a;
      uint8_t b;
    }; // declares type but not member

    union MyUnion2 {
      uint32_t a;
      uint8_t b;
    } data; // declares type but not member
  }