Wrapper de bibliotecas para APIs de Android   Parte de Android Game Development Kit.

El wrapper de bibliotecas es una herramienta de línea de comandos (CLI) que genera un código de wrapper en lenguaje C para las APIs de Android escritas en Java. Puedes usar este código en apps para Android nativas para llamar a las APIs de Java sin necesidad de crear una interfaz nativa de Java (o JNI) de forma manual. Esta herramienta puede simplificar las apps de desarrollo para Android escritas principalmente en C o C++.

La herramienta funciona con la generación de código C para los símbolos públicos que se encuentran en los archivos de Java (JAR) que proporcionas, o en las clases definidas en el archivo de configuración de la herramienta, o en ambos. El código generado por la herramienta no reemplaza las APIs de Java, sino que actúa como puente entre tu código C y Java. Tu app aún requiere que las bibliotecas de Java que unes se incluyan en el proyecto.

Descarga

Descarga el archivo wrapper de la biblioteca y descomprime su contenido en el directorio que elijas.

Sintaxis

La herramienta del wrapper de bibliotecas tiene la siguiente sintaxis de línea de comandos:

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]
Parámetro Descripción
-i jar-file-to-be-wrapped Archivo JAR para generar el código de wrapper en C. Se pueden especificar varios archivos JAR, por ejemplo:
-i first_library.jar -i second_library.jar...
-o output-path Ubicación del sistema de archivos para el código generado.
-c config-file Es la ruta de acceso del sistema de archivos al archivo de configuración del wrapper de bibliotecas. Para obtener más detalles, consulta la sección Configuración.
-fa allow-list-file Una ruta de acceso a un archivo de filtro en el que puedes especificar símbolos para que la herramienta una. Para obtener más detalles, consulta la sección Filtro.
-fb block-list-file Una ruta de acceso a un archivo de filtro que contiene símbolos excluidos de la unión. Para obtener más detalles, consulta la sección Filtro.
--skip_deprecated_symbols Indica a la herramienta del wrapper que omita los símbolos @Deprecated.

Archivo de configuración del wrapper

La configuración del wrapper de bibliotecas es un archivo JSON que te permite controlar el proceso de generación de código. El archivo usa la siguiente estructura.

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

Cómo filtrar archivos

Puede resultar útil excluir algunos símbolos de los archivos JAR que planeas unir. Puedes especificar un archivo de filtro en tu configuración para excluir símbolos. Un archivo de filtro es un archivo de texto simple en el que cada línea define un símbolo para unir. Los archivos de filtro usan la siguiente sintaxis:

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

El siguiente es un ejemplo de archivo de filtro:

# 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

Proporciona a la configuración un archivo de filtro que especifique los símbolos permitidos con el parámetro -fa y los símbolos bloqueados con el parámetro -fb. Ambos parámetros se pueden usar simultáneamente. Si se proporcionan ambos filtros, un símbolo se unirá cuando se defina en el archivo de filtro de permiso y no esté presente en el archivo de filtro de bloqueo.

Situación de ejemplo

Supongamos que necesitas unir el archivo JAR ChatLibrary.jar que contiene la siguiente clase:

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

Tu proyecto en C requiere que generes un wrapper nativo para este JAR, lo que permite que tu app para Android nativa lo llame durante el tiempo de ejecución. Genera este código con el wrapper de bibliotecas con el siguiente comando:

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

El comando anterior genera el código fuente C en el directorio ./generated_code. El archivo generado chat_manager.h contiene un código similar al siguiente, lo que te permite llamar a la biblioteca de tu proyecto:

#include "java/lang/string.h"

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

Para obtener un ejemplo detallado, consulta la Guía del wrapper de bibliotecas.

Detalles de la herramienta

En las siguientes secciones, se proporciona información detallada sobre la funcionalidad del wrapper de bibliotecas.

Estructura del directorio de salida

Todos los archivos fuente y de encabezado en C se encuentran en subdirectorios que reflejan el nombre del paquete de la clase Java unida. Por ejemplo, el código de wrapper para el JAR java.lang.Integer especificado se genera en el directorio ./java/lang/integer.[h/cc].

Puedes controlar este comportamiento de salida mediante el archivo de configuración de la herramienta.

Ciclo de vida de los objetos

Los objetos Java se representan en el código C como punteros opacos, llamados wrappers. Un wrapper administra una referencia de la JNI para el objeto Java correspondiente. Se puede crear un wrapper en las siguientes situaciones:

  • Cuando se une una referencia a la JNI existente mediante el llamado a la función MyClass_wrapJniReference(jobject jobj). La función no toma la propiedad de la referencia proporcionada, sino que crea su propia referencia global de la JNI.
  • Cuando se crea un objeto nuevo, que equivale a llamar a un constructor en Java: MyClass_construct().
  • Cuando se muestra un wrapper nuevo desde una función, por ejemplo: Score* Leaderboard_getScore(Leaderboard* instance, String* leaderboard_name).

Es necesario destruir todos los wrappers cuando ya no se usen. Para ello, llama a la función destroy() dedicada MyClass_destroy(MyClass* instance).

Las funciones que muestran wrappers les asignan una memoria nueva para cada llamada, incluso si los wrappers representan la misma instancia de Java.

Por ejemplo, cuando el método Singleton.getInstance() de Java siempre muestra la misma instancia, la función equivalente de C creará una instancia nueva de un wrapper para la misma instancia de 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.

Cómo controlar clases sin referencia

Cuando no se puede encontrar una clase en el archivo JAR proporcionado, el wrapper de bibliotecas crea una implementación básica que consta de un puntero opaco y los siguientes métodos:

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

Detalles de la generación de código

Cuando se ejecuta, el wrapper de bibliotecas genera código C en función de los símbolos públicos de los archivos JAR que proporcionas a la herramienta. El código C generado puede mostrar diferencias con el código Java unido. Por ejemplo, C no admite funciones como OOP, tipos genéricos, sobrecarga de métodos y otras funciones de Java.

El código C generado que refleja estas situaciones puede diferir del tipo de código que esperan los desarrolladores de C. En los ejemplos de las siguientes secciones, se proporciona contexto sobre cómo la herramienta puede generar código C desde Java. Nota: En los fragmentos de código, los siguientes ejemplos incluyen fragmentos de código C/C++ y Java. El único objetivo de estos fragmentos es demostrar cómo la herramienta genera código para cada situación determinada.

Clases

En C, las clases se representan como punteros opacos.

C/C++

typedef struct MyClass_ MyClass;

Java

public class MyClass { ... }

Las instancias de punteros opacos se denominan wrappers. La herramienta del wrapper genera funciones de asistencia adicionales para cada clase. Para la clase de ejemplo anterior MyClass, se generan las siguientes funciones:

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

Constructores

Las clases con constructores públicos o predeterminados se representan con funciones especiales:

C/C++

MyClass* MyClass_construct(String* data);

Java

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

Métodos

Los métodos se representan como funciones normales. El nombre de una función contiene el nombre de la clase original. Las funciones que representan métodos de instancias no estáticos tienen como primer parámetro un puntero hacia una estructura que representa un objeto Java, en cuyo nombre se llama a la función. Este enfoque es similar al puntero 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) { ... }
}

Clases internas

Las clases internas se representan de forma cercana a las clases normales, con la excepción de que el nombre de la estructura en C correspondiente contiene los nombres encadenados de las clases externas:

C/C++

typedef struct MyClass_InnerClass_ MyClass_InnerClass;

Java

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

Métodos de clases internas

Los métodos de las clases internas se representan de la siguiente manera:

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

Tipos genéricos

El wrapper de bibliotecas no une directamente los tipos genéricos, sino que solo genera wrappers para instancias de tipos genéricos.

Por ejemplo, cuando existe una clase MyGeneric<T> en una API y hay dos instancias de esta clase, como MyGeneric<Integer> y MyGeneric<String>, se generan wrappers para esas dos instancias. Esto significa que no puedes crear instancias nuevas de tipo MyGeneric<T> mediante diferentes configuraciones de tipo. Observa el siguiente ejemplo:

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ómo implementar interfaces

Para implementar una interfaz C, llama a implementInterface() y proporciona una función de devolución de llamada para cada método de interfaz. Solo se pueden implementar interfaces de esta manera; no se admiten las clases ni las clases abstractas. Observa el siguiente ejemplo:

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

Ejemplo de uso:

void onAction1() {
  // Handle action 1
}

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

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

Limitaciones

La herramienta del wrapper de bibliotecas está en versión beta. Es posible que tengas las siguientes limitaciones:

Construcciones de Java no compatibles

La versión beta del wrapper de bibliotecas no admite las siguientes construcciones:

  • Sobrecarga de métodos

    El lenguaje C no permite declarar dos funciones con el mismo nombre. Si la clase usa sobrecarga de métodos, no se compilará el código C generado. La solución alternativa es usar un solo método con un conjunto de parámetros suficiente. Las funciones restantes se pueden filtrar con los filtros. Esto también se aplica a los constructores.

  • Métodos basados en plantillas

  • Campos distintos de static final int y static final String

  • Arrays

Posibles colisiones de nombres

Debido a la forma en que se representan las clases de Java en el código C, puede haber conflictos de nombres en casos muy poco frecuentes. Por ejemplo, una clase Foo<Bar> y una clase interna Bar dentro de una clase Foo se representan con el mismo símbolo en C: typedef struct Foo_Bar_ Foo_Bar;

Asistencia

Si encuentras un problema con el wrapper de bibliotecas, comunícate con nosotros.

Explora errores Informa un error
Ingeniería
Documentación