קצוות עורפיים של 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.

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

בכל מודול של Android.bp cc_ או java_ (או במודולים המקבילים שלהם ב-Android.mk), ניתן לציין קובצי .aidl כקובצי מקור. במקרה הזה, נתוני Java/CPP נעשה שימוש בקצוות העורפיים של AIDL (לא בקצה העורפי NDK), ובמחלקות להשתמש קובצי AIDL תואמים נוספים למודול באופן אוטומטי. אפשרויות כמו local_include_dirs, שמציין למערכת ה-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 או חבילה. ב-Android 13 ומעלה, T יכול להיות כל סוג לא פרימיטיבי (כולל סוגי ממשק) מלבד מערכים. לפי המלצת AOSP משתמשים בסוגי מערכים כמו T[], כי הם פועלים בכל הקצוות העורפיים.

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

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

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

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

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

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

כשמציינים את סוגי הארגומנטים לפונקציות, אפשר לציין אותם בתור in, out או inout. ההגדרה הזו קובעת באיזה כיוון המידע לגבי הכיוון הועבר לשיחת IPC. in הוא כיוון ברירת המחדל, והוא מציין שהנתונים שהועבר מהמתקשר אל מקבל הקריאה החוזרת. המשמעות של out היא שהנתונים מועברים אל המתקשר. inout הוא שילוב של שניהם. אבל, צוות Android ממליץ להימנע משימוש בציון הארגומנטים inout. אם אתם משתמשים ב-inout עם ממשק עם גרסאות ומקבל קריאה ישן יותר, שדות נוספים שקיימים רק מבצע הקריאה החוזרת יאופסו לברירת המחדל שלהם ערכים. ביחס לחלודה, סוג 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 הצהרת parcelable נראית כך:

    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 ניסיוני), להצהרה על חלודה בהתאמה אישית אפשר לחלק ב-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 OS מספקת סוגי שגיאות מובנים לשירותים שאפשר להשתמש בהם לדיווח שגיאות. הפרטים האלה משמשים את binder, ויכול להיות שהם ישמשו את כל השירותים שמטמיעים ממשק binder. השימוש בהן מתועד היטב בהגדרת AIDL והם לא דורשים סטטוס או סוג החזרה בהגדרת המשתמש.

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

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

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

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

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

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

קוד מקורי ב-Android לא משתמש בחריגים. הקצה העורפי של CPP משתמש android::binder::Status הקצה העורפי של NDK משתמש ב-ndk::ScopedAStatus. הכול שנוצרה על ידי AIDL, מחזירה אחד מהם, שמייצג את הסטטוס של . בקצה העורפי של Rust נעשה שימוש באותם ערכי קוד חריגים כמו ה-NDK, אבל ממירה אותן לשגיאות חלודה נייטיב (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 הדף הזה.

סוגי ייבוא

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

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 הוא סוג השורש של עליך לכלול את <my/package/IFoo.h> עבור הקצה העורפי של CPP (או <aidl/my/package/IFoo.h> לקצה העורפי של NDK).

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

כדי להטמיע שירות, צריך לקבל בירושה ממחלקת ה-stub של ה-Native. הכיתה הזו קוראת פקודות ממנהל ההתקן של הקישור ומבצעת את השיטות להטמיע. נניח שיש לכם קובץ 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 אסינכרוני ובסביבת זמן ריצה עם שרשור יחיד. הדבר כי צריך לתת לטוקיו שרשור שבו הוא יכול לבצע משימות שנוצרו על ידי AI. לחשבון בדוגמה הזו, ה-thread הראשי ישרת את המטרה הזו. כל משימה שנוצרה באמצעות האירוע tokio::spawn יופעל בשרשור הראשי.

בקצה העורפי האסינכרוני של 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 מרובת-השרשורים, המשימות שנוצרות לא מבוצעות של שרשור. לכן כדאי יותר להתקשר למספר join_thread_pool כך שה-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). שימו לב שבגלל הקריאה החוזרת (callback) נמצאת בבעלות DeathRecipient. עליך להשאיר את האובייקט פעיל כל עוד שרוצים לקבל התראות.

פרטי המתקשר

כשמקבלים קריאה ל-kernel של 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 של בזמן ניפוי באגים.

כדי להוסיף פלט מותאם אישית לשירות, אפשר לבטל את 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, מאגר השרשורים לא משותף.

בקצה העורפי של 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, אפשר לשנות שמות של שדות ועדיין לשמור על תאימות לפרוטוקול; ייתכן שיהיה צורך לעדכן כדי להמשיך לבנות, אבל כל תוכנה שכבר נוצרה פעולה הדדית.

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