‫Library wrapper for Android APIs   Part of Android Game Development Kit.

העטיפה של הספרייה היא כלי של שורת הפקודה (CLI) שיוצר קוד עטיפה בשפת C עבור ממשקי Android API שנכתבו ב-Java. אפשר להשתמש בקוד הזה באפליקציות מקוריות ל-Android כדי לקרוא לממשקי Java API בלי ליצור באופן ידני ממשק Java מקורי, או JNI. הכלי הזה יכול לפשט את הפיתוח של אפליקציות ל-Android שנכתבות בעיקר בשפות C או C++.

הכלי פועל על ידי יצירת קוד C עבור הסמלים הציבוריים שנכללים בקובצי Java Archive ‏ (JAR) שאתם מספקים, או מחלקות שמוגדרות בקובץ ההגדרות של הכלי, או שניהם. הקוד שנוצר על ידי הכלי לא מחליף את ממשקי ה-API של Java, אלא משמש כגשר בין קוד C לבין Java. האפליקציה עדיין צריכה לכלול בפרויקט את ספריות Java שעוטפות.

הורדה

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

תחביר

כלי העטיפה של הספריות כולל את תחביר שורת הפקודה הבא:

java -jar lw.jar \
  [-i jar-file-to-be-wrapped] \
  [-o output-path] \
  [-c config-file] \
  [-fa allow-list-file] \
  [-fb block-list-file] \
  [--skip_deprecated_symbols]
פרמטר תיאור
-i jar-file-to-be-wrapped קובץ JAR כדי ליצור קוד C wrapper. אפשר לציין כמה קובצי JAR, לדוגמה:
-i first_library.jar -i second_library.jar...
-o output-path מיקום במערכת הקבצים של הקוד שנוצר.
-c config-file נתיב במערכת הקבצים לקובץ התצורה של wrapper הספרייה. פרטים נוספים מופיעים בקטע בנושא הגדרה.
-fa allow-list-file נתיב לקובץ מסנן שבו אפשר לציין סמלים שהכלי יקיף. פרטים נוספים זמינים בקטע Filter.
-fb block-list-file נתיב לקובץ מסנן שמכיל סמלים שמוחרגים מהעטיפה. לפרטים נוספים, אפשר לעיין בקטע Filter.
--skip_deprecated_symbols הוראה לכלי העטיפה לדלג על סמלים מסוג @Deprecated.

קובץ התצורה של הראפר

ההגדרה של wrapper הספרייה היא קובץ JSON שמאפשר לכם לשלוט בתהליך יצירת הקוד. הקובץ צריך להיות מובנה בפורמט הבא:

{
  // An array of type-specific configs. A type config is useful when a user wants to map
  // a Java type to a manually defined C type without generating the code. For example, when a developer
  // has their own implementation of the "java.lang.String" class, they can tell the generator to use it
  // instead of generating it.
  "type_configs": [
    {
      // [Required] Name of a fully qualified Java type.
      "java_type": "java.lang.String",
      // The C type that the java_type will be mapped to.
      "map_to": "MyOwnStringImplementation",
      // A header file that contains the declaration of the "map_to" type.
      "source_of_definition": "my_wrappers/my_own_string_implementation.h",
      // Controls if a value should be passed by pointer or value.
      "pass_by_value": false
    }
  ],
  // An array of package-specific configs.
  "package_configs": [
    {
      // [Required] A name of a Java package that this section regards. A wildchar * can be used at the
      // end of the package name to apply this config to all packages whose name starts with this value.
      "package_name": "androidx.core.app*",
      // A subdirectory relative to the root directory where the generated code will be located.
      "sub_directory": "androidx_generated/",
      // If true, the generated file structure reflects the package name. For example, files generated
      // for the package com.google.tools will be placed in the directory com/google/tools/.
      "file_location_by_package_name": true,
      // A prefix added to all class names from this package.
      "code_prefix": "Gen",
      // A prefix added to all generated file names from this package.
      "file_prefix": = "gen_"
    }
  ],
  // An array of manually defined classes for wrapping. Defining classes manually is useful when a
  // jar file with desired classes are not available or a user needs to wrap just a small part of an SDK.
  "custom_classes": [
    {
      // [Required] A fully-qualified Java class name. To define inner class, use symbol "$", for example
      // "class com.example.OuterClass$InnerClass".
      "class_name": "class java.util.ArrayList<T>",
      // List of methods.
      "methods": [
        "ArrayList()", // Example of a constructor.
        "boolean add(T e)", // Example of a method that takes a generic parameter.
        "T get(int index)", // Example of a method that returns a generic parameter.
        "int size()" // Example of parameterless method.
      ]
    },
  ]
}

