קצוות עורפיים של 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.
  • תיבת הכלים של 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 (שמציינת למערכת הבנייה את נתיב הבסיס לקובצי AIDL במודול הזה) במודולים האלה בקבוצה aidl:.

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

aidl_interface

הסוגים שמשמשים עם מערכת ה-build‏ aidl_interface צריכים להיות מובנים. כדי להיות מובנים, אובייקטים מסוג Parcelable צריכים להכיל שדות ישירות ולא להיות הצהרות של סוגים שהוגדרו ישירות בשפות היעד. במאמר Structured לעומת stable AIDL מוסבר איך 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 backends תמיד נעשה שימוש במחרוזות UTF-8. מידע נוסף על ההערה utf8InCpp זמין במאמר utf8InCpp.

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

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

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

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

דוגמה להצהרה על חבילה (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, כדי להצהיר על חבילה מותאמת אישית של 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::make<Foo::stringField>("abc")); // make<tag>(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::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 כ-const int או כ-int עם גיבוי של enum, ולא מתבצע ניתוח שלהן על ידי 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 בדפוסי Rust ב-Android.

סוגי ייבוא

לא משנה אם הסוג המוגדר הוא ממשק, 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, אבל ב-CPP וב-NDK backends צריך לכלול את הכותרת של סוג הבסיס. לדוגמה, כשמייבאים סוג מקונן 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 = 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 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()
}

ב-backend אסינכרוני של Rust, עם זמן ריצה של thread יחיד:

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 שרשור שבו הוא יכול להריץ משימות שהופעלו. בדוגמה הבאה, ה-main 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 הראשי כדי שה-thread הראשי לא יהיה במצב idle. כדי לצאת מההקשר האסינכרוני, צריך להוסיף את הקריאה לתג block_in_place.

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

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

פרטי המתקשר

כשמתקבלת קריאה ל-kernel binder, פרטי המתקשר זמינים בכמה ממשקי API. מזהה התהליך (PID) מתייחס למזהה התהליך של לינוקס, של התהליך ששולח טרנזקציה. מזהה המשתמש (UI) מתייחס למזהה המשתמש ב-Linux. כשמקבלים שיחה חד-כיוונית, ה-PID של השיחה הוא 0. מחוץ להקשר של טרנזקציה של קובץ מאגד, הפונקציות האלה מחזירות את ה-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]. ב-backends של 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<()>

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

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

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

ב-CPP backend, הסוג החלש הוא wp<IFoo>.

ב-NDK backend, משתמשים ב-ScopedAIBinder_Weak:

#include <android/binder_auto_utils.h>

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

ב-Rust backend, משתמשים ב-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 לא תומכים ביכולת הזו.

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

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

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