torutkのブログ

ソフトウェア・エンジニアのブログ

Java読書会 会場とオンラインの併用を試みる(準備)

今月のJava読書会で会場での開催にオンラインで参加できるようにしたい

Java読書会BOF は、明日1月30日(土)に「データ指向アプリケーションデザイン」を読む会(第4回)を開催します。

今回は、コロナ禍の2回目の緊急事態宣言発令下での開催となり、会場の定員も半分に削減することとされています。ただし、参加に不安を感じる人もいますので、会場だけでなくリモートからも参加できるよう試みることとしました。

Java読書会BOFでは、以前にコロナ禍の対応として2020年4月、5月、6月の3回をオンライン読書会として実施しました。

昨年のオンライン読書会との違い

昨年は、全員がオンライン参加としたので、参加者がそれぞれマイク・スピーカーを揃えてTeams会議に参加する形でした。今回、会場でのリアル開催とオンラインをつなぐので、会場にリアル参加の会話とオンライン参加の会話がつながるようにする必要があります。

読書会は朝10:00から夕5:00までと長丁場でその間音声・動画を流すので、時間やパケット量に制限のあるインターネット接続サービス(携帯電話や上限のあるモバイルルータ)は不適です。また、利用するWeb会議サービスも参加人数や利用時間が読書会開催に足りることも必要です。

今回利用する会場には、インターネットと接続できるWiFiサービスが提供されているので、オンラインとつなぐことはできそうです。

Web会議サービスは、昨年のオンライン読書会と同様 Microsoft Teams(無償版)としました。これは参加人数と利用時間が読書会開催に十分足りています。

アイテムの準備

続いて、次のアイテムを準備することにしました。

  1. 会議用のマイク・スピーカー
  2. 会場全体を映すカメラ
マイク・スピーカー

Java読書会は、参加者が順番に書籍を朗読し、随時議論を行うので、無指向性のマイクを真ん中に置く構成とします。といってもJava読書会は手弁当で開催しているので高いアイテムを買うのは厳しいのでなるべく廉価でそこそこ使えそうなものを探しました。

この中から、価格と無線接続が可能なLunaを選び購入しました。バッテリー搭載で無線でPCと接続できるので、会議室の真ん中に、電源やPCとの有線接続をせずに置くことができる点を評価しました。

カメラ

ノートPCのカメラで会場を映すと画角が狭く正面の参加者しか映りません。そこで、広角のカメラを用意すべく探してみたところ、スマフォのカメラに被せて広角にするレンズなら安価に用意できそうでした。

1000円未満の格安帯と、1000~5000円当たりの帯と、さらに高価な帯とがありました。格安なものはケラレという現象が出るとかがあり、その上を狙いました。

広角なカメラは120度が多いようですが、中に165度という超広角がありました。会場で自席に置いたノートPCからなので、この超広角を選び購入しました。

Teams(無料版)

今回もTeamsを使う想定です。

Microsoftアカウント

Teamsは、参加者もアカウントが必要です。アカウントは、Microsoftアカウントです(無料で作成可能)。 次のリンク先で作成します。 https://account.microsoft.com/account/

メールアドレスがキーとなります。作成、使用時にはキーとしたメールアドレスにセキュリティコードが送られるので、そのメールアドレス宛のメールを読み取れる環境が必要です。

Microsoftアカウントを作成し、Teamsアプリを起動すると、「まだTeamsに登録していませんが、組織でセットアップできます」のメッセージと[Teamsに新規登録]ボタンが表示されてしまいます。これは

参加に必要なこと
  • Microsoftアカウントを保有している
  • 会議への招待のリンクを開く
会議の作成(管理者)

最近のTeams(無料版)は、「今すぐ会議」機能だけでなく、「会議のスケジュール設定」機能が追加され、事前に未来の会議を作成し招待リンクを事前配布することができるようになりました。

動作確認

自宅のデスクトップPCとノートPCとでMicrosoftアカウントを別にしてTeamsに接続、会議を作成し、招待リンクを辿って会議に参加し、eMeetのマイクが使えることまで確認しました。

あとは明日会場で確認することとなります。

Java読書会 今月から読む本

はじめに

前回の本と台風とコロナ禍と

Java読書会BOF 主催のJava読書会は、昨年2019年11月から毎月1回のペースで「The Java Module System」(洋書)を読む会を開催し、先月2020年9月に読了しました。

この本の読書会期間中には、台風直撃とコロナ禍と大きな出来事が重なりました。

台風による中止

まず、予定していた第1回(2019年10月12日)は関東に台風19号が直撃し、中止としました。この台風では、鉄道の運休、多摩川の氾濫が発生するといった状況のため翌月に延期となりました。Java読書会BOF主催の読書会は毎月開催をしていますが、これが途切れたのは2001年6月以来で18年ぶりのことでした。

オンライン(Web会議)開催

また、2020年4月7日にはコロナ禍による緊急事態宣言が発出され、4月11日(土)に予定していた第6回は会場での開催ができなくなり、初のオンライン開催となりました。

緊急事態宣言の発出がほぼ確実視された時点でオンライン読書会を実施するためのサービスを調べました。当時の調査結果は次です1

Java読書会オンライン開催検討 - ソフトウェアエンジニアリング - Torutk

この中から、読書会参加人数の平均である11人程度をカバーし、開催時間である朝10:00~夕17:00までの7時間をカバーするサービスで無償なものとなると、Microsoft Teamsの無償版がほぼ一択でした。そこで、第6回~8回(4月、5月、6月)はTeamsを利用したオンライン開催としました。

読書会開催データ

2019年9月時点での読書会開催に関するデータです。

項目
開催回数 257回
書籍数 39冊
総ページ数 15,183ページ
1回の平均ページ数 59ページ
平均参加人数 11人
のべ参加人数 2,821人

10月からの本

Java読書会の継続に影響を及ぼす事象は自然災害だけではなく、Java技術に関する読書会向きな書籍の候補がでないという事象もあります。世の中的に出版不況や技術書籍の刊行が(初心者向けの入門本を除くと)不調というのがあるのかもしれません。

このような中、課題図書の候補のリストアップと投票を実施したところ、「データ指向アプリケーションデザイン」に決まりました。

投票の状況は次のとおりです。1票差と接戦でした。

書籍名 投票数
データ指向アプリケーションデザイン 9票
マイクロサービスパターン 8票
テスト駆動開発 2票
実践テスト駆動開発 1票
Design It!ープログラマーのためのアーキテクティング入門 1票

著者(Martin Kleppmann氏)について

著者の自己紹介ページ https://martin.kleppmann.com/

著者のMartin Kleppmannは現在英国ケンブリッジ大学で分散システムの研究者(Senior Research Associate/Affiliated Lecturer)。以前はソフトウェアエンジニア&起業家として活動。 また、オープンソースプロジェクト automerge、apache avro、apache samza の貢献者。 分散システムに関するカンファレンスのスピーカー。

内容(超概要)

分散システムにおいて主にデータを扱うシステムを構築するための様々な技術(原理)を解説しています。 データモデル、ストレージ、レプリケーション、パーティショニング、トランザクション、一貫性と合意、バッチ処理、ストリーム処理といった章題が並びます。

この書籍の感想ブログへのリンク

この書籍について感想を書かれているブログが多数ありました。 検索で見かけたものをいくつかピックアップします。

hydrakecat.hatenablog.jp

kuromt.hatenablog.com

takezoe.hatenablog.com

takuti.me


  1. 2020年4月10日時点の情報で、その後コロナ禍対応のためそれぞれのサービスが向上しています。

Androidアプリで使用するロギング考

Androidアプリケーションから使用するロギングの種類と選択

プログラミングをしてデバッグをする際、ロギングは欠かせません。Androidアプリケーションも昨今は規模が大きくなってきており(なんせGUIからデータベースからネットワーク、そしてデバイスを扱うところまで含まれますから)、デバッグは単純ではなくなってきています。

昔はワークステーションと呼ばれたUNIX計算機や、PC等でデスクトップアプリケーションやサーバーアプリケーション を開発してきた経験上、しっかりとしたロギング機構を使うことが不可欠です。

Androidアプリケーション開発では、標準ライブラリに含まれる android.util.Log を使ってロギングをすることが多いようです。Androidの標準ログ機構に記録されたログを読み出す開発ツールが logcat ですが、ロギングのことを logcat と呼んでいる記事等も数多く見かけます。では、Android アプリケーションのプログラミングではAndroid標準ログ(logcat)を使えば十分でしょうか? ちょっと調べてみました。

Android標準ログ(logcat)を調べたところ

  • AndroidJavaアプリケーション用に提供される標準APIに含まれる
  • 設定不要で、複数のレベルから1つを選択してログ出力する
  • 引数にはTAGと呼ぶ識別子(文字列)と、メッセージ文字列を指定する
  • ログを見るときは、adbシェル経由でlogcatコマンドを実行するが、昨今のIDEにはlogcatに気の利く機能が追加され便利につかえる
  • ログはAndroid上で動作するプログラムやOSからの出力が混ざっている。logcatを実行するとデフォルトでは大量のログが表示されるので、logcatのコマンドラインオプションでフィルターすることが多い
  • Android StudioなどのIDEではlogcatが常時実行されログが表示される
Android標準ログの問題点

ところが、このAndroid標準ログには問題もあります。

