טיפים בנושא JNI

JNI הוא Java Native Interface. הוא מגדיר דרך לקוד הבינארי ש-Android אוספת מקוד מנוהל (שנכתב בשפות התכנות Java או Kotlin) כדי לקיים אינטראקציה עם קוד מקומי (שנכתב ב-C/C++). ‏JNI הוא ניטרלי לספקים, יש לו תמיכה בטעינה של קוד מספריות ספריות משותפות דינמיות, ולפעמים הוא מסורבל אבל יעיל למדי.

הערה: כי מערכת Android מהדרת את Kotlin ל-bytecode שמתאים ל-ART, בדומה לשפת התכנות Java, אפשר ליישם את ההנחיות שבדף הזה את שפות התכנות Kotlin ו-Java במונחים של ארכיטקטורת JNI והעלויות שלהן. מידע נוסף זמין במאמר הבא: Kotlin ו-Android.

אם אתם לא מכירים את JNI, כדאי לקרוא את מפרט Java Native Interface כדי להבין איך JNI פועל ואיזה תכונות זמינות. יש היבטים מסוימים בממשק שלא ברורים במבט ראשון, ולכן כדאי לעיין בקטעים הבאים.

כדי לעיין בהפניות JNI גלובליות ולראות איפה נוצרות ונמחקות הפניות JNI גלובליות, צריך להשתמש ב- תצוגת ערימת JNI ב-Memory Profiler ב-Android Studio 3.2 ואילך.

טיפים כלליים

כדאי לצמצם את טביעת הרגל של שכבת ה-JNI. יש כאן כמה מאפיינים שצריך להביא בחשבון. פתרון ה-JNI שלכם צריך לנסות לעמוד בהנחיות האלה (מפורטות בהמשך לפי סדר החשיבות, שמתחילה בחשוב ביותר):

  • לצמצם את הסידור של המשאבים בשכבת ה-JNI. ריצות עם הכדור בשכבת ה-JNI יש עלויות לא טריוויאליות. כדאי לתכנן ממשק שמצמצם את כמות הנתונים שצריך לארגן ואת התדירות שבה צריך לארגן את הנתונים.
  • להימנע מתקשורת אסינכרונית בין קוד שנכתב בתכנות מנוהל בשפה ובקוד שנכתבים ב-C++ כשהדבר אפשרי. כך יהיה קל יותר לתחזק את ממשק ה-JNI. בדרך כלל אפשר להשתמש בפורמט אסינכרוני יותר ממשק המשתמש מתעדכן על ידי שמירה על העדכון האסינכרוני באותה שפה שבה נמצא ממשק המשתמש. לדוגמה, במקום להפעיל פונקציית C++ משרשור ממשק המשתמש בקוד Java דרך JNI, עדיף לביצוע קריאה חוזרת (callback) בין שני תהליכונים בשפת Java, באחד מהם ביצוע הפעלת C++ חוסם, ולאחר מכן הודעה לשרשור בממשק המשתמש כשהקריאה לחסימה הושלם.
  • צמצום מספר השרשור ש-JNI צריך לגעת בהם או שהם צריכים לגעת ב-JNI אם צריך להשתמש במאגרי שרשורים גם בשפת Java וגם בשפת C++, כדאי לנסות לא להשתמש ב-JNI תקשורת בין בעלי המאגרים ולא בין השרשורים של העובדים השונים.
  • כדאי לשמור את קוד הממשק במספר קטן של מיקומי מקור ב-C++‎ וב-Java שקל לזהות, כדי להקל על שינויים מבניים עתידיים. כדאי להשתמש בגנרציה אוטומטית של JNI לפי הצורך.

JavaVM ו-JNIEnv

ב-JNI מוגדרים שני מבני נתונים מרכזיים, JavaVM ו-JNIEnv. ושניהם בעצם מצביעים לטבלאות של פונקציות. (בגרסת C++ יש כיתות עם מצביע אל טבלת פונקציות ופונקציית חבר בכל פונקציית JNI שעקיפה דרך הטבלה.) ‏JavaVM מספק את הפונקציות של 'ממשק ההפעלה', שמאפשרות ליצור JavaVM ולהרוס אותו. בתיאוריה אפשר ליצור כמה מכונות JavaVM לכל תהליך, אבל ב-Android מותר להשתמש רק פעם אחת.

ה-JNIEnv מספק את רוב הפונקציות של JNI. כל הפונקציות המקוריות מקבלות JNIEnv כ- הארגומנט הראשון, מלבד @CriticalNative methods, ראו שיחות מותאמות מהירות יותר.

ה-JNIEnv משמש לאחסון מקומי בפרוטוקול Thread. לכן לא ניתן לשתף JNIEnv בין שרשורים. אם לקטע קוד אין דרך אחרת לקבל את ה-JNIEnv שלו, צריך לשתף את ה-JavaVM ומשתמשים ב-GetEnv כדי לגלות את ה-JNIEnv של השרשור. (בהנחה שיש לו מאפיין כזה, יש לעיין בAttachCurrentThread בהמשך).

