Config File Schema API の実装

Android プラットフォームには、audio config などの構成データを保存するための XML ファイルが多数含まれています。XML ファイルの多くは vendor パーティションに保存されていますが、読み込まれるのは system パーティションです。この場合、XML ファイルのスキーマは 2 つのパーティション間のインターフェースとして機能するため、明示的にスキーマを指定し、下位互換性を持たせる必要があります。

Android 10 より前のプラットフォームには、XML スキーマの指定と使用を要求するメカニズムや、互換性が失われるスキーマ変更を防ぐメカニズムはありませんでした。Android 10 には、そのようなメカニズムとして Config File Schema API が用意されています。このメカニズムは、xsdc というツールと xsd_config というビルドルールで構成されています。

xsdc ツールは、XML スキーマ ドキュメント(XSD)コンパイラです。XML ファイルのスキーマを記述した XSD ファイルを解析し、Java と C++ のコードを生成します。生成されたコードは、XSD スキーマに準拠する XML ファイルを、XML タグをモデル化したオブジェクトのツリーに解析して変換します。XML 属性は、オブジェクトのフィールドとしてモデル化されます。

xsd_config ビルドルールは、xsdc ツールをビルドシステムに統合します。 与えられた XSD 入力ファイルに対して、ビルドルールは Java と C++ のライブラリを生成します。XSD に準拠する XML ファイルが読み取られて使用されるモジュールにライブラリをリンクできます。system パーティションと vendor パーティションで使用される独自の XML ファイルにビルドルールを使用できます。

Config File Schema API のビルド

ここでは、Config File Schema API をビルドする方法について説明します。

Android.bp で xsd_config ビルドルールを構成する

xsd_config ビルドルールは、xsdc ツールでパーサーコードを生成します。xsd_config ビルドルールの package_name プロパティにより、生成される Java コードのパッケージ名が決定されます。

Android.bpxsd_config ビルドルールの例は以下のとおりです。

xsd_config {
    name: "hal_manifest",
    srcs: ["hal_manifest.xsd"],
    package_name: "hal.manifest",
}

ディレクトリ構造の例は以下のとおりです。

├── Android.bp
├── api
│   ├── current.txt
│   ├── last_current.txt
│   ├── last_removed.txt
│   └── removed.txt
└── hal_manifest.xsd

ビルドシステムは、生成された Java コードを使用して API リストを生成し、そのリストを使って API をチェックします。この API チェックは DroidCore に追加され、m -j で実行されます。

API リストファイルの作成

API チェックには、ソースコード内に API リストファイルが必要です。

API リストファイルには次のファイルが含まれます。

  • current.txtremoved.txt は、ビルド時に生成された API ファイルとの比較により、API が変更されているかどうかをチェックします。
  • last_current.txtlast_removed.txt は、API ファイルとの比較により、API に下位互換性があるかどうかをチェックします。

API リストファイルを作成するには:

  1. 空のリストファイルを作成します。
  2. make update-api コマンドを実行します。

生成されたパーサーコードの使用

生成された Java コードを使用するには、Java の srcs プロパティに含まれる xsd_config モジュール名にプレフィックスとして : を追加します。生成された Java コードのパッケージは package_name プロパティと同じです。

java_library {
    name: "vintf_test_java",
    srcs: [
        "srcs/**/*.java"
        ":hal_manifest"
    ],
}

生成された C++ コードを使用するには、generated_sources プロパティと generated_headers プロパティに xsd_config モジュール名を追加します。また、生成されたパーサーコードには libxml2 が必要であるため、libxml2static_libs または shared_libs に追加します。生成された C++ コードの名前空間は package_name プロパティと同じです。たとえば、xsd_config モジュール名が hal.manifest の場合、名前空間は hal::manifest です。

cc_library{
    name: "vintf_test_cpp",
    srcs: ["main.cpp"],
    generated_sources: ["hal_manifest"],
    generated_headers: ["hal_manifest"],
    shared_libs: ["libxml2"],
}

パーサーの使用

Java パーサーコードを使用するには、XmlParser#read または read{class-name} メソッドを使ってルート要素のクラスを返します。この時点で解析が行われます。

import hal.manifest.*;

…

class HalInfo {
    public String name;
    public String format;
    public String optional;
    …
}

void readHalManifestFromXml(File file) {
    …
    try (InputStream str = new BufferedInputStream(new FileInputStream(file))) {
        Manifest manifest = XmlParser.read(str);
        for (Hal hal : manifest.getHal()) {
            HalInfo halinfo;
            HalInfo.name = hal.getName();
            HalInfo.format = hal.getFormat();
            HalInfo.optional = hal.getOptional();
            …
        }
    }
    …
}

C++ パーサーコードを使用するには、まずヘッダー ファイルを含めます。ヘッダー ファイルの名前は、パッケージ名のピリオド(.)をアンダースコア(_)に変換したものです。次に、read または read{class-name} メソッドを使用して、ルート要素のクラスを返します。この時点で解析が行われます。戻り値は std::optional<> です。

include "hal_manifest.h"

…
using namespace hal::manifest

struct HalInfo {
    public std::string name;
    public std::string format;
    public std::string optional;
    …
};

void readHalManifestFromXml(std::string file_name) {
    …
    Manifest manifest = *read(file_name.c_str());
    for (Hal hal : manifest.getHal()) {
        struct HalInfo halinfo;
        HalInfo.name = hal.getName();
        HalInfo.format = hal.getFormat();
        HalInfo.optional = hal.getOptional();
        …
    }
    …
}

パーサーを使用するために用意されているすべての API は api/current.txt にあります。統一を図るため、すべての要素と属性の名前はキャメルケース(ElementName など)に変換され、対応する変数、メソッド、クラス名として使用されます。解析されたルート要素のクラスは、read{class-name} 関数で取得できます。ルート要素が 1 つしかない場合、関数名は read です。解析されたサブ要素または属性の値は、get{variable-name} 関数で取得できます。

パーサーコードの生成

ほとんどの場合、xsdc を直接実行する必要はありません。代わりに xsd_config ビルドルールを使用します。これについては、Android.bp で xsd_config ビルドルールを構成するで説明されています。説明を完全なものにするため、このセクションでは xsdc コマンドライン インターフェースについて補足します。このインターフェースはデバッグに役立ちます。

xsdc ツールに対して XSD ファイルのパスとパッケージを指定する必要があります。Java コードの場合はパッケージ名、C++ コードの場合は名前空間を指定します。Java コードを生成する場合は -j オプション、C++ コードを生成する場合は -c オプションをそれぞれ使用します。-o オプションでは、出力ディレクトリのパスを指定します。

usage: xsdc path/to/xsd_file.xsd [-c] [-j] [-o <arg>] [-p]
 -c,--cpp           Generate C++ code.
 -j,--java          Generate Java code.
 -o,--outDir <arg>  Out Directory
 -p,--package       Package name of the generated java file. file name of
                    generated C++ file and header

コマンドの例は以下のとおりです。

$ xsdc audio_policy_configuration.xsd -p audio.policy -j