AIDL ליעדי HAL

ב-Android 11 הוספנו את היכולת להשתמש ב-AIDL לנכסי HAL ב-Android. כך אפשר להטמיע חלקים של Android בלי HIDL. יש להעביר HALs כדי להשתמש ב-AIDL באופן בלעדי כשזה אפשרי (כש-HALs ב-upstream משתמשים ב-HIDL, חובה להשתמש ב-HIDL).

רכיבי HAL שמשתמשים ב-AIDL כדי לתקשר בין רכיבי framework, כמו של system.img, ורכיבי חומרה, כמו אלה של vendor.img, חייבים להשתמש ב-Stable AIDL. עם זאת, כדי לתקשר בתוך קטגוריה, למשל בין HAL אחד לאחר, אין הגבלה על מנגנון ה-IPC.

מוטיבציה

ה-AIDL פועל במשך יותר זמן מ-HIDL, ומשמש במקומות רבים אחרים, למשל בין רכיבי framework של Android או באפליקציות. עכשיו, אחרי ש-AIDL יש תמיכה ביציבות, אפשר להטמיע סטאק שלמה עם סביבת זמן ריצה אחת של IPC. ב-AIDL יש גם מערכת ניהול גרסאות טובה יותר מ-HIDL.

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

פיתוח בהתאם לסביבת זמן הריצה של AIDL

ל-AIDL יש שלושה קצוות עורפיים שונים: Java, NDK ו-CPP. כדי להשתמש ב-Stable AIDL, תמיד צריך להשתמש בעותק של המערכת של Libbinder ב-system/lib*/libbinder.so ולדבר ב-/dev/binder. לקוד בתמונת הספק, המשמעות היא שאי אפשר להשתמש ב-libbinder (מ-VNDK): לספרייה הזו יש API לא יציב של C++ ופנימיות לא יציבות. במקום זאת, קוד הספק המקורי צריך להשתמש בקצה העורפי NDK של AIDL, לקשר ל-libbinder_ndk (שמגובה על ידי המערכת libbinder.so) ולקשר לספריות ה-NDK שנוצרו על ידי רשומות aidl_interface. השמות המדויקים של המודולים מופיעים במאמר כללים למתן שמות למודולים.

כתיבת ממשק AIDL HAL

כדי להשתמש בממשק AIDL בין המערכת לבין הספק, צריך לבצע בממשק שני שינויים:

  • לכל הגדרה של סוג צריך להוסיף הערה @VintfStability.
  • ההצהרה aidl_interface צריכה לכלול stability: "vintf",.

רק הבעלים של הממשק יכול לבצע את השינויים האלה.

כדי לבצע את השינויים האלה, הממשק חייב להופיע במניפסט של VINTF. מומלץ לבדוק את זה (ואת הדרישות שקשורות אליו, כמו לוודא שהממשקים שפורסמו הוקפאו) באמצעות בדיקת VTS vts_treble_vintf_vendor_test. אפשר להשתמש בממשק @VintfStability בלי הדרישות האלו על ידי קריאה ל-AIBinder_forceDowngradeToLocalStability בקצה העורפי של NDK, ל-android::Stability::forceDowngradeToLocalStability בקצה העורפי של C++ או ל-android.os.Binder#forceDowngradeToSystemStability בקצה העורפי של Java באובייקט binder לפני שהוא נשלח לתהליך אחר. אי אפשר לשדרג לאחור שירות ליציבות הספק ב-Java כי כל האפליקציות פועלות בהקשר של המערכת.

בנוסף, כדי לנייד את הקוד המקסימלי וכדי להימנע מבעיות פוטנציאליות כמו ספריות נוספות שאין בהן צורך, צריך להשבית את הקצה העורפי של CPP.

שימו לב שהשימוש ב-backends בקוד לדוגמה שבהמשך הוא נכון, כי יש שלושה קצוות עורפיים (Java , NDK ו-CPP). הקוד שבהמשך מסביר איך לבחור באופן ספציפי את הקצה העורפי של CPP כדי להשבית אותו.

    aidl_interface: {
        ...
        backends: {
            cpp: {
                enabled: false,
            },
        },
    }

