מדריך ל- JNI (ממשק מקורי Java)

1. הקדמה

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

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

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

יכולות להיות כמה סיבות לצורך בשימוש בקוד מקורי:

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

כדי להשיג זאת, ה- JDK מציג גשר בין קוד הביץ 'הפועל ב- JVM שלנו לבין הקוד המקורי (נכתב בדרך כלל ב- C או C ++).

הכלי נקרא Java Native Interface. במאמר זה נראה כיצד לכתוב איתו קוד כלשהו.

2. איך זה עובד

2.1. שיטות ילידיות: ה- JVM עומד בקוד המחובר

Java מספקת את יָלִיד מילת מפתח המשמשת לציון כי יישום השיטה יסופק על ידי קוד מקורי.

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

  • Libs סטטי - כל קבצי ה- binar של הספרייה ייכללו כחלק מההפעלה שלנו במהלך תהליך הקישור. לפיכך, לא נצטרך יותר את ה- libs, אך זה יגדיל את גודל קובץ ההפעלה שלנו.
  • Libs משותף - להפעלה הסופית יש רק הפניות ל libs, ולא לקוד עצמו. זה מחייב שלסביבה בה אנו מריצים את הפעלתנו גישה לכל קבצי ה- libs המשמשים את התוכנית שלנו.

האחרון הוא הגיוני עבור JNI מכיוון שאיננו יכולים לערבב קוד byt וקוד מקומי לאותו קובץ בינארי.

לכן, ה- lib המשותף שלנו ישמור על הקוד המקורי בנפרד .so / .dll / .dylib קובץ (תלוי באיזו מערכת הפעלה אנו משתמשים) במקום להיות חלק מהשיעורים שלנו.

ה יָלִיד מילת המפתח הופכת את השיטה שלנו למעין שיטה מופשטת:

יליד פרטי בטל aNativeMethod ();

עם ההבדל העיקרי זה במקום להיות מיושם על ידי מחלקת Java אחרת, היא תיושם בספריה משותפת מקומית מופרדת.

טבלה עם מצביעים בזיכרון ליישום של כל השיטות המקוריות שלנו תוקם כך שניתן יהיה לקרוא להן מקוד ה- Java שלנו.

2.2. רכיבים דרושים

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

  • קוד Java - השיעורים שלנו. הם יכללו לפחות אחד יָלִיד שיטה.
  • קוד מקורי - ההיגיון האמיתי של השיטות המקוריות שלנו, בדרך כלל מקודדות ב- C או C ++.
  • קובץ כותרת JNI - קובץ כותרת זה עבור C / C ++ (כוללים / jni.h בספריית JDK) כולל את כל ההגדרות של רכיבי JNI בהם אנו עשויים להשתמש בתוכניות המקוריות שלנו.
  • מהדר C / C ++ - אנו יכולים לבחור בין GCC, Clang, Visual Studio, או כל אחר שאנחנו אוהבים ככל שהוא מסוגל ליצור ספרייה משותפת מקומית לפלטפורמה שלנו.

2.3. אלמנטים של JNI בקוד (Java ו- C / C ++)

אלמנטים של Java:

  • מילת מפתח "יליד" - כפי שכבר סיקרנו, כל שיטה המסומנת כילידה חייבת להיות מיושמת בליבון מקומי משותף.
  • System.loadLibrary (שם מחרוזת) - שיטה סטטית הטוענת ספרייה משותפת ממערכת הקבצים בזיכרון והופכת את הפונקציות המיוצאות שלה לזמינות עבור קוד ה- Java שלנו.

אלמנטים C / C ++ (רבים מהם מוגדרים בתוך jni.h)

  • JNIEXPORT - מסמן את הפונקציה לתוך ה- lib המשותף כניתן לייצוא כך שהיא תיכלל בטבלת הפונקציות, וכך JNI יכול למצוא אותה
  • JNICALL - בשילוב עם JNIEXPORT, זה מבטיח שהשיטות שלנו יהיו זמינות למסגרת JNI
  • JNIEnv - מבנה המכיל שיטות שבהן אנו יכולים להשתמש בקוד המקורי שלנו לגישה לאלמנטים של Java
  • JavaVM - מבנה המאפשר לנו לתפעל JVM פועל (או אפילו להתחיל אחד חדש) להוסיף לו אשכולות, להרוס אותו וכו '...

3. שלום עולם JNI

הַבָּא, בואו נסתכל איך JNI עובד בפועל.

במדריך זה נשתמש ב- C ++ כשפת האם וב- G ++ כמהדר וכקישור.

אנו יכולים להשתמש בכל מהדר אחר המועדף עלינו, אך כיצד להתקין את G ++ ב- Ubuntu, Windows ו- MacOS:

  • אובונטו לינוקס - הפעלת פקודה "Sudo apt-get install build-essential" בטרמינל
  • Windows - התקן את MinGW
  • MacOS - הפעל פקודה "G ++" במסוף ואם הוא עדיין לא קיים, הוא יתקין אותו.

3.1. יצירת מחלקת Java

נתחיל ליצור את תוכנית ה- JNI הראשונה שלנו על ידי יישום "שלום עולם" קלאסי.

כדי להתחיל, אנו יוצרים את מחלקת Java הבאה הכוללת את השיטה המקורית שתבצע את העבודה:

חבילה com.baeldung.jni; מחלקה ציבורית HelloWorldJNI {static {System.loadLibrary ("יליד"); } ראשי ריק סטטי ציבורי (String [] args) {HelloWorldJNI חדש (). sayHello (); } // הכריז על שיטה מקורית sayHello () שאינה מקבלת טיעונים ומחזירה בטל יליד פרטי ריק sayHello (); }

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

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

3.2. יישום שיטה ב- C ++

כעת עלינו ליצור את הטמעת השיטה המקורית שלנו ב- C ++.

בתוך C ++ נשמרים בדרך כלל ההגדרה והיישום .h ו .cpp קבצים בהתאמה.

ראשון, כדי ליצור את הגדרת השיטה עלינו להשתמש ב- דגל מהדר Java:

javac -h. HelloWorldJNI.java

זה ייצור a com_baeldung_jni_HelloWorldJNI.h קובץ עם כל השיטות המקוריות הכלולות בכיתה שהועברו כפרמטר, במקרה זה, רק אחת:

JNIEXPORT בטל JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, משימה); 

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

כמו כן, משהו מעניין שנוכל להבחין בו אנו מקבלים שני פרמטרים המועברים לפונקציה שלנו; מצביע לזרם JNIEnv; וגם אובייקט ג'אווה אליו מצורפת השיטה, המופע שלנו HelloWorldJNI מעמד.

עכשיו, עלינו ליצור חדש .cpp קובץ ליישום ה- תגיד שלום פוּנקצִיָה. זה המקום בו נבצע פעולות שמדפיסות את "שלום עולם" למסוף.

אנחנו נקרא את שמו .cpp קובץ עם אותו שם כמו ה- .h המכיל את הכותרת והוסף קוד זה ליישום הפונקציה המקורית:

JNIEXPORT בטל JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv * env, jobject thisObject) {std :: cout << "שלום מ- C ++ !!" << std :: endl; } 

3.3. קומפילציה וקישור

בשלב זה, יש לנו את כל החלקים הדרושים לנו ויש ביניהם קשר.

עלינו לבנות את הספרייה המשותפת שלנו מקוד C ++ ולהריץ אותה!

לשם כך עלינו להשתמש במהדר G ++, לא לשכוח לכלול את כותרות ה- JNI מהתקנת Java JDK שלנו.

גרסת אובונטו:

g ++ -c -fPIC -I $ {JAVA_HOME} / include -I $ {JAVA_HOME} / include / linux com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

גרסת Windows:

g ++ -c -I% JAVA_HOME% \ include -I% JAVA_HOME% \ include \ win32 com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

גרסת MacOS;

g ++ -c -fPIC -I $ {JAVA_HOME} / include -I $ {JAVA_HOME} / include / darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

ברגע שקובץ את הקוד לפלטפורמה שלנו לקובץ com_baeldung_jni_HelloWorldJNI.oעלינו לכלול אותו בספרייה משותפת חדשה. לא משנה מה שנחליט למנות אותו הוא הטיעון המועבר לשיטה System.loadLibrary.

קראנו לשלנו "יליד" ואנו נטען אותו בעת הפעלת קוד הג'אווה שלנו.

מקשר G ++ מקשר את קובצי האובייקט C ++ לספריה המגושרת שלנו.

גרסת אובונטו:

g ++ -shared -fPIC -o libnative.so com_baeldung_jni_HelloWorldJNI.o -lc

גרסת Windows:

g ++ -shared -o native.dll com_baeldung_jni_HelloWorldJNI.o -Wl, - add-stdcall-alias

גרסת MacOS:

g ++ -dynamiclib -o libnative.dylib com_baeldung_jni_HelloWorldJNI.o -lc

וזה הכל!

כעת אנו יכולים להריץ את התוכנית משורת הפקודה.

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

java -cp. -Djava.library.path = / NATIVE_SHARED_LIB_FOLDER com.baeldung.jni.HelloWorldJNI

פלט קונסולה:

שלום מ- C ++ !!

4. שימוש בתכונות JNI מתקדמות

להגיד שלום זה נחמד אבל לא מאוד שימושי. בדרך כלל נרצה להחליף נתונים בין קוד Java ו- C ++ ולנהל נתונים אלה בתוכנית שלנו.

4.1. הוספת פרמטרים לשיטות הילידים שלנו

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

סכום שלם ארוך ילידים פרטיים (int הראשון, int השני); יליד פרטי מחרוזת sayHelloToMe (שם מחרוזת, בוליאני הוא נקבה);

ואז חזור על ההליך ליצירת קובץ .h חדש עם "javac -h" כפי שעשינו קודם.

כעת צור את קובץ ה- cpp המתאים עם יישום השיטה החדשה C ++:

... JNIEXPORT jlong ​​JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers (JNIEnv * env, projectject thisObject, jint first, jint second) {std :: cout << "C ++: המספרים שהתקבלו הם:" << ראשון << "ו-" << שני NewStringUTF (fullName.c_str ()); } ...

השתמשנו במצביע * env מהסוג JNIEnv כדי לגשת לשיטות המסופקות על ידי מופע הסביבה JNI.

JNIEnv מאפשר לנו, במקרה זה, לעבור את Java מיתרים לקוד C ++ שלנו ולצאת מבלי לדאוג ליישום.

אנו יכולים לבדוק את השקילותם של סוגי Java ו- C JNI בתיעוד הרשמי של Oracle.

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

4.2. שימוש באובייקטים ושיחות שיטות Java מקוד מקורי

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

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

חבילה com.baeldung.jni; מחלקה ציבורית UserData {שם מחרוזת ציבורי; איזון כפול ציבורי; מחרוזת ציבורית getUserInfo () {return "[name] =" + name + ", [balance] =" + balance; }}

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

... UserData ציבורי מקומי createUser (שם מחרוזת, איזון כפול); יליד ציבור מחרוזת printUserData (UserData user); 

פעם נוספת, בואו ניצור את .h כותרת ואז יישום C ++ של השיטות המקוריות שלנו על חדש .cpp קוֹבֶץ:

JNIEXPORT משימה JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser (JNIEnv * env, jobject thisObject, jstring name, jdouble balance) {// צור את האובייקט של מחלקת UserData jclass userDataClass = env-> FindClass ("com / usereld / jobject newUserData = env-> AllocObject (userDataClass); // קבל את הגדרת שדות UserData jfieldID nameField = env-> GetFieldID (userDataClass, "name", "Ljava / lang / String;"); jfieldID balanceField = env-> GetFieldID (userDataClass, "balance", "D"); env-> SetObjectField (newUserData, nameField, name); env-> SetDoubleField (newUserData, balanceField, balance); להחזיר newUserData; } JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData (JNIEnv * env, jobject thisObject, jobject userData) {// מצא את המזהה של שיטת Java שייקרא jclass userDataClass = env-> GetObjectClass (userData); jmethodID methodId = env-> GetMethodID (userDataClass, "getUserInfo", "() Ljava / lang / String;"); תוצאת jstring = (jstring) env-> CallObjectMethod (userData, methodId); תוצאת החזרה; } 

שוב, אנו משתמשים ב- JNIEnv * env מצביע כדי לגשת למחלקות, האובייקטים, השדות והשיטות הדרושים מה- JVM הפועל.

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

אנחנו אפילו יוצרים מופע של הכיתה com.baeldung.jni.UserData בקוד המקורי שלנו. ברגע שיש לנו את המופע, אנו יכולים לתפעל את כל המאפיינים והשיטות שלו באופן דומה לבבואה של Java.

אנו יכולים לבדוק את כל שיטות אחרות של JNIEnv לתיעוד הרשמי של אורקל.

4. חסרונות השימוש ב- JNI

לגישור JNI יש את החסרונות שלו.

החיסרון העיקרי הוא התלות בפלטפורמה הבסיסית; למעשה אנו מאבדים את ה"כתוב פעם אחת, רץ לכל מקום " תכונה של Java. המשמעות היא שנצטרך לבנות lib חדש עבור כל שילוב חדש של פלטפורמה וארכיטקטורה שאנחנו רוצים לתמוך בה. דמיין לעצמך את ההשפעה שיש לכך על תהליך הבנייה אם נתמוך ב- Windows, Linux, Android, MacOS ...

JNI לא רק מוסיף שכבת מורכבות לתוכנית שלנו. זה גם מוסיף שכבת תקשורת יקרה בין הקוד שרץ ל- JVM לקוד המקורי שלנו: עלינו להמיר את הנתונים שהוחלפו בשני הכיוונים בין Java ו- C ++ בתהליך מרשל / ביטול רישום.

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

5. מסקנה

קומפילציה של הקוד לפלטפורמה ספציפית (בדרך כלל) הופכת אותו למהיר יותר מאשר הפעלת bytecode.

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

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

לכן זה בדרך כלל רעיון טוב השתמש ב- JNI רק במקרים בהם אין חלופה של Java.

כמו תמיד הקוד למאמר זה זמין ב- GitHub.


$config[zx-auto] not found$config[zx-overlay] not found