ניהול גרסאות של ממשק

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

מבנה הקוד של HIDL

קוד HIDL מאורגן בסוגי ממשקים וחבילות שהוגדרו על ידי משתמשים:

  • סוגים מוגדרים על ידי משתמשים (UDT). ‏HIDL מספק גישה לקבוצה של סוגי נתונים פרימיטיביים שאפשר להשתמש בהם כדי ליצור סוגים מורכבים יותר באמצעות מבנים, איחודים ומניינים. משתני UDT מועברים לשיטות של ממשקים, וניתן להגדיר אותם ברמת החבילה (שמשותפת לכל הממשקים) או באופן מקומי לממשק.
  • ממשקים. ממשק הוא אבן בניין בסיסית של HIDL, והוא מורכב מ-UDT ומהצהרות על שיטות. ממשקים יכולים גם לרשת ממשק אחר.
  • חבילות. ארגון של ממשקי HIDL קשורים וסוגים של נתונים שבהם הם פועלים. חבילה מזוהה לפי שם וגרסה, והיא כוללת את הפריטים הבאים:
    • קובץ הגדרה של סוג נתונים שנקרא types.hal.
    • אפס ממשקים או יותר, כל אחד בקובץ .hal משלו.

קובץ ההגדרה של סוגי הנתונים types.hal מכיל רק UDT (כל סוגי ה-UDT ברמת החבילה נשמרים בקובץ אחד). הייצוגים בשפת היעד זמינים לכל הממשקים בחבילה.

פילוסופיית ניהול הגרסאות

לאחר פרסום של חבילת HIDL (כמו android.hardware.nfc) לגרסה מסוימת (כמו 1.0), אי אפשר לשנות אותה. אפשר לבצע שינויים בממשקים בחבילה או שינויים ב-UDT שלה רק בחבילה אחרת.

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

מבחינה מושגית, חבילה יכולה להכיל חבילה אחרת באחת מכמה דרכים:

  • בכלל לא.
  • יכולת התאמה לעומס (extendibility) ברמת החבילה עם תאימות לאחור. המצב הזה מתרחש בגרסאות חדשות של גרסאות משנה (next incremented revision) של חבילה. לחבילה החדשה יש את אותו שם ואותה גרסה ראשית כמו לחבילה הישנה, אבל גרסה משנה גבוהה יותר. מבחינה פונקציונלית, החבילה החדשה היא קבוצה רחבה יותר מהחבילה הישנה, כלומר:
    • ממשקי הרמה העליונה של החבילה ההורה נמצאים בחבילה החדשה, אבל יכול להיות שהממשקים יכללו שיטות חדשות, UDT מקומיים חדשים לממשק (התוסף ברמת הממשק שמתואר בהמשך) ו-UDT חדשים ב-types.hal.
    • אפשר גם להוסיף ממשקים חדשים לחבילה החדשה.
    • כל סוגי הנתונים של החבילה ההורה נמצאים בחבילה החדשה, וניתן לטפל בהם באמצעות השיטות מהחבילה הישנה (שעשויות להיות מוטמעות מחדש).
    • אפשר גם להוסיף סוגי נתונים חדשים לשימוש בשיטות חדשות של ממשקים קיימים ששודרגו, או בממשקים חדשים.
  • יכולת הרחבה ברמת הממשק שתואמת לאחור. החבילה החדשה יכולה גם להרחיב את החבילה המקורית על ידי שימוש בממשקים נפרדים לוגית שמספקים פונקציונליות נוספת, ולא את הפונקציות המרכזיות. לשם כך, כדאי לבצע את הפעולות הבאות:
    • ממשקים בחבילה החדשה צריכים להשתמש בסוגי הנתונים של החבילה הישנה.
    • ממשקים בחבילה חדשה יכולים להרחיב ממשקים של חבילת קוד ישנה אחת או יותר.
  • להרחיב את חוסר התאימות לדורות קודמים. זוהי גרסה משופרת של החבילה בגרסה הראשית, ואין צורך בקורלציה כלשהי ביניהן. אם יש תכונה כזו, אפשר לבטא אותה באמצעות שילוב של סוגים מהגרסה הישנה של החבילה וירושה של קבוצת משנה של ממשקי החבילה הישנה.

מבנה הממשק

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

‏Treble תומך ברכיבי מערכת ורכיבי ספק שנוצרו בנפרד, שבהם אפשר ליצור את vendor.img במכשיר ואת system.img בנפרד. כל האינטראקציות בין vendor.img ל-system.img צריכות להיות מוגדרות באופן מפורש ומקיף כדי שהן יוכלו להמשיך לפעול במשך שנים רבות. הוא כולל ממשקי API רבים, אבל אחד מהם הוא מנגנון ה-IPC שבו HIDL משתמש לתקשורת בין תהליכים בגבול system.img/vendor.img.

דרישות

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

  • אפשר לתאר אותם ישירות ב-HIDL (באמצעות structs, enums וכו') עם שמות ומשמעות סמנטיים.
  • ניתן לתאר אותם באמצעות תקן ציבורי כמו ISO/IEC 7816.
  • אפשר לתאר אותו באמצעות תקן חומרה או פריסה פיזית של חומרה.
  • אם יש צורך, יכולים להיות אלה נתונים אטומים (כמו מפתחות ציבוריים, מזהים וכו').

אם משתמשים בנתונים אטומים, הם צריכים להיקרא רק על ידי צד אחד של ממשק HIDL. לדוגמה, אם קוד vendor.img מעביר לרכיב ב-system.img הודעת מחרוזת או נתוני vec<uint8_t>, הרכיב ב-system.img לא יכול לנתח את הנתונים האלה. אפשר רק להעביר אותם חזרה ל-vendor.img לצורך פרשנות. כשמעבירים ערך מ-vendor.img לקוד הספק ב-system.img או למכשיר אחר, צריך לתאר במדויק את הפורמט של הנתונים ואת אופן הפענוח שלהם, והם עדיין חלק מהממשק.

הנחיות

אתם אמורים להיות מסוגלים לכתוב הטמעה או לקוח של HAL באמצעות קובצי ה-hal. בלבד (כלומר, לא צריך לעיין במקורות של Android או בתקנים הציבוריים). מומלץ לציין את ההתנהגות הנדרשת בדיוק. הצהרות כמו "הטמעה עשויה לבצע פעולה א' או ב'" מעודדות הטמעות להשתלב עם הלקוחות שבהם הן מפותחות.

פריסת הקוד של HIDL

HIDL כולל חבילות ליבה וחבילות של ספקים.

ממשקי הליבה של HIDL הם אלה שצוינו על ידי Google. השמות של החבילות שהן שייכות אליהן מתחילים ב-android.hardware., והן נקראות לפי מערכת המשנה, עם אפשרות לשימוש ברמות עץ של שמות. לדוגמה, שם החבילה של ה-NFC הוא android.hardware.nfc ושם החבילה של המצלמה הוא android.hardware.camera. באופן כללי, לחבילת הליבה יש את השם android.hardware.[name1].[name2]…. לחבילות HIDL יש גרסת build בנוסף לשם שלהן. לדוגמה, יכול להיות שהגרסה של החבילה android.hardware.camera היא 3.4. זה חשוב כי גרסת החבילה משפיעה על המיקום שלה בעץ המקור.

כל חבילות הליבה ממוקמות ב-hardware/interfaces/ במערכת ה-build. החבילה android.hardware.[name1].[name2]… בגרסה $m.$n נמצאת בתיקייה hardware/interfaces/name1/name2//$m.$n/. החבילה android.hardware.camera בגרסה 3.4 נמצאת בתיקייה hardware/interfaces/camera/3.4/.. יש מיפוי מקודד בין הקידומת של החבילה android.hardware. לבין הנתיב hardware/interfaces/.

חבילות שאינן ליבה (של ספק) הן חבילות שספק ה-SoC או ה-ODM ייצר. הקידומת של חבילות שאינן ליבה היא vendor.$(VENDOR).hardware., כאשר $(VENDOR) מתייחס לספק SoC או ל-OEM/ODM. המיפוי הזה מתבצע לנתיב vendor/$(VENDOR)/interfaces בעץ (גם המיפוי הזה מקודד מראש).

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

ב-HIDL, לכל UDT יש שם מלא שמורכב משם ה-UDT, שם החבילה שבה ה-UDT מוגדר וגרסה של החבילה. השם המלא משמש רק כשמגדירים את המופעים של הסוג, ולא כשמגדירים את הסוג עצמו. לדוגמה, נניח שחבילת android.hardware.nfc, בגרסה 1.0 מגדירה struct בשם NfcData. באתר ההצהרה (ב-types.hal או בהצהרה של ממשק), ההצהרה פשוט כוללת את הפרטים הבאים:

struct NfcData {
    vec<uint8_t> data;
};

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

android.hardware.nfc@1.0::NfcData

התחביר הכללי הוא PACKAGE@VERSION::UDT, כאשר:

  • PACKAGE הוא השם של חבילת HIDL שמופרד בנקודות (למשל, android.hardware.nfc).
  • VERSION הוא הפורמט של גרסת החבילה, בפורמט major.minor-version שמופרד בנקודות (למשל, 1.0).
  • UDT הוא השם של HIDL UDT, שמופרד בנקודות. מאחר ש-HIDL תומך ב-UDTs בתצוגת עץ וממשקי HIDL יכולים להכיל UDTs (סוג של הצהרה בתצוגת עץ), נקודות משמשות לגישה לשמות.

לדוגמה, אם ההצהרה ההטמעתית הבאה הוגדרה בקובץ הסוגים הנפוצים בחבילה android.hardware.example בגרסה 1.0:

// types.hal
package android.hardware.example@1.0;
struct Foo {
    struct Bar {
        // …
    };
    Bar cheers;
};

השם המלא של Bar הוא android.hardware.example@1.0::Foo.Bar. אם ההצהרה בתצוגת עץ נמצאת גם בחבילה שלמעלה וגם בממשק שנקרא IQuux:

// IQuux.hal
package android.hardware.example@1.0;
interface IQuux {
    struct Foo {
        struct Bar {
            // …
        };
        Bar cheers;
    };
    doSomething(Foo f) generates (Foo.Bar fb);
};

השם המלא של Bar הוא android.hardware.example@1.0::IQuux.Foo.Bar.

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

// IQuux.hal
doSomething(android.hardware.example@1.0::IQuux.Foo f) generates (android.hardware.example@1.0::IQuux.Foo.Bar fb);

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

אם UDT הוא סוג enum, לכל ערך מסוג enum יש שם מלא שמתחיל בשם המלא של סוג ה-enum, ואחריו נקודתיים ואז השם של ערך ה-enum. לדוגמה, נניח שגרסת החבילה android.hardware.nfc,1.0 מגדירה את סוג המאפיין NfcStatus:

enum NfcStatus {
    STATUS_OK,
    STATUS_FAILED
};

כשמדברים על STATUS_OK, השם המלא הוא:

android.hardware.nfc@1.0::NfcStatus:STATUS_OK

התחביר הכללי הוא PACKAGE@VERSION::UDT:VALUE, כאשר:

  • PACKAGE@VERSION::UDT הוא אותו שם מלא ומוגדר לחלוטין של סוג הטיפוס המנוהל.
  • VALUE הוא שם הערך.

כללי הסקה אוטומטית

אין צורך לציין שם UDT מלא. אפשר להשמיט בשם UDT את הפרטים הבאים ללא חשש:

  • החבילה, למשל @1.0::IFoo.Type
  • גם החבילה וגם הגרסה, למשל IFoo.Type

HIDL מנסה להשלים את השם באמצעות כללי התערבות אוטומטית (מספר כלל נמוך יותר מציין עדיפות גבוהה יותר).

כלל 1

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

interface Nfc {
    typedef string NfcErrorMessage;
    send(NfcData d) generates (@1.0::NfcStatus s, NfcErrorMessage m);
};

המערכת מחפשת את NfcErrorMessage באופן מקומי, ומוצאת את typedef שמעליו. גם המשתנה NfcData מחפש באופן מקומי, אבל מכיוון שהוא לא מוגדר באופן מקומי, נעשה שימוש בכללים 2 ו-3. @1.0::NfcStatus מציין גרסה, ולכן כלל 1 לא חל.

כלל 2

אם כלל 1 נכשל ורכיב מהשם המלא חסר (חבילה, גרסה או חבילה וגרסה), הרכיב יתמלא באופן אוטומטי במידע מהחבילה הנוכחית. לאחר מכן, המהדר של HIDL מחפש בקובץ הנוכחי (ובכל הייבוא) כדי למצוא את השם המלא המלא שהושלם באופן אוטומטי. בהתאם לדוגמה שלמעלה, נניח שהצהרת ExtendedNfcData נעשתה באותה חבילה (android.hardware.nfc) ובאותה גרסה (1.0) כמו NfcData, באופן הבא:

struct ExtendedNfcData {
    NfcData base;
    // … additional members
};

המהדר של HIDL ממלא את שם החבילה ואת שם הגרסה מהחבילה הנוכחית כדי ליצור את השם המלא של ה-UDT‏ android.hardware.nfc@1.0::NfcData. מכיוון שהשם קיים בחבילה הנוכחית (בהנחה שהיא מיובאת כראוי), הוא משמש להצהרה.

שם בחבילה הנוכחית מיובא רק אם אחד מהמצבים הבאים מתקיים:

  • הוא מיובא באופן מפורש באמצעות משפט import.
  • הוא מוגדר ב-types.hal בחבילה הנוכחית

אותו תהליך מתבצע אם NfcData הוגדר רק לפי מספר הגרסה:

struct ExtendedNfcData {
    // autofill the current package name (android.hardware.nfc)
    @1.0::NfcData base;
    // … additional members
};

כלל 3

אם כלל 2 לא מוצא התאמה (ה-UDT לא מוגדר בחבילה הנוכחית), המהדר של HIDL סורק את כל החבילות המיובאות כדי למצוא התאמה. לפי הדוגמה שלמעלה, נניח ש-ExtendedNfcData מוצהר בגרסה 1.1 של החבילה android.hardware.nfc, ש-1.1 מייבא את 1.0 כראוי (ראו תוספים ברמת החבילה) וההגדרה מציינת רק את שם ה-UDT:

struct ExtendedNfcData {
    NfcData base;
    // … additional members
};

המהדר מחפש UDT בשם NfcData ומוצא אחד ב-android.hardware.nfc בגרסה 1.0, וכתוצאה מכך נוצר UDT מלא עם הכיתוב android.hardware.nfc@1.0::NfcData. אם נמצאת יותר מהתאמה אחת ל-UDT מסוים עם הסמכה חלקית, מתקבלת שגיאה במהלך הידור ה-HIDL.

דוגמה

לפי כלל 2, סוג שיובא מוגדר בחבילה הנוכחית מקבל עדיפות על פני סוג שיובא מחבילה אחרת:

// hardware/interfaces/foo/1.0/types.hal
package android.hardware.foo@1.0;
struct S {};

// hardware/interfaces/foo/1.0/IFooCallback.hal
package android.hardware.foo@1.0;
interface IFooCallback {};

// hardware/interfaces/bar/1.0/types.hal
package android.hardware.bar@1.0;
typedef string S;

// hardware/interfaces/bar/1.0/IFooCallback.hal
package android.hardware.bar@1.0;
interface IFooCallback {};

// hardware/interfaces/bar/1.0/IBar.hal
package android.hardware.bar@1.0;
import android.hardware.foo@1.0;
interface IBar {
    baz1(S s); // android.hardware.bar@1.0::S
    baz2(IFooCallback s); // android.hardware.foo@1.0::IFooCallback
};
  • הערך של S מחושב כ-android.hardware.bar@1.0::S, והוא נמצא ב-bar/1.0/types.hal (כי types.hal מיובאת באופן אוטומטי).
  • הערך IFooCallback מומר ל-android.hardware.bar@1.0::IFooCallback באמצעות כלל 2, אבל אי אפשר למצוא אותו כי הערך bar/1.0/IFooCallback.hal לא מיובא באופן אוטומטי (בניגוד ל-types.hal). לכן, כלל 3 פותר את הערך ל-android.hardware.foo@1.0::IFooCallback במקום זאת, שמיובא דרך import android.hardware.foo@1.0;).

types.hal

כל חבילת HIDL מכילה קובץ types.hal שמכיל UDTs ששותפים בין כל הממשקים שמשתתפים בחבילה. סוגי HIDL הם תמיד ציבוריים. לא משנה אם ה-UDT מוצהר ב-types.hal או בהצהרת ממשק, אפשר לגשת לסוגי ה-UDT מחוץ להיקף שבו הם מוגדרים. types.hal לא מיועד לתאר את ה-API הציבורי של החבילה, אלא לארח UDTs שבהם משתמשים כל הממשקים בחבילה. בגלל אופי ה-HIDL, כל ה-UDTs הם חלק מהממשק.

types.hal מורכב מ-UDT ומהוראות import. מאחר ש-types.hal זמין לכל ממשק בחבילה (זהו ייבוא משתמע), הצהרות ה-import האלה הן ברמת החבילה מעצם הגדרתן. טיפוסי UDT ב-types.hal יכולים גם לכלול ממשקים וטיפוסי UDT שיובאו כך.

לדוגמה, עבור IFoo.hal:

package android.hardware.foo@1.0;
// whole package import
import android.hardware.bar@1.0;
// types only import
import android.hardware.baz@1.0::types;
// partial imports
import android.hardware.qux@1.0::IQux.Quux;
// partial imports
import android.hardware.quuz@1.0::Quuz;

הנתונים הבאים מיובאים:

  • android.hidl.base@1.0::IBase (באופן משתמע)
  • android.hardware.foo@1.0::types (באופן משתמע)
  • כל מה ששייך ל-android.hardware.bar@1.0 (כולל כל הממשקים ו-types.hal שלו)
  • types.hal מ-android.hardware.baz@1.0::types (ממשקים ב-android.hardware.baz@1.0 לא מיובאים)
  • IQux.hal ו-types.hal מ-android.hardware.qux@1.0
  • Quuz מ-android.hardware.quuz@1.0 (בהנחה ש-Quuz מוגדר ב-types.hal, כל הקובץ types.hal מנותח, אבל סוגי נתונים שאינם Quuz לא מיובאים).

ניהול גרסאות ברמת הממשק

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

package android.hardware.nfc@1.0;

ב-HIDL, ממשקים יכולים לרשת ממשקים אחרים באמצעות מילת המפתח extends. כדי שממשק יוכל להרחיב ממשק אחר, צריכה להיות לו גישה אליו באמצעות משפט import. השם של הממשק שרוצים להרחיב (ממשק הבסיס) צריך לעמוד בכללים למתן שמות של סוגי נתונים שמפורטים למעלה. ממשק יכול לרשת רק ממשק אחד.‏ HIDL לא תומך בירושה מרובה.

בדוגמאות הבאות לניהול גרסאות של uprev נעשה שימוש בחבילה הבאה:

// types.hal
package android.hardware.example@1.0
struct Foo {
    struct Bar {
        vec<uint32_t> val;
    };
};

// IQuux.hal
package android.hardware.example@1.0
interface IQuux {
    fromFooToBar(Foo f) generates (Foo.Bar b);
}

כללי Uprev

כדי להגדיר את החבילה package@major.minor, התנאי A או כל התנאים של B חייבים להתקיים:

כלל א' 'גרסה משנית ראשונית': אסור להגדיר את כל הגרסאות המשניות הקודמות, package@major.0, ‏ package@major.1,‏ …,‏ package@major.(minor-1).
או
כלל ב'

כל התנאים הבאים מתקיימים:

  1. 'גרסה קודמת משנית תקפה': צריך להגדיר את package@major.(minor-1) ולפעול לפי אותו כלל א' (אף אחת מהגרסאות package@major.0 עד package@major.(minor-2) לא מוגדרת) או כלל ב' (אם זו גרסה משודרגת מ-@major.(minor-2)).

    וגם

  2. "Inherit at least one interface with the same name": יש ממשק package@major.minor::IFoo שמרחיב את package@major.(minor-1)::IFoo (אם לחבילה הקודמת יש ממשק).

    וגם

  3. "אין ממשק שעובר בירושה עם שם שונה": אסור שיהיה package@major.minor::IBar שמרחיב את package@major.(minor-1)::IBaz, כאשר IBar ו-IBaz הם שני שמות שונים. אם יש ממשק עם אותו שם, package@major.minor::IBar חייב להרחיב את package@major.(minor-k)::IBar כך שלא יהיה IBar עם k קטן יותר.

בגלל כלל א':

  • החבילה יכולה להתחיל עם כל מספר גרסה משני (לדוגמה, android.hardware.biometrics.fingerprint מתחיל ב-@2.1).
  • הדרישה 'android.hardware.foo@1.0 לא מוגדרת' פירושה שספריית hardware/interfaces/foo/1.0 לא אמורה להתקיים בכלל.

עם זאת, כלל א' לא משפיע על חבילה עם אותו שם חבילה אבל עם גרסה ראשית שונה (לדוגמה, ב-android.hardware.camera.device מוגדרים גם @1.0 וגם @3.2, ו-@3.2 לא צריך לקיים אינטראקציה עם @1.0). לכן, @3.2::IExtFoo יכול להרחיב את @1.0::IFoo.

אם שם החבילה שונה, אפשר להרחיב את package@major.minor::IBar מממשק עם שם שונה (לדוגמה, android.hardware.bar@1.0::IBar יכול להרחיב את android.hardware.baz@2.2::IBaz). אם ממשק לא מצהיר במפורש על סופר-סוג באמצעות מפתח המילה extend, הוא מרחיב את android.hidl.base@1.0::IBase (למעט IBase עצמו).

צריך לפעול לפי השלבים ב-B.2 וב-B.3 בו-זמנית. לדוגמה, גם אם android.hardware.foo@1.1::IFoo הוא תת-מסנן של android.hardware.foo@1.0::IFoo שעובר את כלל B.2, אם android.hardware.foo@1.1::IExtBar הוא תת-מסנן של android.hardware.foo@1.0::IBar, עדיין לא מדובר בגרסה משופרת תקפה.

ממשקי Uprev

כדי לשדרג את android.hardware.example@1.0 (שצוין למעלה) ל-@1.1:

// types.hal
package android.hardware.example@1.1;
import android.hardware.example@1.0;

// IQuux.hal
package android.hardware.example@1.1
interface IQuux extends @1.0::IQuux {
    fromBarToFoo(Foo.Bar b) generates (Foo f);
}

זהו import ברמת החבילה של גרסת 1.0 של android.hardware.example ב-types.hal. אמנם לא נוספו UDT חדשים בגרסה 1.1 של החבילה, אבל עדיין יש צורך בהפניות ל-UDT בגרסה 1.0, ולכן מתבצע ייבוא ברמת החבילה ב-types.hal. (אפשר היה להשיג את אותו אפקט באמצעות ייבוא ברמת הממשק ב-IQuux.hal).

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

הממשק החדש IQuux לא מכריז מחדש על השיטה fromFooToBar() שעוברת בירושה מ-@1.0::IQuux, אלא פשוט מציג את השיטה החדשה שהוא מוסיף fromBarToFoo(). ב-HIDL, אי אפשר להצהיר שוב על שיטות שעברו בירושה בממשקי הצאצאים, ולכן הממשק IQuux לא יכול להצהיר על השיטה fromFooToBar() באופן מפורש.

מוסכמות של Uprev

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

// in parent hal file
enum Brightness : uint32_t { NONE, WHITE };

// in child hal file extending the existing set with additional similar values
enum Brightness : @1.0::Brightness { AUTOMATIC };

// extending the existing set with values that require a new, more descriptive name:
enum Color : @1.0::Brightness { HW_GREEN, RAINBOW };

אם אפשר לתת לשיטה שם סמנטי חדש (למשל fooWithLocation), זה עדיף. אחרת, השם צריך להיות דומה לשם של הספרייה שאליה הוא מרחיב. לדוגמה, השיטה foo_1_1 ב-@1.1::IFoo יכולה להחליף את הפונקציונליות של השיטה foo ב-@1.0::IFoo אם אין שם חלופי טוב יותר.

ניהול גרסאות ברמת החבילה

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

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

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

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

אם החבילה עומדת בדרישות האלה, hidl-gen אוכף כללי תאימות לאחור.