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

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

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

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

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

מערכות build

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

מערכת 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 צריכים להכיל שדות ישירות ולא להיות הצהרות של סוגים שהוגדרו ישירות בשפות היעד. במאמר Structured לעומת stable AIDL מוסבר איך structured AIDL משתלב עם stable 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 בשיחה: &str
מחוץ לשיחה: String
android.os.Parcelable android::Parcelable לא רלוונטי לא רלוונטי
IBinder android::IBinder ndk::SpAIBinder binder::SpIBinder
T[] std::vector<T> std::vector<T> בשיחה: &[T]
מחוץ לשיחה: Vec<T>
byte[] std::vector std::vector1 בשיחה: &[u8]
מחוץ לשיחה: Vec<u8>
List<T> std::vector<T>2 std::vector<T>3 בשיחה: In: &[T]4
יצא: 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. כדי ליצור אובייקט של binder SharedRefBase, משתמשים ב-SharedRefBase::make\<My\>(... args ...). הפונקציה הזו יוצרת אובייקט std::shared_ptr\<T\>, שמנוהל גם הוא באופן פנימי, אם ה-binder הוא בבעלות של תהליך אחר. יצירת האובייקט בדרכים אחרות גורמת לבעלות כפולה.

8. אפשר לעיין גם בסוג Java או AIDL‏ byte[].

כיווניות (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 backend, אפשר לבחור אם המחרוזות יהיו בפורמט UTF-8 או UTF-16. מגדירים מחרוזות כ-@utf8InCpp String ב-AIDL כדי להמיר אותן אוטומטית ל-UTF-8. הקצה העורפי של NDK ו-Rust תמיד משתמש במחרוזות UTF-8. מידע נוסף על ההערה utf8InCpp זמין במאמר utf8InCpp.

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

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

חבילות נתונים בהתאמה אישית

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

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

    package my.pack.age;
    parcelable Foo;

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

כדי להצהיר על חבילה של CPP backend בהתאמה אישית ב-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);
    };

כדי להצהיר על אובייקט Parcelable מותאם אישית של 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, כדי להצהיר על חבילת נתונים (parcelable) מותאמת אישית של 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 backend custom parcelables כדי להשתמש בהם ב-union.

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

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

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

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

במערכות עורפיות אחרות, שדות מאותחלים עם ערכי ברירת מחדל אם לא מוגדרים ערכי ברירת מחדל. לדוגמה, ב-backend של 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.setStringField("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::makeFoo::s>tringField("<abc>")); // maketag(value)

דוגמה ל-Rust

ב-Rust, איגודים מיושמים כסוגי enum ואין להם פונקציות 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::String>Field(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 backend מופיעות בדוגמה ל-Rust AIDL בדפוסי Android Rust.

סוגי ייבוא

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

import my.package.IFoo;

או בחלק האחורי של CPP:

#include <my/package/IFoo.h>

או ב-NDK backend (שימו לב למרחב השמות הנוסף aidl):

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

או ב-backend של Rust:

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

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

הטמעה של ממשק

כדי להטמיע ממשק, צריך לבצע ירושה ממחלקת ה-stub המקורית. בדרך כלל, כשמטמיעים ממשק ורושמים אותו במנהל השירות או ב-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 backend (שימו לב למרחב השמות הנוסף 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 = ch<eckS>erviceIFoo(String16("s&ervice-name"), myService);
    // waiting until service comes up (new in Android 11)
    myServ<ice >= waitForServiceIFoo(String16("service-name"));
    // waiting for declared (VINTF) service to come up (new in Android 11)
    mySe<rvic>e = waitForDeclaredServiceIFoo(String16("service-name"));

ב-NDK backend (שימו לב למרחב השמות הנוסף 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, עם זמן ריצה של single-threaded:

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 שרשור שבו הוא יכול להריץ משימות שהופעלו. בדוגמה הבאה, ה-thread הראשי משמש למטרה הזו. כל המשימות שנוצרו באמצעות tokio::spawn מבוצעות בשרשור הראשי.

ב-backend אסינכרוני של 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.

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

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

פרטי המתקשר

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

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

בבק אנד של Java:

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

בבק אנד של CPP:

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

בבק אנד של NDK:

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

ב-backend של 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. כשעושים את זה, צריך להגביל את ה-dump להרשאה של האפליקציה android.permission.DUMP או להגביל את ה-dump למזהי משתמש ספציפיים.

בבק אנד של 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;

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

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

שימוש במצביעים חלשים

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

‫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();

קבלת מתאר ממשק באופן דינמי

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

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

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

בבק אנד של CPP:

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

היכולת הזו לא נתמכת ב-NDK וב-Rust backends.

קבלת מתאר של ממשק באופן סטטי

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

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

בבק אנד של CPP:

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

ב-NDK backend (שימו לב למרחב השמות הנוסף 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>()

בבק אנד של NDK:

   ::ndk::enum_range<MyEnum>()

בבק אנד של Rust:

    MyEnum::enum_values()

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

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

ב-Java backend, אפשר רק להגדיל את גודל ה-threadpool (כי הוא כבר התחיל):

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

שמות שמורים

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

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

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

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