סינון קבצים

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

java-symbol-name java-jni-type-signature

קובץ מסננים לדוגמה:

# Class filter
java.util.ArrayList Ljava.util.ArrayList;

# Method filter
java.util.ArrayList.lastIndexOf (Ljava.lang.Object;)I

# Field filter
android.view.KeyEvent.KEYCODE_ENTER I

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

תרחיש לדוגמה

נניח שצריך לעטוף את קובץ ה-JAR‏ ChatLibrary.jar שמכיל את המחלקה הבאה:

public class ChatManager {
  public static void sendMessage(int userId, String message) {...}
}

בפרויקט C צריך ליצור עטיפה מקורית ל-JAR הזה, כדי שאפליקציית Android המקורית תוכל להפעיל אותו במהלך זמן הריצה. כדי ליצור את הקוד הזה, משתמשים ב-wrapper של הספרייה עם הפקודה הבאה:

java -jar lw.jar -i ChatLibrary.jar -o ./generated_code/

הפקודה שלמעלה יוצרת קוד מקור C בספרייה ./generated_code. הקובץ שנוצר, chat_manager.h, מכיל קוד שדומה לקוד הבא, ומאפשר לכם להפעיל את הספרייה בפרויקט:

#include "java/lang/string.h"

typedef struct ChatManager_ ChatManager;
void ChatManager_sendMessage(int32_t user_id, String* message);

דוגמה מפורטת לתרחיש מופיעה במדריך לעטיפת ספריות.

פרטי הכלי

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

מבנה ספריית הפלט

כל קובצי המקור והכותרות של C נמצאים בספריות משנה שמשקפות את שם החבילה של מחלקת Java העטופה. לדוגמה, קוד wrapper עבור קובץ ה-JAR שצוין java.lang.Integer נוצר בספרייה ./java/lang/integer.[h/cc].

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

מחזור החיים של אובייקט

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

  • על ידי שימוש בהפניה קיימת של JNI באמצעות קריאה לפונקציה MyClass_wrapJniReference(jobject jobj). הפונקציה לא מקבלת בעלות על ההפניה שסופקה, אלא יוצרת הפניה משלה ל-JNI גלובלי.
  • יצירת אובייקט חדש, ששווה לקריאה של בנאי ב-Java: ‏ MyClass_construct()
  • לדוגמה, אפשר להחזיר עטיפה חדשה מפונקציה: Score* Leaderboard_getScore(Leaderboard* instance, String* leaderboard_name)

צריך להשמיד את כל העטיפות כשאין בהן יותר שימוש. כדי לעשות את זה, צריך להפעיל את הפונקציה הייעודית destroy() MyClass_destroy(MyClass* instance).

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

לדוגמה, אם השיטה Singleton.getInstance() ב-Java תמיד מחזירה את אותו מופע, הפונקציה המקבילה בצד C תיצור מופע חדש של wrapper לאותו מופע Java:

Singleton* singleton_a = Singleton_getInsance();
Singleton* singleton_b = Singleton_getInsance();

// singleton_a and singleton_b are different pointers, even though they represent the same Java instance.

טיפול בכיתות שלא מקושרות

אם לא ניתן למצוא כיתה בקובץ JAR שסופק, עוטף הספרייה יוצר הטמעה בסיסית שמורכבת ממצביע אטום ומהשיטות הבאות:

  • wrapJniReference()
  • getJniReference()
  • destroy()

פרטים על יצירת קוד

כשמריצים את עטיפת הספרייה, היא יוצרת קוד C על סמך הסמלים הציבוריים בקובצי ה-JAR שסיפקתם לכלי. יכול להיות שיהיו הבדלים בין קוד C שנוצר לבין קוד Java שעבר עטיפה. לדוגמה, שפת C לא תומכת בתכונות כמו OOP,‏ generic types,‏ method overloading או בתכונות אחרות של Java.

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

שיעורים

הכיתות מיוצגות כמצביעים אטומים ב-C:

C/C++‎

typedef struct MyClass_ MyClass;

