קצה עורפי AIDL הוא יעד ליצירת קוד stub. כשמשתמשים בקובצי AIDL, תמיד משתמשים בהם בשפה מסוימת עם סביבת זמן ריצה ספציפית. בהתאם להקשר, צריך להשתמש בקצוות עורפיים שונים של AIDL.
בטבלה הבאה, היציבות של פלטפורמת ה-API מתייחסת ליכולת להדר קוד מול פלטפורמת ה-API הזו כך שניתן יהיה להעביר את הקוד בנפרד מהקובץ הבינארי system.img
libbinder.so
.
ל-AIDL יש את הקצוות העורפיים הבאים:
קצה עורפי | Language | פלטפורמת API | פיתוח מערכות |
---|---|---|---|
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 משתמשים בארגז הקלסרים באותו אופן כמו כל אחד אחר בצד המערכת. החלק של Rust מגיע ב-APEX והוא נשלח בתוכו. הפעולה הזו תלויה ב-libbinder_ndk.so
במחיצת המערכת.
פיתוח מערכות
בהתאם לקצה העורפי, יש שתי דרכים להדר את AIDL לקוד stub. מידע נוסף על מערכות ה-build זמין בחומר העזר בנושא Soong Module.
מערכת build מרכזית
בכל מודול של Android.bp cc_
או java_
(או במודולים המקבילים ב-Android.mk
),
אפשר לציין קובצי .aidl
כקובצי מקור. במקרה כזה, נעשה שימוש בקצוות העורפיים של Java/CPP של AIDL (ולא בקצה העורפי NDK), והמחלקות לשימוש בקובצי ה-AIDL התואמות מתווספות למודול באופן אוטומטי. אפשרויות כמו local_include_dirs
, שלפיהן מערכת ה-build נותנת למערכת ה-build את הנתיב הבסיסי (root) לקובצי AIDL במודול הזה, אפשר לציין במודולים האלה תחת קבוצה aidl:
. שימו לב שהקצה העורפי Rust מיועד לשימוש רק עם חלודה. הטיפול במודולים של rust_
שונה בכך שקובצי AIDL לא מוגדרים כקובצי מקור.
במקום זאת, המודול aidl_interface
מייצר rustlib
בשם <aidl_interface name>-rust
שאפשר לקשר אליו. למידע נוסף, ראו דוגמה של Rust AIDL.
aidl_interface
הסוגים שבהם נעשה שימוש במערכת ה-build הזו צריכים להיות מובנים. כדי שהחבילה תהיה מובנית, היא צריכה להכיל שדות באופן ישיר, ולא להיות הצהרות מסוגים שהוגדרו ישירות בשפות היעד. למידע על האופן שבו המבנה של AIDL משתלב עם AIDL יציב, אפשר לעיין במאמר AIDL מובנה לעומת יציב.
סוגים
אפשר להשתמש במהדר (compiler) aidl
כחומר עזר להטמעת סוגים.
כשיוצרים ממשק, מפעילים את aidl --lang=<backend> ...
כדי לראות את קובץ הממשק שמתקבל. כשמשתמשים במודול aidl_interface
, אפשר לצפות בפלט ב-out/soong/.intermediates/<path to module>/
.
סוג Java/AIDL | סוג C++ | סוג NDK | סוג החלודה |
---|---|---|---|
בוליאני | בוליאני | בוליאני | בוליאני |
בייט | int8_t | int8_t | i8 |
תו | 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 | לא רלוונטי | לא רלוונטי |
איבינדר | android::IBinder | ndk::SpAIBinder | binder::SpIBinder |
ט[] | std::vector<T> | std::vector<T> | In: &[T] Out: Vec<T> |
בייט[] | std::vector<uint8_t> | std::vector<int8_t>1 | In: &[u8] Out: Vec<u8> |
רשימה<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 | קלסר::חזק |
סוג החבילה (T) | T | T | T |
סוג איחוד (T)5 | T | T | T |
T[N] 6 | std::מערך<T, N> | std::מערך<T, N> | [T; לא] |
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
או מגרש. ב-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
. ההגדרה הזאת קובעת לאיזה כיוון יועבר המידע של קריאת IPC. in
הוא כיוון ברירת המחדל, והוא מציין שהנתונים הועברו מהמתקשר למקבל הקריאה החוזרת. המשמעות של 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. צריך להצהיר
על מחרוזות כ-@utf8InCpp String
ב-AIDL כדי להמיר אותן באופן אוטומטי ל-utf-8.
קצוות העורפיים של NDK ו-Rust תמיד משתמשים במחרוזות utf-8. למידע נוסף על ההערה utf8InCpp
, ראו הערות ב-AIDL.
ערך אפס
אפשר להוסיף הערות לסוגים שיכולים להיות null בקצה העורפי של Java באמצעות @nullable
, כדי לחשוף ערכי null לקצוות העורפיים של CPP ו-NDK. בקצה העורפי של Rust, סוגי @nullable
האלה נחשפים בתור Option<T>
. שרתים נייטיב דוחים ערכי null כברירת מחדל. היוצאים מן הכלל היחידים הם סוגי interface
ו-IBinder
, שתמיד יכולים להיות null בקריאת NDK וכתיבה ב-CPP/NDK. למידע נוסף על ההערה nullable
, ראו הערות ב-AIDL.
מגרשים בהתאמה אישית
חבילה בהתאמה אישית היא חבילה שמוטמעת באופן ידני בקצה עורפי של יעד. מומלץ להשתמש במגרשים בהתאמה אישית רק כשמנסים להוסיף תמיכה בשפות אחרות למגרש קיים בהתאמה אישית שלא ניתן לשנות.
כדי להצהיר על מגרש בהתאמה אישית כדי ש-AIDL ידע עליו, הצהרת ה-AIDL של חכירה נראית כך:
package my.pack.age;
parcelable Foo;
כברירת מחדל, המדיניות הזו מצהירה על חבילת Java שבה my.pack.age.Foo
הוא מחלקה של Java שמטמיעה את הממשק של Parcelable
.
להצהרה על חבילת קצה עורפי בהתאמה אישית מסוג 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 (AOSP ניסיוני), אם רוצים להצהיר על חבילת Rust בהתאמה אישית ב-AIDL, משתמשים ב-rust_type
:
package my.pack.age;
@RustOnlyStableParcelable parcelable Foo rust_type "rust_crate::Foo";
הטמעת חלודה ב-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);
לאחר מכן אפשר להשתמש בחבילה הזו כסוג בקובצי AIDL, אבל הוא לא ייווצר על ידי AIDL. כדי להשתמש בחבילות בהתאמה אישית בקצה העורפי של CPP/NDK, צריך לספק האופרטורים <
ו-==
בשביל מגרשים בהתאמה אישית ב-union
.
ערכי ברירת מחדל
במגרשים מובנים אפשר להצהיר על ערכי ברירת מחדל לכל שדה לפרימיטיבים, 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
, ולא מנותחות על ידי קישור.
ב-Java, השגיאות ממופות לחריגות, כמו android.os.RemoteException
. בחריגים ספציפיים לשירות, Java משתמש ב-android.os.ServiceSpecificException
יחד עם השגיאה בהגדרת המשתמש.
קוד מקורי ב-Android לא משתמש בחריגים. הקצה העורפי של CPP משתמש ב-android::binder::Status
. הקצה העורפי של NDK משתמש ב-ndk::ScopedAStatus
. כל method שנוצרת על ידי AIDL מחזירה את אחד מהמאפיינים האלה, שמייצג את הסטטוס של ה-method. בקצה העורפי של 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 templates.
סוגי ייבוא
אפשר לייבא אותו ב-Java גם אם הסוג שהוגדר הוא ממשק, parcelable או איחוד:
import my.package.IFoo;
או בקצה העורפי של הקישור לדף מוצר ב-CSS:
#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
הוא סוג הרמה הבסיסית (root) של הקובץ), צריך לכלול את <my/package/IFoo.h>
בקצה העורפי של CPP (או <aidl/my/package/IFoo.h>
בשביל הקצה העורפי של NDK).
הטמעת שירותים
כדי להטמיע שירות, אתם צריכים לקבל בירושה ממחלקת ה-stub המקורית. הכיתה הזו קוראת פקודות מהמנהל של מאגר התגים ומריצה את השיטות שמטמיעים. נניח שיש לכם קובץ 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(())
}
}
או באמצעות חלודה אסינכרונית:
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 אסינכרוני ובסביבת זמן ריצה עם שרשור יחיד. הסיבה לכך היא שצריך לתת לטוקיו שרשור שבו הוא יכול לבצע משימות שנוצרו במקור. בדוגמה הזו, ה-thread הראשי ישרת את המטרה הזו. כל המשימות שייווצרו באמצעות tokio::spawn
יבוצעו ב-thread הראשי.
בקצה העורפי האסינכרוני של 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 מרובת-השרשורים, המשימות שנוצרות לא מבוצעות ב-thread הראשי. לכן כדאי לקרוא ל-join_thread_pool
ב-thread הראשי כדי שה-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
, צריך להשאיר את האובייקט פעיל כל עוד רוצים לקבל התראות.
פרטי המתקשר
כשמקבלים קריאה ל-kernele binder, פרטי המתקשר זמינים בחלק מממשקי ה-API. ה-PID (או מזהה התהליך) מתייחס למזהה התהליך ב-Linux של התהליך ששולח טרנזקציה. ה-UID (או User ID) מתייחס למזהה המשתמש של Linux. כשמקבלים שיחה חד-כיוונית, ה-PID של השיחה הוא 0. מחוץ להקשר של טרנזקציה של קישור, הפונקציות האלה מחזירות את ה-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, אפשר לקבוע את הסדר של העברת השירותים על ידי הוספת ארגומנטים ל-addService
. אפשר גם להשתמש ב-dumpsys --pid SERVICE
כדי לקבל את ה-PID של שירות בזמן ניפוי באגים.
כדי להוסיף פלט מותאם אישית לשירות, אתם יכולים לשנות את ה-method dump
באובייקט השרת כמו שמטמיעים כל שיטת IPC אחרת שמוגדרת בקובץ AIDL. כשעושים זאת, צריך להגביל את ההעלאה להרשאת האפליקציה 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<()>
קבלה דינמית של מתאר ממשק
מתאר הממשק מזהה את סוג הממשק. האפשרות הזו שימושית בניפוי באגים או כשיש קישור לא מוכר.
ב-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>()
בקצה העורפי של NDK:
::ndk::enum_range<MyEnum>()
בקצה העורפי של Rust:
MyEnum::enum_values()
ניהול שרשורים
כל מופע של libbinder
בתהליך שומר מאגר אחד של שרשורים. ברוב המקרים, מדובר במאגר שרשורים אחד בלבד שמשותף לכל הקצוות העורפיים.
יוצא הדופן היחיד הוא כאשר קוד הספק עשוי לטעון עותק נוסף של libbinder
כדי לדבר עם /dev/vndbinder
. מכיוון שהוא נמצא בצומת binder נפרד, מאגר ה-threads לא משותף.
בקצה העורפי של 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, אפשר לשנות שמות של שדות בבטחה ועדיין לשמור על תאימות לפרוטוקול. יכול להיות שתצטרכו לעדכן את הקוד כדי להמשיך לפתח, אבל תוכנות שכבר נוצרו ימשיכו לפעול באופן הדדי.
שמות שיש להימנע מהם: