קצה עורפי של 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 באפליקציות זמין במסמכי התיעוד למפתחים.
- הקצה העורפי של Rust הושק ב-Android 12, והקצה העורפי של NDK זמין החל מ-Android 10.
- גרסת Rust נוצרה על גבי
libbinder_ndk
, מה שמאפשר לה להיות יציבה וניתנת לניוד. מודעות APEX משתמשות ב-binder crate באותו אופן שבו משתמשים בו כל הגורמים האחרים בצד המערכת. החלק של Rust מקובץ ב-APEX ונשלח בתוכו. זה תלוי ב-libbinder_ndk.so
במחיצה של המערכת.
מערכות build
בהתאם לקצה העורפי, יש שתי דרכים לקמפל את 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 הזו חייבים להיות מובְנים. כדי להיות מובנים, רכיבים שניתן לחלק חייבים להכיל שדות ישירות ולא להיות הצהרות על סוגים שמוגדרים ישירות בשפות היעד. במאמר Structured versus stable AIDL מוסבר איך AIDL מובנה משתלב עם AIDL יציב.
סוגים
אפשר להתייחס למהדר aidl
כאל הטמעת עזר של סוגים.
כשיוצרים ממשק, מפעילים את aidl --lang=<backend> ...
כדי לראות את קובץ הממשק שנוצר. כשמשתמשים במודול aidl_interface
, אפשר לראות את הפלט ב-out/soong/.intermediates/<path to module>/
.
סוג Java/AIDL | סוג C++ | סוג NDK | סוג חלודה |
---|---|---|---|
בוליאני | bool | bool | bool |
byte8 | 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 | קלט: &str פלט: מחרוזת |
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] Out: Vec<u8> |
List<T> | std::vector<T>2 | std::vector<T>3 | 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. אפשר לעיין גם ב-Java/AIDL type byte[]
.
כיוון (in/out/inout)
כשמציינים את סוגי הארגומנטים לפונקציות, אפשר לציין אותם בתור in
, out
או inout
. ההגדרה הזו קובעת באיזה כיוון המידע מועבר בקריאה ל-IPC. in
הוא כיוון ברירת המחדל, והוא מציין שהנתונים מועברים מהמבצע של הקריאה (caller) לגורם שאליו הופנתה הקריאה (callee). out
מציין שהנתונים מועברים מהגורם שנקרא לגורם שקורא. inout
הוא השילוב של שתיהן. עם זאת, צוות Android ממליץ להימנע משימוש במפריד הארגומנט inout
.
אם משתמשים ב-inout
עם ממשק בגרסה מסוימת ובנמען קריאה ישן יותר, השדות הנוספים שנמצאים רק בקריאה יאופסו לערכי ברירת המחדל שלהם. ב-Rust, סוג 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. כדי להמיר אותן באופן אוטומטי ל-utf-8, צריך להצהיר על מחרוזות כ-@utf8InCpp String
ב-AIDL.
הקצוות העורפיים של NDK ו-Rust תמיד משתמשים במחרוזות UTF-8. למידע נוסף על ההערה utf8InCpp
, ראו הערות ב-AIDL.
מאפיין המציין אם ערך יכול להיות ריק (nullability)
אפשר להוסיף הערות לסוגים שיכולים להיות null באמצעות @nullable
.
מידע נוסף על ההערה nullable
זמין במאמר הערות ב-AIDL.
פריטים מותאמים אישית לחלוקה
Parcelable בהתאמה אישית הוא parcelable שמוטמע באופן ידני בקצה העורפי של היעד. מומלץ להשתמש ב-Parcelable בהתאמה אישית רק כשמנסים להוסיף תמיכה בשפות אחרות ל-Parcelable בהתאמה אישית קיים שלא ניתן לשנות.
כדי להצהיר על רכיב parcelable בהתאמה אישית כך ש-AIDL ידע עליו, ההצהרה על הרכיב ב-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. מספקים אופרטורים <
ו-==
ל-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.
איגודים
איחודים של 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 מספקת סוגי שגיאות מובְנים לשירותים שאפשר להשתמש בהם כשמדווחים על שגיאות. השירותים שמטמיעים ממשק של 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 Rust Patterns.
סוגי ייבוא
בין שהסוג המוגדר הוא ממשק, אובייקט שניתן לחלוקה או איחוד, אפשר לייבא אותו ב-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 class קורא פקודות מהדריבר של ה-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 will run on this thread.
std::future::pending().await
}
הבדל חשוב אחד מהאפשרויות האחרות הוא שלא קוראים ל-join_thread_pool
כשמשתמשים ב-Rust אסינכררוני ובזמן ריצה עם ליבה חד-תלולית. הסיבה לכך היא שצריך לתת ל-Tokio שרשור שבו הוא יוכל להריץ משימות שנוצרו. בדוגמה הזו, השרשור הראשי ישמש למטרה הזו. משימות שנוצרות באמצעות tokio::spawn
יתבצעו בשרשור הראשי.
בקצה העורפי של Rust (async), עם סביבת זמן ריצה עם כמה שרשורים:
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 של התהליך ששולח את העסקה. ה-UID (או מזהה המשתמש) מתייחס למזהה המשתמש ב-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
בכל השירותים שמתאימים ל-Service Manager כדי לדחוף את המידע שלהם לדוח הבאג. אפשר גם להשתמש ב-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
, אבל היא לא תומכת בהפניות של קישורים חלשים בשכבה המקורית.
בקצה העורפי של 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>()
בקצה העורפי של 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, אבל כל התוכניות שכבר נוצרו ימשיכו לפעול.
שמות שכדאי להימנע מהם: