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

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

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

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

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

מערכות build

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

מערכת build ליבה

בכל קובץ cc_ או java_ Android.bp module (או בקובצי Android.mk המקבילים), אפשר לציין קבצי AIDL (.aidl) כקובצי מקור. במקרה כזה, נעשה שימוש בקצוות העורפי של Java או CPP ב-AIDL (לא הקצה העורפי של NDK), והכיתות לשימוש בקובצי ה-AIDL התואמים מתווספות למודול באופן אוטומטי. אפשר לציין אפשרויות כמו local_include_dirs (שמציינת למערכת ה-build את נתיב השורש לקובצי AIDL במודול הזה) במודולים האלה בקבוצה aidl:.

הקצה העורפי של Rust מיועד לשימוש רק עם Rust. הטיפול במודולים מסוג rust_ שונה, כי קובצי AIDL לא מצוינים כקובצי מקור. במקום זאת, המודול aidl_interface יוצר rustlib שנקרא aidl_interface_name-rust, שאפשר לקשר אליו. לפרטים נוספים, ראו הדוגמה ל-Rust AIDL.

aidl_interface

הסוגים שנעשה בהם שימוש במערכת ה-build של aidl_interface חייבים להיות מובְנים. כדי שהם יהיו מובְנים, אובייקטים מסוג Parcelable חייבים להכיל שדות ישירות ולא להיות הצהרות על סוגים שהוגדרו ישירות בשפות היעד. במאמר AIDL מובנה לעומת AIDL יציב מוסבר איך AIDL מובנה משתלב עם AIDL יציב.

סוגים

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

סוג Java או AIDL טיפוס C++‎ סוג NDK סוג החלודה
boolean bool bool bool
byte8 int8_t int8_t i8
char char16_t char16_t u16
int int32_t int32_t i32
long int64_t int64_t i64
float float float f32
double double double f64
String android::String16 std::string In: &str
Out: String
android.os.Parcelable android::Parcelable לא רלוונטי לא רלוונטי
IBinder android::IBinder ndk::SpAIBinder binder::SpIBinder
T[] std::vector<T> std::vector<T> In: &[T]
Out: Vec<T>
byte[] std::vector std::vector1 In: &[u8]
Out: Vec<u8>
List<T> std::vector<T>2 std::vector<T>3 In: In: &[T]4
Out: Vec<T>
FileDescriptor android::base::unique_fd לא רלוונטי לא רלוונטי
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor
סוג הממשק (T) android::sp<T> std::shared_ptr<T>7 binder::Strong
סוג Parcelable (T) T T T
סוג איחוד (T)5 T T T
T[N]6 std::array<T, N> std::array<T, N> [T; N]

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 או parcelable. ב-Android 13 ואילך, T יכול להיות כל סוג שאינו פרימיטיבי, מלבד מערכי נתונים.

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

5. יש תמיכה בסוגי יוניון ב-Android מגרסה 12 ואילך.

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

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

8. מידע נוסף זמין בנושא סוג byte[] ב-Java או ב-AIDL.

כיוון (in,‏ out ו-inout)

כשמציינים את סוגי הארגומנטים לפונקציות, אפשר לציין אותם בתור in,‏ out או inout. ההגדרה הזו קובעת את הכיוון שבו המידע מועבר בקריאה ל-IPC.

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

  • משמעותו של ספציפי הארגומנט out היא שהנתונים מועברים מהגורם שנקרא לגורם הקורא.

  • ספציפי הארגומנט inout הוא השילוב של שניהם. עם זאת, מומלץ להימנע משימוש במפריד הארגומנט inout. אם משתמשים ב-inout עם ממשק בגרסה קודמת ועם צד נמען ישן יותר, השדות הנוספים שנמצאים רק בצד הקורא יאופסו לערכים שמוגדרים כברירת מחדל. ב-Rust, טיפוס inout רגיל מקבל את הערך &mut 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);
}

UTF-8 ו-UTF-16

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

מאפיין המציין אם ערך יכול להיות ריק (nullability)

אפשר להוסיף הערות לסוגים שיכולים להיות null באמצעות @nullable. למידע נוסף על ההערה nullable, ראו nullable.

פריטים מותאמים אישית לחלוקה

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

דוגמה להצהרה על Parcelable ב-AIDL:

    package my.pack.age;
    parcelable Foo;

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

כדי להצהיר על צד לקוח (back-end) בהתאמה אישית של 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, כדי להצהיר על רכיב Rust בהתאמה אישית שאפשר לשלוח ב-AIDL, צריך להשתמש ב-rust_type:

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

ההטמעה של Rust ב-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);

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

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

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

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

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

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

איגודים

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

    union Foo {
      int intField;
      long longField;
      String stringField;
      MyParcelable parcelableField;
      ...
    }

דוגמה ל-Java

    Foo u = Foo.intField(42);              // construct

    if (u.getTag() == Foo.intField) {      // tag query
      // use u.getIntField()               // getter
    }

    u.setSringField("abc");                // setter

דוגמה ל-C++‎ ו-NDK

    Foo u;                                            // default constructor

    assert (u.getTag() == Foo::intField);             // tag query
    assert (u.get<Foo::intField>() == 0);             // getter

    u.set<Foo::stringField>("abc");                   // setter

    assert (u == Foo::make<Foo::stringField>("abc")); // make<tag>(value)

דוגמה ל-Rust

ב-Rust, איחודים מיושמים בתור enums ואין להם פונקציות getter ו-setter מפורשות.

    let mut u = Foo::Default();              // default constructor
    match u {                                // tag match + get
      Foo::IntField(x) => assert!(x == 0);
      Foo::LongField(x) => panic!("Default constructed to first field");
      Foo::StringField(x) => panic!("Default constructed to first field");
      Foo::ParcelableField(x) => panic!("Default constructed to first field");
      ...
    }
    u = Foo::StringField("abc".to_string()); // set

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

מערכת ההפעלה Android מספקת סוגי שגיאות מובְנים לשירותים שאפשר להשתמש בהם בדיווח על שגיאות. אלה משמשים את ה-binders, וניתן להשתמש בהם בכל שירות שמטמיע ממשק של 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, אבל ממיר אותם לשגיאות מקוריות של 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 בדוגמאות ל-Rust ב-Android.

סוגי ייבוא

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

import my.package.IFoo;

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

#include <my/package/IFoo.h>

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

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

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

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

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

הטמעת ממשק

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

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

ב-Java, צריך להרחיב את הכיתה Stub שנוצרה:

    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(())
        }
    }

או באמצעות Rust אסינכרוני:

    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 run on this thread.
    std::future::pending().await
}

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

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

פרטי המתקשר

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

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

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

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

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

ב-back-end של 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, אפשר לקבוע את הסדר שבו השירותים יועברו ל-dump באמצעות ארגומנטים נוספים ל-addService. אפשר גם להשתמש ב-dumpsys --pid SERVICE כדי לקבל את ה-PID של שירות בזמן ניפוי באגים.

כדי להוסיף פלט מותאם אישית לשירות, משנים את השיטה dump באובייקט השרת, כמו שמטמיעים כל שיטה אחרת של IPC שמוגדרת בקובץ AIDL. כשעושים זאת, צריך להגביל את הטמעת הנתונים (dumping) להרשאת האפליקציה 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;

ב-back-end של 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<()>

שימוש ב-weak pointers

אפשר להחזיק הפניה חלשה לאובייקט של מקשר.

Java תומכת ב-WeakReference, אבל היא לא תומכת בהפניות של קישורים חלשים בשכבה המקורית.

בקצה העורפי של CPP, הסוג החלש הוא wp<IFoo>.

בקצה העורפי של NDK, משתמשים ב-ScopedAIBinder_Weak:

#include <android/binder_auto_utils.h>

AIBinder* binder = ...;
ScopedAIBinder_Weak myWeakReference = ScopedAIBinder_Weak(AIBinder_Weak_new(binder));

בקצה העורפי של Rust, משתמשים ב-WpIBinder או ב-Weak<IFoo>:

let weak_interface = myIface.downgrade();
let weak_binder = myIface.as_binder().downgrade();

אחזור דינמי של מתאר ממשק

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

ב-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.

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

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

    ::android::enum_range<MyEnum>()

ב-back-end של NDK:

   ::ndk::enum_range<MyEnum>()

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

    MyEnum::enum_values()

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

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

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

    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 עם תמיכה ב-async, צריך שני מאגרי חוטים: binder ו-Tokio. כלומר, אפליקציות שמשתמשות ב-Rust אסינכרוני צריכות להביא בחשבון שיקולים מיוחדים, במיוחד כשמדובר בשימוש ב-join_thread_pool. מידע נוסף זמין בקטע בנושא רישום שירותים.

שמות שמורים

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

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

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

שמות שכדאי להימנע מהם: