קצוות עורפיים של AIDL

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

בטבלה הבאה, היציבות של פלטפורמת ה-API מתייחסת ליכולת להדר קוד מול פלטפורמת ה-API הזו כך שניתן יהיה להעביר את הקוד בנפרד מהקובץ הבינארי system.img libbinder.so.

ל-AIDL יש את הקצוות העורפיים הבאים:

קצה עורפי Language פלטפורמת API פיתוח מערכות
Java Java SDK/SystemApi (יציב*) כולם
NDK C++‎ libbinder_ndk (יציב*) aidl_interface
על"ט C++‎ libbinder (לא יציב) כולם
Rust Rust libbinder_rs (stable*) aidl_interface
  • פלטפורמות ה-API האלה יציבות, אבל רבים מממשקי ה-API, כמו אלה של ניהול שירות, שמורים לשימוש פנימי בפלטפורמה והם לא זמינים לאפליקציות. מידע נוסף על השימוש ב-AIDL באפליקציות זמין במסמכי התיעוד למפתחים.
  • הקצה העורפי Rust הושק ב-Android 12. הקצה העורפי NDK זמין החל מ-Android 10.
  • ארגז החלודה מבוסס על libbinder_ndk, מה שמאפשר לו להיות יציב ונייד. APEX משתמשים בארגז הקלסרים באותו אופן כמו כל אחד אחר בצד המערכת. החלק של Rust מגיע ב-APEX והוא נשלח בתוכו. הפעולה הזו תלויה ב-libbinder_ndk.so במחיצת המערכת.

פיתוח מערכות

בהתאם לקצה העורפי, יש שתי דרכים להדר את AIDL לקוד stub. מידע נוסף על מערכות ה-build זמין בחומר העזר בנושא Soong Module.

מערכת build מרכזית

בכל מודול של Android.bp cc_ או java_ (או במודולים המקבילים ב-Android.mk), אפשר לציין קובצי .aidl כקובצי מקור. במקרה כזה, נעשה שימוש בקצוות העורפיים של Java/CPP של AIDL (ולא בקצה העורפי NDK), והמחלקות לשימוש בקובצי ה-AIDL התואמות מתווספות למודול באופן אוטומטי. אפשרויות כמו local_include_dirs, שלפיהן מערכת ה-build נותנת למערכת ה-build את הנתיב הבסיסי (root) לקובצי AIDL במודול הזה, אפשר לציין במודולים האלה תחת קבוצה aidl:. שימו לב שהקצה העורפי Rust מיועד לשימוש רק עם חלודה. הטיפול במודולים של rust_ שונה בכך שקובצי AIDL לא מוגדרים כקובצי מקור. במקום זאת, המודול aidl_interface מייצר rustlib בשם <aidl_interface name>-rust שאפשר לקשר אליו. למידע נוסף, ראו דוגמה של Rust AIDL.

aidl_interface

הסוגים שבהם נעשה שימוש במערכת ה-build הזו צריכים להיות מובנים. כדי שהחבילה תהיה מובנית, היא צריכה להכיל שדות באופן ישיר, ולא להיות הצהרות מסוגים שהוגדרו ישירות בשפות היעד. למידע על האופן שבו המבנה של AIDL משתלב עם AIDL יציב, אפשר לעיין במאמר AIDL מובנה לעומת יציב.

סוגים

אפשר להשתמש במהדר (compiler) aidl כחומר עזר להטמעת סוגים. כשיוצרים ממשק, מפעילים את aidl --lang=<backend> ... כדי לראות את קובץ הממשק שמתקבל. כשמשתמשים במודול aidl_interface, אפשר לצפות בפלט ב-out/soong/.intermediates/<path to module>/.

סוג Java/AIDL סוג C++ סוג NDK סוג החלודה
בוליאני בוליאני בוליאני בוליאני
בייט int8_t int8_t i8
תו char16_t char16_t u16
INT int32_t int32_t i32
ארוך int64_t int64_t i64
float float float f32
כפול כפול כפול f64
מחרוזת android::String16 std::string מחרוזת
android.os.Parcelable android::Parcelable לא רלוונטי לא רלוונטי
איבינדר android::IBinder ndk::SpAIBinder binder::SpIBinder
ט[] std::vector<T> std::vector<T> In: &[T]
Out: Vec<T>
בייט[] std::vector<uint8_t> std::vector<int8_t>1 In: &[u8]
Out: Vec<u8>
רשימה<T> std::vector<T>2 std::vector<T>3 In: &[T]4
חוץ: Vec<T>
FileDescriptor (תיאור קובץ) android::base::Unique_fd לא רלוונטי binder::parcel::ParcelFileDescriptor
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor
סוג הממשק (T) android::sp<T> std::shared_ptr<T>7 קלסר::חזק
סוג החבילה (T) T T T
סוג איחוד (T)5 T T T
T[N] 6 std::מערך<T, N> std::מערך<T, N> [T; לא]

1. ב-Android מגרסה 12 ואילך, מערכים של בייטים משתמשים ב-uint8_t במקום ב-int8_t מסיבות תאימות.

2. הקצה העורפי של C++ תומך ב-List<T> כאשר T הוא אחד מהערכים String, IBinder, ParcelFileDescriptor או parcelable. ב-Android מגרסה 13 ואילך, T יכול להיות כל סוג לא פרימיטיבי (כולל סוגי ממשקים) מלבד מערכים. AOSP ממליץ להשתמש בסוגי מערכים כמו T[], כי הם פועלים בכל הקצוות העורפיים.

3. הקצה העורפי NDK תומך ב-List<T> כאשר T הוא אחד מתוך String, ParcelFileDescriptor או מגרש. ב-Android מגרסה 13 ואילך, T יכול להיות כל סוג לא פרימיטיבי מלבד מערכים.

4. הסוגים מועברים בצורה שונה עבור קוד Rust בהתאם לקלט (ארגומנט) או לפלט (ערך מוחזר).

5. סוגי איחוד נתמכים ב-Android מגרסה 12 ואילך.

6. ב-Android מגרסה 13 ואילך יש תמיכה במערכים בגודל קבוע. מערכים בגודל קבוע יכולים להיות בעלי מספר מאפיינים (למשל, int[3][4]). בקצה העורפי של Java, מערכים בגודל קבוע מיוצגים כסוגי מערך.

7. כדי ליצור אובייקט SharedRefBase של binder, צריך להשתמש ב-SharedRefBase::make\<My\>(... args ...). הפונקציה הזו יוצרת אובייקט std::shared_ptr\<T\> שמנוהל גם באופן פנימי, למקרה שהקישור נמצא בבעלות תהליך אחר. יצירת האובייקט בדרכים אחרות גורמת לבעלות כפולה.

כיוון תנועה (כניסה/יציאה/יציאה)

כשמציינים את סוגי הארגומנטים לפונקציות, אפשר לציין אותם בתור in, out או inout. ההגדרה הזאת קובעת לאיזה כיוון יועבר המידע של קריאת IPC. in הוא כיוון ברירת המחדל, והוא מציין שהנתונים הועברו מהמתקשר למקבל הקריאה החוזרת. המשמעות של out היא שהנתונים מועברים מהמתקשר למתקשר. inout הוא שילוב של שניהם. עם זאת, צוות Android ממליץ להימנע משימוש בציון הארגומנטים inout. אם משתמשים ב-inout עם ממשק שיש בו גרסאות ועם מקבל קריאה ישן יותר, השדות הנוספים שקיימים רק במבצע הקריאה החוזרת מתאפסים לערכי ברירת המחדל שלהם. ביחס ל-Rust, סוג רגיל של inout מקבל &mut Vec<T>, וסוג רשימה inout מקבל &mut Vec<T>.

interface IRepeatExamples {
    MyParcelable RepeatParcelable(MyParcelable token); // implicitly 'in'
    MyParcelable RepeatParcelableWithIn(in MyParcelable token);
    void RepeatParcelableWithInAndOut(in MyParcelable param, out MyParcelable result);
    void RepeatParcelableWithInOut(inout MyParcelable param);
}

UTF8/UTF16

בקצה העורפי של CPP אפשר לבחור אם המחרוזות יהיו utf-8 או utf-16. צריך להצהיר על מחרוזות כ-@utf8InCpp String ב-AIDL כדי להמיר אותן באופן אוטומטי ל-utf-8. קצוות העורפיים של NDK ו-Rust תמיד משתמשים במחרוזות utf-8. למידע נוסף על ההערה utf8InCpp, ראו הערות ב-AIDL.

ערך אפס

אפשר להוסיף הערות לסוגים שיכולים להיות null בקצה העורפי של Java באמצעות @nullable, כדי לחשוף ערכי null לקצוות העורפיים של CPP ו-NDK. בקצה העורפי של Rust, סוגי @nullable האלה נחשפים בתור Option<T>. שרתים נייטיב דוחים ערכי null כברירת מחדל. היוצאים מן הכלל היחידים הם סוגי interface ו-IBinder, שתמיד יכולים להיות null בקריאת NDK וכתיבה ב-CPP/NDK. למידע נוסף על ההערה nullable, ראו הערות ב-AIDL.

מגרשים בהתאמה אישית

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

כדי להצהיר על מגרש בהתאמה אישית כדי ש-AIDL ידע עליו, הצהרת ה-AIDL של חכירה נראית כך:

    package my.pack.age;
    parcelable Foo;

כברירת מחדל, המדיניות הזו מצהירה על חבילת Java שבה my.pack.age.Foo הוא מחלקה של Java שמטמיעה את הממשק של Parcelable.

להצהרה על חבילת קצה עורפי בהתאמה אישית מסוג CPP ב-AIDL, צריך להשתמש ב-cpp_header:

    package my.pack.age;
    parcelable Foo cpp_header "my/pack/age/Foo.h";

הטמעת C++ ב-my/pack/age/Foo.h נראית כך:

    #include <binder/Parcelable.h>

    class MyCustomParcelable : public android::Parcelable {
    public:
        status_t writeToParcel(Parcel* parcel) const override;
        status_t readFromParcel(const Parcel* parcel) override;

        std::string toString() const;
        friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
        friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
    };

כדי להצהיר על חבילת NDK בהתאמה אישית ב-AIDL, צריך להשתמש ב-ndk_header:

    package my.pack.age;
    parcelable Foo ndk_header "android/pack/age/Foo.h";

הטמעת ה-NDK ב-android/pack/age/Foo.h נראית כך:

    #include <android/binder_parcel.h>

    class MyCustomParcelable {
    public:

        binder_status_t writeToParcel(AParcel* _Nonnull parcel) const;
        binder_status_t readFromParcel(const AParcel* _Nonnull parcel);

        std::string toString() const;

        friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
        friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
    };

ב-Android 15 (AOSP ניסיוני), אם רוצים להצהיר על חבילת Rust בהתאמה אישית ב-AIDL, משתמשים ב-rust_type:

package my.pack.age;
@RustOnlyStableParcelable parcelable Foo rust_type "rust_crate::Foo";

הטמעת חלודה ב-rust_crate/src/lib.rs נראית כך:

use binder::{
    binder_impl::{BorrowedParcel, UnstructuredParcelable},
    impl_deserialize_for_unstructured_parcelable, impl_serialize_for_unstructured_parcelable,
    StatusCode,
};

#[derive(Clone, Debug, Eq, PartialEq)]
struct Foo {
    pub bar: String,
}

impl UnstructuredParcelable for Foo {
    fn write_to_parcel(&self, parcel: &mut BorrowedParcel) -> Result<(), StatusCode> {
        parcel.write(&self.bar)?;
        Ok(())
    }

    fn from_parcel(parcel: &BorrowedParcel) -> Result<Self, StatusCode> {
        let bar = parcel.read()?;
        Ok(Self { bar })
    }
}

impl_deserialize_for_unstructured_parcelable!(Foo);
impl_serialize_for_unstructured_parcelable!(Foo);

לאחר מכן אפשר להשתמש בחבילה הזו כסוג בקובצי AIDL, אבל הוא לא ייווצר על ידי AIDL. כדי להשתמש בחבילות בהתאמה אישית בקצה העורפי של CPP/NDK, צריך לספק האופרטורים < ו-== בשביל מגרשים בהתאמה אישית ב-union.

ערכי ברירת מחדל

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

    parcelable Foo {
      int numField = 42;
      String stringField = "string value";
      char charValue = 'a';
      ...
    }

בקצה העורפי של Java כשחסרים ערכי ברירת מחדל, השדות מאותחלים כערכי אפס לסוגים פרימיטיביים ו-null לסוגים לא פרימיטיביים.

בקצוות עורפיים אחרים, השדות מופעלים עם ערכים מאותחלים כברירת מחדל כשלא מוגדרים ערכי ברירת מחדל. לדוגמה, בקצה העורפי של C++ , השדות String מאותחלים כמחרוזת ריקה, והשדות List<T> מאותחלים כ-vector<T> ריק. השדות @nullable מאותחלים כשדות null-value.

טיפול בשגיאות

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

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

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

באילו ערכי שגיאות להשתמש

אפשר להשתמש ברבים מערכי השגיאות המובנים בכל ממשק AIDL, אבל יש התייחסות מיוחדת לחלקם. לדוגמה, מותר להשתמש ב-EX_UNSUPPORTED_OPERATION וב-EX_ILLEGAL_ARGUMENT כשהם מתארים את תנאי השגיאה, אבל לא כדאי להשתמש ב-EX_TRANSACTION_FAILED כי הטיפול בהם הוא מיוחד בתשתית הבסיסית. למידע נוסף על הערכים המובְנים האלה, כדאי לעיין בהגדרות הספציפיות לקצה העורפי.

אם בממשק ה-AIDL נדרשים ערכי שגיאה נוספים שלא מכוסים בסוגי השגיאות המובְנים, יכול להיות שהם משתמשים בשגיאה המובנית המיוחדת לשירות, שמאפשרת לכלול ערך שגיאה ספציפי לשירות שהוגדר על ידי המשתמש. השגיאות שספציפיות לשירות מוגדרות בדרך כלל בממשק AIDL כ-enum עם גיבוי const int או int, ולא מנותחות על ידי קישור.

ב-Java, השגיאות ממופות לחריגות, כמו android.os.RemoteException. בחריגים ספציפיים לשירות, Java משתמש ב-android.os.ServiceSpecificException יחד עם השגיאה בהגדרת המשתמש.

קוד מקורי ב-Android לא משתמש בחריגים. הקצה העורפי של CPP משתמש ב-android::binder::Status. הקצה העורפי של NDK משתמש ב-ndk::ScopedAStatus. כל method שנוצרת על ידי AIDL מחזירה את אחד מהמאפיינים האלה, שמייצג את הסטטוס של ה-method. בקצה העורפי של Rust משתמשים באותם ערכי קוד חריגים כמו ב-NDK, אבל הם ממירים אותם לשגיאות Rust מקוריות (StatusCode, ExceptionCode) לפני ששולחים אותם למשתמש. במקרה של שגיאות ספציפיות לשירות, הערכים Status או ScopedAStatus שמוחזרים משתמשים ב-EX_SERVICE_SPECIFIC יחד עם השגיאה בהגדרת המשתמש.

סוגי השגיאות המובְנים מופיעים בקבצים הבאים:

קצה עורפי הגדרה
Java android/os/Parcel.java
על"ט binder/Status.h
NDK android/binder_status.h
Rust android/binder_status.h

שימוש בקצוות עורפיים שונים

ההוראות האלה ספציפיות לקוד הפלטפורמה של Android. בדוגמאות האלה נעשה שימוש בסוג מוגדר, my.package.IFoo. להוראות איך להשתמש בקצה העורפי Rust, ראו דוגמה ל-Rust AIDL בדף Android Rust templates.

סוגי ייבוא

אפשר לייבא אותו ב-Java גם אם הסוג שהוגדר הוא ממשק, parcelable או איחוד:

import my.package.IFoo;

או בקצה העורפי של הקישור לדף מוצר ב-CSS:

#include <my/package/IFoo.h>

או בקצה העורפי של NDK (שימו לב למרחב השמות הנוסף aidl):

#include <aidl/my/package/IFoo.h>

או בקצה העורפי של Rust:

use my_package::aidl::my::package::IFoo;

למרות שאפשר לייבא סוג מקונן ב-Java, בקצוות העורפיים של CPP/NDK צריך לכלול את הכותרת עבור סוג הרמה הבסיסית (root). לדוגמה, כשמייבאים סוג מקונן Bar שמוגדר ב-my/package/IFoo.aidl (IFoo הוא סוג הרמה הבסיסית (root) של הקובץ), צריך לכלול את <my/package/IFoo.h> בקצה העורפי של CPP (או <aidl/my/package/IFoo.h> בשביל הקצה העורפי של NDK).

הטמעת שירותים

כדי להטמיע שירות, אתם צריכים לקבל בירושה ממחלקת ה-stub המקורית. הכיתה הזו קוראת פקודות מהמנהל של מאגר התגים ומריצה את השיטות שמטמיעים. נניח שיש לכם קובץ AIDL כזה:

    package my.package;
    interface IFoo {
        int doFoo();
    }

