קצה עורפי של 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 (stable*) | aidl_interface |
- ממשקי ה-API האלה יציבים, אבל הרבה ממשקי API, כמו אלה לניהול שירותים, שמורים לשימוש פנימי בפלטפורמה ולא זמינים לאפליקציות. לקבלת מידע נוסף על השימוש ב-AIDL באפליקציות, אפשר לעיין במאמר מסמכי תיעוד למפתחים.
- הקצה העורפי של Rust הושק ב-Android 12. ה הקצה העורפי NDK זמין החל מ-Android 10.
- ארגז החלודה מבוסס על
libbinder_ndk
, מה שמאפשר לו יציב ונייד. מודולי APEX משתמשים ב-binder crate באותו אופן שבו משתמשים בו כל הגורמים האחרים בצד המערכת. החלק של 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 את נתיב הבסיס לקובצי AIDL באותו מודול, בקבוצה aidl:
. חשוב לזכור שאפשר להשתמש בקצה העורפי של Rust רק עם Rust. הטיפול במודולים מסוג rust_
שונה, כי קובצי AIDL לא מצוינים כקובצי מקור.
במקום זאת, המודול aidl_interface
יוצר rustlib
שנקרא <aidl_interface name>-rust
שאפשר לקשר אליו. למידע נוסף, ראו הדוגמה ל-Rust AIDL.
aidl_interface
הסוגים שנעשה בהם שימוש במערכת ה-build הזו חייבים להיות מובְנים. כדי להיות מובנים, רכיבים שניתן לחלק צריכים להכיל שדות ישירות ולא להיות הצהרות על סוגים שמוגדרים ישירות בשפות היעד. האופן שבו AIDL מובנה משתלב ל-AIDL יציבות, אפשר לעיין במאמר מערכת AIDL מובנית לעומת יציבה.
סוגים
אפשר להתייחס למהדר aidl
כאל הטמעת עזר של סוגים.
כשיוצרים ממשק, צריך להפעיל את aidl --lang=<backend> ...
כדי לראות
את קובץ הממשק שנוצר. כשמשתמשים במודול aidl_interface
, אפשר לראות את הפלט ב-out/soong/.intermediates/<path to module>/
.
סוג Java/AIDL | סוג C++ | סוג NDK | סוג החלודה |
---|---|---|---|
בוליאני | בוליאני | בוליאני | בוליאני |
בייט | int8_t | int8_t | i8 |
char | 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 | לא רלוונטי | לא רלוונטי |
IBinder | android::IBinder | ndk::SpAIBinder | binder::SpIBinder |
T[] | std::vector<T> | std::vector<T> | In: &[T] Out: Vec<T> |
byte[] | std::vector<uint8_t> | std::vector<int8_t>1 | In: &[u8] חוץ: Vec<u8> |
List<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 | קלסר::חזק |
סוג 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
או חבילה. ב-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
של binder, צריך להשתמש ב-
SharedRefBase::make\<My\>(... args ...)
. הפונקציה הזו יוצרת אובייקט std::shared_ptr\<T\>
שמנוהל גם הוא באופן פנימי, למקרה שהמַאגר הוא בבעלות של תהליך אחר. יצירת האובייקט בדרכים אחרות גורמת לבעלות כפולה.
כיוון (in/out/inout)
כשמציינים את סוגי הארגומנטים לפונקציות, אפשר לציין אותם בתור 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.
מאפיין המציין אם ערך יכול להיות ריק (nullability)
ב-@nullable
אפשר להוסיף הערות לסוגים שיכולים להיות null.
למידע נוסף על ההערה nullable
, ראו הערות ב-AIDL.
פריטים מותאמים אישית לחלוקה
Parcelable בהתאמה אישית הוא parcelable שמוטמע באופן ידני בקצה העורפי של היעד. יש להשתמש במגרשים בהתאמה אישית רק כשמנסים להוסיף תמיכה לאחרים של חבילה מותאמת אישית שכבר קיימת, ואי אפשר לשנות אותם.
כדי להצהיר על מגרש בהתאמה אישית כדי ש-AIDL ידע עליו, ה-AIDL הצהרת parcelable נראית כך:
package my.pack.age;
parcelable Foo;
כברירת מחדל, המדיניות הזו מצהירה על חבילת Java שבה 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. מספקים אופרטורים <
ו-==
ל-parcelables בהתאמה אישית לקצה העורפי של CPP/NDK כדי להשתמש בהם ב-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-value.
טיפול בשגיאות
מערכת ההפעלה Android מספקת סוגי שגיאות מובְנים לשירותים שאפשר להשתמש בהם בדיווח על שגיאות. הפרטים האלה משמשים את binder, ויכול להיות שהם ישמשו את כל השירותים שמטמיעים ממשק binder. השימוש בהן מתועד היטב בהגדרת AIDL והם לא דורשים סטטוס או סוג החזרה בהגדרת המשתמש.
פרמטרים של פלט עם שגיאות
כשפונקציית AIDL מדווחת על שגיאה, יכול להיות שהפונקציה לא תופעל או
לשנות פרמטרים של פלט. באופן ספציפי, יכול להיות שפרמטרים של פלט ישתנו אם השגיאה מתרחשת במהלך ביטול האריזות, ולא במהלך עיבוד העסקה עצמה. באופן כללי, כשמקבלים שגיאה מ-AIDL
הפונקציה, את כל הפרמטרים inout
ו-out
וגם את הערך המוחזר (
פועל כמו פרמטר out
בקצוות עורפיים מסוימים)
מצב בלתי מוגבל.
באילו ערכי שגיאות להשתמש
אפשר להשתמש ברבים מערכי השגיאה המובנים בכל ממשק AIDL, אבל יש
מטופלים באופן מיוחד. לדוגמה, EX_UNSUPPORTED_OPERATION
וגם
אפשר להשתמש ב-EX_ILLEGAL_ARGUMENT
כשהם מתארים את מצב השגיאה, אבל
אין להשתמש ב-EX_TRANSACTION_FAILED
מפני שהוא נחשב למיוחד על ידי
בתשתית הבסיסית. מידע נוסף זמין בהגדרות הספציפיות לקצה העורפי
על הערכים המובְנים האלה.
אם בממשק AIDL נדרשים ערכי שגיאה נוספים שלא נכללים ב
סוגי השגיאות המובנות, יכול להיות שהם ישתמשו בשגיאות
שמאפשרת לכלול ערך שגיאה ספציפי לשירות,
מוגדר על ידי המשתמש. שגיאות אלה שספציפיות לשירות מוגדרות בדרך כלל
ממשק AIDL בתור enum
עם גיבוי const int
או int
, שלא מנותחים על ידי
ב-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
בדפוסי החלודה של 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 צריך
כוללים את הכותרת עבור סוג הרמה הבסיסית (root). לדוגמה, כשמייבאים סוג בתצוגת עץ Bar
שמוגדר ב-my/package/IFoo.aidl
(IFoo
הוא סוג הבסיס של הקובץ), צריך לכלול את <my/package/IFoo.h>
לקצה העורפי של CPP (או את <aidl/my/package/IFoo.h>
לקצה העורפי של NDK).
הטמעת שירותים
כדי להטמיע שירות, צריך לרשת מהכיתה הילידים של הסטאב. הכיתה הזו קוראת פקודות ממנהל ההתקן של הקישור ומבצעת את השיטות להטמיע. נניח שיש לכם קובץ 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(())
}
}
או באמצעות 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 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)
. חשוב לזכור של-DeathRecipient
יש בעלות על פונקציית ה-callback, ולכן צריך לשמור על האובייקט הזה בחיים כל עוד רוצים לקבל התראות.
פרטי המתקשר
כשמקבלים קריאה ל-kernel של binder, פרטי המתקשר זמינים בכמה ממשקי API. ה-PID (או מזהה התהליך) מתייחס למזהה התהליך ב-Linux של התהליך ששולח את העסקה. UID (או User ID) מתייחס מזהה משתמש של Linux. כשמקבלים שיחה חד-כיוונית, הערך של PID של מבצע הקריאה הוא 0. כשהן לא נמצאות בהקשר של עסקה ב-binder, הפונקציות האלה מחזירות את ה-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, אפשר לקבוע את הסדר שבו השירותים יועברו ל-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;
בקצה העורפי של 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
, אבל היא לא תומכת בהפניות חלשים של binder
בשכבה המקורית.
בקצה העורפי של 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.
ל-MyEnum
של enum שמוגדר ב-AIDL, האיטרציה מסופקת באופן הבא.
בקצה העורפי של CPP:
::android::enum_range<MyEnum>()
ב-back-end של 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, אפשר לשנות שמות של שדות ועדיין לשמור על תאימות לפרוטוקול; ייתכן שיהיה צורך לעדכן כדי להמשיך לבנות, אבל כל תוכנה שכבר נוצרה פעולה הדדית.
שמות שיש להימנע מהם: