适用于 Android API 的库封装容器   Android Game Development Kit 的一部分。

库封装容器是一个命令行工具 (CLI),可为使用 Java 编写的 Android API 生成 C 语言封装容器代码。您可以在原生 Android 应用中使用此代码调用 Java API,而无需手动创建 Java 原生接口 (JNI)。该工具可以简化主要使用 C 或 C++ 编写的 Android 应用的开发过程。

该工具的工作原理是为您提供的 Java 软件包 (JAR) 文件中包含的公共符号或为该工具配置文件中定义的类生成 C 代码,或者同时为这两者生成 C 代码。该工具生成的代码不会替代 Java API,而是充当 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 用于生成 C 封装容器代码的 JAR 文件。可以指定多个 JAR 文件, 例如:
-i first_library.jar -i second_library.jar...
-o output-path 所生成代码的文件系统位置。
-c config-file 库封装容器配置文件的文件系统路径。如需了解详情,请参阅配置部分。
-fa allow-list-file 过滤器文件的路径,您可以在该文件中指定工具要封装的符号。如需了解详情,请参阅过滤器部分。
-fb block-list-file 过滤器文件的路径,该文件包含要从封装中排除的符号。如需了解详情,请参阅过滤器部分。
--skip_deprecated_symbols 指示封装容器工具跳过带有 @Deprecated 注释的符号。

封装容器配置文件

库封装容器配置文件是一个 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 应用在运行时调用该封装容器。使用以下命令,通过库封装容器生成此代码:

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

上述命令会在目录 ./generated_code 中生成 C 源代码。生成的文件 chat_manager.h 包含类似以下内容的代码,您可以通过此代码调用项目中的库:

#include "java/lang/string.h"

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

如需深入了解示例场景,请参阅库封装容器指南

工具详细信息

以下部分详细介绍了库封装容器的功能。

输出目录结构

所有 C 源文件和头文件所在的子目录反映了所封装 Java 类的软件包名称。例如,为指定的 JAR 文件 java.lang.Integer 生成的封装容器代码位于目录 ./java/lang/integer.[h/cc] 中。

您可以使用工具的配置文件来控制此输出行为。

对象生命周期

Java 对象在 C 代码中表示为不透明指针,称为封装容器。封装容器管理相应 Java 对象的 JNI 引用。在以下情况下会创建封装容器:

  • 通过调用函数 MyClass_wrapJniReference(jobject jobj) 封装现有 JNI 引用。该函数不会获取所提供引用的所有权,而是会创建自己的全局 JNI 引用。
  • 创建新对象,相当于在 Java 中调用构造函数:MyClass_construct()
  • 从函数返回一个新的封装容器,例如:Score* Leaderboard_getScore(Leaderboard* instance, String* leaderboard_name)

您需要销毁所有不再使用的封装容器。为此,请调用专用的 destroy() 函数 MyClass_destroy(MyClass* instance)

返回封装容器的函数会在每次调用时为封装容器分配新内存,即使封装容器表示的是同一 Java 实例也是如此。

例如,当 Java 方法 Singleton.getInstance() 始终返回同一实例时,C 端的等效函数将为同一 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()

代码生成详情

运行时,库封装容器会根据您提供给工具的 JAR 文件中的公共符号生成 C 代码。生成的 C 代码可能与封装的 Java 代码有所不同。例如,C 代码不支持 OOP、泛型类型、方法重载等 Java 功能。

如果生成的 C 代码出现这类情况,其代码类型可能与 C 语言开发者的预期不同。以下部分中的示例提供了相关背景信息,可让您了解工具如何从 Java 代码生成 C 代码。注意:在代码段中,以下示例包括 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 指针。

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

泛型类型

库封装容器不会直接封装泛型类型,该工具仅会为泛型类型的实例生成封装容器。

例如,当 API 中存在 MyGeneric<T> 类,并且这个类有两个实例(例如 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();
}

实现接口

通过调用 implementInterface() 并为每个接口方法提供一个回调函数来实现 C 接口。这种方式只能实现接口;不支持实现类和抽象类。请参阅以下示例:

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

限制

库封装容器工具目前处于 Beta 版阶段。您可能会遇到以下限制:

不受支持的 Java 构造方法

库封装容器 Beta 版不支持以下构造方法:

  • 方法重载

    C 语言不允许声明两个同名函数。如果类使用了方法重载,生成的 C 代码将无法编译。权宜解决方法是仅使用一种方法,且该方法的参数集足够使用。其余函数可以使用过滤器滤除。这种方法同样适用于构造函数。

  • 模板化方法

  • static final intstatic final String 以外的字段

  • 数组

潜在的名称冲突

由于 Java 类在 C 代码中的表示方式,在极少数情况下可能会出现名称冲突。例如,Foo<Bar> 类和 Foo 类中的内部类 Bar 在 C 代码中使用相同的符号来表示:typedef struct Foo_Bar_ Foo_Bar;

支持

如果您发现库封装容器存在问题,请告知我们。

浏览 bug 提交 bug
工程
文档