ב-Java, עליך להרחיב מהכיתה הזו:

    import my.package.IFoo;
    public class MyFoo extends IFoo.Stub {
        @Override
        int doFoo() { ... }
    }

בקצה העורפי של CPP:

    #include <my/package/BnFoo.h>
    class MyFoo : public my::package::BnFoo {
        android::binder::Status doFoo(int32_t* out) override;
    }

בקצה העורפי של NDK (שימו לב למרחב השמות הנוסף aidl):

    #include <aidl/my/package/BnFoo.h>
    class MyFoo : public aidl::my::package::BnFoo {
        ndk::ScopedAStatus doFoo(int32_t* out) override;
    }

בקצה העורפי של Rust:

    use aidl_interface_name::aidl::my::package::IFoo::{BnFoo, IFoo};
    use binder;

    /// This struct is defined to implement IRemoteService AIDL interface.
    pub struct MyFoo;

    impl Interface for MyFoo {}

    impl IFoo for MyFoo {
        fn doFoo(&self) -> binder::Result<()> {
           ...
           Ok(())
        }
    }

או באמצעות חלודה אסינכרונית:

    use aidl_interface_name::aidl::my::package::IFoo::{BnFoo, IFooAsyncServer};
    use binder;

    /// This struct is defined to implement IRemoteService AIDL interface.
    pub struct MyFoo;

    impl Interface for MyFoo {}

    #[async_trait]
    impl IFooAsyncServer for MyFoo {
        async fn doFoo(&self) -> binder::Result<()> {
           ...
           Ok(())
        }
    }

הרשמה וקבלת שירותים

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

ב-Java:

    import android.os.ServiceManager;
    // registering
    ServiceManager.addService("service-name", myService);
    // return if service is started now
    myService = IFoo.Stub.asInterface(ServiceManager.checkService("service-name"));
    // waiting until service comes up (new in Android 11)
    myService = IFoo.Stub.asInterface(ServiceManager.waitForService("service-name"));
    // waiting for declared (VINTF) service to come up (new in Android 11)
    myService = IFoo.Stub.asInterface(ServiceManager.waitForDeclaredService("service-name"));

בקצה העורפי של CPP:

    #include <binder/IServiceManager.h>
    // registering
    defaultServiceManager()->addService(String16("service-name"), myService);
    // return if service is started now
    status_t err = checkService<IFoo>(String16("service-name"), &myService);
    // waiting until service comes up (new in Android 11)
    myService = waitForService<IFoo>(String16("service-name"));
    // waiting for declared (VINTF) service to come up (new in Android 11)
    myService = waitForDeclaredService<IFoo>(String16("service-name"));

בקצה העורפי של NDK (שימו לב למרחב השמות הנוסף aidl):

    #include <android/binder_manager.h>
    // registering
    binder_exception_t err = AServiceManager_addService(myService->asBinder().get(), "service-name");
    // return if service is started now
    myService = IFoo::fromBinder(ndk::SpAIBinder(AServiceManager_checkService("service-name")));
    // is a service declared in the VINTF manifest
    // VINTF services have the type in the interface instance name.
    bool isDeclared = AServiceManager_isDeclared("android.hardware.light.ILights/default");
    // wait until a service is available (if isDeclared or you know it's available)
    myService = IFoo::fromBinder(ndk::SpAIBinder(AServiceManager_waitForService("service-name")));

בקצה העורפי של Rust:

use myfoo::MyFoo;
use binder;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_binder(
        my_service,
        BinderFeatures::default(),
    );
    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
    // Does not return - spawn or perform any work you mean to do before this call.
    binder::ProcessState::join_thread_pool()
}

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

use myfoo::MyFoo;
use binder;
use binder_tokio::TokioRuntime;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");

    // Sleeps forever, but does not join the binder threadpool.
    // Spawned tasks will run on this thread.
    std::future::pending().await
}

הבדל חשוב בין האפשרויות האחרות הוא שאנחנו לא קוראים לפונקציה join_thread_pool כשמשתמשים ב-Rust אסינכרוני ובסביבת זמן ריצה עם שרשור יחיד. הסיבה לכך היא שצריך לתת לטוקיו שרשור שבו הוא יכול לבצע משימות שנוצרו במקור. בדוגמה הזו, ה-thread הראשי ישרת את המטרה הזו. כל המשימות שייווצרו באמצעות tokio::spawn יבוצעו ב-thread הראשי.

