Otoczka biblioteki dla interfejsów Android API.   Część Android Game Development Kit.

Otoczka biblioteki to narzędzie wiersza poleceń, które generuje kod otoczki w języku C dla interfejsów API Androida napisanych w języku Java. Możesz użyć tego kodu w natywnych aplikacjach na Androida, aby wywoływać interfejsy Java API bez konieczności ręcznego tworzenia Java Native Interface, czyli JNI. To narzędzie może uprościć tworzenie aplikacji na Androida napisanych głównie w C lub C++.

Narzędzie generuje kod C dla symboli publicznych zawartych w dostarczonych plikach JAR (Java Archive) lub klasach zdefiniowanych w pliku konfiguracyjnym narzędzia albo w obu tych miejscach. Kod wygenerowany przez to narzędzie nie zastępuje interfejsów API Javy, ale działa jako pomost między kodem C a Javą. Aplikacja nadal wymaga, aby biblioteki Java, które opakowujesz, były uwzględnione w projekcie.

Pobierz

Pobierz archiwum otoki biblioteki i rozpakuj jego zawartość do wybranego katalogu.

Składnia

Narzędzie do tworzenia otoki biblioteki ma taką składnię wiersza poleceń:

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]
Parametr Opis
-i jar-file-to-be-wrapped Plik JAR do generowania kodu otoki C. Można określić wiele plików JAR, np.
-i first_library.jar -i second_library.jar...
-o output-path Lokalizacja w systemie plików wygenerowanego kodu.
-c config-file Ścieżka w systemie plików do pliku konfiguracji modułu opakowującego bibliotekę. Więcej informacji znajdziesz w sekcji Konfiguracja.
-fa allow-list-file Ścieżka do pliku filtra, w którym możesz określić symbole, które narzędzie ma otaczać. Więcej informacji znajdziesz w sekcji Filtruj.
-fb block-list-file Ścieżka do pliku filtra zawierającego symbole wykluczone z zawijania. Więcej informacji znajdziesz w sekcji Filtruj.
--skip_deprecated_symbols Nakazuje narzędziu opakowującemu pomijanie symboli @Deprecated.

Plik konfiguracji modułu

Konfiguracja otoki biblioteki to plik JSON, który umożliwia kontrolowanie procesu generowania kodu. Plik ma taką strukturę:

{
  // 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.
      ]
    },
  ]
}

Filtrowanie plików

Warto wykluczyć niektóre symbole z plików JAR, które chcesz opakować. W konfiguracji możesz określić plik filtra, aby wykluczyć symbole. Plik filtra to prosty plik tekstowy, w którym każdy wiersz określa symbol do opakowania. Pliki filtrów mają tę składnię:

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

Oto przykładowy plik filtra:

# 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

konfiguracji podajesz plik filtra, w którym za pomocą parametru -fa określasz dozwolone symbole, a za pomocą parametru -fb – zablokowane. Oba parametry mogą być używane jednocześnie. Jeśli podasz oba filtry, symbol zostanie zawinięty, gdy jest zdefiniowany w pliku filtra dozwolonych i nie występuje w pliku filtra blokującego.

Przykładowy scenariusz

Załóżmy, że musisz opakować plik JAR ChatLibrary.jar zawierający tę klasę:

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

W projekcie C musisz wygenerować natywny moduł opakowujący dla tego pliku JAR, aby natywna aplikacja na Androida mogła go wywoływać w czasie działania. Wygeneruj ten kod za pomocą otoki biblioteki, używając tego polecenia:

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

Poprzednie polecenie generuje kod źródłowy w języku C w katalogu ./generated_code. Wygenerowany plik chat_manager.h zawiera kod podobny do tego, który umożliwia wywoływanie biblioteki w projekcie:

#include "java/lang/string.h"

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

Szczegółowy przykładowy scenariusz znajdziesz w przewodniku po otoczce biblioteki.

Szczegóły narzędzia

W kolejnych sekcjach znajdziesz szczegółowe informacje o funkcjach opakowania biblioteki.

Struktura katalogu wyjściowego

Wszystkie pliki źródłowe i nagłówkowe C znajdują się w podkatalogach, które odzwierciedlają nazwę pakietu opakowanej klasy Java. Na przykład kod opakowujący dla określonego pliku JAR java.lang.Integer jest generowany w katalogu ./java/lang/integer.[h/cc].

Możesz kontrolować to zachowanie wyjściowe za pomocą pliku konfiguracji narzędzia.

Cykl życia obiektu

Obiekty Javy są reprezentowane w kodzie C jako nieprzezroczyste wskaźniki, zwane otoczkami. Otoka zarządza odwołaniem JNI do odpowiedniego obiektu Java. Element opakowujący można utworzyć w tych sytuacjach:

  • Poprzez opakowanie istniejącego odwołania JNI przez wywołanie funkcji MyClass_wrapJniReference(jobject jobj). Funkcja nie przejmuje własności podanego odwołania, ale tworzy własne globalne odwołanie JNI.
  • Utwórz nowy obiekt, co jest równoznaczne z wywołaniem konstruktora w języku Java: MyClass_construct()
  • Na przykład przez zwrócenie nowego obiektu opakowującego z funkcji:Score* Leaderboard_getScore(Leaderboard* instance, String* leaderboard_name)

Gdy nie są już używane, musisz usunąć wszystkie moduły. Aby to zrobić, wywołaj funkcję destroy() MyClass_destroy(MyClass* instance).

Funkcje zwracające obiekty opakowujące przydzielają dla nich nową pamięć przy każdym wywołaniu, nawet jeśli obiekty opakowujące reprezentują tę samą instancję Javy.

Jeśli np. metoda Java Singleton.getInstance() zawsze zwraca tę samą instancję, odpowiednia funkcja po stronie C utworzy nową instancję otoki dla tej samej instancji 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.

Obsługa zajęć bez odniesień

Jeśli w podanym pliku JAR nie można znaleźć klasy, otoka biblioteki tworzy podstawową implementację składającą się z nieprzezroczystego wskaźnika i tych metod:

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

Szczegóły generowania kodu

Po uruchomieniu otoczka biblioteki generuje kod C na podstawie symboli publicznych w plikach JAR, które podasz narzędziu. Wygenerowany kod C może różnić się od opakowanego kodu Java. Na przykład język C nie obsługuje funkcji takich jak programowanie obiektowe, typy ogólne, przeciążanie metod ani innych funkcji języka Java.

Wygenerowany kod C odzwierciedlający te sytuacje może różnić się od rodzaju kodu oczekiwanego przez programistów C. Przykłady w kolejnych sekcjach pokazują, jak narzędzie może generować kod C z kodu Java. Uwaga: w fragmentach kodu poniżej znajdziesz przykłady w językach C/C++ i Java. Te fragmenty kodu mają jedynie pokazywać, jak narzędzie generuje kod w poszczególnych sytuacjach.

Zajęcia

Klasy są reprezentowane jako nieprzezroczyste wskaźniki w C:

C/C++

typedef struct MyClass_ MyClass;

Java

public class MyClass { ... }

Instancje nieprzezroczystych wskaźników są określane jako otoczki. Narzędzie do tworzenia otoki generuje dodatkowe funkcje pomocnicze dla każdej klasy. W przypadku klasy MyClass z poprzedniego przykładu generowane są te funkcje:

// 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);

Zespoły

Klasy z konstruktorami publicznymi lub domyślnymi są reprezentowane za pomocą specjalnych funkcji:

C/C++

MyClass* MyClass_construct(String* data);

Java

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

Metody

Metody są reprezentowane jako zwykłe funkcje. Nazwa funkcji zawiera oryginalną nazwę klasy. Funkcje reprezentujące niestatyczne metody instancji mają jako pierwszy parametr wskaźnik do struktury reprezentującej obiekt Java, w imieniu którego funkcja jest wywoływana. To podejście jest analogiczne do wskaźnika 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) { ... }
}

Klasy wewnętrzne

Klasy wewnętrzne są reprezentowane podobnie jak zwykłe klasy, z wyjątkiem tego, że nazwa odpowiadającej im struktury C zawiera połączone nazwy klas zewnętrznych:

C/C++

typedef struct MyClass_InnerClass_ MyClass_InnerClass;

Java

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

Metody klasy wewnętrznej

Metody klasy wewnętrznej są reprezentowane w ten sposób:

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) { ... }
  }
}

Typy ogólne

Biblioteka nie opakowuje bezpośrednio typów ogólnych. Zamiast tego narzędzie generuje tylko otoki dla instancji typu ogólnego.

Jeśli na przykład w interfejsie API istnieje klasa MyGeneric<T>, a także 2 jej instancje, np. MyGeneric<Integer>MyGeneric<String>, generowane są otoczki dla tych 2 instancji. Oznacza to, że nie możesz tworzyć nowych instancji typu MyGeneric<T> przy użyciu różnych konfiguracji typu. Przyjrzyj się temu przykładowi:

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();
}

Implementowanie interfejsów

Zaimplementuj interfejs C, wywołując implementInterface() i podając funkcję wywołania zwrotnego dla każdej metody interfejsu. W ten sposób można implementować tylko interfejsy. Klasy i klasy abstrakcyjne nie są obsługiwane. Przyjrzyj się temu przykładowi:

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);
}

Przykład użycia:

void onAction1() {
  // Handle action 1
}

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

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

Ograniczenia

Narzędzie do tworzenia bibliotek jest w wersji beta. Możesz napotkać te ograniczenia:

Nieobsługiwane konstrukcje języka Java

Wersja beta modułu opakowującego bibliotekę nie obsługuje tych konstrukcji:

  • Przeciążanie metod

    Język C nie zezwala na deklarowanie dwóch funkcji o tej samej nazwie. Jeśli klasa używa przeciążania metod, wygenerowany kod C nie będzie się kompilować. Obejście polega na użyciu tylko jednej metody z odpowiednim zestawem parametrów. Pozostałe funkcje można odfiltrować za pomocą filtrów. Dotyczy to również konstruktorów.

  • Metody szablonowe

  • Pola inne niż static final intstatic final String

  • Tablice

Możliwość konfliktów nazw

Ze względu na sposób reprezentowania klas Javy w kodzie C w bardzo rzadkich przypadkach mogą wystąpić konflikty nazw. Na przykład klasa Foo<Bar> i klasa wewnętrzna Bar w klasie Foo są reprezentowane przez ten sam symbol w C:typedef struct Foo_Bar_ Foo_Bar;

Pomoc

Jeśli zauważysz problem z biblioteką, daj nam znać.

Przeglądanie błędów Zgłoś błąd
Inżynieria
Dokumentacja