חיפוש ממשקי AIDL HAL

ממשקי AOSP Stable AIDL ל-HAL נמצאים באותן ספריות בסיס כמו ממשקי HIDL, בתיקיות aidl.

  • חומרה/ממשקים
  • frameworks/חומרה/ממשקים
  • מערכת/חומרה/ממשקים

צריך למקם ממשקי תוספים בספריות משנה אחרות ב-hardware/interfaces ב-vendor או ב-hardware.

ממשקי תוספים

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

תוספים יכולים להירשם בשתי דרכים שונות:

עם זאת, תוסף נרשם, כאשר רכיבים ספציפיים לספק (כלומר לא חלק מ-upstream AOSP) משתמשים בממשק, אין אפשרות למיזוג. עם זאת, כשמתבצעים שינויים ב-downstream ברכיבי AOSP ב-upstream, עלולות להיווצר התנגשויות בין מיזוגים, ומומלץ להשתמש באסטרטגיות הבאות:

  • ניתן להעלות את השידור של התוספות לממשק ל-AOSP בגרסה הבאה
  • בגרסה הבאה יש הוספות לממשקים שמאפשרות גמישות נוספת, ללא התנגשויות מיזוג.

מתקנים לתוספים: ParcelableHolder

ParcelableHolder הוא Parcelable שיכול להכיל עוד Parcelable. התרחיש לדוגמה העיקרי של ParcelableHolder הוא ליצור Parcelable שניתן להרחיב. לדוגמה, תמונה שמטמיעי המכשירים מצפים שתהיה אפשרות להרחיב Parcelable, AospDefinedParcelable בהגדרת AOSP, כדי לכלול את התכונות שלהם לערך מוסף.

בעבר בלי ParcelableHolder, מטמיעי מכשירים לא יכלו לשנות ממשק AIDL יציב שמוגדר ל-AOSP, כי הוספה של שדות נוספים הייתה תגרום לשגיאה:

parcelable AospDefinedParcelable {
  int a;
  String b;
  String x; // ERROR: added by a device implementer
  int[] y; // added by a device implementer
}

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

באמצעות ParcelableHolder, הבעלים של מגרש יכולים להגדיר נקודת תוסף ב-Parcelable.

parcelable AospDefinedParcelable {
  int a;
  String b;
  ParcelableHolder extension;
}

לאחר מכן, מטמיעי המכשירים יכולים להגדיר Parcelable משלהם לתוסף שלהם.

parcelable OemDefinedParcelable {
  String x;
  int[] y;
}

לבסוף, אפשר לצרף את Parcelable החדש אל Parcelable המקורי באמצעות השדה ParcelableHolder.


// Java
AospDefinedParcelable ap = ...;
OemDefinedParcelable op = new OemDefinedParcelable();
op.x = ...;
op.y = ...;

ap.extension.setParcelable(op);

...

OemDefinedParcelable op = ap.extension.getParcelable(OemDefinedParcelable.class);

// C++
AospDefinedParcelable ap;
OemDefinedParcelable op;
std::shared_ptr<OemDefinedParcelable> op_ptr = make_shared<OemDefinedParcelable>();

ap.extension.setParcelable(op);
ap.extension.setParcelable(op_ptr);

...

std::shared_ptr<OemDefinedParcelable> op_ptr;

ap.extension.getParcelable(&op_ptr);

// NDK
AospDefinedParcelable ap;
OemDefinedParcelable op;
ap.extension.setParcelable(op);

...

std::optional<OemDefinedParcelable> op;
ap.extension.getParcelable(&op);

// Rust
let mut ap = AospDefinedParcelable { .. };
let op = Rc::new(OemDefinedParcelable { .. });

ap.extension.set_parcelable(Rc::clone(&op));

...

let op = ap.extension.get_parcelable::<OemDefinedParcelable>();

שמות מכונות של שרתי AIDL HAL

