שימוש ב- JNA כדי לגשת לספריות דינמיות מקומיות

1. סקירה כללית

במדריך זה נראה כיצד להשתמש בספריית Java Native Access (בקיצור JNA) כדי לגשת לספריות מקוריות מבלי לכתוב קוד JNI (Java Native Interface).

2. מדוע JNA?

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

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

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

  • נדרש ממפתחים לכתוב C / C ++ "קוד דבק" כדי לגשר על Java וקוד מקורי
  • דורש ערכת כלים קומפילציה וקישור מלאה הזמינה לכל מערכת יעד
  • השמדת ערכים אל מחוץ ל- JVM ואינם מאמצים אותם היא משימה מייגעת ונוטה לטעויות
  • דאגות משפטיות ותמיכה בעת ערבוב Java וספריות מקומיות

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

כמובן שיש כמה פשרות:

  • איננו יכולים להשתמש ישירות בספריות סטטיות
  • איטי יותר בהשוואה לקוד JNI בעבודת יד

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

3. הגדרת פרויקט JNA

הדבר הראשון שעלינו לעשות כדי להשתמש ב- JNA הוא להוסיף את התלות שלו לפרויקטים שלנו pom.xml:

 net.java.dev.jna פלטפורמת jna 5.6.0 

הגרסה האחרונה של פלטפורמת jna ניתן להוריד מ Maven Central.

4. שימוש ב- JNA

השימוש ב- JNA הוא תהליך דו-שלבי:

  • ראשית, אנו יוצרים ממשק Java המרחיב את JNA סִפְרִיָה ממשק לתיאור השיטות והסוגים המשמשים בעת קריאה לקוד המקורי של היעד
  • לאחר מכן, אנו מעבירים ממשק זה ל- JNA המחזיר יישום קונקרטי של ממשק זה בו אנו משתמשים כדי להפעיל שיטות מקוריות

4.1. שיטות שיחה מספריית C הרגילה

לדוגמא ראשונה, נשתמש ב- JNA כדי לקרוא ל- קוש פונקציה מספריית C הרגילה, הזמינה ברוב המערכות. שיטה זו אורכת א לְהַכפִּיל טיעון ומחשב את הקוסינוס ההיפרבולי שלו. תוכנית A-C יכולה להשתמש בפונקציה זו רק על ידי הכללת ה- קובץ הכותרת:

# כלול # כלול int main (int argc, char ** argv) {v כפול = cosh (0.0); printf ("תוצאה:% f \ n", v); }

בואו ניצור את ממשק Java הדרוש כדי לקרוא לשיטה זו:

ממשק ציבורי CMath מרחיב את הספרייה {cosh כפול (ערך כפול); } 

לאחר מכן אנו משתמשים ב- JNA יָלִיד בכיתה ליצירת יישום קונקרטי של ממשק זה כדי שנוכל להתקשר ל- API שלנו:

CMath lib = Native.load (Platform.isWindows ()? "Msvcrt": "c", CMath.class); תוצאה כפולה = lib.cosh (0); 

החלק המעניין באמת כאן הוא השיחה ל לִטעוֹן() שיטה. נדרשים שני טיעונים: שם הספרייה הדינמית וממשק Java המתאר את השיטות בהן נשתמש. זה מחזיר יישום קונקרטי של ממשק זה, ומאפשר לנו להתקשר לכל אחת מהשיטות שלה.

כעת, שמות ספריות דינמיות בדרך כלל תלויים במערכת, וספריית תקן C אינה יוצאת דופן: libc.so ברוב המערכות מבוססות לינוקס, אבל msvcrt.dll ב- Windows. זו הסיבה שהשתמשנו ב- פּלַטפוֹרמָה שיעורי עוזר, הכלולים ב- JNA, כדי לבדוק באיזו פלטפורמה אנו רצים ובחירת שם הספרייה המתאים.

שימו לב שאנחנו לא צריכים להוסיף את ה- .כך אוֹ .dll כמו שהם משתמעים. כמו כן, עבור מערכות מבוססות לינוקס, איננו צריכים לציין את קידומת "lib" שהיא סטנדרטית עבור ספריות משותפות.

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

ממשק ציבורי CMath מרחיב את הספרייה {CMath INSTANCE = Native.load (Platform.isWindows ()? "msvcrt": "c", CMath.class); כפול קוש (ערך כפול); } 

4.2. מיפוי סוגי בסיסים

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

  • char => בתים
  • קצר => קצר
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • ארוך ארוך => ארוך
  • לצוף => לצוף
  • כפול => כפול
  • char * => מחרוזת

מיפוי שעשוי להראות מוזר הוא זה המשמש עבור הילידים ארוך סוּג. הסיבה לכך היא שב- C / C ++, ה- ארוך סוג עשוי לייצג ערך 32 או 64 סיביות, תלוי אם אנו פועלים על מערכת 32 או 64 סיביות.

כדי לטפל בבעיה זו, JNA מספקת את NativeLong סוג, המשתמש בסוג המתאים בהתאם לארכיטקטורה של המערכת.

4.3. מבנים ואיגודים

תרחיש נפוץ נוסף הוא התמודדות עם ממשקי API מקוריים הצפויים למצביע לחלקם struct אוֹ הִתאַחֲדוּת סוּג. בעת יצירת ממשק Java כדי לגשת אליו, הארגומנט או ערך ההחזרה המתאימים חייבים להיות מסוג Java המתרחב מבנה או איחוד, בהתאמה.

למשל, בהתחשב במבנה C זה:

struct foo_t {int field1; int field2; char * field3; };

מחלקת עמיתים Java שלה תהיה:

@FieldOrder ({"field1", "field2", "field3"}) מחלקה ציבורית FooType מרחיב מבנה {int field1; int field2; שדה מחרוזת 3; };

JNA דורש את @FieldOrder ביאור כדי שיוכל לסדר נתונים כראוי למאגר זיכרון לפני שישתמש בהם כטיעון לשיטת היעד.

לחלופין, אנו יכולים לעקוף את ה- getFieldOrder () שיטה לאותו אפקט. כאשר מכוונים לארכיטקטורה / פלטפורמה אחת, השיטה הקודמת בדרך כלל טובה מספיק. אנו יכולים להשתמש באחרונים כדי להתמודד עם בעיות יישור בין פלטפורמות, שלעתים דורשות הוספת שדות ריפוד נוספים.

איגודים לעבוד באופן דומה, למעט כמה נקודות:

  • אין צורך להשתמש ב- @FieldOrder ביאור או יישום getFieldOrder ()
  • אנחנו חייבים להתקשר setType () לפני שקוראים לשיטה הילידית

בואו נראה איך לעשות זאת בדוגמה פשוטה:

המעמד הציבורי MyUnion מרחיב את האיחוד {public String foo; בר כפול ציבורי; }; 

עכשיו, בואו נשתמש MyUnion עם ספרייה היפותטית:

MyUnion u = MyUnion חדש (); u.foo = "מבחן"; u.setType (String.class); lib.some_method (u); 

אם שניהם foo ו בָּר היכן מאותו סוג, נצטרך להשתמש בשם השדה במקום:

u.foo = "מבחן"; u.setType ("foo"); lib.some_method (u);

4.4. באמצעות מצביעים

JNA מציעה מַצבִּיעַ הפשטה המסייעת להתמודד עם ממשקי API שהוכרזו באמצעות מצביע לא מוקלט - בדרך כלל א בטל *. מחלקה זו מציעה שיטות המאפשרות גישה לקריאה וכתיבה למאגר הזיכרון המקורי הבסיסי, שיש בו סיכונים ברורים.

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

בהנחה שאנחנו יודעים מה אנחנו עושים (כמו תמיד), בואו נראה כיצד נוכל להשתמש במוכר malloc () ו חינם() מתפקד עם JNA, המשמש להקצאה ושחרור מאגר זיכרון. ראשית, בואו ניצור שוב את ממשק העטיפה שלנו:

ממשק ציבורי StdC מרחיב את הספרייה {StdC INSTANCE = // ... יצירת מופע הושמטה מצביע malloc (ארוך n); ללא ריק (מצביע p); } 

עכשיו, בואו נשתמש בו להקצאת חיץ ונשחק איתו:

StdC lib = StdC.INSTANCE; מצביע p = lib.malloc (1024); p.setMemory (0l, 1024l, (byte) 0); lib.free (p); 

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

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

4.5. טיפול בשגיאות

גרסאות ישנות של ספריית C הרגילה השתמשו בגלובלית ארנו משתנה כדי לאחסן את הסיבה ששיחה מסוימת נכשלה. למשל, זה טיפוסי לִפְתוֹחַ() שיחה תשתמש במשתנה הגלובלי הזה ב- C:

int fd = פתוח ("נתיב כלשהו", O_RDONLY); אם (fd <0) {printf ("פתיחה נכשלה: errno =% d \ n", errno); יציאה (1); }

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

// ... קטע מתוך bits / errno.h ב- Linux # הגדר errno (* __ errno_location ()) // ... קטע מתוך Visual Studio # הגדר errno (* _errno ())

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

כל שיטה שהוכרזה במעטפת מתממשקת עם זורק LastErrorException יכלול באופן אוטומטי בדיקת שגיאה לאחר שיחה מקורית. אם היא מדווחת על שגיאה, JNA תזרוק LastErrorException, הכולל את קוד השגיאה המקורי.

בואו נוסיף כמה שיטות ל- StdC ממשק עטיפה שהשתמשנו בו בעבר כדי להציג תכונה זו בפעולה:

ממשק ציבורי StdC מרחיב את הספרייה {// ... שיטות אחרות שהושמטו int open (נתיב מחרוזת, דגלים int) זורק LastErrorException; int close (int fd) זורק LastErrorException; } 

עכשיו, אנחנו יכולים להשתמש לִפְתוֹחַ() בסעיף ניסיון / תפיסה:

StdC lib = StdC.INSTANCE; int fd = 0; נסה {fd = lib.open ("/ some / path", 0); // ... השתמש ב- fd} catch (LastErrorException err) {// ... טיפול בשגיאות} לבסוף {if (fd> 0) {lib.close (fd); }} 

בתוך ה לתפוס נוכל לחסום LastErrorException.getErrorCode () כדי לקבל את המקור ארנו ערך והשתמש בו כחלק מהלוגיקה לטיפול בשגיאות.

4.6. טיפול בהפרות גישה

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

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

  • הגדרת ה- jna.protected נכס מערכת ל נָכוֹן
  • יִעוּד Native.setProtected (נכון)

לאחר שהפעלנו את המצב המוגן הזה, JNA יתפוס שגיאות בהפרת גישה שבדרך כלל יביאו להתרסקות ויזרקו א java.lang. שגיאה יוצא מן הכלל. אנו יכולים לוודא שהדבר פועל באמצעות מַצבִּיעַ מאותחל עם כתובת לא חוקית ומנסה לכתוב לה נתונים:

Native.setProtected (נכון); מצביע p = מצביע חדש (0l); נסה {p.setMemory (0, 100 * 1024, (בתים) 0); } לתפוס (שגיאת שגיאה) {// ... טיפול בשגיאות הושמט} 

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

5. מסקנה

במאמר זה, הראינו כיצד להשתמש ב- JNA כדי לגשת לקוד מקורי בקלות בהשוואה ל- JNI.

כרגיל, כל הקוד זמין ב- GitHub.