Windows用のアプリケーションを開発しようと考えると、Windows上に統合開発環境などを整えて開発するのが基本だと思いますが、Linux環境とコマンドラインインターフェースばかり使っていると、再現性の低いGUI環境とUNIXコマンドの使えない環境にストレスを覚えるようになったため、無理やりにでもLinux上でWindows上で動作する実行ファイルをコンパイルすべく、開発環境構築だけでほぼ丸3日ほど試行錯誤したのでその時の記録。
目的:
- Windows環境で動作する実行ファイル(exe)をLinux(Ubuntu)環境でクロスコンパイルして作成する
- Linux環境でGUI環境のIDE等を用いず、再現性のあるコマンドコンパイルでWindows用にアプリを作りたい方
- C/C++の簡単な知識
内容の試用コード:
https://github.com/sukima-log/TEST_Linux_MinGW_CMake
上記のお試しコードは、スクリプト一発で開発環境を整え、すぐに動作させることができるように作っています。
以下では、そのコードと環境構築の方法について、もう少し詳細にまとめています。
コードやスクリプトを見た方が早いという方は、散らかっているなりにコード内にも、そこそこコメントをまめに入れているので、そちらをご覧ください。
LinuxによるWindowsアプリ開発概要
最初に開発における流れと開発環境について、あらましと前提を簡単にまとめます。
WindowsとLinuxで異なる実行ファイル形式
そもそも、Windows環境とLinux環境で実行することができるファイルが違うということ自体、理解してなかったのでそこから
Windows実行ファイル |
|
Linux実行ファイル |
|
Wineなどのソフトウェアを使うと、Windows用に作成されたアプリケーションをLinux上で動作させたりもできます。
昔ゲームなんかを、これで動かしたりしてたことがありました。
開発全体の流れ
- クロスコンパイルのためのツール「MinGW」開発環境構築
- CMakeを用いたWindows用のビルド自動化環境構築
- 必要なライブラリおよび、ヘッダーファイルのインストール
- コード記述
- 動作確認
開発全体の流れとしては上のようになります。4、5は一般的な開発手順と変わりませんが、2、3はクロスコンパイルをする上で、環境の構築に少し込み入った作業が必要で、ここで一番手間取りました。
環境構築が面倒な場合は、今回のお試しコードが動く環境構築用のスクリプトを、試用コード内に置いているので、READMEなどに従って実行すればひとまず動く環境は構築できるはずです。
開発環境
今回あえてLinux上でWindowsアプリを開発する理由としては、使い慣れたUNIXコマンドが使える点が個人的にメリットとして大きいためです。また、環境構築などスクリプト、あるいはDockerfileにまとめておけば、再現性のある環境を構築することができる点も、GUIで動作するIDEを使わず、コマンドライン上で開発するメリットと言えます。
Windows上で動かすためのアプリケーションを開発するために、WindowsPC上にLinux環境を整えるというと、余計な手間をかけているような気がしますが、とりあえず、簡単にLinux環境を整えられるソフトウェアを以下に書き出してみました。
ツール | 自己評価 | 所感 |
---|---|---|
Cygwin | △ | 環境構築がめんどくさ過ぎる |
WSL2 | 〇 | 使い慣れたUbuntuが使える |
VirtualBox | 〇 | 使い慣れたUbuntuが使える |
個人的に、Cygwinはパッケージを適切にインストールして環境を構築するのが面倒過ぎました。Dockerは、あまりWindows上で使わないので今回は検討から外れています。
VirtualBoxはVagrantを使用することで、設定ファイルを元に環境を作成してくれるので、今でもよく使います。ですが、最近は手軽さからWSL推しなので今回の実装は、WSL(Ubuntu)上で動作を確認しました。
言語 | C/C++ |
---|---|
ライブラリ | PortAudio |
ライブラリのリンクをしたいので、あえてHello,Worldなどの簡単な動作テストではなく、ライブラリを使った実装を行っています。
動作確認のためのコード概要
ここで、今回の手順で構築できる開発環境において、動作を確認するためのサンプルコードの動作と構造について簡単に見ておきます。
アプリケーションの目的
Linux環境でWindows用の実行ファイルをクロスコンパイルして、動作を確認するためのチュートリアル的なコードを以下GitHub上に共有しています。
▼確認用コード
マイクからリアルタイムに声を入力して、スピーカーなどの標準出力からそのまま、その音声を出力するだけの簡単なコードです。
単純にHello,Worldなどで確認するのに比べて、マイク入出力のためのライブラリのインストールなどの手順を踏む必要があるため、今回の環境構築のための手順が一通りこなせるはずです。
マイクなどの入力の無い環境の方は、main.cppの中身を自分で「Hello,World」を表示するコードに書き換えて、MinGWを用いてクロスコンパイルしたプログラムがWindows側で無事動作することを確認するだけでも構いません。
アプリケーションの構造と動作
ここでは、コード内の説明を行います。あくまで、機能の部分的な説明になるため、実際動作する全文は、上で共有したGitHubリポジトリ内のmain.cpp等をご覧ください。
プログラムの全体像としては、main関数内で、入力デバイス(マイクなど)と、出力デバイス(スピーカーやイヤフォン)の設定を行って、コールバック関数を呼び出すことで、入力から音を取り込んで、出力バッファにその音を書き込むことで音声を出力しています。

ノイズキャンセリングなど、このコード内の処理だけでは行われないので、普段Web会議やゲーム内ではカットされてしまって聞こえない、わずかなノイズ音もマイクが普段拾っていることを体感することができると思います。
使用するライブラリやヘッダーファイル
今回は、マイクの入出力を行うために、「PortAudio」というライブラリを用いています。そこで、ファイル先頭ではincludeでportaudio.hというヘッダーファイルを読み込んでいます。
C++:
#include <stdio.h>
#include <math.h>
#include "portaudio.h"
#define SAMPLE_RATE (44100)
#define PA_SAMPLE_TYPE paFloat32
#define FRAMES_PER_BUFFER (64)
次に、PortAudioの音声処理に用いる定数を以下の表の通りに設定しています。
SAMPLE_RATE | サンプリング周波数 |
PA_SAMPLE_TYPE | 単精度32bit浮動小数点型 |
FRAMES_PER_BUFFER | バッファーのサイズ(フレーム数) |
音声処理部
main関数内で呼び出されたコールバック関数内で音声の処理を行います。ここでは、入力バッファから読み出したモノラルの音声データを、出力バッファにステレオで書き出すだけの処理を行っています。
C++:
static int ioCallback (
const void *inputBuffer
, void *outputBuffer
, unsigned long framesPerBuffer
, const PaStreamCallbackTimeInfo* timeInfo
, PaStreamCallbackFlags statusFlags
, void *userData
) {
SAMPLE *out = (SAMPLE*)outputBuffer;
const SAMPLE *in = (const SAMPLE*)inputBuffer;
unsigned int i;
/* Prevent unused variable warnings. */
(void) timeInfo;
(void) statusFlags;
(void) userData;
if( inputBuffer == NULL) {
for( i=0; i<framesPerBuffer; i++ ) {
*out++ = 0;
*out++ = 0;
}
}
else {
for( i=0; i<framesPerBuffer; i++ ) {
*out++ = *in;
*out++ = *in++;
}
}
return paContinue;
}
入出力デバイスの設定
以下はPortAudioで入出力デバイスを設定するための一例です。
C++:
/* デフォルトインプットデバイス */
inputParameters.device = Pa_GetDefaultInputDevice();
inputParameters.channelCount = 1; /* モノラルインプット */
inputParameters.sampleFormat = PA_SAMPLE_TYPE;
inputParameters.suggestedLatency = Pa_GetDeviceInfo( inputParameters.device )->defaultLowInputLatency;
inputParameters.hostApiSpecificStreamInfo = NULL;
/* デフォルトアウトプットデバイス */
outputParameters.device = Pa_GetDefaultOutputDevice();
outputParameters.channelCount = 2; /* ステレオアウトプット */
outputParameters.sampleFormat = PA_SAMPLE_TYPE;
outputParameters.suggestedLatency = Pa_GetDeviceInfo( outputParameters.device )->defaultLowOutputLatency;
outputParameters.hostApiSpecificStreamInfo = NULL;
MinGW環境構築
Linuxで一般的に用いられるGCCコンパイラはあくまでLinux用で、C/C++で記述したコードをコンパイルしても、Windows上で動作する実行ファイルを生成することができないため、MinGW(Minimalist GNU for Windows)を用います。
MinGWインストール
MinGWは、「Minimalist GNU for Windows」の略らしく、GCCをWindows用に利用できるようにしたもののようです。
インストール前のアップデート:
sudo apt-get update
必要だったり、必要でなかったりするみたいですが、大は小を兼ねるので今回はとりあえず以下すべてをインストールしています。細かいことはよく知りません。
sudo apt-get install mingw-w64 -y
sudo apt-get install g++-mingw-w64-i686
sudo apt-get install binutils-mingw-w64-x86-64 -y
sudo apt-get install mingw-w64-common -y
sudo apt-get install mingw-w64-x86-64-dev -y
sudo apt-get install mingw-w64-tools -y
sudo apt-get install gcc-mingw-w64-base -y
sudo apt-get install gcc-mingw-w64-x86-64 -y
sudo apt-get install g++-mingw-w64-x86-64 -y
sudo apt-get install libsndfile1-dev -y
バージョン確認:
x86_64-w64-mingw32-gcc --version
CMake環境構築
今後、必死に手作業でライブラリをかき集めて用意したり、コンパイル時にたくさんのオプションを調べて付与するのが面倒すぎるので、CMakeというツールを用います。
CMakeを使うことでmakefile(コンパイルのコマンド群)の作成から、ライブラリをビルド用フォルダに集めるところまで自動化できるので後々作業が非常に楽になります。
CMakeのインストール
CMakeはクロスプラットフォームのビルド自動化ツールで、C、C++、およびその他の言語用のビルドシステムを生成するためのメタビルドシステムです。
インストール前の下準備:
sudo apt-get update
sudo apt install cmake -y
cmake --version
Cmake使わなくても手動で行うか。Makefileやスクリプトファイルを自分で書くことで全てできますが、逐一ライブラリを探して取ってこなければいけなかったり、多少の設定変更した場合の該当箇所の書き換えのが面倒なので、できるだけCMakeを用いることにします。
toolchain.cmakeの記述
CMakeにターゲットとなるシステムやプラットフォームを指定してやらなければ、クロスコンパイルできないのでtouchコマンドなどで「toolchain.cmake」というファイルを作ります。
ファイルの中に、設定を記述することで、このファイルを使いまわして、ターゲットシステム用にライブラリをインストールしたり、実行ファイルをクロスコンパイルすることが可能です。
詳しくは、以下公式資料などが参考になります。
今回使用するToolchain Fileは以下のようになっています。ターゲットシステムをWindowsとして、MinGWを用いて、C/C++やfortranなどをクロスコンパイルするための設定を記述したものです。
このファイルの配置場所に特に決まりはなく、任意の場所に配置すればいいです。今回の試用コードでは、main.cppのある階層に置いています。
toolchain.cmake:
set(CMAKE_SYSTEM_NAME Windows)
set(TOOLCHAIN_PREFIX x86_64-w64-mingw32)
# cross compilers to use for C, C++ and Fortran
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)
set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran)
set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres)
# target environment on the build host system
set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX})
# adjust the default behaviour of the FIND_XXX() commands:
# search headers and libraries in the target environment, search
# programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
ライブラリ/ヘッダ汎用インストール手順
Linuxで用いられるaptやyumなどのパッケージ管理ツールは、Windows用のアプリケーションにコンパイルする時には使えません。
ライブラリインストールの課題
C/C++のコードを書く際、「stdio.h」や「iostream」などのヘッダーファイルやライブラリをinclude文を用いてインストールしていると思います。
さらに、今回の例で挙げているコードのように、GCCに標準で揃えられているもの以外に、アプリケーションに要求される機能に応じて追加のライブラリやヘッダをインポートする場合には、それらをインストールしてこなければいけません。
ここで、少し厄介なのが、Linuxで広く使用されているパッケージ管理ツール「apt」などでいつものように「apt install」しても、MinGWがライブラリ等を拾ってきてくれません。
Linux上で動作する実行ファイルにGCCを用いてコンパイルするだけなら、apt-getなどのパッケージ管理システムでインストールしてくればよいですが、MinGWで参照されるインクルードディレクトリのパスは、「/usr/x86_64-w64-mingw32」ディレクトリ以下になるため、主に「/usr/lib」や「/usr/include」以下をコンパイルパス、ライブラリパスに設定しているaptでは、インストールすることができません。
Compiler | include Path |
---|---|
GCC | 「/usr/lib/」もしくは「/usr/include/」以下 |
MinGW | 「/usr/x86_64-w64-mingw32/」以下 |
そもそも、MinGWの見るパスを変更できたとしても、Windowsの実行ファイルとLinuxの実行ファイルでは、同じライブラリを用いていてもプラットフォームごとに要求するライブラリの性質が異なり、互換性もないため使えません。また、ライブラリの性質自体が異なるため、インストールするディレクトリは分けておいた方が良いとされています。
ファイル拡張子 | 対象プラットフォーム |
---|---|
「.so」ファイル | Unix/Linuxシステムで使用される |
「.dll」ファイル | Windowsシステムで使用される |
動的ライブラリ「.dll」は「/usr/x86_64-w64-mingw32/bin/」以下に格納されています。PoatAudioなどでは静的ライブラリ「.lib」は提供されていないため、不可能ではないらしいですが、静的リンクは一般的には用いません。
Linux上でMinGWを用いたクロスコンパイルのためにライブラリを取得するには、Gitなどから「git clone」する、あるいは、wgetなどでダウンロードしてきて、手動でmakeコマンドなどを用いてインストールする必要があるため、パッケージ管理システムで簡単に環境が整えられるのに比べると、少し癖があるように思います。
具体的には、以下の手順でインストールを進めていきます。先に上のCMakeの章で説明した環境構築をやっておく必要があるので注意が必要です。CMakeが実行できる環境に整えておきましょう。
ライブラリ/ヘッダファイルインストール方法
インストールしてくるライブラリにもよって、多少手順が違うかもしれませんが、ほとんど以下の方法で必要なものはインストールできるテンプレート手順をまとめます。
(共通)ライブラリインストールに必要になるもの:
sudo apt-get install gpg wget git -y
cd ディレクトリ
git clone リモートリポジトリ
wget ダウンロードしてくるコンテンツ
tar zxvf ダウンロードコンテンツ.tar.gz
cd リポジトリ内
ここまでは、大まかに共通の手順となります。
環境にあわせたmakefile作成とインストール
ここから、インストールしてきたディレクトリ内にあるものによって、多少操作が変わります。
例えばSoundTouchライブラリを例に挙げると、以下のようなファイルが含まれます。たいていどんなライブラリでも似たような構成になっているはず、、です。
tree:
soundtouch-2.3.0
├── CMakeLists.txt
├── Makefile.am
├── SoundTouchConfig.cmake.in
├── bootstrap
├── configure.ac
├── make-win.bat
├── soundtouch.pc.in
...
ざっくりとした各ファイルの説明は以下の通りです。
ファイル種別 | 説明 |
---|---|
CMakeLists.txt | CMakeコマンドを実行するときに使う |
Makefile.am | autoreconfコマンドを実行するときに使う |
*.cmake.in | CMakeの設定ファイル |
bootstrap | 関連するファイルを生成するスクリプト |
configure.ac | autoreconfコマンドを実行するときに使う |
make-win.bat | Windows側でmakeを実行するためのバッチファイル |
*.pc.in | CMakeやautoconfコマンドでpcファイルを生成、パッケージ情報を記述したファイルで他のプログラムが依存関係を解決する際に使用 |
以下の処理は複数該当する項目がある場合は、容量制限の問題などは置いておくと、とりあえずすべて実行しておけば問題ないと思われます。実際、一つだけ行っても、必要なファイルがすべてインストールされないことがありました。
▶「CMakeLists.txt」がある場合
cmake例:
cmake -DCMAKE_INSTALL_PREFIX=/usr/x86_64-w64-mingw32 \
-DCMAKE_TOOLCHAIN_FILE=$CURRENT/../toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON ..
「$CURRENT/../toolchain.cmake」は作成した適切な任意のtoolchain.cmake位置、また、末尾の「..」はリポジトリなどの「CMakeLists.txt」のある相対位置に変換してください。
make:
make
sudo make install
▶「configure.ac」「Makefile.am」などがある場合
autoreconf -i
configureができるので
./configure --host=x86_64-w64-mingw32 --prefix=/usr/x86_64-w64-mingw32/
make
sudo make install
▶「bootstrap」がある場合
./bootstrap
configureができるので
./configure --host=x86_64-w64-mingw32 --prefix=/usr/x86_64-w64-mingw32/
make
sudo make install
以上の方法で現状インストールできることを試したライブラリは以下です。
- Portaudio
- SoundTouch
- World
補足:エディタ機能で可視化されるエラー
基本的にVSCodeなど、エディタ側で特別に設定しない限り、エディタはGCCでインクルードされるヘッダーファイルのディレクトリしか見ていないと思います。
このとき、MinGWでしかインストールしていないライブラリを使っていると、エディタ側の補完機能や、強調表示などが働かなかったり、定義されていない表記として、エラーのように表示されることがあります。
非常に気にはなるのですが、動作上は問題ありません。
どうしても気になるようなら、エディタ側のインクルード先のディレクトリの設定を変更するか、いっそのこと、「apt install」で、ライブラリをインストールするなどすれば、エラーは消えて、コード補完などにも対応できます。
クロスコンパイルして動作させる手順
ここまでで、あらかたクロスコンパイルする準備は整ったので、最後に動作させる手順についてまとめておきます。
CMakeLists.txtの記述
toolchain.cmakeはすでに上で作成しているので、あと足りないものは自分のコードをコンパイルするための設定を書いた「CMakeLists.txt」です。必要なライブラリや実行ファイルの名前などの設定を記入していきます。
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(myproject)
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# アプリケーション(実行ファイル)名指定
add_executable(myexe main.cpp)
# ライブラリ名指定
target_link_libraries(myexe portaudio)
# 実行ファイルと依存するライブラリを含むフォルダを指定する
set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install")
install(TARGETS myexe RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}")
# ここでMinGW用のライブラリがある場所に指定しておかないと、.dllじゃなく.soファイルが入る
install(DIRECTORY "/usr/x86_64-w64-mingw32/" DESTINATION "${CMAKE_INSTALL_PREFIX}")
8行目の「myexe」のところが生成される実行ファイルの名前になります。
また、add_executable(myexe main.cpp function1.cpp...)のように、ビルドするファイルが複数ある場合には、続ける形で記述していきます。
10行目のtarget_link_libraries(myexe portaudio)のように、libportaudio.dllファイルを動的リンクすることを明記しています。lib「ライブラリ名」.dllのライブラリ名のところを書いておけば良いようです。
大文字と小文字は区別するようなので、キャメルケースで大文字の入っているようなライブラリ名の場合、しっかりと大文字で記述しないと、そのようなライブラリは見つかりませんといった、エラーを出されます。
実行方法
コンパイルはbuildディレクトリを作成して、その下で作業を行うのが基本らしいです。初期化する際には、buildディレクトリごと消してしまえるので、たしかにその方が楽だと思います。
手順1. buildディレクトリの作成&移動:
mkdir build
cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake -DCMAKE_BUILD_TYPE=Release ..
「toolchain.cmake」の場所は自分がファイルを配置した場所へ変更し、末尾の「..」は「CMakeLists.txt」のある場所を示すようにします。
手順3. クロスコンパイル:
make
make install
buildディレクトリ以下に実行ファイル(ここではmyexe.exe)ができ、/bin/install以下にライブラリが入ります。
手順5. 実行ファイルおよび、ライブラリをWindows環境へ移動
buildディレクトリごと移動させると意外と重たいので、「/build」以下にある実行ファイルを「/build/install/bin」以下に移動させて、binディレクトリごと、動的にリンクするためのライブラリと実行ファイルを移動させます。※WSL2なら「/mnt/」以上の任意の場所
実行ファイルが、ライブラリのある「/build/install/bin」以下にないと、ライブラリを見つけてくれないので実行できません。(もしかしたらもっといい方法があるかも)
手順6. 実行ファイル「myexe.exe」を実行
マイクで声を入力して、出力(ヘッドホンやスピーカー)などからその声が帰ってきたら成功
立ち上がり、エラーが出て即座に終了した場合、コードは正常に動いていますが、デフォルトのマイクやスピーカー(標準入出力)が見つからず終了した可能性が高いです。
Windowsの設定でマイクやスピーカーの設定を見直してみてください。
補足:libstdc++-6.dllが見つからないため、
今回のPortAudioだけを使ったコードではないですが、MinGWをWindows上にインストールしておらず、パスが通っていない場合、Windows上でアプリを実行しようとすると以下のようなエラーを出される場合があります。
エラーの例:
「libstdc++-6.dllが見つからないため、」
「libgcc_s_seh-1.dllが見つからないため、」

必要なライブラリは、開発者がLinux上にMinGWをインストールした時点で入っており、アプリの利用者全員がMinGWをインストールしているわけでもないことを考えると、findコマンドで探して来て、アプリのあるフォルダ上にコピーして配布するか、あるいはCMake側に必要なライブラリとして記載するなど、何らかの形でライブラリを取ってこなければいけません。
参考:
https://kamino.hatenablog.com/entry/cmake_tutorial1
https://heavywatal.github.io/dev/autotools.html
以上、ありがとうございました。