המוסכמה, לשירותי AIDL HAL יש שם מכונה בפורמט $package.$type/$instance. לדוגמה, מופע של HAL של הרטט רשום בתור android.hardware.vibrator.IVibrator/default.

כתיבה של שרת AIDL HAL

צריך להצהיר על @VintfStability על שרתי AIDL במניפסט של VINTF, לדוגמה:

    <hal format="aidl">
        <name>android.hardware.vibrator</name>
        <version>1</version>
        <fqname>IVibrator/default</fqname>
    </hal>

אחרת, הם צריכים לרשום שירות AIDL כרגיל. כשמריצים בדיקות VTS, כל אובייקטי ה-AIDL HAL המוצהרים צריכים להיות זמינים.

כתיבה של לקוח AIDL

לקוחות AIDL חייבים להצהיר על עצמם במטריצת התאימות, לדוגמה, כך:

    <hal format="aidl" optional="true">
        <name>android.hardware.vibrator</name>
        <version>1-2</version>
        <interface>
            <name>IVibrator</name>
            <instance>default</instance>
        </interface>
    </hal>

המרה של HAL קיים מ-HIDL ל-AIDL

משתמשים בכלי hidl2aidl כדי להמיר ממשק HIDL ל-AIDL.

התכונות של hidl2aidl:

  • יצירת קובצי .aidl על סמך קובצי ה-.hal של החבילה הנתונה
  • יצירת כללי build לחבילת AIDL החדשה שנוצרה כאשר כל הקצוות העורפיים מופעלים
  • יצירה של שיטות תרגום בקצה העורפי Java, CPP ו-NDK לתרגום מסוגי HIDL לסוגי AIDL
  • יצירת כללי build לתרגום של ספריות עם יחסי תלות נדרשים
  • יוצרים הצהרות סטטיות כדי לוודא שלמספרי HIDL ו-AIDL יש אותם ערכים בקצוות העורפיים של CPP ו-NDK

כדי להמיר חבילה של קובצי .hal לקובצי .aidl, פועלים לפי השלבים הבאים:

  1. בניית הכלי נמצאת ב-system/tools/hidl/hidl2aidl.

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

    m hidl2aidl
    
  2. מריצים את הכלי עם ספריית פלט ואחריה את החבילה שרוצים להמיר.

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

    hidl2aidl -o <output directory> -l <file with license> <package>
    

    לדוגמה:

    hidl2aidl -o . -l my_license.txt android.hardware.nfc@1.2
    
  3. קוראים את הקבצים שנוצרו ומתקנים בעיות בהמרה.

    • בכלי conversion.log יש בעיות שלא טופלו שצריך לתקן קודם.
    • יכול להיות שבקובצי .aidl שנוצרו יש אזהרות והצעות שעשויות לחייב פעולה כלשהי. התגובות האלה מתחילות ב-//.
    • כדאי לנצל את ההזדמנות כדי לפנות מקום ולשפר את החבילה.
    • כדאי לעיין בהערה @JavaDerive לגבי תכונות שאולי נחוצות, כמו toString או equals.
  4. בונים רק את היעדים הנחוצים.

    • השבתת קצוות עורפיים שלא יהיו בשימוש. עדיף להשתמש בקצה העורפי NDK על פני הקצה העורפי של CPP. מידע נוסף זמין בקטע בחירת זמן ריצה.
    • מסירים את ספריות התרגום או כל קוד שנוצר בהן שלא יהיה בשימוש.
  5. ראו הבדלים משמעותיים ב-AIDL/HIDL.

    • השימוש בחריגים וב-Status המובנים של AIDL בדרך כלל משפר את הממשק ומבטל את הצורך בסוג סטטוס אחר שספציפי לממשק.
    • כברירת מחדל, ארגומנטים של ממשק AIDL ב-method לא @nullable כמו שהם היו ב-HIDL.

SEPolicy ל-AIDL HALs

סוג שירות AIDL שגלוי לקוד הספק חייב לכלול את המאפיין hal_service_type. במקרים אחרים, ההגדרות האישיות של מדיניות השירות זהות לכל שירות AIDL אחר (למרות שיש מאפיינים מיוחדים ל-HAL). לפניכם הגדרה לדוגמה בהקשר של שירות HAL:

    type hal_foo_service, service_manager_type, hal_service_type;

לרוב השירותים שמוגדרים על ידי הפלטפורמה, כבר התווסף הקשר שירות מהסוג הנכון (לדוגמה, android.hardware.foo.IFoo/default כבר יסומן כ-hal_foo_service). עם זאת, אם לקוח framework תומך בשמות של מספר מכונות, צריך להוסיף שמות מכונות נוספים בקובצי service_contexts שספציפיים למכשיר.

    android.hardware.foo.IFoo/custom_instance u:object_r:hal_foo_service:s0

צריך להוסיף מאפייני HAL כשאנחנו יוצרים סוג חדש של HAL. מאפיין HAL ספציפי יכול להיות משויך לכמה סוגי שירותים (בכל אחד מהם יכולות להיות כמה מופעים, כמו שהסברנו). ל-HAL, foo, יש לנו את hal_attribute(foo). המאקרו הזה מגדיר את המאפיינים hal_foo_client ו-hal_foo_server. לגבי דומיין נתון, פקודות המאקרו hal_client_domain ו-hal_server_domain משייכות דומיין למאפיין HAL נתון. לדוגמה, אם שרת המערכת הוא לקוח של ה-HAL הזה, הוא תואם למדיניות hal_client_domain(system_server, hal_foo). שרת HAL כולל באופן דומה את hal_server_domain(my_hal_domain, hal_foo). בדרך כלל, בשביל מאפיין HAL נתון, אנחנו יוצרים דומיין כמו hal_foo_default לקובצי עזר או ל-HAL לדוגמה. עם זאת, מכשירים מסוימים משתמשים בדומיינים האלה עבור השרתים שלהם. הבחנה בין דומיינים של מספר שרתים חשובה רק אם יש לנו מספר שרתים שמשרתים את אותו הממשק, ועליך להגדיר הרשאה שונה בהטמעות שלהם. בכל פקודות המאקרו האלה, hal_foo הוא לא בעצם אובייקט Sepolicy. במקום זאת, בפקודות המאקרו משתמשים באסימון הזה כדי להתייחס לקבוצת המאפיינים שמשויכת לצמד שרתי לקוח.

עם זאת, עד עכשיו לא שויכו hal_foo_service ו-hal_foo (צמד המאפיינים מ-hal_attribute(foo)). מאפיין HAL משויך לשירותי AIDL HAL באמצעות המאקרו hal_attribute_service (HIDL HALs משתמשים במאקרו hal_attribute_hwservice). לדוגמה, hal_attribute_service(hal_foo, hal_foo_service). כלומר, תהליכי hal_foo_client יכולים לקבל HAL, ותהליכים של hal_foo_server יכולים לרשום את HAL. מנהל ההקשר (servicemanager) מבצע את האכיפה של כללי הרישום האלה. שימו לב: יכול להיות ששמות השירותים לא תמיד יתאימו למאפייני HAL. לדוגמה, יכול להיות שתראו את הערך hal_attribute_service(hal_foo, hal_foo2_service). עם זאת, בדרך כלל, מדובר בשירותים שתמיד משתמשים יחד, ולכן אולי כדאי להסיר את hal_foo2_service ולהשתמש ב-hal_foo_service בכל הקשרי השירות שלנו. רוב תנאי ה-HAL שמגדירים כמה hal_attribute_service נובעים מכך ששם המאפיין המקורי של HAL לא כללי מספיק ואי אפשר לשנות אותו.