Java

public class MyClass { ... }

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

// Wraps a JNI reference with MyClass. The 'jobj' must represent MyClass on the Java side.
MyClass* MyClass_wrapJniReference(jobject jobj);

// Return JNI reference associated with the 'MyClass' pointer.
jobject MyClass_getJniReference(const MyClass* object);

// Destroys the object and releases underlying JNI reference.
void MyClass_destroy(const MyClass* object);

יצרנים

מחלקות עם בנאים ציבוריים או בנאים שמוגדרים כברירת מחדל מיוצגות באמצעות פונקציות מיוחדות:

C/C++‎

MyClass* MyClass_construct(String* data);

Java

public class MyClass {
  public MyClass(String data) { ... }
}

שיטות

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

C/C++‎

Result* MyClass_doAction(const MyClass* my_class_instance, int32_t action_id, String* data);
int32_t MyClass_doAction(int32_t a, int32_t b);

Java

public class MyClass {
  public Result doAction(int actionId, String data) { ... }
  public static int doCalculations(int a, int b) { ... }
}

מחלקות פנימיות

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

C/C++‎

typedef struct MyClass_InnerClass_ MyClass_InnerClass;

Java

public class MyClass {
  public class InnerClass {...}
}

שיטות של מחלקה פנימית

שיטות של מחלקה פנימית מיוצגות כך:

C/C++‎

bool MyClass_InnerClass_setValue(MyClass_InnerClass* my_class_inner_class_instance, int32_t value);

Java

public class MyClass {
  public class InnerClass {
    public boolean setValue(int value) { ... }
  }
}

סוגים גנריים

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

לדוגמה, אם מחלקה MyGeneric<T> קיימת בממשק API, ויש שני מקרים של יצירת מופע של המחלקה הזו, כמו MyGeneric<Integer> ו-MyGeneric<String>, נוצרים עטיפות לשני המקרים האלה. המשמעות היא שלא ניתן ליצור מופעים חדשים מסוג MyGeneric<T> באמצעות הגדרות שונות של סוגים. מקרה לדוגמה:

C/C++‎

// result.h

typedef struct Result_Integer_ Result_Integer;
typedef struct Result_Float_ Result_Float;

Integer* Result_Integer_getResult(const Result_Integer* instance);
Float* Result_Float_getResult(const Result_Float* instance);

// data_processor.h

typedef struct DataProcessor_ DataProcessor;

Result_Integer* DataProcessor_processIntegerData(const DataProcessor* instance);
Result_Float* DataProcessor_processFloatData(constDataProcessor* instance);

Java

public class Result<T> {
  public T getResult();
}

public class DataProcessor {
  public Result<Integer> processIntegerData();
  public Result<Float> processFloatData();
}

הטמעה של ממשקים

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

C/C++‎

// observer.h

typedef struct Observer_ Observer;
typedef void (*Observer_onAction1Callback)();
typedef void (*Observer_onAction2Callback)(int32_t data);

Observer* Observer_implementInterface(
Observer_onAction1Callback observer_on_action1_callback,
Observer_onAction2Callback observer_on_action2_callback);

Java

public interface Observer {
  void onAction1();
  void onAction2(int data);
}

public class Subject {
  public void registerObserver(Observer observer);
}

דוגמה לשימוש:

void onAction1() {
  // Handle action 1
}

void onAction2(int32_t data) {
  // Handle action 2
}

Observer* observer = Observer_implementInterface(onAction1, onAction2);
Subject_registerObserver(subject, observer);

מגבלות

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

מבני Java שלא נתמכים

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

  • העמסת יתר של שיטות

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

  • שיטות מבוססות-תבנית

  • שדות אחרים מלבד static final int ו-static final String

  • מערכים

התנגשויות פוטנציאליות בשמות

בגלל האופן שבו מחלקות Java מיוצגות בקוד C, יכול להיות שיהיו התנגשויות בשמות במקרים נדירים מאוד. לדוגמה, מחלקה Foo<Bar> ומחלקה פנימית Bar בתוך מחלקה Foo מיוצגות על ידי אותו סמל ב-C: typedef struct Foo_Bar_ Foo_Bar;

תמיכה

אם נתקלתם בבעיה ב-wrapper של הספרייה, נשמח לשמוע על כך.

עיון בבאגים דיווח על באג
הנדסה
מסמכי תיעוד