הצהרות C של JNIEnv ו-JavaVM שונות מ-C++ וההצהרות שלו. קובץ ההכללה "jni.h" מספק הגדרות typedef שונות בהתאם להכללתו ב-C או ב-C++. לכן, לא כדאי לכלול ארגומנטים מסוג JNIEnv בקובצי כותרת הכלולים בשתי השפות. (דרך נוספת: אם בקובץ הכותרת נדרש #ifdef __cplusplus, ייתכן שתצטרכו לבצע עוד קצת עבודה אם שהכותרת מתייחסת ל-JNIEnv.)

שרשורים

כל התהליכים הם תהליכים של Linux, שמתוזמנים על ידי הליבה. בדרך כלל הם מופעלים מקובץ קוד מנוהל (באמצעות Thread.start()), אבל אפשר גם ליצור אותם במקום אחר ולאחר מכן לצרף אותם ל-JavaVM. עבור למשל, שרשור שהתחיל עם pthread_create() או עם std::thread ניתן לצרף באמצעות הפונקציה AttachCurrentThread() או פונקציות AttachCurrentThreadAsDaemon(). עד שהשרשור מצורף, הוא לא מכיל JNIEnv ולא ניתן לבצע קריאות JNI.

בדרך כלל מומלץ להשתמש ב-Thread.start() כדי ליצור כל חוט שצריך להפעיל קוד Java. הפעולה הזו תבטיח שיש לכם מספיק מקום בסטאק, שאתם נמצאים ב-ThreadGroup הנכון ושאתם משתמשים באותו ClassLoader כמו בקוד Java. קל יותר גם להגדיר את שם השרשור לניפוי באגים ב-Java מאשר מקוד מקורי (ראו pthread_setname_np() אם יש לכם pthread_t או thread_t, ו-std::thread::native_handle() אם יש לכם std::thread ואתם רוצים pthread_t).

הצמדת חוט שנוצר באופן מקורי גורמת ליצירה של אובייקט java.lang.Thread ולהוספתו ל-ThreadGroup 'הראשי', כך שהוא יהיה גלוי למנטר הבאגים. קריאה ל-AttachCurrentThread() בשרשור שכבר מצורף היא פעולה ללא תוצאה.

Android לא משהה חוטים שמריצים קוד מקומי. אם המיקום מתבצע איסוף אשפה, או שהכלי לניפוי באגים הנפיק השעיה בקשה זו, Android ישהה את השרשור בפעם הבאה שתתבצע שיחת JNI.

שרשורים שמצורפים דרך JNI חייבים להפעיל קריאה DetachCurrentThread() לפני היציאה. אם התכנות באופן ישיר הוא מוזר, ב-Android 2.0 (Eclair) ואילך יכול להשתמש ב-pthread_key_create() כדי להגדיר כלי הרס לפונקציה שתיקרא לפני היציאה של ה-thread, להתקשר אל DetachCurrentThread() משם. (השתמשו ב... מפתח עם pthread_setspecific() כדי לאחסן את ה-JNIEnv thread-local-storage; כך הוא יועבר ל-Destructor, הארגומנט.)

jclass , jmethodID ו-jfieldID

כדי לגשת לשדה של אובייקט מקוד מקורי, מבצעים את הפעולות הבאות:

  • קבלת ההפניה לאובייקט המחלקה עבור המחלקה באמצעות FindClass
  • קבלת מזהה השדה של השדה GetFieldID
  • לקבל את התוכן של השדה עם משהו מתאים, כמו GetIntField

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

אם הביצועים חשובים לך, מומלץ לבדוק את הערכים פעם אחת ולשמור את התוצאות במטמון בקוד ה-Native שלכם. מכיוון שיש מגבלה של JavaVM אחד לכל תהליך, זה סביר כדי לאחסן את הנתונים האלה במבנה מקומי סטטי.

הפניות לכיתות, מזהי השדות ומזהי השיטות יהיו תקפים עד להסרת המחלקה. מחלקות פורקים את הטעינה רק אם אפשר לאסוף אשפה את כל הכיתות המשויכות ל-ClassLoader, זה נדיר, אבל לא יהיה בלתי אפשרי ב-Android. עם זאת, שימו לב jclass הוא הפניה לכיתה וחייב להיות מוגן באמצעות אל NewGlobalRef (מידע נוסף מפורט בקטע הבא).

אם אתם רוצים לשמור את המזהים במטמון בזמן טעינת כיתה, ולאחסן אותם מחדש באופן אוטומטי במטמון אם יתבצע פריקים וטעינה מחדש של הכיתה, הדרך הנכונה לאתחל היא הם מוסיפים למחלקה המתאימה קטע קוד שנראה כך:

Kotlin

companion object {
    /*
     * We use a static class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private external fun nativeInit()

    init {
        nativeInit()
    }
}

Java

    /*
     * We use a class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private static native void nativeInit();

    static {
        nativeInit();
    }

יוצרים רכיב method nativeClassInit בקוד C/C++ שמבצע את חיפושי המזהים. הקוד יבוצע פעם אחת, אחרי שהמחלקה מאותחלת. אם הכיתה תהיה זמינה שוב, היא תבוצע שוב.

הפניות מקומיות וגלובליות

כל ארגומנט שמועבר ל-method נייטיב, וכמעט כל אובייקט הוחזר בפונקציה JNI היא "הפניה מקומית". כלומר, היא תקפה משך הזמן של שיטת ה-Native הנוכחית ב-thread הנוכחי. גם אם האובייקט עצמו ממשיך לפעול אחרי השיטה המקורית מחזירה, ההפניה לא חוקית.

הכלל הזה חל על כל תתי-הקטגוריות של jobject, כולל jclass,‏ jstring ו-jarray. (סביבת זמן הריצה תציג אזהרה לגבי רוב השימושים הלא נכונים בהפניות כאשר מאריכים את JNI בדיקות מופעלות).

הדרך היחידה לקבל הפניות לא מקומיות היא באמצעות הפונקציות NewGlobalRef וגם NewWeakGlobalRef

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

דפוס זה נפוץ כששומרים במטמון jclass שהוחזר מ-FindClass, למשל:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

כל השיטות של JNI מקבלות גם הפניות מקומיות וגם הפניות גלובליות כארגומנטים. ייתכן שלהפניות לאותו אובייקט יהיו ערכים שונים. לדוגמה, ערכים חזרה מבקשות רצופות ל-NewGlobalRef באותו אובייקט עשויים להיות שונים. כדי לבדוק אם שתי הפניות מפנות לאותו אובייקט, צריך להשתמש בפונקציה IsSameObject. לעולם אל תבצעו השוואה בין הפניות ל-== בקוד מקורי.

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

מתכנתים נדרשים "לא להקצות יותר מדי" הפניות מקומיות. במילים מעשיות, המשמעות היא שאם אתם יוצרים כמות גדולה של הפניות מקומיות, אולי במהלך ריצה של מערך של אובייקטים, עליכם לפנות אותן באופן ידני באמצעות DeleteLocalRef במקום לאפשר ל-JNI לעשות זאת בשבילכם. נדרשת רק כדי לשריין משבצות 16 הפניות מקומיות, כך שאם דרוש לך יותר מזה, עליך למחוק אותו לפי הצורך או להשתמש EnsureLocalCapacity/PushLocalFrame כדי להזמין מקומות נוספים.

חשוב לשים לב שהשדות jfieldID ו-jmethodID אטומים ולא הפניות לאובייקטים, ואין להעביר אותם אל NewGlobalRef. גם הפונקציות שמחזירות את הפונקציות של הנתונים הגולמיים, כמו GetStringUTFChars ו-GetByteArrayElements, לא הן אובייקטים. (אפשר להעביר אותם בין שרשורים, והם בתוקף עד לקריאה התואמת של Release).

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

חשוב להיזהר כשמשתמשים בהפניות גלובליות. הפניות גלובליות יכולות להיות בלתי נמנעות, אבל קשה מאוד כדי לנפות באגים, ועלולים לגרום להתנהגות שגויה של זיכרון. כשכל שאר התנאים שווים, עם פחות הפניות גלובליות, סביר להניח שהוא עדיף.

מחרוזות מסוג UTF-8 ו-UTF-16

שפת התכנות Java משתמשת ב-UTF-16. לנוחיותכם, JNI מספקת שיטות שפועלות עם גם UTF-8 שעבר שינוי. שינוי הקידוד הוא שימושי לקוד C, כי הוא מקודד \u0000 לפי 0xc0 0x80 במקום 0x00. היתרון בכך הוא שאפשר לסמוך על מחרוזות מסוג C עם סיומת אפס, שמתאימות לשימוש עם פונקציות מחרוזת רגילות של libc. הצד הנגדי הוא שאי אפשר לעבור לנתוני UTF-8 שרירותיים ל-JNI והם מצפים שהם יפעלו כמו שצריך.

כדי לקבל ייצוג UTF-16 של String, צריך להשתמש ב-GetStringChars. הערה: מחרוזות UTF-16 אינן עם סיום אפס, ומותר להשתמש בערך \u0000 לכן צריך להיצמד לאורך המחרוזת ולסמן ה-jchar.

אל תשכחו Release את המחרוזות שאתם Get. פונקציות המחרוזת מחזירות את הערך jchar* או jbyte*, הם סימנים בסגנון C לנתונים פרימיטיביים ולא להפניות מקומיות. הם מובטחים בתוקף עד שמפעילים את Release, כלומר הם לא משוחררים כשה-method המקורי חוזר.

הנתונים שמועברים אל NewStringUTF צריכים להיות בפורמט UTF-8 שעבר שינוי. שגיאה נפוצה היא קריאת נתוני תווים מקובץ או מזרם רשת והעברתם אל NewStringUTF בלי לסנן אותם. אם אתם לא יודעים שהנתונים הם בפורמט MUTF-8 תקין (או ASCII‏ 7 סיביות, שהוא קבוצת משנה תואמת), עליכם להסיר תווים לא חוקיים או להמיר אותם לפורמט UTF-8 שעבר שינוי בצורה נכונה. אם לא תעשו זאת, סביר להניח שההמרה ל-UTF-16 תיתן תוצאות לא צפויות. CheckJNI – שמופעלת כברירת מחדל באמולטורים – סורק מחרוזות ומבטלת את המכונה הווירטואלית (VM) אם היא מקבלת קלט לא חוקי.

לפני Android 8, הפעולה בדרך כלל הייתה מהירה יותר עם מחרוזות UTF-16 בתור Android לא נדרש עותק ב-GetStringChars, ואילו במסגרת GetStringUTFChars נדרשו הקצאה והמרה ל-UTF-8. ב-Android 8, בוצע שינוי של הייצוג String לשימוש ב-8 ביט לכל תו למחרוזות ASCII (כדי לחסוך בזיכרון) ולהתחיל להשתמש עובר אוסף אשפה. התכונות האלה מצמצמות במידה משמעותית את מספר המקרים שבהם מודל ART יכול לספק מצביע לנתונים של String בלי ליצור עותק, אפילו עבור GetStringCritical. אבל אם רוב המחרוזות מעובדות על ידי הקוד הן קצרות, וברוב המקרים ניתן להימנע מההקצאה ומהעסקה באמצעות מאגר נתונים זמני שהוקצה במקבץ ו-GetStringRegion או GetStringUTFRegion. לדוגמה:

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr<jchar[]> heap_buffer;
    jchar* buffer = stack_buffer;
    jsize length = env->GetStringLength(str);
    if (length > kStackBufferSize) {
      heap_buffer.reset(new jchar[length]);
      buffer = heap_buffer.get();
    }
    env->GetStringRegion(str, 0, length, buffer);
    process_data(buffer, length);

מערכים פרימיטיביים

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

כדי שהממשק יהיה יעיל ככל האפשר בלי להגביל בהטמעה של ה-VM, השדה Get<PrimitiveType>ArrayElements ממשפחת הקריאות מאפשרת לסביבת זמן הריצה להחזיר מצביע אל הרכיבים עצמם, או להקצות קצת זיכרון וליצור עותק. בכל מקרה, מובטח שהצבען הגולמי המוחזר יהיה תקין עד להפעלת הקריאה המתאימה ל-Release (כלומר, אם הנתונים לא הועתקו, אובייקט המערך יהיה מוצמד ולא ניתן יהיה להעביר אותו למיקום אחר כחלק מצמצום האשפה). חובה Release כל מערך שאתם Get. כמו כן, אם Get הקריאה תיכשל, עליך לוודא שהקוד לא מנסה Release NULL את הסמן בשלב מאוחר יותר.

כדי לקבוע אם הנתונים הועתקו או לא, מעבירים למשתנה ה-isCopy הפניה שאינה NULL. זה שימושי לעיתים רחוקות.

הפונקציה Release מקבלת ארגומנט mode שיכול יש אחד מתוך שלושה ערכים. הפעולות הביצוע של סביבת זמן הריצה תלויות ב- האם הוא החזיר מצביע לנתונים בפועל או עותק שלהם:

  • 0
    • בפועל: אובייקט המערך לא מוצמד.
    • העתקה: הנתונים מועתקים בחזרה. מאגר הנתונים עם העותק מתפנה.
  • JNI_COMMIT
    • בפועל: לא עושה כלום.
    • העתקה: הנתונים מועתקים בחזרה. מאגר הנתונים הזמני עם העותק לא נשלח.
  • JNI_ABORT
    • בפועל: אובייקט המערך לא מוצמד. מוקדם יותר לא מבוטלים.
    • העתקה: מאגר הנתונים עם העותק מתפנה, וכל השינויים בו יאבדו.

אחת מהסיבות לבדוק את הדגל isCopy היא לדעת אם עליך להתקשר אל Release עם JNI_COMMIT אחרי ביצוע שינויים במערך – אם אתם מחליפים בין ביצוע שינויים והרצת קוד שמשתמש בתוכן של המערך, יכול לוותר על ההתחייבות. סיבה אפשרית נוספת לבדיקת הדגל היא טיפול יעיל בJNI_ABORT. לדוגמה, ייתכן שתרצו לקבל מערך, לשנות אותו למקומו, להעביר חלקים לפונקציות אחרות ומוחקים את השינויים. אם אתם יודעים ש-JNI יוצר עותק חדש עבור אין צורך ליצור אפשרות עריכה אחרת. העתקה. אם JNI מעביר לכם את המקור, תצטרכו ליצור עותק משלכם.

טעות נפוצה (חוזרת על אותה קוד לדוגמה) היא להניח שאפשר לדלג על הקריאה Release אם הערך *isCopy לא נכון. זה לא המצב. אם לא קיים מאגר נתונים זמני להעתיק הוקצה, אז צריך להצמיד את הזיכרון המקורי ואי אפשר להעביר אותו אוסף האשפה.

חשוב גם לציין שהדגל JNI_COMMIT לא משחרר את המערך, ובסופו של דבר תצטרכו לבצע קריאה חוזרת ל-Release עם דגל אחר.

שיחות אזוריות

יש חלופה לשיחות כמו Get<Type>ArrayElements ו-GetStringChars שיכולה להיות מאוד שימושית כשכל מה שאתם רוצים לעשות הוא להעתיק נתונים פנימה או החוצה. כדאי להביא בחשבון את הדברים הבאים:

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

הפעולה הזו גורמת לצילום של המערך והעתקה של הבייט הראשון (len) ואז משחררים את המערך. בהתאם לתרחיש לדוגמה הקריאה ל-Get תצמיד או תעתיק את המערך תוכן. הקוד מעתיק את הנתונים (אולי בפעם השנייה), ולאחר מכן קורא ל-Release; במקרה הזה בעזרת JNI_ABORT, אין סיכוי לעותק שלישי.

אפשר להשיג את אותו הדבר בצורה פשוטה יותר:

    env->GetByteArrayRegion(array, 0, len, buffer);

יש לכך כמה יתרונות:

  • נדרשת קריאת JNI אחת במקום 2, וכך התקורה מצטמצמת.
  • לא נדרשת הצמדה או עותקי נתונים נוספים.
  • מפחית את הסיכון לשגיאות מתכנתים — אין סיכון לשכוח להתקשר אל Release אחרי שמשהו נכשל.

באותו אופן, אפשר להשתמש בפונקציה Set<Type>ArrayRegion כדי להעתיק נתונים למערך, GetStringRegion או GetStringUTFRegion כדי להעתיק תווים מתוך String

חריגים

אין לקרוא לרוב הפונקציות JNI בזמן שיש חריג בהמתנה. הקוד אמור להופיע כחריג (באמצעות הערך המוחזר של הפונקציה, ExceptionCheck או ExceptionOccurred) וחוזרים, או למחוק את החריג ולטפל בו.

פונקציות ה-JNI היחידות שאליהן ניתן להפעיל אם יש חריג חריג בהמתנה:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

הרבה קריאות JNI יכולות להוביל לחריגה, אבל לעיתים קרובות הן מספקות דרך פשוטה יותר בבדיקת הכשל. לדוגמה, אם הפונקציה NewString מחזירה ערך שאינו NULL, אין צורך לחפש חריגה. אבל אם קוראים ל-method (באמצעות פונקציה כמו CallObjectMethod), תמיד צריך לבדוק אם יש יוצא מן הכלל, כי הערך המוחזר יהיה תקף אם נרשמה חריגה.

חשוב לזכור שהחרגות שמשוחררות על ידי קוד מנוהל לא מבטלות את מסגרות ה-stack הילידיות. (וכן, אסור להפעיל חריגות של C++ מעבר לגבול המעבר של JNI מקידוד C++ לקידוד מנוהל). ההוראות Throw ו-ThrowNew של JNI רק מגדירות מציין חריגה בשרשור הנוכחי. בחזרה לחשבון מנוהל מקוד מקורי, החריג יסומן ויטופל בהתאם.

קוד מקורי יכול "לתפוס" חריג: חיוג אל ExceptionCheck או ExceptionOccurred, ומנקים אותה באמצעות ExceptionClear. כרגיל, ביטול חריגות מבלי לטפל בהן עלול לגרום לבעיות.

אין פונקציות מובנות לביצוע מניפולציות על האובייקט Throwable עצמו, לכן אם רוצים (למשל) לקבל את מחרוזת החריגה, צריך מציאת הכיתה Throwable, חיפוש מזהה השיטה של getMessage "()Ljava/lang/String;", יש להפעיל אותו, ואם התוצאה הוא לא NULL. השתמשו ב-GetStringUTFChars כדי לקבל משהו שאתם יכולים בכתב יד printf(3) או מקבילה.

בדיקה מורחבת

ב-JNI מתבצעת בדיקת שגיאות מועטה מאוד. שגיאות בדרך כלל גורמות לקריסה. ב-Android יש גם מצב בשם CheckJNI, שבו מצביעי הטבלה של JavaVM ו-JNIEnv עוברים לטבלאות של פונקציות שמבצעות סדרה מורחבת של בדיקות לפני קריאה להטמעה הרגילה.

הבדיקות הנוספות כוללות:

  • מערכי: ניסיון להקצות מערך בגודל שלילי.
  • סמנים לא תקינים: העברת jarray/jclass/jobject/jstring פגום אל קריאה ל-JNI, או העברת מצביע NULL לקריאה של JNI עם ארגומנט שאינו null.
  • שמות של כיתות: העברה של כל דבר מלבד סגנון שם הכיתה 'java/lang/String' לקריאה ל-JNI.
  • קריאות קריטיות: ביצוע קריאה ל-JNI בין קריאה מסוג 'קריטית' לבין השחרור התואם שלה.
  • ByteBuffers ישירה: העברת ארגומנטים לא תקינים אל NewDirectByteBuffer.
  • חריגות: ביצוע קריאה ל-JNI בזמן שיש חריגה בהמתנה.
  • JNIEnv*s: שימוש ב-JNIEnv* מהשרשור הלא נכון.
  • jfieldID: שימוש ב-jfieldID NULL, או שימוש ב-jfieldID כדי להגדיר שדה לערך מסוג שגוי (בניסיון להקצות StringBuilder לשדה String (למשל), שימוש ב-jfieldID בשדה סטטי כדי להגדיר שדה מופע או להיפך, או שימוש ב-jfieldID ממחלקה אחת עם מופעים של מחלקה אחרת.
  • jmethodID: שימוש ב-jmethodID מסוג שגוי כשמבצעים קריאת JNI של Call*Method: סוג החזרה שגוי, אי התאמה סטטית/לא סטטית, סוג שגוי ל-'this' (לשיחות שאינן סטטיות) או מחלקה שגויה (לקריאות סטטיות).
  • הפניות: שימוש ב-DeleteGlobalRef/DeleteLocalRef בסוג שגוי של הפניה.
  • מצבי פרסום: העברת מצב פרסום שגוי לקריאה לפרסום (משהו שאינו 0,‏ JNI_ABORT או JNI_COMMIT).
  • בטיחות סוג: החזרת סוג לא תואם מהשיטה המקורית (החזרת StringBuilder משיטה שהוצהרה להחזרת מחרוזת, למשל).
  • UTF-8: העברה של רצף בייטים לא חוקי של UTF-8 שעבר שינוי לקריאה ל-JNI.

(הנגישות של שיטות ושדות עדיין לא מסומנת: הגבלות הגישה לא חלות על קוד נייטיב).

יש כמה דרכים להפעיל את CheckJNI.

אם משתמשים באמולטור, CheckJNI מופעל כברירת מחדל.

אם יש לכם מכשיר עם הרשאות בסיס, תוכלו להשתמש ברצף הפקודות הבא כדי להפעיל מחדש את סביבת זמן הריצה כש-CheckJNI מופעל:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

בכל אחד מהמקרים האלה, כשתתחילו את סביבת זמן הריצה, תראו משהו כזה בפלט ה-Logcat:

D AndroidRuntime: CheckJNI is ON

אם יש לכם מכשיר רגיל, תוכלו להשתמש בפקודה הבאה:

adb shell setprop debug.checkjni 1

הפעולה הזו לא תשפיע על אפליקציות שכבר פועלות, אבל בכל אפליקציה שתופעל מאותו רגע ואילך תופעל CheckJNI. (יש לשנות את הנכס לכל ערך אחר או פשוט להפעיל מחדש ישבית שוב את CheckJNI). במקרה כזה, בפעם הבאה שאפליקציה תופעל יופיע משהו כזה בפלט של logcat:

D Late-enabling CheckJNI

אפשר גם להגדיר את המאפיין android:debuggable במניפסט של האפליקציה כך: הפעלת CheckJNI רק לאפליקציה שלך. שימו לב שכלי ה-build של Android יבצעו את הפעולה הזו באופן אוטומטי סוגים מסוימים של גרסאות build.

ספריות מקוריות

אפשר לטעון קוד מקורי מספריות משותפות באמצעות הפונקציה הרגילה System.loadLibrary.

בפועל, בגרסאות ישנות יותר של Android היו באגים ב-PackageManager שגרמו להתקנה עדכון של ספריות נייטיב באופן שלא יהיה מהימן. המקשר מחדש מציע דרכים לעקוף את הבעיה הזו ובעיות טעינה אחרות של ספריות נייטיב.

התקשרות אל System.loadLibrary (או ReLinker.loadLibrary) מכיתה סטטית של האתחול. הארגומנט הוא 'לא מקושט' שם הספרייה, לכן כדי לטעון את libfubar.so שהיית מעביר ב-"fubar".

אם יש לכם רק כיתה אחת שמתבססת על שיטות נייטיב, הגיוני להפעיל את הקריאה System.loadLibrary כדי להיות במאתחל סטטי בשביל המחלקה הזו. אחרת ייתכן אני רוצה לבצע את השיחה מ-Application כדי שתדע שהספרייה תמיד נטענת, ותמיד נטענות מוקדם.

יש שתי דרכים שבהן סביבת זמן הריצה יכולה למצוא את השיטות המקומיות. אפשר לרשום אותן באופן מפורש באמצעות RegisterNatives, או לאפשר לסביבת זמן הריצה לחפש אותן באופן דינמי באמצעות dlsym. היתרונות של RegisterNatives הם שמקבלים מראש לבדוק שהסמלים קיימים, ואפשר גם לשתף ספריות קטנות ומהירות יותר ייצוא כל דבר מלבד JNI_OnLoad. היתרון של מתן אפשרות לסביבת זמן הריצה לגלות את הוא מצריך קצת פחות קוד לכתיבה.

כדי להשתמש באפליקציה RegisterNatives:

  • צריך לציין את הפונקציה JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved).
  • בJNI_OnLoad, רושמים את כל השיטות המותאמות באמצעות RegisterNatives.
  • צור באמצעות סקריפט גרסה (מועדף) או משתמשים -fvisibility=hidden כך שרק JNI_OnLoad מיוצא מהספרייה שלך. כך נוצר קוד מהיר יותר וקטן יותר, התנגשויות עם ספריות אחרות שנטענות לאפליקציה שלכם (אבל הן יוצרות דוחות קריסות פחות שימושיים אם האפליקציה קורסת בקוד נייטיב).

המאתחל הסטטי אמור להיראות כך:

Kotlin

companion object {
    init {
        System.loadLibrary("fubar")
    }
}

Java

static {
    System.loadLibrary("fubar");
}

הפונקציה JNI_OnLoad אמורה להיראות כך אם היא נכתבת ב-C++‎:

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // Find your class. JNI_OnLoad is called from the correct class loader context for this to work.
    jclass c = env->FindClass("com/example/app/package/MyClass");
    if (c == nullptr) return JNI_ERR;

    // Register your class' native methods.
    static const JNINativeMethod methods[] = {
        {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
        {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
    };
    int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
    if (rc != JNI_OK) return rc;

    return JNI_VERSION_1_6;
}

כדי להשתמש במקום זאת ב'גילוי' של שיטות מקומיות, צריך לתת להן שם באופן ספציפי (פרטים מופיעים במפרט JNI). המשמעות היא שאם חתימת שיטה שגויה, לא תדעו עליה עד בפעם הראשונה שהשיטה מופעלת בפועל.

כל השיחות של FindClass שיבוצעו מ-JNI_OnLoad יטופלו ככיתות ההקשר של טוען הכיתות ששימש לטעינת הספרייה המשותפת. כשמפעילים את FindClass מהקשרים אחרים, הוא משתמש במטען הכיתבים שמשויך לשיטה בחלק העליון של סטאק Java. אם אין כזה (כי הקריאה מגיעה מחוט מקורי שצורף זה עתה), הוא משתמש במטען הכיתבים 'system'. טוען מחלקות המערכת לא יודע על בכיתות האלה, לא תוכלו לחפש כיתות משלכם שמכילות את FindClass הקשר מסוים. לכן, JNI_OnLoad הוא מקום נוח לחיפוש כיתות ולהטמעה שלהן במטמון: אחרי שיוצרים הפניה גלובלית תקינה ל-jclass, אפשר להשתמש בה מכל שרשור שמצורף.

שיחות מקוריות מהירות יותר עם @FastNative ועם @CriticalNative

אפשר להוסיף הערות לשיטות מותאמות עם @FastNative או @CriticalNative (אבל לא לשניהם) כדי לזרז את המעברים בין קוד מנוהל לקוד מקורי. עם זאת, ההערות האלה מביאות לשינויים מסוימים בהתנהגות, שצריך להביא בחשבון לפני שמשתמשים בהן. אומנם אנחנו מציינים בקצרה את השינויים האלה בהמשך, אפשר לעיין במסמכים לקבלת הפרטים.

אפשר להחיל את ההערה @CriticalNative רק על שיטות מותאמות שלא עושות משתמשים באובייקטים מנוהלים (בפרמטרים או בערכים מוחזרים, או ב-this משתמע), שמשנה את ה-ABI של המעבר מ-JNI. ההטמעה המקורית חייבת להחריג את הפרמטרים JNIEnv ו-jclass מחתימת הפונקציה.

בזמן הפעלה של @FastNative או @CriticalNative, האשפה לא ניתן להשעות את השרשור עקב עבודות חיוניות, ויכול להיות שהוא ייחסם. אל תשתמשו בהערות האלה בשיטות ממושכות, כולל שיטות שבדומה לרוב הן מהירות אבל בדרך כלל ללא הגבלה. במיוחד, אסור שהקוד יבצע פעולות קלט/פלט משמעותיות או יקבל מנעולים מקומיים שיכולים להישאר נעולים במשך זמן רב.

ההערות האלה הוטמעו לשימוש המערכת, Android 8 והפך לציבורי שנבדקו באמצעות CTS API ב-Android 14. סביר להניח שהאופטימיזציות האלה יפעלו גם במכשירי Android מגרסה 8-13 (אבל ללא אחריות CTS חזקה), אך החיפוש הדינמי של שיטות נייטיב נתמך רק Android מגרסה 12 ואילך, נדרש רישום מפורש ב-JNI RegisterNatives למכשירים עם גרסאות Android 8-11. ב-Android 7- המערכת מתעלמת מההערות האלה, או חוסר ההתאמה ב-ABI עבור @CriticalNative יוביל לסידור שגוי של ארגומנטים, וסביר להניח לקריסות.

אם מדובר בשיטות חיוניות לשיפור הביצועים שזקוקות להערות האלה, מומלץ מאוד לרשום באופן מפורש את השיטות עם JNI RegisterNatives במקום להסתמך על "Discovery" מבוסס-שם של שיטות נייטיב. כדי לקבל ביצועים אופטימליים בהפעלת האפליקציה, מומלץ לכלול בפרופיל הבסיס את הגורמים שמפעילים את השיטות @FastNative או @CriticalNative. החל מ-Android 12, קריאה לשיטה נייטיב @CriticalNative משיטה מנוהלת שעברה הידור תהיה דומה זול כמו קריאה שאינה מוטבעת ב-C/C++ כל עוד כל הארגומנטים מתאימים לרישום (לדוגמה עד 8 ארגומנטים של אינטגרל ועד 8 ארגומנטים של נקודה צפה (floating-point) בזרוע 64).

לפעמים עדיף לפצל את השיטה המקורית לשתיים, שיטה מהירה מאוד נכשלים ועוד צוות שמטפל במקרים האיטיים. לדוגמה:

Kotlin

fun writeInt(nativeHandle: Long, value: Int) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value)
    }
}

@CriticalNative
external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean

external fun nativeWriteInt(nativeHandle: Long, value: Int)

Java

void writeInt(long nativeHandle, int value) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value);
    }
}

@CriticalNative
static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value);

static native void nativeWriteInt(long nativeHandle, int value);

שיקולים ל-64-ביט

כדי לתמוך בארכיטקטורות שמשתמשות בסמנים של 64 ביט, צריך להשתמש בשדה long במקום int כשמאחסנים מצביע למבנה מותאם בשדה Java.

תכונות שלא נתמכות/תאימות לאחור

יש תמיכה בכל התכונות של JNI 1.6, מלבד:

  • ההטמעה של DefineClass לא מוטמעת. ב-Android לא משתמשים קטעי קוד של Java או קובצי מחלקה, כך שההעברה של נתוני מחלקה בינארית לא עובדת.

כדי לשמור על תאימות לאחור לגרסאות Android ישנות יותר, כדאי לדעת:

  • חיפוש דינמי של פונקציות נייטיב

    עד Android 2.0‏ (Eclair), התו '$' לא הומר כראוי ל-‎"_00024" במהלך חיפושים של שמות שיטות. כדי לעקוף את הבעיה הזו, צריך להשתמש ברישום מפורש או להעביר את השיטות הילידיות מתוך הכיתות הפנימיות.

  • ניתוק שרשורים

    עד ל-Android 2.0 (Eclair), לא ניתן היה להשתמש ב-pthread_key_create בונה שנמנעים מ"יש לנתק את ה-thread לפני יציאה" לסמן. (סביבת זמן הריצה משתמשת גם בפונקציית pthread destructor), אז זה יהיה מרוץ שצריך לראות למי להתקשר קודם.)

  • הפניות גלובליות חלשות

    עד Android 2.2 (Froyo), לא הוטמעו הפניות גלובאליות חלשות. גרסאות ישנות יותר יידחו בתוקף ניסיונות להשתמש בהן. אפשר להשתמש בקבצים הקבועים של גרסאות פלטפורמת Android כדי לבדוק את התמיכה.

    עד ל-Android 4.0 (Ice Cream סנדוויץ'), ניתן להשתמש בהפניות גלובליות חלשים רק מועבר אל NewLocalRef, NewGlobalRef ו- DeleteWeakGlobalRef. (המפרט מעודד מאוד למתכנתים ליצור התייחסויות קשות לשאלות חלשות לפני שהם עושים זאת כל דבר איתם, כך שזה לא אמור להגביל כלל).

    החל מ-Android 4.0 (Ice Cream Sandwich), אפשר להשתמש בהפניות גלובליות חלשות כמו בכל הפניות אחרות של JNI.

  • הפניות מקומיות

    עד ל-Android 4.0 (Icecream סנדוויץ'), ההפניות המקומיות היו למעשה מצביעים ישירים. גלידה הוסיפה את העקיפה שנדרשים כדי לתמוך בסוחרי אשפה טובים יותר, אבל פירוש הדבר הוא מבאגי JNI שלא ניתן לזהות בגרסאות ישנות יותר. לפרטים נוספים, ראו שינויים בהפניות המקומיות של JNI ב-ICS.

    בגרסאות Android שקודמות ל-Android 8.0, מספר ההפניות המקומיות מוגבל לגרסה ספציפית. החל מגרסה 8.0 של Android, יש תמיכה בהפניות מקומיות ללא הגבלה.

  • קובעים את סוג קובץ העזר עם GetObjectRefType

    עד Android 4.0 (Icecream סנדוויץ'), כתוצאה מהשימוש ב- מצביעים ישירים (ראו למעלה), לא היה אפשר להטמיע GetObjectRefType נכון. במקום זאת, השתמשנו בהווריסטיקה שבדקה את טבלת המשתנים הגלובליים החלשים, את הארגומנטים, את טבלת המשתנים המקומיים ואת טבלת המשתנים הגלובליים בסדר הזה. בפעם הראשונה שהאפליקציה מצאה מצביע ישיר, הוא ידווח שההפניה שלך הייתה מהסוג הזה שצריך לבחון. לדוגמה, אם התקשרת אל GetObjectRefType ב-jclass גלובלי, יהיה זהה ל-jclass שמועבר כארגומנט מרומז מקבלים JNILocalRefType במקום JNIGlobalRefType.

  • @FastNative ו-@CriticalNative

    עד Android 7, המערכת התעלמה מהערות האופטימיזציה האלה. אי ההתאמה של ABI ל-@CriticalNative תוביל לסידור שגוי של הארגומנטים ולקריסות.

    חיפוש דינמי של פונקציות נייטיב עבור @FastNative ו השיטות של @CriticalNative לא הוטמעו ב-Android 8-10, מכיל באגים ידועים ב-Android 11. שימוש באופטימיזציות האלה בלי רישום מפורש ב-JNI RegisterNatives עלול לגרום לקריסות ב-Android 8 עד 11.

  • FindClass זורקת ClassNotFoundException

    כדי לשמור על תאימות לאחור, כשהמערכת לא מוצאת כיתה באמצעות FindClass, היא גורמת ל-Android להקפיץ את השגיאה ClassNotFoundException במקום NoClassDefFoundError. ההתנהגות הזו תואמת לממשק ה-API של Java Re Recallion. Class.forName(name)

שאלות נפוצות: למה מוצג לי UnsatisfiedLinkError?

כשעובדים על קוד מקורי, לפעמים יש כשל כזה:

java.lang.UnsatisfiedLinkError: Library foo not found

במקרים מסוימים, המשמעות היא בדיוק מה שכתוב – הספרייה לא נמצאה. לחשבון מקרים אחרים שבהם הספרייה קיימת אבל לא ניתן לפתוח אותה על ידי dlopen(3), וגם פרטי הכשל מופיעים בהודעה המפורטת של החריגה.

סיבות נפוצות לבעיות עם הכיתוב "הספרייה לא נמצאה" חריגים:

  • הספרייה לא קיימת או שאינה נגישה לאפליקציה. כדאי להשתמש adb shell ls -l <path> כדי לבדוק את הנוכחות שלהם והרשאות.
  • הספרייה לא נבנתה באמצעות ה-NDK. כתוצאה מכך ותלויות בפונקציות או בספריות שלא קיימות במכשיר.

מחלקה אחרת של UnsatisfiedLinkError כשלים נראית כך:

java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

ב-Logcat יופיעו:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

זה אומר שסביבת זמן הריצה ניסתה למצוא שיטה תואמת, אבל הפעולה נכשלה. בין הסיבות הנפוצות לכך:

  • הספרייה לא נטענת. צריך לבדוק את פלט ה-Logcat של הודעות על טעינת הספרייה.
  • השיטה לא נמצאה עקב אי-התאמה בשם או בחתימה. הזה נגרמת בדרך כלל על ידי:
    • בחיפוש שיטה מדורגת, אין הצהרה על פונקציות C++ עם extern "C" ומתאימים החשיפה (JNIEXPORT). שימו לב שלפני גלידה כריך, המאקרו JNIEXPORT היה שגוי, לכן שימוש ב-GCC חדש עם jni.h ישן לא יפעל. אפשר להשתמש ב-arm-eabi-nm כדי לראות את הסמלים כפי שהם מופיעים בספרייה. אם הם מסתכלים מעוות (בערך כמו _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass ולא Java_Foo_myfunc), או אם סוג הסמל הוא 't' קטנה במקום 'T' באותיות רישיות, אז צריך צריך לתקן את ההצהרה.
    • ברישום מפורש, שגיאות קלות בזמן הזנת חתימה של שיטות. חשוב לוודא שהנתונים שאתם מעבירים לקריאה לרישום תואמים לחתימה בקובץ היומן. חשוב לזכור ש-'B' הוא byte ו-'Z' הוא boolean. רכיבי השם של הכיתה בחתימות מתחילים ב-'L' ומסתיימים ב-';', משתמשים ב-'/' כדי להפריד בין שמות חבילות/כיתות, ומשתמשים ב-'$' כדי להפריד בין שמות של כיתות פנימיות (למשל, Ljava/util/Map$Entry;).

שימוש ב-javah כדי ליצור כותרות JNI באופן אוטומטי יכול לעזור למנוע בעיות מסוימות.

שאלות נפוצות: למה FindClass לא מצאה את הכיתה שלי?

(רוב העצות האלה רלוונטיות באותה מידה גם לכשל במציאת שיטות עם GetMethodID או GetStaticMethodID, או שדות עם GetFieldID או GetStaticFieldID).

צריך לוודא שהמחרוזת של שם הכיתה בפורמט הנכון. שיעור JNI השמות מתחילים בשם החבילה ומופרדים בלוכסנים, כמו java/lang/String. אם אתם מחפשים מחלקה של מערך, צריך להתחיל במספר המתאים של סוגריים מרובעים חייב להקיף את המחלקה באותיות 'L' ו-';', כלומר מערך חד-ממדי של String יהיה [Ljava/lang/String;. אם אתם מחפשים כיתה פנימית, צריך להשתמש ב-'$' במקום '.'. באופן כללי, שימוש ב-javap בקובץ ה- .class הוא דרך טובה לגלות השם הפנימי של הכיתה שלכם.

אם מפעילים כיווץ קוד, צריך לוודא להגדיר איזה קוד לשמור. מתבצעת הגדרה חשוב לשמור על כללי שמירה מתאימים כי מכווץ הקוד עלול להסיר מחלקות, methods, או שדות שמשתמשים בהם רק מ-JNI.

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

    Foo.myfunc(Native Method)
    Foo.main(Foo.java:10)

השיטה העליונה ביותר היא Foo.myfunc. FindClass מוצא את האובייקט ClassLoader שמשויך לאובייקט Foo והם משתמשים בו.

בדרך כלל זה עושה את מה שרוצים. אם תיצרו את השרשור בעצמכם (אולי באמצעות קריאה ל-pthread_create ואז צירוף באמצעות AttachCurrentThread), לא יהיו מסגרות סטאק מהאפליקציה. אם תתקשר למספר FindClass מהשרשור הזה, JavaVM יתחיל ב'מערכת' הכלי לטעינת כיתות, במקום זאת המשויך באפליקציה, ולכן ניסיונות למצוא כיתות ספציפיות לאפליקציה ייכשלו.

יש כמה דרכים לעקוף את הבעיה הזו:

  • מבצעים את השאילתות של FindClass פעם אחת, ב-JNI_OnLoad, ושומרים במטמון את הפניות הכיתתיות לשימוש מאוחר יותר. כל השיחות מסוג FindClass שיבוצעו כחלק מביצוע JNI_OnLoad ישתמש בטעינת הכיתה שמשויכת אל את הפונקציה שנקראת System.loadLibrary כלל מיוחד, כדי להפוך את אתחול הספרייה לנוח יותר). אם קוד האפליקציה טוען את הספרייה, FindClass ישתמש בטוען הכיתות הנכון.
  • מעבירים מופע של המחלקה לפונקציות שצריכות באמצעות הצהרה על השיטה המקורית לקחת ארגומנט Class לאחר מכן מעבירים את Foo.class פנימה.
  • שומרים במטמון הפניה לאובייקט ClassLoader במקום נוח, ומפעילים קריאות ל-loadClass ישירות. זה דורש מאמץ.

שאלות נפוצות: איך משתפים נתונים גולמיים עם קוד מקורי?

ייתכן שתיתקלו במצב שבו תצטרכו לגשת לפריט גדול מאגר של נתונים גולמיים מקוד מנוהל וגם מקוד נייטיב. דוגמאות נפוצות לכלול מניפולציה של מפות סיביות או דגימות צליל. יש שתי פלטפורמות גישות בסיסיות.

אפשר לאחסן את הנתונים בbyte[]. כך אפשר לגשת במהירות רבה מקוד מנוהל. אבל בהיבט המקומי, אין כל אפשרות לגשת לנתונים מבלי להעתיק אותם. בהטמעות מסוימות, הפונקציות GetByteArrayElements ו-GetPrimitiveArrayCritical יחזירו הפניות בפועל לנתונים הגולמיים ב-heap המנוהל, אבל בהטמעות אחרות הן יוקצו מאגר ב-heap המקורי ויעתיקו את הנתונים.

האפשרות החלופית היא לאחסן את הנתונים במאגר בייט ישיר. האלה אפשר ליצור באמצעות java.nio.ByteBuffer.allocateDirect, או את הפונקציה JNI NewDirectByteBuffer. בניגוד לרגיל בתהליך אגירת נתונים של בייטים, האחסון לא מוקצה לערימה המנוהלת, תמיד ניגשים אליו ישירות מקוד נייטיב (מקבלים את הכתובת עם GetDirectBufferAddress). בהתאם למידת הישירה הוטמעה גישה למאגר נתונים זמני של בייטים, בגישה לנתונים מקוד מנוהל יכול להיות איטי מאוד.

הבחירה באיזה אופן להשתמש תלויה בשני גורמים:

  1. האם רוב הגישות לנתונים יתרחשו מקוד שנכתב ב-Java או ב-C/C++ ?
  2. אם הנתונים מועברים בסופו של דבר ל-API של מערכת, באיזה פורמט הם צריכים להיות? (לדוגמה, אם הנתונים מועברים בסופו של דבר לפונקציה שמקבלת byte[], יכול להיות שלא כדאי לבצע עיבוד ישירות ב-ByteBuffer).

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