סיכום של כל המידע הזה, לדוגמה HAL, נראה כך:

    public/attributes:
    // define hal_foo, hal_foo_client, hal_foo_server
    hal_attribute(foo)

    public/service.te
    // define hal_foo_service
    type hal_foo_service, hal_service_type, protected_service, service_manager_type

    public/hal_foo.te:
    // allow binder connection from client to server
    binder_call(hal_foo_client, hal_foo_server)
    // allow client to find the service, allow server to register the service
    hal_attribute_service(hal_foo, hal_foo_service)
    // allow binder communication from server to service_manager
    binder_use(hal_foo_server)

    private/service_contexts:
    // bind an AIDL service name to the selinux type
    android.hardware.foo.IFooXxxx/default u:object_r:hal_foo_service:s0

    private/<some_domain>.te:
    // let this domain use the hal service
    binder_use(some_domain)
    hal_client_domain(some_domain, hal_foo)

    vendor/<some_hal_server_domain>.te
    // let this domain serve the hal service
    hal_server_domain(some_hal_server_domain, hal_foo)

ממשקי תוספים מצורפים

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

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

כדי להגדיר תוסף ב-binder, צריך להשתמש בממשקי ה-API הבאים:

  • בקצה העורפי של NDK: AIBinder_setExtension
  • בקצה העורפי של Java: android.os.Binder.setExtension
  • בקצה העורפי של CPP: android::Binder::setExtension
  • בקצה העורפי של Rust: binder::Binder::set_extension

כדי לקבל הארכה ב-Binder, צריך להשתמש בממשקי ה-API הבאים:

  • בקצה העורפי של NDK: AIBinder_getExtension
  • בקצה העורפי של Java: android.os.IBinder.getExtension
  • בקצה העורפי של CPP: android::IBinder::getExtension
  • בקצה העורפי של Rust: binder::Binder::get_extension

מידע נוסף על ממשקי ה-API האלה זמין במאמרי העזרה של הפונקציה getExtension בקצה העורפי המתאים. דוגמה לאופן השימוש בתוספים מופיעה במאמר hardware/interfaces/tests/extension/vibrator.

הבדלים משמעותיים ב-AIDL וב-HIDL

כשמשתמשים ב-AIDL HAL או בממשקי AIDL HAL, חשוב לשים לב להבדלים בהשוואה לכתיבה של HIDL HAL.

  • התחביר של שפת AIDL יותר קרוב ל-Java. תחביר HIDL דומה ל-C++.
  • לכל ממשקי AIDL יש סטטוסים מובנים של שגיאות. במקום ליצור סוגי סטטוסים מותאמים אישית, צריך ליצור קובצי int של סטטוס קבוע בקובצי הממשק ולהשתמש ב-EX_SERVICE_SPECIFIC בקצוות העורפיים של CPP/NDK וב-ServiceSpecificException בקצה העורפי של Java. ראו טיפול בשגיאות.
  • טכנולוגיית AIDL לא מפעילה מאגרי שרשורים באופן אוטומטי כשאובייקטים של קישור נשלחים. צריך להפעיל אותם באופן ידני (ראו ניהול שרשורים).
  • המערכת של AIDL לא מבטלת שגיאות תעבורה לא מסומנות (HIDL Return מבטלת את הסימון של שגיאות שלא נבדקו).
  • ב-AIDL אפשר להצהיר רק על סוג אחד בכל קובץ.
  • אפשר לציין ארגומנטים של AIDL גם בתור 'in/out' או 'inout' בנוסף לפרמטר הפלט (אין 'קריאות חוזרות סינכרוניות').
  • AIDL משתמש ב-fd כסוג הראשוני במקום בכינוי.
  • ב-HIDL נעשה שימוש בגרסאות ראשיות לשינויים לא תואמים, ובגרסאות משניות כדי לבצע שינויים תואמים. ב-AIDL, מתבצעים שינויים שתואמים לאחור. ב-AIDL אין מושג מפורש של גרסאות ראשיות. במקום זאת, הוא משולב בשמות של חבילות. לדוגמה, AIDL עשוי להשתמש בשם החבילה bluetooth2.
  • כברירת מחדל, מערכת AIDL לא מקבלת עדיפות בזמן אמת. כדי לאפשר ירושה של עדיפות בזמן אמת צריך להשתמש בפונקציה setInheritRt בכל קלסר.