wrapper של ספרייה לממשקי API של Android חלק מ-Android Game Development Kit.

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

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

הורדה

מורידים את הארכיון של ה-wrapper של הספרייה ופורקים את התוכן שלו בספרייה לבחירתכם.

תחביר

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

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 כדי ליצור קוד wrapper של C. אפשר לציין כמה JARs, לדוגמה:
-i first_library.jar -i second_library.jar...
-o output-path מיקום מערכת הקבצים עבור הקוד שנוצר.
-c config-file נתיב של מערכת קבצים לקובץ התצורה של wrapper של הספרייה. לפרטים, עיינו בקטע Configuration (הגדרה).
-fa allow-list-file נתיב לקובץ סינון שבו אפשר לציין סמלים שהכלי יבצע לעטוף. מידע נוסף מופיע בקטע מסנן.
-fb block-list-file נתיב לקובץ מסנן שמכיל סמלים שלא נכללים באריזה. עבור ראו את הקטע מסנן.
--skip_deprecated_symbols המדיניות נותנת לכלי ה-wrapper הנחיה לדלג @הוצא משימוש סמלים.

קובץ תצורה של Wrapper

תצורת ה-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 שלכם צריך ליצור wrapper מקורי עבור ה-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);

לתרחיש מפורט לדוגמה, תוכלו להיעזר ב-Library wrapper guide (מדריך wrapper של הספרייה).

פרטי הכלי

בקטעים הבאים מוצג מידע מפורט לגבי ה-wrapper של הספרייה החדשה.

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

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

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

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

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

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

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

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

לדוגמה, כששיטת Java Singleton.getInstance() תמיד מחזירה את הערך אבל הפונקציה המקבילה בצד 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 שסופק, ה-wrapper של הספרייה יוצר יישום בסיסי שכולל מצביע אטום ואת השיטות הבאות:

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

פרטי יצירת הקוד

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

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

שיעורים

מחלקות מיוצגות כמצביעים אטומים ב-C:

C/C++

typedef struct MyClass_ MyClass;

Java

public class MyClass { ... }

למופעים של מצביעים אטומים יש הפניה כ-wrappers. כלי ה-wrapper יוצרות פונקציות תמיכה נוספות לכל כיתה. בדוגמה שלמעלה מחלקה 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) { ... }
}

שיטות

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

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 של הספרייה לא כולל ישירות סוגים גנריים. במקום זאת, יוצרת wrappers להמחשות גנריות של סוגים.

לדוגמה, כשמחלקה MyGeneric<T> קיימת ב-API, יש יצירתיות של הכיתה הזו, כמו MyGeneric<Integer> MyGeneric<String>, נוצרים wrappers לשתי ההמחשות האלה. הזה המשמעות היא שלא ניתן ליצור אובייקטים חדשים מסוג 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);

מגבלות

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

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

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

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

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

  • שיטות של תבניות

  • שדות שאינם static final int ו-static final String

  • מערכים

התנגשות אפשרית של שמות

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

תמיכה

אם יש בעיה עם ה-wrapper של הספרייה, נשמח לדעת.

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