エンジニアなプログラマ

プログラミング(特にvala言語関連)の話題を取り上げていきます。

現在、新しいクラス図描画ソフトを開発中! http://gridraw.com/

静的ライブラリ内のprivateクラスへアクセスする

目的

複数のvalaソースに分割されたプログラムをmakeする際、 静的ライブラリを生成してリンクすることはよく行われる。

この時、静的ライブラリ内で使用しているprivateなクラスを、 外部から使用する方法を検討する。

結論

valacのオプション-H--internal-header--internal-vapiを使用する。

背景

静的ライブラリ内のprivateなクラスを、外部から使用するケースがあるのか? そもそも、そのようなリンク構成にすることが設計ミスではないか? と思われるかもしれない。

そこで、本題に入る前に、本稿の背景を説明する。

ある日、名前空間Aに属するクラスの単体テストを記述することを考えた。 名前空間Aにはpublicなクラスとprivateなクラスが存在している。 これらクラスが記述されたファイルをa.valaとしよう。

単体テストコードは、リリースバイナリに含めたくない。 そこで、a.valaとは別の単体テスト用のソースファイルを作成した。 そのファイルをut.valaとする。

ut.valaには名前空間Aに属する単体テスト用のクラスを作成した。 単体テストでは名前空間Aのprivateなクラスもテスト対象としたかったので、単体テスト用クラスは名前空間Aに属させている。

では、単体テスト用バイナリをmakeしよう。 静的ライブラリを作成しない場合は、valacへa.valaとut.valaを渡せばよい。

しかし、今回はそれができない理由があった。 automakeにおけるvala言語対応の制限だ。 1つのMakefile.am内に、1つのvalaソースファイルが依存するターゲットを複数作成することができない。

(関連記事)automakeにおけるVALAFLAGSの制限 - エンジニアなプログラマ

具体的には、a.valaを、単体テスト用のターゲットと、リリース用のターゲットの2つに 依存するファイルとして指定できない。

これは何を意味するのか。

単体テスト用ターゲットとリリース用ターゲットに分けて1つのMakefile.amに記述できない。 つまり、1つのMakefile.amで単体テストターゲット、 別のMakefile.amはリリース用ターゲット、というように Makefileを分けなければならない。 これは、あまりにも非効率だ。

そこで、1つのMakefile.amにおいて テスト用ターゲットとリリース用ターゲットを共存させる方法を模索したのが、今回の検討の始まりである。

ちなみに、通常は(C言語等であれば)、 同じソースファイルを複数のターゲットの依存ファイルとすることができる。 つまり、単体テスト用のターゲットと、リリース用のターゲットを1つのMakefile.amに記述できる。 これがautomakeの「あるべき」仕様である。

Vala言語に対する制限は、少なくとも「現時点での」制限と考えるべきであろう。 将来的には解消することを期待している。

ソース

gitリポジトリはこちら

https://git.codebreak.com/yusukecb/access-private-class-in-liba.git

a1.vala

namespace A {
    public class PublicClass {
        public int y;
        public double sq_add(int x) {
            var priv = new PrivateClass();
            priv.x = x;
            return priv.sq() + y;
        }
    }
}

a1.valaでは名前空間A内のパブリックなクラスを定義している。

a2.vala

namespace A {
    private class PrivateClass {
        public int x;
        public double sq() {
            return (double)x * x;
        }
    }
}

a2.valaでは名前空間A内のプライベートなクラスを定義している。 a1.valaとa2.valaは同じ名前空間Aのクラスを定義していることになる。

test.vala

namespace A {
    public class PublicClass2 {
        public double pow4(int n) {
            var priv = new PrivateClass();
            priv.x = n;
            return priv.sq() * priv.sq();
        }
    }
}

static int main(string[] args)
{
    var pub = new A.PublicClass();
    pub.y = 5;
    print("2^2 + 5 = %.1f\n", pub.sq_add(2));

    var pub2 = new A.PublicClass2();
    print("2^4 = %.1f\n", pub2.pow4(2));

    return 0;
}

test.valaでは名前空間A内のパブリックなクラスを定義している。 さらに、main関数も含まれる。

Makefile

all: test.vala liba.a
  valac --save-temps -g -X -w -X -L. -X -la test.vala a.vapi

liba.a: a1.o a2.o
  ar rcsv liba.a a1.o a2.o

a1.o a2.o: a1.vala a2.vala
  valac --save-temps -H public.h --internal-header=internal.h --internal-vapi=a.vapi \
       -g -c -X -w -c a1.vala a2.vala

#a1.o a2.o: a1.vala a2.vala
#  valac --save-temps -H public.h --vapi=a.vapi \
#       -g -c -X -w -c a1.vala a2.vala

clean:
  rm test *.c *.o *.h *.a *.vapi

最終的な成果物はtestという名称の実行バイナリである。 testはliba.aをリンクする。 liba.aはa1.valaとa2.valaから生成される。

Makefileの説明

スタティックライブラリliba.aを作る

liba.aはa1.valaとa2.valaから作成する。

流れとしては、

となる。

では、Makefileを見ていく。 まずは、ターゲットa1.oとa2.oについて。

a1.o a2.o: a1.vala a2.vala
  valac --save-temps -H public.h --internal-header=internal.h --internal-vapi=a.vapi \
       -g -c -X -w -c a1.vala a2.vala

--save-tempsコンパイルの過程で生成される.cファイルを消去しない設定だ。 .cファイルはデバッグ時に有用な手がかりとなるので指定している。 (無くても構わない)

-Hはヘッダーファイルを生成するオプションだ。 ただし、このオプションで生成されるヘッダーファイルには、プライベートクラスが含まれない。 そこで、次に示すオプションが必要となる。

--internal-headerは今回のポイントとなるオプションだ。 これは、プライベートクラスを含むヘッダーファイルを生成する。 ライブラリに含まれるプライベートクラスを使用する場合には必須のオプションだ。

--internal-vapiはプライベートクラスを含むvapiファイルを生成する。 ヘッダーファイルにはC言語へ変換後の情報しか含まれない。 よって、vala特有の名前空間やクラス定義は失われてしまう。 これらvala特有の情報が記述されているのがvapiファイルだ。

なお、valacの仕様で、--internal-vapiを指定すると-H--internal-headerを指定しなければならない。 しかし、--internal-vapiで生成したvapiファイルは--internal-headerで生成するヘッダーファイルしか使用しない。 つまり、--internal-vapiのみを使用する限り-Hで指定したヘッダーファイルは不要なのだ。

-gデバッグ情報を付与してコンパイルする。 指定しなくても構わない。

-cコンパイルのみでリンクしないことを指定する。 この指定をすると、.oファイルが生成されるようになる。

-X -wgccへオプション-wを渡す。 これは全ての警告を表示することを指定している。 無くても構わない。

次にliba.aの生成ルールについて見ていく。

liba.a: a1.o a2.o
  ar rcsv liba.a a1.o a2.o

valacによって、a1.oとa2.oが生成されるので、 arコマンドでそれらをアーカイブすればliba.aが作成できる。

test.valaをコンパイルしてliba.aとリンクする

Makefileの生成ルールは次のようになっている。

all: test.vala liba.a
  valac --save-temps -g -X -w -X -L. -X -la test.vala a.vapi

--save-tempsは前述したように.cファイルを消去しない設定だ。 無くても構わない。

-gも前述したようにデバッグ情報を付与してコンパイルする。 無くても構わない。

-X -wgccへオプション-wを渡す。 無くても構わない。

-X -L.gccへオプション-L.を渡す。 これは、ライブラリの存在するディレクトリを指定している。 この場合.(現在のディレクトリ)を指定している。 生成したliba.aをリンクするための情報で必須のオプションだ。

-X -lagccへオプション-laを渡す。 これは、ライブラリaをリンクするよう指示している。 ライブラリaとはliba.aのことである。

a.vapiはa1.valaとa2.valaのコンパイル時に生成したvapiファイルである。

valacのオプションを見ると、せっかく生成したヘッダーファイルは指定していない。 実はvapi内にヘッダーファイル名が記述されており、ちゃんと使用されているのだ。

クラスアクセスの説明

名前空間、クラス、ファイルの関係は次の図のようになっている。 以下では、下図を参照しながら説明していく。

image

(A) main→A::PublicClassへのアクセス

liba.aはtest.valaとリンクされる。

A::PublicClassはパブリッククラスなので、普通にリンクするだけでアクセス可能である。

(B) A::PublicClass→A::PrivateClassへのアクセス

これらは同じ名前空間に属しているが、a1.valaとa2.vala間のファイルを越えたアクセスとなっている。 このアクセスは可能なのだろうか。

ここで、Makefileのliba.aを生成する過程を見てみる。 a1.valaとa2.valaは、両方とも一度にvalacへ渡されていた。

valacは引数で与えられた.valaファイルに関する名前解決は全て自動的に行ってくれる。 よって、この場合のファイルを越えたアクセスに関しては、特別意識する必要はない。 同じ名前空間であるプライベートクラス(A::PrivateClass)へのアクセスは可能である。

(C) A::PublicClass2→A::PrivateClassへのアクセス

このアクセスが今回のポイントである。

これらのクラスは同じ名前空間に属しているが別々のファイルで定義されている。 加えて、プライベートクラス(A::PrivateClass)は別のライブラリファイル内で定義されている。

(B)の場合との違いは、test.valaとa2.valaを一緒にvalacへ渡さない点だ。 liba.aを介してアクセスしている。

このアクセスを実現させているのが、valacのオプション -H--internal-header--internal-vapiである。

とくに--internal-xxxが重要である。 このオプションを使用してライブラリを生成しないと、 ライブラリ内のプライベートクラスにはアクセスできない。

(D) main→A::PublicClass2へのアクセス

同じファイル内かつ、名前空間Aのパブリッククラスへのアクセスである。 このアクセスには何も障害はない。

まとめ

冒頭でも記述した通り、ポイントは、 valacのオプション-H--internal-header--internal-vapiの3つである。

このオプションを使用して、適切にリンクすることで、 ライブラリ内のプライベートクラスにアクセス可能である。

※ 背景ではautomakeに言及しているが、automakeでも上記3オプションを適宜設定すればよい。その話は気が向いたときに記述しよう…。