android.util.Log へログを出力すると、JNI呼び出しでネイティブコードからliblogdの関数が実行され、Android端末上にいるlogdプロセスにソケット通信でログエントリが渡され、リングバッファのデバイス(/dev/log/*)に格納されます。UNIXLinux での syslog に出力するイメージでしょうか。これは気軽にログを出すと性能が著しく劣化しそうです。

また、このLogは基本全てのログ出力呼び出しをだだ流しします。Javaアプリケーション側でログ出力を設定で抑制する手段がないので、if文でログ出力を制御するしかありません。それは避けたい・・・。

Googleのドキュメントにも、アプリケーションをリリースビルドするときはログ出力を削除しようと書かれています。でも、ログはアプリケーションが実際に使われている環境で生じた問題点を追究するほぼ唯一の手がかりです。ログを出さないということは考えにくいことです。

また、ログをためるバッファはそれほど大きくなく、端末により異なりますが手元のPixel 3の場合は256KBです。一日運用してトラブルがあった場合にログファイルを取得するといったことが難しいです。

Android以外の環境と共通化したいモジュールの作成では、Android固有のAPIを使えません。

代替のロギング手段

まずは、Android上でロギングを行うことができるライブラリをざっと調べてみたところ、次がでてきました(2,3年内にリリースがあったもの)。

  1. Java SE の標準ロギングである java.util.loggingパッケージが利用できる模様
  2. Timber というオープンソースライブラリを利用する
  3. SLF4J と Logback for android を組み合わせて使う
  4. SLF4J と Timber を組み合わせて使う

Timberは、Android標準ログをラッパーしたものです。

ログの出力の抑制がJavaアプリケーション内でif文等の制御をせずにでき、性能がそこそこでリリース時にファイルにログが取れ、他のプラットフォームと共用できるものは、1と3になります。

Androidアプリケーションプログラミングを5年振りに

はじめに

Androidアプリケーションのプログラミングは5年前に半年ちょっと程の期間取り組んで以来となります。

2015年初頭のAndroid開発環境は、最新OSがAndorid 4.4(KitKat)から 5.0(Lollipop)になった頃で、開発環境はそれまで主流のEclipseからAndroid Studioに移行が始まった頃です。

Javaは、Java SE 8仕様のJDK 8がリリースされ1年、ラムダ式が導入された等話題となりましたが、Android開発環境へのJava SE 8は進展がほとんどありませんでした。

2015年のAndroidアプリケーション・アーキテクチャ

画面レイアウトをXMLで記述し、アクティビティ、ブロードキャストレシーバ、サービス、コンテンツプロバイダと4種類のコンポーネントを必要に応じて組み合わせてAndroidアプリケーションを構成していました。

画面レイアウトがView、ActivityがController、コンテンツプロバイダがModel1となるMVCアーキテクチャと認識されていたようです。ただ、Activityがかなり頑張るファットコントローラーになっていました。

Activityの肥大化を抑制するため、Butter Knife、EventBusなどのライブラリが作られ割と広まっていたようです。 Modelのデータを永続化する際は、SQLiteアクセスを簡単にするORM(Object Relation Mapper)ライブラリがいくつかありました(例:Active Android)。

画面遷移を伴うアプリケーションでは、Activityを複数用意してIntentで切り替えるのではなく、Fragmentを導入して画面の一部を切り替えることが推奨でした。

この頃のアプリケーション実行はDalvik VMで、DEXファイルにはメソッド総数の制限等がありました。

5年振りのプログラミング(リハビリ期)

まず、開発環境としてAndroid Studioをインストールします。現時点での最新リリースバージョンは4.0.1でした。

アプリケーション実行はDalvik VMに替わりARTとなっていますが、ARTも初期のAOT(Ahead Of Time Compile)だけではなく、JITも取り入れたハイブリッドとなっていました。

Java SE 8(JDK 8)対応も進んでおり、ラムダ式(メソッド参照)、デフォルトメソッド等のJava SE 8言語仕様やJava SE 8の標準ライブラリを取り込みつつあります。これは朗報です。

また、Kotlin言語の使用も随分浸透しているように見えます。

いい感じに成熟してきているではないですか。

2020年のAndroidアプリケーション・アーキテクチャ

それでは、次はアプリケーションの構成をどうしようかと考えはじめてみたところ、

MVP、MVVM、DataBinding、LiveData、Jetpack、Room、DDD、Clean Architecture、‥‥と怒涛のようにキーワードが噴出してきました。

MVPとMVVMは、GUIアプリケーションを作成する際のアーキテクチャパターンで、Windows等のデスクトップアプリケーションでは以前から登場していたパターンです。DDDはドメイン駆動開発の略ですし、Clean Architectureはロバート C. マーチン氏の著になる書籍です。これらがAndroidアプリケーションのアーキテクチャの文脈に揃って出てきているのに驚きを覚えました。いったい何が起きているんだろう?

状況を整理するには、Jetpackを中心に調べるのがよさそうです。というのは、JetpackはGoole I/O 2018で発表された、次世代のコンポーネント、ツール、アーキテクチャガイダンスであり、今後のAndroidアプリケーション開発の指針およびライブラリ集となる存在です。DataBinding、LiveData、Room2 といったライブラリもJetpackに含まれています。 ViewModelライブラリも含まれ、これはどうやらMVVMパターンを構成するようです。その他お馴染みの Support Libraryも含まれます。

ということで、今後のAndoridアプリケーション・アーキテクチャはこのJetpackに沿っていくものと思われます。これから新規にアプリケーションを作成する際は、Jetpackのガイダンスとライブラリ集を利用するのがよいようです。

参考資料

developer.android.com

developer.android.com


  1. 別なアプリケーションとModelを共有せず、そのアプリケーション内で使うだけならばコンテンツプロバイダでなくPOJO(ふつうのJavaクラス)で実装する

  2. RoomはSQLiteを利用するORMで、@Entity、@Dao、@Database の3つのクラスでデータベースアクセスを実装

jlinkのオプション指定でカスタムJREのサイズ削減

jlinkのオプション指定でカスタムJREのサイズ削減

はじめに

先週のJava読書会BOF主催「The Java Module System」を読む会(第10回)では、jlinkコマンドで実行イメージを生成する章を主に読みました。 そこには、生成する実行イメージのサイズを小さくする幾つかのオプションが紹介されていました。 そこで、オプションを指定するとどれだけ実行イメージが小さくなるのか、手元の環境で確認しました。

確認環境

項目 内容
OS Windows 10 Pro 1909 64bit 日本語版
JDK Liberica JDK 14.0.2 full 64bit版

確認結果

サイズ削減に関するオプション指定のないデフォルトでの実行イメージ生成のコマンドラインが次です。 (実行イメージには、java.baseモジュールだけを含む指定)

D:\work> jlink --add-modules java.base --output runtime

ここに、いくつかサイズ削減に関するオプションを指定し、どれだけ実行イメージが削減されるかを見てみました。

No. 指定したオプション サイズ(MB)
1 デフォルト 46.1
2 --compress=1 39.0
3 --compress=2 33.7
4 --strip-debug 40.6
5 --no-header-files 45.9
6 --strip-native-commands 46.0
7 No.3、No.4、No.5 31.0
compressオプション

1を指定すると、文字列リテラルの重複をなくし、共有します。

2を指定すると、lib/modulesファイルをzip圧縮します。

strip-debugオプション

JDK 13より前は、Javaデバッグ情報を削除します。 JDK 13からは、Javaデバッグ情報に加えてネイティブコマンド・ネイティブライブラリからもデバッグ情報を削除します。

[JDK-8219207] Add --strip-java-debug-attributes jlink option - Java Bug System

[JDK-8219257] Add --strip-native-debug-symbols jlink plugin - Java Bug System

no-header-filesオプション

JNIライブラリ作成時に使用するC/C++のヘッダーファイルを削除します。

strip-native-commands

javaコマンド等を生成しないようにします。このオプションを指定して生成した実行イメージにはjavaコマンドがないのでアプリケーションが実行できなくなります。

何故このオプションがあるのかなと考えると、jpackageツール(JDK 14から搭載)でインストーラをつくるとアプリケーション起動専用実行コマンドが作られるので、javaコマンドがなくてもアプリケーションを実行することができるからではないかと思います。

jpackageコマンドが使用するjlinkのオプション

JDK 14から搭載されたjpackageコマンドは、--runtime-imageオプションで予め生成した実行イメージを指定しない場合、内部でjlinkコマンドを呼び出しアプリケーション実行イメージを生成します。このとき、--strip-debug、--no-header-files、--no-manpages、--strip-native-commands のオプションをjlinkに対して使用します。

まとめ

jlinkコマンドで実行イメージを小さくする幾つかのオプションが用意されています。 配布する実行イメージを生成するときは、必要がなければこれらのオプションを指定してイメージサイズを小さくするとよいです。

C++クラスにstd::string型静的メンバー文字列定数を定義する

C++のstd::string型クラス静的メンバー変数に文字列定数を定義するいくつかの方法

はじめに

10年とちょっと前、C++C++98/03規格)でプログラミングする際に文字列定数をどうやって表現しようかと考え、クラスの静的メンバ変数にconst修飾子を付けました。

// Hello.h
class Hello {
public:
    void greet();
private:
    static const std::string MESSAGE;
};
  • ヘッダーファイルのクラス定義に静的メンバ定数(constなメンバ変数)を含める場合、組込み型は値を記述した定義を記述できますが、非組込み型は値を記述できず変数名までの宣言を記述し、実装ファイルに初期化を定義します。
// Hello.cpp
const std::string Hello::MESSAGE = "Hello, World!";
void Hello::greet() {
    std::cout << MESSAGE << std::endl;
}

LinuxGCCで、__attribute__((constructor))を付けた関数内でこのクラスの静的メンバ変数を使用するコードを書いていましたが、Red Hat Enterprise Linux 7(CentOS 7)にビルドを変更(GCCのバージョンがGCC4.4→4.8)したところ、プロセス起動時にエラー(Segmentation fault)となってしまいました。この直接の原因追究が難航したので、回避策の検討としてクラスの静的メンバに文字列定数を定義する別な方法を調べました。

調査の範囲

  • LinuxCentOS 7)でGCCを使用します。GCCはOS標準搭載の4.8系と、Software Collections(SLC)提供の8系および9系で確認します。
  • std::string型の定数を用意し、値だけでなく参照も同一とすることを前提とします。
  • モダンC++C++11以降)の規格を調査対象に含めます。

調査結果

C++17で導入された次の2つの機能を用いる方法がある。

  • constexprでstd::string_viewを用いる
  • inline指定をする
constexprとstd::string_view(C++17)

constexprを使ってクラスの静的メンバ変数に文字列定数を定義する方法は、C++11ではエラーとなりました(後述)。 これは、C++17で導入されたstd::string_viewにより解決します。

// Hello.h
class Hello {
public:
    void greet();
private:
    static constexpr std::string_view MESSAGE { "Hello, World!" };
};
inline(C++17)

inlineを使ってクラスの静的メンバ変数に文字列定数を定義します。

// Hello.h
class Hello {
public:
    void greet();
private:
    inline static constexpr std::string MESSAGE { "Hello, World!" };
};

うまくいかなかったアプローチ

namespace静的定数(C++98)

ヘッダーファイルにnamespace静的定数を定義する方法は、ヘッダーファイルをインクルードした翻訳単位毎にオブジェクトが割当てられるため、値は一緒だがアドレスが異なってしまいます。

// Hello.h
namespace hello {
static const std::string MESSAGE = "Hello, World!";
}
  • Hello.h をincludeするAlfa.cppとBravo.cppがあると、Alfa.cpp内の関数でhello::MESSAGEのアドレスを取り出し、Bravo.cpp内の関数でhello::MESSAGEのアドレスを取り出して両者を照合すると異なるアドレスとなります。
constexpr(C++11)

C++11では、constexprキーワードが導入されコンパイル時定数が定義できるようになりました。 しかし、std::string (実際にはbasic_string)がリテラル型ではないとコンパイルエラーになります。

error: the type 'const string {aka const std::basic_string<char>}' of constexpr variable 'Hello::MESSAGE' is not literal
     static constexpr std::string MESSAGE = "Hello, World!";

環境メモ

GCCC++11/14/17対応バージョン
  • C++11に対応したのはGCC 4.8
  • C++14に対応したのはGCC 5.1
  • C++17に対応したのはGCC 8
CentOS 7でGCCを使い分ける方法

CentOS 7はOS標準搭載GCCCentOSのパッケージリポジトリで提供されるGCC)のバージョンは4.8です。 それより新しいGCCバージョンを使う場合、Software Collectionsリポジトリから取得するのが楽です。

Software Collectionsは、Red Hat Enterprise LinuxFedoraCentOS、およびScientific Linux向けにソフトウェア集を提供するコミュニティ・プロジェクトです。 CentOSyumリポジトリ(extras)に、Software Collectionsを利用するyum設定を含むパッケージ centos-release-scl および centos-release-scl-rhが含まれます。 GCC 8や9を利用するには、centos-release-scl-rhをインストールしてから、devtoolset-9-gcc-c++ 等をインストールします。

~$ sudo yum install centos-release-scl-rh
:
~$ sudo yum install devtoolset-9-gcc-c++
:

Software Collectionsからインストールしたソフトウェアセットは次のコマンドで確認できます。

~$ scl -l
devtoolset-8
devtoolset-9
rh-git218

Software CollectionsからインストールしたGCCは、/opt/rh/devtoolset-9/root/ 以下に展開されます。ここには環境変数等は通っていないので、利用するにはひと手間必要です。

一時的に環境変数等が設定されたbash対話環境を使用する

環境変数が設定されたbashを新たに起動

~$ scl enable devtoolset-9 bash
~$

現在のbash環境変数の設定を取り込み

~$ source scl_source enable devtoolset-9
~$
永続的に環境変数等を設定する

そのマシンのユーザー全ての環境を変更します。

~$ sudo ln -s /opt/rh/devtoolset-9/enable /etc/profile.d/devtoolset-9.sh
ビルドツール Gradle

コマンドラインで直接コンパイルコマンド、リンクコマンドを叩くのは大変、でも Makefile を書くのも手間、というときにJavaがちょっとわかるプログラマー向けの推奨ビルドツールがGradleです。Gradleは、Javaのプログラムをビルドする3大ツールの一つとして有名ですが、C++のビルドにも対応しています。今回のように小さな検証プログラムをささっとビルドして実行するにはとっても楽です(共有ライブラリの作成も簡単)。

Gradleのインストールと設定

GradleはJavaで書かれたツールなので、実行にはJavaJDK 8以降)が必要です。 OS標準リポジトリからJDK 8をインストールします(パッケージ名はJDK 8の内部バージョンである1.8.0の表記となっています)。

~$ sudo yum install java-1.8.0-openjdk
  :

Gradleをインストールします。Gradleは Yumパッケージにはないので、手作業でダウンロードしインストールします。

https://gradle.org/releases/ から最新のバイナリをダウンロードします。 インストール手順に沿って、/opt/gradle の下に zipファイルを展開します。

~$ sudo unzip gradle-6.5.1-bin.zip -d /opt/gradle

環境変数PATHを、/opt/gradle/gradle-6.5.1/bin に通します。ローカルマシンの全ユーザーに永続的に設定するなら、/etc/profile.d/gradle.shを作成します。

export PATH=$PATH:/opt/gradle/gradle-6.5.1/bin
簡単Gradleビルドプロジェクト作成

ビルドプロジェクトのディレクトリを作成し、ビルド定義ファイル build.gradle を作成します。

~$ mkdir juliet
~$ cd juliet
juliet $ emacs build.gradle
 :
  • build.gradle
plugins {
    id 'cpp-library'
    id 'cpp-unit-test'
}

これは、共有ライブラリファイルを作成するプロジェクトで、テストコードの作成と実行も定義するビルド定義ファイルとなります。 ビルドするソースファイル名やインクルードパスなどは何も書いていませんが、これはデフォルトのディレクトリに放り込んでおけば勝手にビルドしてくれます。 デフォルトのディレクトリは次です。このディレクトリ構成を作成し、ソースファイル、ヘッダーファイル、テストソースファイルを配置します。

juliet/
  +-- build.gradle
  +-- src/
        +-- main/
        |     +-- cpp/
        |     |     +-- ソースファイル(Hello.cpp)
        |     +-- public/
        |           +-- ヘッダーファイル(Hello.h)
        +-- test/
              +-- cpp/
                    +-- テストコードのソースファイル(HelloTest.cpp)

src/main の下にあるファイルを共有ライブラリファイルにビルドし、test/mainの下にあるファイルを実行ファイルにビルドし共有ライブラリファイルにリンクして実行します。 C/C++プログラマーには奇異に映る構成かもしれませんが、ビルド定義で好みのディレクトリに設定することは可能です。

テスト実行(依存するコンパイル・リンクも自動的に行われる)は次のコマンドです。

juliet$ gradle runTest
> Task :runTest
Hello, world!

BUILD SUCCESSFUL in 2s
:

ビルド結果は build ディレクトリ下に生成されます。

juliet/
  +-- build/
        +-- exe/
        |     +-- test/
        |           +-- julietTest

プロジェクトの基点ディレクトリ名にちなんだビルド結果ファイルが生成されます。 このファイルには、テストコードとソースファイルとが1つにリンクされています。

ライブラリのビルドは次のコマンドです。

julite$ gradle build
  :

ビルド結果は build ディレクトリ下に生成されます。

juliet/
  +-- build/
        +-- lib/
        |     +-- main/
        |           +-- debug/
        |                 +-- libjuliet.so
Gradleメモ

gcc -v でgccのバージョンを出力するメッセージが日本語化されているとgradleがgccを見つけられません。 ロケールを英語に切り替えます。

juliet$ LC=ALL=C bash
juliet$ 

build.gradle に以下を追記します。

tasks.withType(CppCompile) {
    compilerArgs = [ '-std=c++11' ]
}

おわりに

C++であれこれ調べたのは5年振りくらいです。C++のコードをあれこれ書いたのは10年よりちょっと前でまだC++11規格が正式採択前です。 この10年でC++も随分変わっていることが実感できました。

JavaアプリケーションのJPMSモジュール化をGradleで

Javaアプリケーションをモジュール化し、Gradleでビルドするようにした

先日のブログの続きです。

torutk.hatenablog.jp

いろいろな問題点に直面してしまいました。その後の紆余曲折を書いていきます。

使用するIDE

Gradleプロジェクトとして作ったディレクトリは、直接NetBeans IDEIntelliJ IDEAで開くことができます。そこで、Gradle化にあたっては、IDE固有のプロジェクト設定ファイルは設けずにGradleのビルド定義だけを置くことにします。リポジトリに置くソースファイルは文字コードUTF-8とします。

NetBeans IDEの注意点

Windows日本語版上のNetBeans IDE 12.0で文字コードUTF-8のソースファイルを持つGradleプロジェクトを開くと文字化けします。回避策はNetBeans IDEの起動オプションで-J-Dfile.encoding=UTF-8を指定します。ただし、JDK 9~14ではこのオプションを指定するとJavaVMのバグによりクラッシュするので、NetBeans IDEJDK 8で動かします。NetBeans IDEJDK 8で動かしても、プロジェクトのビルドは任意のJDKバージョンを設定できます。

IntelliJ IDEAは問題なく

Windows日本語版上のIntelliJ IDEA 2020で文字コードUTF-8のソースファイルを持つGradleプロジェクトを開くと、UTF-8文字コードを認識し文字化けは発生しません。IntelliJ IDEAの内蔵JDKが独自のパッチを当ててクラッシュを避けているようです。

プロジェクトの構造

JPMS/Gradle 対応前の構造

JPMS/Gradle化する前の EarthGadgetアプリケーションのソースコード構造の概要は次です。

EarthGadget
  +- nbproject/                   <-- NetBeans IDEのプロジェクト定義等が格納
  +- src/                         <-- EarthGadgetアプリケーションのソースコード(画像含む)
  +- build.xml                    <-- NetBeans IDEのAntビルド定義
  +- javafx-gadgetsupport/        <-- Gitサブモジュールで取り込んだライブラリリポジトリ

EarthGadget はライブラリとして別リポジトリにあるjavafx-gadgetsupportを使用しています。javafx-gadgetsupport を GitのサブモジュールとしてEarthGadgetのプロジェクトツリー配下に配置し、一緒にビルドする構造としています。EarthGadgetをビルドすると依存関係からjavafx-gadgetsupportをビルドしてバイナリを生成します。

JPMS/Gradle 対応の構造

Gradle化する際には、EarthGadgetをビルドすると依存関係からjavafx-gadgetsupportをビルドするようにする仕組みを維持したいのでその方法を探りました。

独立したGradleプロジェクトとして構成すると、順番にビルドする必要が生じますが、マルチプロジェクトとして構成すると、依存関係をプロジェクトに対して設定できます。

次はマルチプロジェクトのディレクトリ構成です。チュートリアル、ユーザーマニュアルでは、ルートプロジェクトの下に実際のプロジェクトを並べる構成が記載されています。それにならうと、

EarthGadget
  +-- settings.gradle               <-- サブプロジェクト(earthgadget, javafx-gadgetsupport)を定義
  +-- earthgadget/                  <-- EarthGadgetアプリケーションのGradleサブプロジェクト
  +-- javafx-gadgetsupport/         <-- Gitサブモジュールで取り込んだJavaFX GadgetSupportライブラリのGradleサブプロジェクト

という構成です。これでGradle化してビルドができました。 次は、元の構造に近い形でマルチプロジェクトのディレクトリが構成できないかと試してみました。

EarthGadget
  +-- settings.gradle               <-- サブプロジェクト(javafx-gadgetsupport)を定義
  +-- build.gradle                  <-- EarthGadgetアプリケーションのビルド定義
  +-- src/                          <-- EarthGadgetアプリケーションのソースコード(画像含む)
  +-- javafx-gadgetsupport/         <-- Gitサブモジュールで取り込んだJavaFX GadgetSupportライブラリのGradleサブプロジェクト    

マルチプロジェクト構成のルートプロジェクトにEarthGadgetアプリケーションのビルド定義とソース一式を配置、サブプロジェクトにjavafx-gadgetsupportライブラリを配置します。

移行作業

gradle化作業をgitの新たなブランチで行う

JPMS対応およびGradle対応を、gitの新しいブランチ(migrate_to_gradle)を設けてその上で作業します。

  • Gradle化前のEarthGadgetプロジェクトをgithubからクローン
D:\work> git clone --recursive https://github.com/torutk/EarthGadget.git
Cloning into 'EarthGadget'...
remote: Enumerating objects: 79, done.
remote: Total 79 (delta 0), reused 0 (delta 0), pack-reused 79
Unpacking objects: 100% (79/79), 412.56 KiB | 625.00 KiB/s, done.
Submodule 'javafx-gadgetsupport' (https://github.com/torutk/javafx-gadgetsupport.git) registered for path 'javafx-gadgetsupport'
Cloning into 'D:/work/EarthGadget/javafx-gadgetsupport'...
remote: Enumerating objects: 18, done.
remote: Counting objects: 100% (18/18), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 74 (delta 0), reused 13 (delta 0), pack-reused 56
Submodule path 'javafx-gadgetsupport': checked out 'd7cbb2d2d9605769d40769133a55ef5a60049261'
D:\work>
  • Gradle化の作業を行うブランチをmasterから新規作成
D:\work> cd EarthGadget
D:\work\EarthGadget> git checkout -b migrate_to_gradle
Switched to a new branch 'migrate_to_gradle'

D:\work\EarthGadget> git branch
  master
* migrate_to_gradle

D:\work\EarthGadget> cd javafx-gadgetsupport

D:\work\EarthGadget\javafx-gadgetsupport> git checkout -b migrate_to_gradle
Switched to a new branch 'migrate_to_gradle'
 
D:\work\EarthGadget\javafx-gadgetsupport> git branch
  master
* migrate_to_gradle

D:\work\EarthGadget\javafx-gadgetsupport> 
javafx-gadgetsupportライブラリのGradle対応

Gradleプロジェクトとするには、gradle initコマンドで必要なファイル群を作成します。その際、種類をbasicにしておくと、雛形のソースコードやそのコードを使いサンプルのライブラリへの依存がないビルド定義ファイルが生成されます。

  • gradle init でGradleプロジェクトの作成
D:\work\EarthGadget\javafx-gadgetsupport> gradle init --type basic --dsl groovy --project-name javafx-gadgetsupport
  • NetBeans固有のプロジェクト定義を削除
D:\work\EarthGadget\javafx-gadgetsupport> git rm -r build.xml nbproject
rm 'build.xml'
rm 'nbproject/build-impl.xml'
rm 'nbproject/genfiles.properties'
rm 'nbproject/project.properties'
rm 'nbproject/project.xml'
  • Javaのライブラリを生成するGradleビルド定義を記述
plugins {
    id 'java-library'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
    modularity.inferModulePath = true
}

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
    }
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

ライブラリをビルドするので、プラグインjava-libraryを指定します。 Javaソースコードのバージョンとコンパイル後のクラスファイルのバージョンをJava SE 11に指定します。 JPMSモジュール対応の設定をします。 ソースファイルのディレクトリは、gradleのデフォルトであるsrc/main/java以下ではなく、src以下にあることを設定します。 ソースファイルの文字コードUTF-8であることを指定します。

  • module-info.java をsrc直下に新規作成
module com.torutk.gadget.support {
    requires javafx.controls;
    requires transitive java.prefs;
    exports com.torutk.gadget.support;
}

モジュール名は、代表公開パッケージと同じ名前としています。 requiresにはこのライブラリが使用するライブラリを提供するモジュールを指定しています。 このライブラリのAPIでは、Java Preferences APIの型を使っていますが、ライブラリ利用者はAPIの利用箇所にほぼ限定してJava Preferences APIを使うので、transitiveを指定し、ライブラリ利用者のモジュール定義にjava.prefsモジュールを指定しなくてもよいようにしています。

EarthGadgetアプリケーションのGradle対応
  • gradle init でGradleプロジェクトの作成
D:\work\EarthGadget> gradle init --type basic --dsl groovy --project-name EarthGadget
  • NetBeans固有のプロジェクト定義を削除
D:\work\EarthGadget> git rm -r build.xml nbproject
rm 'build.xml'
rm 'nbproject/build-impl.xml'
rm 'nbproject/genfiles.properties'
rm 'nbproject/project.properties'
rm 'nbproject/project.xml'
  • settings.gradle にjavafx-gadgetsupportライブラリのサブプロジェクトを指定
rootProject.name = 'EarthGadget'
include 'javafx-gadgetsupport'
  • Javaのアプリケーションを生成するGradleビルド定義を記述
plugins {
    id 'application'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
    modularity.inferModulePath = true
}

dependencies {
    implementation project(':javafx-gadgetsupport')
}

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
        resources {
            srcDirs = ['src']
        }
    }
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

application {
    mainModule = 'com.torutk.gadget.earth'
    mainClass = 'com.torutk.gadget.earth.EarthGadgetApp'
}

アプリケーションをビルドするので、プラグインにapplicationを指定します。 Javaソースコードのバージョンとコンパイル後のクラスファイルのバージョンをJava SE 11に指定します。 JPMSモジュール対応の設定をします。 依存関係でjavafx-gadgetsupportを指定します。マルチプロジェクトでは依存関係に別のサブプロジェクトを指定することができます。 ソースファイルのディレクトリは、gradleのデフォルトであるsrc/main/java以下ではなく、src以下にあることを設定します。また、リソースファイルのディレクトリも、gradleのデフォルトであるsrc/main/resources以下ではなく、src以下にあることを設定します。 ソースファイルの文字コードUTF-8であることを指定します。 アプリケーションとしてJPMSのメインモジュールと、メインクラスを指定します。

  • module-info.java をsrc直下に新規作成
module com.torutk.gadget.earth {
    requires com.torutk.gadget.support;
    requires javafx.controls;
    opens com.torutk.gadget.earth to javafx.graphics;
}

モジュール名は、代表パッケージと同じ名前としています。 requiresにはこのアプリケーションが使用するライブラリを提供するモジュールを指定しています。 JavaFXのアプリケーションでは、アプリケーション側で作成したクラスに対してJavaFXライブラリ側からリフレクションをかけるので、opensでJavaFXライブラリから実行時にアクセスすることを許可しています。

ビルドと実行
  • ビルド
D:\work\EarthGadget> gradle build
  :
  • 実行
D:\work\EarthGadget>gradle run

WindowsインストーラMSI)の作成

EarthGadgetアプリケーションには、javapackagerコマンドを使ってWindowsインストーラーを作成するバッチファイルを含めていました。しかしjavapackagerコマンドはJDK 11で削除されてしまいました。

javapackagerコマンドの後継として、JDK 14からjpackageコマンドが含まれるようになりました(incubator段階)。今回は、JDK 14のjpackageコマンドを使ってWindowsインストーラー(MSI)を生成するようバッチファイルを修正します。

ツール種類 ツール
JDK Liberica JDK 14
MSIインストーラー作成ツール WiX Toolset 3.10
@echo off

%JAVA_HOME%\bin\jpackage ^
--type msi ^
--win-upgrade-uuid 38d49c58-102e-486b-bfac-8a0f6e796a93 ^
--win-menu ^
--win-menu-group "Tiny Gadgets" ^
--win-shortcut ^
--app-version 0.2.2 ^
--description "Earth 3D rounding on desktop" ^
--name "EarthGadget" ^
--dest build\installer ^
--vendor Takahashi ^
--module-path build\libs;javafx-gadgetsupport\build\libs ^
--module com.torutk.gadget.earth/com.torutk.gadget.earth.EarthGadgetApp ^
--verbose
オプション 意味 指定例
--type インストーラー種類を指定 msi
--win-upgrade-uuid バージョンアップ時に、既にインストール済みの前のバージョンがあればそれを置き換えるための識別子
--win-menu スタートのメニューに追加する
--win-menu-group スタートメニューのフォルダ名
--win-shortcut デスクトップにショートカットを作成
--app-version インストーラに設定するバージョン番号
--description 説明文字列
--name アプリケーション名、実行ファイルの基底名
--dest 作成したインストーラファイルの格納先
--vendor 作成者・組織名
--module-path インストーラに含めるモジュールの置かれているディレクトリのリスト
--module 実行するメインモジュール名
  • --module では、MainClass属性を持つモジュールであればモジュール名のみ指定、MainClass属性を持たないモジュールであればモジュール名/メインクラス名を指定
  • インストーラー実行時にインストールディレクトリを選択可能とするには、--win-dir-chooser オプションを指定
  • インストーラーのインストール先をシステム共通の場所(C:\Program Files 以下)ではなく個人のディレクトリ下とするには、--win-per-user-install オプションを指定
  • JavaVMオプションを指定したい場合は、--java-optionsに指定、インストール後の設定ファイル(<インストールディレクトリ>\app\<アプリ名>.cfg)のJavaOptions項にオプションが記載
  • jpackageコマンドが裏で実行するjlinkの生成ファイルやWiX Toolsetの設定・オブジェクトファイルを確認したい場合は、--tempで空のディレクトリを指定

バッチファイル実行時に、PATHにJDK 14、WiX Toolsetを入れておきます。

インストーラーのファイルサイズとインストール後のサイズ

インストーラーファイルの大きさは、28MBでした。 インストール後のインストールディレクトリ以下の大きさは、85MBでした。

JavaFXを同梱するLiberica JDK 14のインストールディレクトリ以下の大きさは400MB強なので、JPMSモジュール機構による必要なモジュールを抜粋したJava実行イメージの削減効果がよく出ています。

jpackage 感想

jlinkを内部で実行するので、jpackageコマンドだけでインストーラが生成されるのはよいです。

バージョンアップ時に、既にインストール済みの古いバージョンがある場合、アップグレードインストール(古いバージョンが削除され新しいバージョンがインストールされる)を可能にするためUUIDを渡せるのはjavapackagerの時に比べて改善されています。