בקצה העורפי האסינכרוני של Rust, עם סביבת זמן ריצה מרובת שרשורים:

use myfoo::MyFoo;
use binder;
use binder_tokio::TokioRuntime;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");

    // Sleep forever.
    tokio::task::block_in_place(|| {
        binder::ProcessState::join_thread_pool();
    });
}

בסביבת זמן הריצה של Tokio מרובת-השרשורים, המשימות שנוצרות לא מבוצעות ב-thread הראשי. לכן כדאי לקרוא ל-join_thread_pool ב-thread הראשי כדי שה-thread הראשי לא יהיה רק פעיל. כדי לצאת מההקשר האסינכרוני, צריך לתחום את הקריאה לפעולה ב-block_in_place.

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

  • ב-Java, צריך להשתמש ב-android.os.IBinder::linkToDeath.
  • בקצה העורפי של CPP, משתמשים ב-android::IBinder::linkToDeath.
  • בקצה העורפי של NDK, צריך להשתמש ב-AIBinder_linkToDeath.
  • בקצה העורפי של Rust, יוצרים אובייקט DeathRecipient ואז קוראים ל-my_binder.link_to_death(&mut my_death_recipient). חשוב לשים לב שמכיוון שהקריאה החוזרת היא בבעלות DeathRecipient, צריך להשאיר את האובייקט פעיל כל עוד רוצים לקבל התראות.

פרטי המתקשר

כשמקבלים קריאה ל-kernele binder, פרטי המתקשר זמינים בחלק מממשקי ה-API. ה-PID (או מזהה התהליך) מתייחס למזהה התהליך ב-Linux של התהליך ששולח טרנזקציה. ה-UID (או User ID) מתייחס למזהה המשתמש של Linux. כשמקבלים שיחה חד-כיוונית, ה-PID של השיחה הוא 0. מחוץ להקשר של טרנזקציה של קישור, הפונקציות האלה מחזירות את ה-PID וה-UID של התהליך הנוכחי.

בקצה העורפי של Java:

    ... = Binder.getCallingPid();
    ... = Binder.getCallingUid();

בקצה העורפי של CPP:

    ... = IPCThreadState::self()->getCallingPid();
    ... = IPCThreadState::self()->getCallingUid();

בקצה העורפי של NDK:

    ... = AIBinder_getCallingPid();
    ... = AIBinder_getCallingUid();

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

    ... = ThreadState::get_calling_pid();
    ... = ThreadState::get_calling_uid();

API לדוחות באגים וניפוי באגים בשירותים

כשדוחות על באגים מריצים (לדוגמה, באמצעות adb bugreport), הם אוספים מידע מכל המערכת כדי לעזור בניפוי באגים בבעיות שונות. לשירותי AIDL, דוחות על באגים משתמשים בקובץ הבינארי dumpsys בכל השירותים שנרשמו במנהל השירות, כדי להוסיף את המידע שלהם לדוח הבאג. אתם יכולים גם להשתמש ב-dumpsys בשורת הפקודה כדי לקבל מידע משירות באמצעות dumpsys SERVICE [ARGS]. בקצה העורפי של C++ ו-Java, אפשר לקבוע את הסדר של העברת השירותים על ידי הוספת ארגומנטים ל-addService. אפשר גם להשתמש ב-dumpsys --pid SERVICE כדי לקבל את ה-PID של שירות בזמן ניפוי באגים.

כדי להוסיף פלט מותאם אישית לשירות, אתם יכולים לשנות את ה-method dump באובייקט השרת כמו שמטמיעים כל שיטת IPC אחרת שמוגדרת בקובץ AIDL. כשעושים זאת, צריך להגביל את ההעלאה להרשאת האפליקציה android.permission.DUMP או להגביל את ההעלאה למזהי UID ספציפיים.

בקצה העורפי של Java:

    @Override
    protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
        @Nullable String[] args) {...}

בקצה העורפי של CPP:

    status_t dump(int, const android::android::Vector<android::String16>&) override;

בקצה העורפי של NDK:

    binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;

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

    fn dump(&self, mut file: &File, args: &[&CStr]) -> binder::Result<()>

קבלה דינמית של מתאר ממשק

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

ב-Java, אפשר לקבל את מתאר הממשק עם קוד כמו:

    service = /* get ahold of service object */
    ... = service.asBinder().getInterfaceDescriptor();

בקצה העורפי של CPP:

    service = /* get ahold of service object */
    ... = IInterface::asBinder(service)->getInterfaceDescriptor();

קצוות העורפי NDK ו-Rust לא תומכים ביכולת הזו.

קבלה סטטית של מתאר ממשק

לפעמים (למשל כשרושמים שירותי @VintfStability), צריך לדעת מהו מתאר הממשק באופן סטטי. ב-Java אפשר לקבל את המתאר על ידי הוספת קוד כמו:

    import my.package.IFoo;
    ... IFoo.DESCRIPTOR

בקצה העורפי של CPP:

    #include <my/package/BnFoo.h>
    ... my::package::BnFoo::descriptor

בקצה העורפי של NDK (שימו לב למרחב השמות הנוסף aidl):

    #include <aidl/my/package/BnFoo.h>
    ... aidl::my::package::BnFoo::descriptor

בקצה העורפי של Rust:

    aidl::my::package::BnFoo::get_descriptor()

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

בקצוות עורפיים מקוריים אפשר לשנות את הערכים האפשריים של enum. בגלל שיקולים בקשר לגודל הקוד, האפשרות הזו לא נתמכת ב-Java.

ל-MyEnum של enum שמוגדר ב-AIDL, האיטרציה מסופקת באופן הבא.

בקצה העורפי של CPP:

    ::android::enum_range<MyEnum>()

בקצה העורפי של NDK:

   ::ndk::enum_range<MyEnum>()

בקצה העורפי של Rust:

    MyEnum::enum_values()

ניהול שרשורים

כל מופע של libbinder בתהליך שומר מאגר אחד של שרשורים. ברוב המקרים, מדובר במאגר שרשורים אחד בלבד שמשותף לכל הקצוות העורפיים. יוצא הדופן היחיד הוא כאשר קוד הספק עשוי לטעון עותק נוסף של libbinder כדי לדבר עם /dev/vndbinder. מכיוון שהוא נמצא בצומת binder נפרד, מאגר ה-threads לא משותף.

בקצה העורפי של Java, מאגר ה-threads יכול לגדול רק כי הוא כבר התחיל):

    BinderInternal.setMaxThreads(<new larger value>);

עבור הקצה העורפי של CPP, הפעולות הבאות זמינות:

    // set max threadpool count (default is 15)
    status_t err = ProcessState::self()->setThreadPoolMaxThreadCount(numThreads);
    // create threadpool
    ProcessState::self()->startThreadPool();
    // add current thread to threadpool (adds thread to max thread count)
    IPCThreadState::self()->joinThreadPool();

באופן דומה, בקצה העורפי של NDK:

    bool success = ABinderProcess_setThreadPoolMaxThreadCount(numThreads);
    ABinderProcess_startThreadPool();
    ABinderProcess_joinThreadPool();

בקצה העורפי של Rust:

    binder::ProcessState::start_thread_pool();
    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
    binder::ProcessState::join_thread_pool();

בקצה העורפי האסינכרוני של Rust צריך להשתמש בשני מאגרי שרשורים: קלסר ו-Tokio. כלומר, אפליקציות שמשתמשות ב-Rust אסינכרוני צריכות שיקולים מיוחדים, במיוחד כשמשתמשים ב-join_thread_pool. למידע נוסף בנושא, ראו קטע על רישום שירותים.

שמות שמורים

C++ , Java ו-Rust שומרים חלק מהשמות כמילות מפתח או לשימוש ספציפי בשפה. AIDL לא אוכף הגבלות על סמך כללי שפה, אבל שימוש בשמות שדות או סוגים שתואמים לשם שמור עלול לגרום לכשל בהידור ב-C++ או ב-Java. ב-Rust, שם השדה או הסוג משתנה באמצעות התחביר 'מזהה גולמי', שאפשר לגשת אליו באמצעות הקידומת r#.

מומלץ להימנע ככל האפשר משימוש בשמות שמורים בהגדרות AIDL, כדי למנוע קישורים לא ארגונומיים או כשל בהידור באופן מיידי.

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

שמות שיש להימנע מהם: