JUCEモジュールを作って外部ライブラリを参照する

これは音楽技術Advent Calendar 2019の3日目(まあ2本目ですが…)とJUCE Advent Calendar 20193日目のクロスポストエントリーです。

長い前書き

JUCEのビルドシステムは、アプリケーションやオーディオプラグインのビルドに必要なコードを原則として全てソースコードからビルドするという仕組みです。JUCEはさまざまなモジュール群から成り立っていますが、その全てがソースからビルドされることになります。Linuxで言えばGentooです*1。これは複数のプラットフォームをターゲットとするビルドシステムを構築する場合には手っ取り早いハック(やっつけ仕事)であるといえます。

世の中には、アプリケーションのビルドに必要なものは全てソースからビルドされるべきだ、という発想の人もいますが、おそらくその他ほとんどの人はビルド済みのライブラリを使ってビルドするほうが賢いと考えます。地球環境のことを考えれば、モジュールを毎回ソースからビルドして電力を浪費するのは悪であるとすらいえます*2。そして、そもそも自分ではビルドできないようなOSのモジュールを、自分のアプリケーションから参照したくなる場合があります。

バイナリのライブラリを使えるようにするためには、それぞれのプラットフォームやビルドツールチェインでそれらを利用できるようにサポートしなければなりません。JUCEはプラットフォームの違いを吸収するだけでなく、ビルドツールチェインの違いも吸収しなければなりません。Visual Studio (for Windows)の複数バージョン、xcodeMakefile, CMake (CLion)と、多大な組み合わせと対峙することになります。

ライブラリの参照の仕方も、ライブラリファイルを直接指定する(ついでにLD_LIBRARY_PATHのようなライブラリ解決パスも追加指定する)方法と、pkg-configのようにパッケージとしてでないと解決できない方法があります。C++には、node/npmやruby/gemやpython/pipや.NET/nugetやJava/mavenなどきちんと整備された言語開発環境とは異なり、プラットフォームの違いを意識せずに利用できる依存関係解決のためのソリューションがありません。

結局JUCE/Projucerでは、まともなプラットフォーム中立の依存関係解消方法を用意できず、Exporter別にオプションでライブラリ参照を解決することにしました。オプションはProjucerのビルド設定ファイルであるところの.jucerファイルに含まれます。Exporter別に設定するのはちょっと面倒くさいですね。結局出力先ごとのオプションの指定方法を調べないといけないことになりますし。

JUCEモジュールを活用してライブラリをソースからビルドする

こういう煩雑さが面倒だ、それならばライブラリをソースからビルドしてインクルードファイルなどは直接参照したい、という人のために、JUCEではユーザーのモジュールから外部ライブラリを取り込む方法が用意されています。今回はこのカスタムJUCEモジュールを作る方法から説明します。

JUCEモジュールというのは、JUCEを使ってコードを書いている人であれば自明かと思いますが、JUCEの機能ごとにまとまったライブラリの一部分のようなものであり、JUCEではモジュール単位でユーザーのプロジェクトに含めるかどうかを指定します。JUCEモジュールには依存モジュールの概念があり、要するにライブラリのようなものです(ただしソースの集合体であり、バイナリを参照することはありません)。JUCE本体にあるjuce_audio_basicsjuce_gui_basicsなどがモジュールです。

このモジュールはユーザーが独自に作成することもできます。作り方は簡単です。foo_barというモジュールを作るには、foo_barというディレクトリを作って、その中にfoo_bar.hfoo_bar.cppというファイルを2つ作成するだけです。ただし、このfoo_bar.hには「JUCEモジュールフォーマット」に準拠したヘッダコメントが必要になります。ヘッダコメントの書式はこんな感じです。ちょっとPIPっぽいですが別物です。

/*******************************************************************************
The block below describes the properties of this module, and is read by
the Projucer to automatically generate project code that uses it.
For details about the syntax and how to create or use a module, see the
JUCE Module Format.txt file.

BEGIN_JUCE_MODULE_DECLARATION

ID: augene_file_watcher
vendor: atsushieno
version: 0.1.0
name: auegne file watcher
description: Classes for file system watcher/notifier
website: https://github.com/atsushieno/augene
license: MIT

END_JUCE_MODULE_DECLARATION
*******************************************************************************/

このヘッダファイルの残りの部分には、自分のクラス定義などを書いても良いですが、一般的には別のヘッダファイルを含めるほうがクリーンに見えるでしょう。そして、ここにはサードパーティーライブラリのヘッダファイルを含めることもできます。

このヘッダファイルは、Projucerが生成するJuceHeader.hの中で自動的にインクルードされるので、その中でサードパーティーライブラリのヘッダファイルも自動的に参照できるわけです。これは楽ちん。

さて、自作したJUCEモジュールは、まずプロジェクトに追加しないと認識してもらえません。ProjucerでModulesのセクションにある+ボタンをクリックして、モジュールのディレクトリを指定します。

f:id:atsushieno:20191203110405p:plain

モジュールが正しく認識されると、Modulesのリストに自分のモジュールが追加されます。(ヘッダファイルが正しいフォーマットになっていないとここで各種エラーメッセージが出ることになるので、メッセージの内容からエラーを推測して修正します。)

f:id:atsushieno:20191203110429p:plain

さて、さっきはもうひとつ.cppファイルも作成しました。一般的なJUCEモジュールでは、この中に実装を書くことは実際にはあまりなく、このファイルにはこのモジュールに含まれるソースコードを #include で列挙することになります(!) つまりMakefileなどでソースファイルを列挙しているような感じですね。そして、ここでもまた、サードパーティーライブラリをビルドする場合は、ここにそのソースコードを列挙すれば、きれいに外部ライブラリがビルドできるではないか、というのがJUCEプロジェクトのスタンスであるようです。

JUCEモジュールでビルド済みバイナリをリンクする

ところでここまで読まれた皆さんはお気づきでしょうか? JUCEモジュールでサードパーティライブラリのソースのビルドなんて実際にはできるはずがないということに(!)

少なくとも、こんなのは一般的に通用するソリューションではありません。

その理由は、一般的にはライブラリはそれぞれのビルドシステムに沿って作成されているものであり、それぞれには適切なコンパイラーオプションやリンカーオプションがMakefileやCMake、xcodeproj, vcxprojなどで指定されているためです。JUCEモジュールで指定できるのはインクルードするヘッダファイルとビルドするソースコードだけです。これでは全く足りません。だいたいLinuxでautotoolsとか使っていたらconfig.hとか生成するわけで、この仕組みではそういうものに全く対応できないわけです。

そういうわけで、サードパーティーライブラリを取り込むなら、別途ビルドして面倒でもそれをExporter別にリンク指定してやるほうが現実的っぽいですが、実はもうひとつプラットフォーム別のビルド指定を回避できる可能性がひとつあります。それはこのJUCEモジュールで利用できるビルド済みバイナリの参照という選択肢です。

JUCEモジュールでは、ビルド済みのサードパーティーライブラリを以下のようなサブディレクトリに置いておくことで、自動的にビルド時にリンク対象として検索してくれます。たとえば…

  • libs/VisualStudio2019/Win32/MTd - Visual Studio 2019用 MTd - あるいはMT(スレッディングモードに合わせて調整します。VSのバージョンも2017などが使えます)
  • libs/MacOS/x86_64 - MacOS
  • libs/Linux/native - Linux用(要注意)

iOSAndroidなどもあるのですが、先にリンクしたJUCE_Module_Format.txtに詳しく書かれています。ただし、ひとつ要注意なのですが、Linuxに関する記述がデタラメで、このドキュメントにはABIをディレクトリ名として使うように書かれているにもかかわらず、実際にはnativeというディレクトリ名がLinuxMakefile/Makefileに追加されます。これはROLIに報告済みのissueで何が問題なのかも明らかになっているのですが、本エントリー公開時点でも未修正です。

(実のところ筆者が検証したのはLinuxWindowsのみで、MacOSについてはドキュメントに書いてあるのをそのまま紹介しているだけです。見ての通り、このドキュメントは信用できないので、実際に通るかどうか気になる人は検証してみてください。)

そして、このビルド済みライブラリは、自動的にリンクされるわけではなく、プラットフォームごとにJUCEモジュールフォーマットの書式に沿って、それぞれのプラットフォームに合ったプロパティとして指定する必要があります。

    linuxLibs: foo_bar
    OSXLibs: foo_bar
    windowsLibs: libfoo_bar // libfoo_bar.dllの場合. foo_bar.dllなら foo_bar でOK

いずれにしろ、サードパーティーライブラリを使う場合は、それぞれのプラットフォーム上でいったんビルドしておいて、ヘッダファイルを所定のディレクトリ上にコピーしてそれをモジュールのヘッダファイルで #include しつつ、モジュールフォーマットに沿ってプロパティを追加すれば、そのライブラリが使えるようになる可能性が少し上がります。

ここまでお膳立てしてやるくらいなら、Exporter別にリンク指定したほうが早いのでは…?とも思ってしまいますが、少なくともオプションの指定方法などを知らない環境向けにオプションを調べて記述するよりは、こっちのほうが楽かもしれません。わたしも実のところvcxprojやxcodeprojでどんなリンク指定を受け付けるのか知らないのですが(もちろん調べればすぐ分かる話ですが)、この方法であればすぐ指定できますし。

補論: インクルードファイル解決パスの問題

実のところ、リンクするライブラリをビルドするより前に、ヘッダファイルが解決できないという問題が生じるのが一般的です。「サードパーティライブラリのヘッダファイルを別のヘッダファイルで #include すればOK」というProjucerの発想はカジュアルなもので、JUCEモジュールのやり方では、pkg-configが追加で-Iオプションを指定してくれるようなことは、モジュール単位では一切やってくれません(Linux Makefile exporterではpkg-configライブラリを指定できます。もちろんLinux専用)。ビルドするサードパーティライブラリがさらに別のライブラリに依存して、それが追加のインクルードパスを必要とする(そうしないと「システム上にインストールされている」インクルードファイルの解決に失敗する)場合には詰むことになります。

これを回避するには、プラットフォーム別のExporterにコンパイラーオプションで-Iを追加すれば良いということになるのですが(追加インクルードパスはプラットフォーム別のDebug/Releaseの設定の中にHeader Search Pathsがあります。ちなみにexterCompilerFlagsというビルド設定に依らないプロパティもあるのですが、CLion Exporterが取り込んでくれない等の問題があります)、それならJUCEモジュールで解決するのは無駄ですし、システムによって場所が異なる可能性があるからこそpkg-configというソリューションがあるわけで、この意味でもProjucerのやり方はお粗末です。まあもともとビルドシステムとしてはやっつけツールだし…?

いくつかのJUCEモジュールで採用されている対策としては、もうモジュールの中に必要なヘッダファイルを直接放り込んだ上で、参照するインクルードファイルの位置を書き換えて対応する、というものです。さすがにシステム上にあるものは解決できないので、これは部分的な解決方法ということになります。

将来の展望

さて、ここまでいろいろな問題の解決方法を紹介してきましたが、いかがでしたか? わたしの個人的な感想としては「もうProjucerのビルドシステムはあきらめてCMakeを使おう」なのですが(パッケージ参照もちゃんと解決できるしVisual StudioでもAndroid StudioでもCLionでも開けるんですよコレ)、Projucerの負の遺産があるうちはまだまだ無理かもしれませんね。

JUCEの本来的な魅力はなんと言ってもクロスオーディオプラグイン・ホスト開発が可能になることなので、その魅力をフルに引き出せるように、JUCEのビルドシステムはもっとさまざまな開発者にリーチできるように発展的に解消していってほしいと思っています。

*1:っていう説明の仕方はちょっと時代遅れかな? 今はバイナリもインストールできるんですよね

*2:いまCI業界を敵に回した気がしてきたぞ