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 モジュール名を追加します。生成された 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"],
    }
    

パーサーの使用

Java パーサーコードを使用するには、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 = read(str);
            for (Hal hal : manifest.getHal()) {
                HalInfor halinfo;
                HalInfo.name = hal.getName();
                HalInfo.format = hal.getFormat();
                HalInfor.optional = hal.getOptional();
                …
            }
        }
        …
    }
    

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

include "hal_manifest.h"

    …
    using namespace hal::manifest

    struct HalInfor {
        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 HalInfor halinfo;
            HalInfo.name = hal.getName();
            HalInfo.format = hal.getFormat();
            HalInfor.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 か C かを判別するオプションは、それぞれ -j-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