LinuxでAndroidSDKの更新をするとき、/tmpの容量不足で失敗する事への対処方法

最近のLinuxディストリビューションは/tmpに物理メモリを割り当てていることがあるようですが、例えば当方が使用しているFedora26は、標準で物理メモリの半分をtmpfsとして/tmpにマウントしているようです。

当方のPCは物理メモリが8Gしか無いので、/tmpは4Gしか使えないことになります。これで、AndroidSDKの更新(特にNDKの更新)をすると、/tmpを使い切ってしまい、「ディスクの空きが足りない」的な英語のエラーメッセージと共に失敗します。この現象の対策をしてみます。

対策

/tmpへのtmpfsのマウントはsystemdがやっているのですが、下手にsystemdのユニットをいじって起動不能になるのは面倒なので、systemdには手を出さないことにします。

javaの一時ファイル用ディレクトリを変更する

というわけで、javaの挙動を変えていきます。java実行環境はシステムプロパティ「java.io.tmpdir」を一時ファイル記憶場所とするので、-Dオプションでこのプロパティを変えてしまえば、任意のディレクトリを一時ファイル記憶場所にできます。

java "-Djava.io.tmpdir=/path/to/tmp" -jar hogehoge.jar

では、どうやってAndroidStudioやsdkmanagerにこのオプションを与えるかですが、まず、AndroidStudioはシェルスクリプトpath/to/AndroidStudio/bin/studio.shを、sdkmanagerはシェルスクリプトpath/to/AndroidSDK/tools/bin/sdkmanagerをそれぞれ実行することで起動しています。これらスクリプトjavaコマンドを起動しているのですが、オプションを与えるためにこれらスクリプトを書き換えてしまうと、逆に/tmpを使いたいときに、また戻さないといけなくなります。

そこで、環境変数「_JAVA_OPTIONS」を使います。例えば、AndroidStudioを起動する場合はこうします。

export _JAVA_OPTIONS="-Djava.io.tmpdir=/path/to/tmp"
#studio.shを含むディレクトリへのパスは通してあるものとします。
studio.sh

スクリプトで自動化

毎度ディレクトリを作ったり、環境変数をexportしたりするのは面倒なのでスクリプト化します。

  • ロックファイルをロック(無ければ作成)
  • 一時ファイルディレクトリを作成
  • 環境変数を設定
  • AndroidStudioを起動
  • 一時ファイルディレクトリを削除
  • ロックファイルをアンロック

この一連の操作を実行するスクリプトを作成します。

ロックファイルは$HOME/.as_lockとし、一時ファイルディレクトリを$HOME/.as_java_tmpとします。PowerShell Core向けスクリプトを作成します。(えっ!bash?だれですかそれ?)

AndroidStudioWithTemporaryDirectoryOnDisk.ps1

#!/usr/bin/pwsh -F

Set-Location $env:HOME
try{
	if(Test-Path AndroidStudioWithTemporaryDirectoryOnDisk_Error_Log.txt){
		Remove-Item AndroidStudioWithTemporaryDirectoryOnDisk_Error_Log.txt
	}
	[System.IO.FileStream]$lock_file = New-Object System.IO.FileStream (".as_lock", [System.IO.FileMode]::Create)
	$lock_file.Lock(0, 1)
	if(!(Test-Path .as_java_tmp)){
		New-Item .as_java_tmp -ItemType Directory
	}
	if(!((Get-Item .as_java_tmp -Force).PSIsContainer)){
		throw "./.as_java_tmp is not directory"
	}
	Set-Item env:_JAVA_OPTIONS -value ($env:_JAVA_OPTIONS + " -Djava.io.tmpdir=" + $env:HOME + "/.as_java_tmp")
	studio.sh
	Remove-Item .as_java_tmp -Force -Recurse
	$lock_file.Unlock(0, 1)
}
catch{
	Write-Host $Error | Out-File AndroidStudioWithTemporaryDirectoryOnDisk_Error_Log.txt
}

このスクリプトを$HOME/.local/binなどPATHの通った場所に置き、実行権を与えておけば、いつでも簡単に$HOME/.as_java_tmpを一時ファイルディレクトリにしてAndroidStudioを開始できます。

マウス操作だけで開けるようにする

どうせなら、アプリケーションメニューやデスクトップ上のアイコンから上記スクリプトを実行できると楽なので、.desktopファイルを作っていきます。

アプリケーションメニューのプログラミングカテゴリにあるAndroidStudioの項目をデスクトップに追加し、テキストエディタで開きます。項目が無い場合、AndroidStudioでTools->Create Desktop Entryを選択して作成できます。開いたらこれを改造します。

#!/usr/bin/env xdg-open
[Desktop Entry]
Version=1.0
Type=Application
Name=Android Studio With Disk Temporary Directory
Icon=/opt/AndroidStudio/bin/studio.png
Exec=AndroidStudioWithTemporaryDirectoryOnDisk.ps1
Comment=The Drive to Develop
Categories=Development;IDE;
Terminal=false
StartupWMClass=jetbrains-studio

Nameをそれっぽい名前にし、Execを先程のスクリプトにします。IconはAndroidStudioのインストール先によって異なるので違うかもしれません。

適当に名前を付けて(ここではjetbrains-studio-disk-tmp.desktop)デスクトップに保存します。これを/usr/local/share/applicationsか$HOME/.local/share/applicationsに入れておけばアプリケーションメニューにも追加されます。

.NET CoreとGtkSharpでGUIアプリを作ってみる[SDLでオーディオ]

前回の記事で.NET CoreGtkSharpで何かしらGUIアプリを作ってみたわけですが、アラームがならないキッチンタイマーというどうしようのないものでした。

geeksinhitachiprovince.hatenablog.com

というわけで、SDL2の補助ライブラリ、SDL2_mixerを使って音楽ファイルを演奏できるようにし、それをアラームということにしてみます。

SDLについては本家ページの他、Wikipediaをご覧ください。

SDLを使用可能にする

Cライブラリのインストール

最近のFedoraの場合

sudo dnf install SDL2_mixer-devel

Windowsの場合

前回の記事同様MSYS2を利用します。

pacman -S mingw-w64-x86_64-SDL2_mixer

.NET CoreアプリからSDL2_mixerを使えるようにする

NuGetでSDL2のC#バインディングを探したのですが、Mono/.NET Framework向けだったり、mixerが使えるのかわからなかったりしたので、自分でバインディングを書くことにしました。今回必要な関数は少ないので簡単です。

SDL2

namespace GIHPLib {
	public class SDL2 {
		public const uint
			/* Constants for init */
			SDL_INIT_TIMER = 0x00000001u,
			SDL_INIT_AUDIO = 0x00000010u,
			SDL_INIT_VIDEO = 0x00000020u  /**< SDL_INIT_VIDEO implies SDL_INIT_EVENTS */,
			SDL_INIT_JOYSTICK = 0x00000200u  /**< SDL_INIT_JOYSTICK implies SDL_INIT_EVENTS */,
			SDL_INIT_HAPTIC = 0x00001000u,
			SDL_INIT_GAMECONTROLLER = 0x00002000u  /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */,
			SDL_INIT_EVENTS = 0x00004000u,
			SDL_INIT_SENSOR = 0x00008000u,
			SDL_INIT_NOPARACHUTE = 0x00100000u  /**< compatibility; this flag is ignored. */,
			SDL_INIT_EVERYTHING = SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR
		;

		public const int
			/* Constants for audio format */
			AUDIO_U8 = 0x0008  /**< Unsigned 8-bit samples */,
			AUDIO_S8 = 0x8008  /**< Signed 8-bit samples */,
			AUDIO_U16LSB = 0x0010  /**< Unsigned 16-bit samples */,
			AUDIO_S16LSB = 0x8010  /**< Signed 16-bit samples */,
			AUDIO_U16MSB = 0x1010  /**< As above, but big-endian byte order */,
			AUDIO_S16MSB    = 0x9010  /**< As above, but big-endian byte order */,
			AUDIO_U16 = AUDIO_U16LSB,
			AUDIO_S16 = AUDIO_S16LSB,
			AUDIO_S32LSB = 0x8020  /**< 32-bit integer samples */,
			AUDIO_S32MSB = 0x9020  /**< As above, but big-endian byte order */,
			AUDIO_S32 = AUDIO_S32LSB,
			AUDIO_F32LSB = 0x8120  /**< 32-bit floating point samples */,
			AUDIO_F32MSB = 0x9120  /**< As above, but big-endian byte order */,
			AUDIO_F32 = AUDIO_F32LSB
		;

		public static readonly int
			AUDIO_U16SYS,
			AUDIO_S16SYS,
			AUDIO_S32SYS,
			AUDIO_F32SYS
		;

		static SDL2(){
			if(System.BitConverter.IsLittleEndian){
				AUDIO_U16SYS = AUDIO_U16LSB;
				AUDIO_S16SYS = AUDIO_S16LSB;
				AUDIO_S32SYS = AUDIO_S32LSB;
				AUDIO_F32SYS = AUDIO_F32LSB;
			}
			else{
				AUDIO_U16SYS = AUDIO_U16MSB;
				AUDIO_S16SYS = AUDIO_S16MSB;
				AUDIO_S32SYS = AUDIO_S32MSB;
				AUDIO_F32SYS = AUDIO_F32MSB;
			}
		}

		[System.Runtime.InteropServices.DllImport("SDL2", EntryPoint="SDL_Init")]
		public static extern int Init(uint flags);

		[System.Runtime.InteropServices.DllImport("SDL2", EntryPoint="SDL_GetError")]
		public static extern System.IntPtr GetError();
		
		[System.Runtime.InteropServices.DllImport("SDL2", EntryPoint="SDL_Quit")]
		public static extern void Quit();
	}
}

今回実際に使ったのはGetError()だけでした。

SDL2_mixer

namespace GIHPLib {
	using Mix = SDL2_mixer;
	using Mix_Music = System.IntPtr;

	public class SDL2_mixer {
		public enum MIX_InitFlags {
			MIX_INIT_FLAC = 0x00000001,
			MIX_INIT_MOD = 0x00000002,
			MIX_INIT_MP3 = 0x00000008,
			MIX_INIT_OGG = 0x00000010,
			MIX_INIT_MID = 0x00000020
		}

		public const int
			MIX_MAX_VOLUME = 128
		;

		public delegate void PlayingFinishedHandler();

		/* mixer初期化 */
		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_Init")]
		public static extern int Init(int flags);

		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_Quit")]
		public static extern void Quit();

		/* audio初期化 */
		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_OpenAudio")]
		public static extern int OpenAudio(int frequency, System.UInt16 format, int channels, int chunksize);

		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_CloseAudio")]
		public static extern void CloseAudio();

		/* 確保、開放 */
		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_LoadMUS")]
		public static extern Mix_Music LoadMUS(string file_name);

		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_FreeMusic")]
		public static extern void FreeMusic(Mix_Music music);

		/* 設定 */
		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_HookMusicFinished")]
		public static extern void HookMusicFinished(PlayingFinishedHandler handler);

		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_VolumeMusic")]
		public static extern int VolumeMusic(int volume);

		/* 操作 */
		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_PlayMusic")]
		public static extern int PlayMusic(Mix_Music music, int loop);

		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_HaltMusic")]
		public static extern int HaltMusic();

		/* 状態取得 */
		[System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_PlayingMusic")]
		public static extern int PlayingMusic();
	}
}

こちらも、全部は使ってないです。

オーディオファイルを演奏する手順

大雑把な流れです。

GIHPLib.SDL2.Init(GIHPLib.SDL2.SDL_INIT_AUDIO);/* 何故か呼ばなくても動きます。実際、今回のアプリでは呼んでません。 */
GIHPLib.SDL2_mixer.OpenAudio(44100, (ushort)GIHPLib.SDL2.AUDIO_S16SYS, 2, 4096);

music = GIHPLib.SDL2_mixer.LoadMUS(filepath);/* flacやmp3等などが開けます。 */
GIHPLib.SDL2_mixer.PlayMusic(music, loopcount);/* 再生を始めます。再生は別スレッドで行われるので、この呼び出しはすぐに帰ってきます。 */
GIHPLib.SDL2_mixer.HaltMusic();/* 再生を停止します。これを呼ばなくてもFreeMusicの呼び出しで停止します。 */
GIHPLib.SDL2_mixer.FreeMusic(music);/* メモリ等のリソースを開放します。 */

GIHPLib.SDL2_mixer.CloseAudio();
GIHPLib.SDL2.Quit();/* Initが要らなかったので、これもいらないです。 */

Mix_OpenAudioのドキュメントによると、先にSDL_Init(SDL_INIT_AUDIO)を呼ばなければならないのですが、これを呼ばずにいきなりSDL2_mixer.OpenAudio(...)を呼んでも問題なかったので、このアプリではSDL2.InitとSDL2.Quitは省略しています。

SDL2_mixerでアラームを鳴らせるようキッチンタイマーを改造

SDL2_mixerの初期化と後処理

まず次のフィールドを追加します。

//クラスMainWindowのフィールド
IntPtr music;
bool counting/*これは前回からありました*/, sdl_init_success;

コンストラクタでSDL2_mixer.OpenAudioを呼び出します。

//MainWindowのコンストラクタ
sdl_init_success = false;
music = IntPtr.Zero;
if(GIHPLib.SDL2_mixer.OpenAudio(44100, (ushort)GIHPLib.SDL2.AUDIO_S16SYS, 2, 4096) != 0) {
	var md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerの初期化に失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError()));
	md.Run();
	md.Destroy();
}
else{
	sdl_init_success = true;
}

メソッドWindow_DeleteEventでSDL2_mixer.CloseAudioを呼び出します。

//メソッドMainWindow.Window_DeleteEvent内
if(sdl_init_success) {
	if(music != IntPtr.Zero) {
		GIHPLib.SDL2_mixer.HaltMusic();
		GIHPLib.SDL2_mixer.FreeMusic(music);
	}
	GIHPLib.SDL2_mixer.CloseAudio();
}

アラーム関連の操作ができるようボタンを追加

GladeでGUIを再設計し、こんな感じにしました。

buttons to alarm are added
アラーム操作のボタン追加

ID割当は、

  • 表示用ラベル:time_display(前回から変更)
  • 分+ボタン:m_up_buton
  • 分-ボタン:m_down_buton
  • 秒+ボタン:s_up_buton
  • 秒-ボタン:s_down_buton
  • アラーム停止ボタン:alarm_stop_button
  • アラーム選択ボタン:alarm_choose_button

このようになりました。

前回、表示用ラベルのID _label1はプロジェクト作成時に生成されたものそのままでした。アンダーバーから始まっていて統一感が無く気持ち悪かったので今回変更しました。

追加したボタンにイベント設定

アラーム停止ボタンとアラーム選択ボタンにイベントハンドラを登録します。

まずはフィールド宣言です。前回のように[UI]属性を付け、IDと同じ名前にします。

//クラスMainWindowのフィールド
[UI] private Button alarm_stop_button = null;
[UI] private Button alarm_choose_button = null;

次にイベントハンドラを登録します。

//MainWindowのコンストラクタ
alarm_stop_button.ButtonReleaseEvent += stopAlarm;
alarm_choose_button.ButtonReleaseEvent += chooseAlarm;

イベントハンドラの本体を書きます。

//MainWindowメンバメソッド
void stopAlarm(object sender, EventArgs e) {
	try {
		GIHPLib.SDL2_mixer.HaltMusic();
	}
	catch(System.Exception err) {
		System.Console.Error.WriteLine(err.Message);
	}
}

void chooseAlarm(object sender, EventArgs e) {
	if(sdl_init_success) {
		FileChooserDialog fc = new FileChooserDialog(
			"報知音に使用する音楽ファイルの選択",
			this,
			FileChooserAction.Open,
			"取り消し", ResponseType.Cancel,
			"開く", ResponseType.Accept
		);

		FileFilter ff;

		ff = new FileFilter();
		ff.AddMimeType("audio/flac");
		ff.AddPattern("*.flac");
		ff.Name = "flacオーディオ";

		fc.AddFilter(ff);

		ff = new FileFilter();
		ff.AddMimeType("audio/wav");
		ff.AddPattern("*.wav");
		ff.Name = "wavオーディオ";

		fc.AddFilter(ff);

		ff = new FileFilter();
		ff.AddMimeType("audio/x_mpg");
		ff.AddMimeType("audio/x_mp3");
		ff.AddPattern("*.mp3");
		ff.Name = "MPEG1 Audio Layer3";

		fc.AddFilter(ff);

		ff = new FileFilter();
		ff.AddMimeType("audio/ogg");
		ff.AddPattern("*.ogg");
		ff.AddPattern("*.oga");
		ff.Name = "Ogg Vorbis";

		fc.AddFilter(ff);

		if(fc.Run() == (int)ResponseType.Accept) {
			if(music != IntPtr.Zero) {
				GIHPLib.SDL2_mixer.HaltMusic();
				GIHPLib.SDL2_mixer.FreeMusic(music);
			}
			music = GIHPLib.SDL2_mixer.LoadMUS(fc.Filename);
			if(music == IntPtr.Zero) {
				MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerでファイルを開くことに失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError()) + "\nなお、Windows版SDL_mixerはASCIIのみのファイル名しか扱えないようです。\nまたLinux版SDL_mixerはディストリビューションによってはmp3は再生できないようです。");
				md.Run();
				md.Destroy();
			}
		}

		fc.Destroy();
	}
	else {
		MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL初期化に失敗しています。報知音を鳴らせません。");
		md.Run();
		md.Destroy();
	}
}

タイマースレッドからアラーム再生イベントを受け付ける

TimerMainクラスにデリゲートとそのイベントフィールドを追加します。

//クラスTimerMainのフィールド
public delegate void Alarmer();
public event Alarmer alarm;

カウントダウンで残り0秒になった瞬間イベントを発生させます。

//メソッドTimerMain.startの0秒以下条件分岐部
if(left_sec <= 0) {
	pause = true;
	if(alarm != null) {
		alarm();
	}
}

MainWindow側にイベントハンドラを用意します。

//MainWindowのコンストラクタ
tm.alarm += alarm;
//MainWindowメンバメソッド
void alarm() {
	Application.Invoke(delegate {
		if(sdl_init_success) {
			if(music != IntPtr.Zero) {
				if(GIHPLib.SDL2_mixer.PlayMusic(music, -1) == -1) {
					MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerでの報知音の再生に失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError()));
					md.Run();
					md.Destroy();
				}
			}
		}
		else {
			MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL初期化に失敗しています。報知音を鳴らせません。");
			md.Run();
			md.Destroy();
		}
	});

}

これで完成

これでとりあえず何かしらをアラームとして鳴らせるようになりました。

complete
完成形

Choose alarmボタンを押すとファイル選択ダイアログが表示されアラームとしてflacやoga、mp3などが開けます。以降の0秒カウント時に鳴らされます。

MainWindow.csの全文も載せます。

using System;
using Gtk;
using UI = Gtk.Builder.ObjectAttribute;

namespace KitchenTimer {
	class MainWindow: Window {
		[UI] private Label time_display = null;
		[UI] private Button m_up_button = null;
		[UI] private Button m_down_button = null;
		[UI] private Button s_up_button = null;
		[UI] private Button s_down_button = null;
		[UI] private Button start_stop_button = null;
		[UI] private Button alarm_stop_button = null;
		[UI] private Button alarm_choose_button = null;
		TimerMain tm;
		IntPtr music;
		bool counting, sdl_init_success;

		public MainWindow() : this(new Builder("MainWindow.glade")) { }

		private MainWindow(Builder builder) : base(builder.GetObject("MainWindow").Handle) {
			builder.Autoconnect(this);
			tm = new TimerMain();
			tm.updateLeftTime += updateLabelText;
			tm.setStat += setStat;
			tm.alarm += alarm;

			start_stop_button.ButtonReleaseEvent += startTimer;

			DeleteEvent += Window_DeleteEvent;
			counting = false;

			m_up_button.ButtonReleaseEvent += incM;
			m_down_button.ButtonReleaseEvent += decM;
			s_up_button.ButtonReleaseEvent += incS;
			s_down_button.ButtonReleaseEvent += decS;
			alarm_stop_button.ButtonReleaseEvent += stopAlarm;
			alarm_choose_button.ButtonReleaseEvent += chooseAlarm;

			updateLabelText();

			sdl_init_success = false;
			music = IntPtr.Zero;
			if(GIHPLib.SDL2_mixer.OpenAudio(44100, (ushort)GIHPLib.SDL2.AUDIO_S16SYS, 2, 4096) != 0) {
				var md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerの初期化に失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError()));
				md.Run();
				md.Destroy();
			}
			else{
				sdl_init_success = true;
			}
		}

		private void Window_DeleteEvent(object sender, DeleteEventArgs a) {
			if(sdl_init_success) {
				if(music != IntPtr.Zero) {
					GIHPLib.SDL2_mixer.HaltMusic();
					GIHPLib.SDL2_mixer.FreeMusic(music);
				}
				GIHPLib.SDL2_mixer.CloseAudio();
			}
			tm.exitThread();
			Application.Quit();
		}

		private void updateLabelText() {
			Application.Invoke(delegate {
				int
					sec = tm.getLeftTime(),
					show_min = sec / 60,
					show_sec = sec - (60 * show_min)
				;

				time_display.Markup = "<span font='32.0' weight='bold'>" + show_min.ToString() + "分\t" + show_sec.ToString() + "秒" + "</span>";
			});

		}

		void startTimer(object sender, EventArgs e) {
			if(counting) {
				tm.pauseCountdown();
			}
			else {
				tm.resumeCountdown();
			}
		}

		void setStat(bool stat) {
			Application.Invoke(delegate {
				counting = stat;
				if(stat) {
					m_up_button.Sensitive = false;
					m_down_button.Sensitive = false;
					s_up_button.Sensitive = false;
					s_down_button.Sensitive = false;
				}
				else {
					m_up_button.Sensitive = true;
					m_down_button.Sensitive = true;
					s_up_button.Sensitive = true;
					s_down_button.Sensitive = true;
				}
			});
		}

		void incM(object sender, EventArgs e) {
			tm.setLeftTime(tm.getLeftTime() + 60);
			updateLabelText();
		}

		void decM(object sender, EventArgs e) {
			int sec = tm.getLeftTime();

			sec -= 60;
			if(sec < 0) {
				sec += 60;
			}

			tm.setLeftTime(sec);
			updateLabelText();
		}

		void incS(object sender, EventArgs e) {
			tm.setLeftTime(tm.getLeftTime() + 1);
			updateLabelText();
		}

		void decS(object sender, EventArgs e) {
			int sec = tm.getLeftTime();

			sec -= 1;
			if(sec < 0) {
				sec = 0;
			}

			tm.setLeftTime(sec);
			updateLabelText();
		}

		void alarm() {
			Application.Invoke(delegate {
				if(sdl_init_success) {
					if(music != IntPtr.Zero) {
						if(GIHPLib.SDL2_mixer.PlayMusic(music, -1) == -1) {
							MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerでの報知音の再生に失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError()));
							md.Run();
							md.Destroy();
						}
					}
				}
				else {
					MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL初期化に失敗しています。報知音を鳴らせません。");
					md.Run();
					md.Destroy();
				}
			});

		}

		void stopAlarm(object sender, EventArgs e) {
			try {
				GIHPLib.SDL2_mixer.HaltMusic();
			}
			catch(System.Exception err) {
				System.Console.Error.WriteLine(err.Message);
			}
		}

		void chooseAlarm(object sender, EventArgs e) {
			if(sdl_init_success) {
				FileChooserDialog fc = new FileChooserDialog(
					"報知音に使用する音楽ファイルの選択",
					this,
					FileChooserAction.Open,
					"取り消し", ResponseType.Cancel,
					"開く", ResponseType.Accept
				);

				FileFilter ff;

				ff = new FileFilter();
				ff.AddMimeType("audio/flac");
				ff.AddPattern("*.flac");
				ff.Name = "flacオーディオ";

				fc.AddFilter(ff);

				ff = new FileFilter();
				ff.AddMimeType("audio/wav");
				ff.AddPattern("*.wav");
				ff.Name = "wavオーディオ";

				fc.AddFilter(ff);

				ff = new FileFilter();
				ff.AddMimeType("audio/x_mpg");
				ff.AddMimeType("audio/x_mp3");
				ff.AddPattern("*.mp3");
				ff.Name = "MPEG1 Audio Layer3";

				fc.AddFilter(ff);

				ff = new FileFilter();
				ff.AddMimeType("audio/ogg");
				ff.AddPattern("*.ogg");
				ff.AddPattern("*.oga");
				ff.Name = "Ogg Vorbis";

				fc.AddFilter(ff);

				if(fc.Run() == (int)ResponseType.Accept) {
					if(music != IntPtr.Zero) {
						GIHPLib.SDL2_mixer.HaltMusic();
						GIHPLib.SDL2_mixer.FreeMusic(music);
					}
					music = GIHPLib.SDL2_mixer.LoadMUS(fc.Filename);
					if(music == IntPtr.Zero) {
						MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerでファイルを開くことに失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError()) + "\nなお、Windows版SDL_mixerはASCIIのみのファイル名しか扱えないようです。\nまたLinux版SDL_mixerはディストリビューションによってはmp3は再生できないようです。");
						md.Run();
						md.Destroy();
					}
				}

				fc.Destroy();
			}
			else {
				MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL初期化に失敗しています。報知音を鳴らせません。");
				md.Run();
				md.Destroy();
			}
		}
	}

	class TimerMain {
		bool exit, pause, sleeping;
		int left_sec;
		System.Threading.Thread th;
		System.Threading.SemaphoreSlim running_lock, pausing_lock, controll_lock;
		public delegate void LeftTimeUpdater();
		public event LeftTimeUpdater updateLeftTime;
		public delegate void CountingStatListener(bool stat);
		public event CountingStatListener setStat;
		public delegate void Alarmer();
		public event Alarmer alarm;

		void start(Object obj) {
			running_lock.Wait();
			sleeping = false;

			while(!exit) {
				if(pause) {
					pausing_lock.Wait();
					if(setStat != null) {
						setStat(false);
					}

					running_lock.Release();

					try {
						sleeping = true;
						System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
					}
					catch(System.Threading.ThreadInterruptedException e) {
						sleeping = false;
					}

					running_lock.Wait();

					if(setStat != null) {
						setStat(true);
					}
					pausing_lock.Release();
				}

				if(updateLeftTime != null) {
					updateLeftTime();
				}

				if(left_sec <= 0) {
					pause = true;
					if(alarm != null) {
						alarm();
					}
				}
				else {
					try {
						System.Threading.Thread.Sleep(999);
					}
					catch(System.Exception e) {

					}
					left_sec--;
				}
			}
		}

		public TimerMain() {
			exit = false;
			pause = true;
			th = new System.Threading.Thread(start);
			running_lock = new System.Threading.SemaphoreSlim(1, 1);
			pausing_lock = new System.Threading.SemaphoreSlim(1, 1);
			controll_lock = new System.Threading.SemaphoreSlim(1, 1);

			th.Start();
		}

		public bool setLeftTime(int sec) {
			using(var l1 = new GIHPLib.Locker(controll_lock)) {
				using(var l2 = new GIHPLib.Locker(running_lock, try_lock: true)) {
					if(l2.hasLock()) {
						left_sec = sec;
						return true;
					}

					return false;
				}
			}
		}

		public int getLeftTime() {
			return left_sec;
		}

		public bool resumeCountdown() {
			using(var l1 = new GIHPLib.Locker(controll_lock)) {
				using(var l2 = new GIHPLib.Locker(running_lock, try_lock: true)) {
					if(l2.hasLock()) {
						if(pause) {
							pause = false;
							th.Interrupt();
						}
						return true;
					}

					return false;
				}
			}
		}

		public bool pauseCountdown() {
			using(var l1 = new GIHPLib.Locker(controll_lock)) {
				using(var l2 = new GIHPLib.Locker(pausing_lock, try_lock: true)) {
					if(l2.hasLock()) {
						pause = true;
						return true;
					}

					return false;
				}
			}
		}

		public void exitThread() {
			using(var l = new GIHPLib.Locker(controll_lock)) {
				exit = true;
				System.Threading.Thread.Yield();
				while(!th.Join(0)) {
					if(sleeping) {
						th.Interrupt();
					}
					System.Threading.Thread.Yield();
				}
			}
		}
	}
}

問題点

まず、Windowsで日本語を含むパスを開けないという致命傷を負っています。

また、LinuxではSDL2_mixerのディストリビューションの公式配布パッケージを使う場合、コンパイル時の設定などによってmp3デコードが不可能になっている場合があります。少なくともFedoraはそうでした。

Windowsの日本語の件に関しては、対処方法を探ってみたいと思います。

あと、アラームを設定として記憶できるようにするべきだとも思います。

.NET CoreとGtkSharpでGUIアプリを作ってみる[GUI作成]

.NET Coreですが、せっかくクロスプラットフォームなのにGUIが無い…orz

…というわけでGtkSharpを使って.NET CoreでGUIアプリを作ってみます。

なお、.NET Coreでは、C#とF#、VBが扱えますが、本記事ではC#を使います。

GtkSharpって?

C言語用のGUIツールキットGTK+C#から使えるようにしたC#ライブラリです。Monoという、クロスプラットフォーム.NET Framework互換環境でGUIツールキットとして利用されてきました。今回使うのは.NET Coreへ対応させたフォークプロジェクト
NuGetのページ)
のものであり、.NET Standard 2.0 を満たす.NET実装から利用できるようで、.NET Core >= 2.0 から利用できるということになります。

Linuxでは一部のGnomeアプリなど(BansheeやGnome-RDPなど)に使われているかと思います。

ただの関数呼び出しの橋渡しではなく、C#らしく使えるようになっており(イベントハンドラ周りや属性を利用したGUIウィジェットの自動割当など)、GTK+をバックエンドとした別物のGUIツールキットと言えるかと思います。

.NET CoreでGtkSharpを使えるようにする

GtkSharpのプロジェクトページに導入方法が記述されています。基本的にはこれに従いますが、Gladeのインストールもついでにしました。(なお、GladeとはGTK+やその各種バインディングで用いられるGUI構築ツールのことです。)

GTK+3とGladeのインストール

最近のFedoraの場合

sudo dnf install gtk3-devel glade

Windowsの場合

MSYS2を利用します。MSYS2は導入済みで、Pathも通してあるものとします。またここでは64bit版を使用します。

pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-glade

GtkSharpアプリプロジェクトの雛形を追加

dotnet new --install GtkSharp.Template.CSharp

雛形gtkappが使用可能になりました。これにより、以降

dotnet new gtkapp

と、コマンドを打つだけでGtkSharpを使った.NET Coreプロジェクトを作成できます。

また、GtkSharp.Template.FSharp、GtkSharp.Template.VBNetを入れることで、F#VBプロジェクト用の雛形も利用できるようです。

試しにキッチンタイマーでも作ってみる

新規プロジェクト作成

GtkSharpを使った新規.NET Coreプロジェクトを作成します。上記でインストールした雛形gtkappを使います

名前はKitchenTimerとしました。

mkdir KitchenTimer
cd KitchenTimer
dotnet new gtkapp

プロジェクト作成直後の生成物

gtkappプロジェクトを作成すると、4つのファイルが作成されます。次に示すのはls -lの出力結果です。

[geeksinhitachiprovince on KitchenTimer]$ll
合計 16
-rw-rw-r--. 1 geeksinhitachiprovince geeksinhitachiprovince  455  1月 20 02:58 KitchenTimer.csproj
-rw-rw-r--. 1 geeksinhitachiprovince geeksinhitachiprovince  910  1月 20 02:58 MainWindow.cs
-rw-rw-r--. 1 geeksinhitachiprovince geeksinhitachiprovince 1844  1月 20 02:58 MainWindow.glade
-rw-rw-r--. 1 geeksinhitachiprovince geeksinhitachiprovince  484  1月 20 02:58 Program.cs

なお、本記事ではProgram.csは一切いじりません。

ビルドして実行すると

GtkSharpHelloWorld
実行直後の状態

こんな感じのHelloWorldアプリになっています。色が黒っぽいのはFedora26のcinnamonのデフォルトテーマによるものです。

ボタンをクリックすると、

button clicked 5 times
クリック回数がカウントされる

Hello World!」のテキストの後にクリック回数が追記されます。

まずはMainWindow.gladeを編集し、GUIを設計

GUI設計はGladeというツールで行います。GtkSharpではGUI部品はGtk.Widgetの派生クラスとなっており、Gladeで設計したGUIの各部品は、コード上でGtk.Widget派生クラスオブジェクトとして扱えます。gladeファイルはこのツールで生成されるGUI設計を記述したxmlファイルです。

では、GladeでMainWindow.gladeを開いて編集してみます。

最初は、HelloWorldアプリのGUI設計になっています。

glade opening MainWindow.glade
MainWindow.gladeを開いたgladeの画面

なお、最新のGladeの見た目が変わっており、最近のGnomeアプリのようにメニューバーを廃止してタイトルバーに各項目を配置しています。下のスクリーンショットはMSYS2で入れたWindows版のものです。

glade Windows ver
glade on MSYS2 on Windows

キッチンタイマーに必要なウィジェットをつけていき、GUI設計をします。ここでは、分、秒の+/-ボタン、スタート/ストップボタン、表示用のラベルを取り付けました。

editing gui design
GUI設計の編集

Gladeのウィンドウのプロパティ設定部(の全般タブ)にIDという項目があります。このIDがC#ソースコード内でのフィールド名になるので、ちゃんと意味のある名前にします。

id as variable name
IDは変数名になる

ここでは、IDを以下のようにしました。

  • 表示用ラベル:_label1(HelloWorldアプリのものをそのまま流用)
  • 分+ボタン:m_up_buton
  • 分-ボタン:m_down_buton
  • 秒+ボタン:s_up_buton
  • 秒-ボタン:s_down_buton

gladeファイルのC#コード上からの利用

MainWindow.csは最初こんな感じになっていると思います。

using System;
using Gtk;
using UI = Gtk.Builder.ObjectAttribute;

namespace KitchenTimer
{
    class MainWindow : Window
    {
        [UI] private Label _label1 = null;
        [UI] private Button _button1 = null;

        private int _counter;

        public MainWindow() : this(new Builder("MainWindow.glade")) { }

        private MainWindow(Builder builder) : base(builder.GetObject("MainWindow").Handle)
        {
            builder.Autoconnect(this);

            DeleteEvent += Window_DeleteEvent;
            _button1.Clicked += Button1_Clicked;
        }

        private void Window_DeleteEvent(object sender, DeleteEventArgs a)
        {
            Application.Quit();
        }

        private void Button1_Clicked(object sender, EventArgs a)
        {
            _counter++;
            _label1.Text = "Hello World! This button has been clicked " + _counter + " time(s).";
        }
    }
}

ソースコードを見ると、MainWindowのコンストラクタ内でgladeファイルを使ってGtk.Builderオブジェクトを構築しています。そしてGtk.BuiderオブジェクトのAutoconnect()メソッドを呼び出しています。

これだけでGUIの構築は済んでしまいます。

後は構築されたGUIウィジェットオブジェクトをフィールドに代入してもらう方法ですが、ここでフィールド宣言部を見てみます。

        [UI] private Label _label1 = null;
        [UI] private Button _button1 = null;

[UI]属性(using UI = Gtk.Builder.ObjectAttribute;)の付いたフィールドが宣言されていて、そのフィールド名がGladeで設定されているIDと一致していることが確認できると思いまが、Autoconnect()メソッドは、この属性とフィールド名を頼りにしているようです。

というわけで、次の形式でフィールドを宣言します。

[UI] private/public 型(Gtk.Widget派生クラス) フィールド名(Gladeで設定したID) = null

このキッチンタイマーアプリでは、ウィジェット用フィールド宣言部は次のようになります。

/*省略*/
class MainWindow: Window {
	[UI] private Label _label1 = null;
	[UI] private Button m_up_button = null;
	[UI] private Button m_down_button = null;
	[UI] private Button s_up_button = null;
	[UI] private Button s_down_button = null;
	[UI] private Button start_stop_button = null;
/*省略*/
}
/*省略*/

Gladeの画面(右)とソースコード(左)の関係を図示すると

connect vars
フィールドの関連付け

こんな感じです。

ボタン操作への反応

Gtk.ButtonにはSystem.EventHandlerデリゲートのイベントプロパティがいくつかあり、そのうちボタン押下への反応に使えるのは、

  • Clicked
  • ButtonPressEvent
  • ButtonReleaseEvent

この3つでしょう。この内の一つにSystem.EventHandlerデリゲートとして使用可能なメソッドを+=で追加するだけです。

/*省略*/
namespace KitchenTimer {
	class MainWindow: Window {
		[UI] private Label _label1 = null;
		[UI] private Button m_up_button = null;
		[UI] private Button m_down_button = null;
		[UI] private Button s_up_button = null;
		[UI] private Button s_down_button = null;
		[UI] private Button start_stop_button = null;
		/*省略*/

		public MainWindow() : this(new Builder("MainWindow.glade")) { }

		private MainWindow(Builder builder) : base(builder.GetObject("MainWindow").Handle) {
			builder.Autoconnect(this);
			/*省略*/
			start_stop_button.ButtonReleaseEvent += startTimer;
			/*省略*/
			m_up_button.ButtonReleaseEvent += incM;
			m_down_button.ButtonReleaseEvent += decM;
			s_up_button.ButtonReleaseEvent += incS;
			s_down_button.ButtonReleaseEvent += decS;
			/*省略*/
		}

		/*省略*/

		void startTimer(object sender, EventArgs e) {
			/*ボタン操作への反応*/
		}

		/*省略*/

		void incM(object sender, EventArgs e) {
			/*ボタン操作への反応*/
		}

		void decM(object sender, EventArgs e) {
			/*ボタン操作への反応*/
		}

		void incS(object sender, EventArgs e) {
			/*ボタン操作への反応*/
		}

		void decS(object sender, EventArgs e) {
			/*ボタン操作への反応*/
		}
	}
}

終了時の後処理

再度生成されたコードを見てみると、

DeleteEvent += Window_DeleteEvent;

という行があります。DeleteEventはGtk.Widgetのイベントフィールドで、ウィンドウが閉じられたときに呼び出されます。よって終了時の後処理をするための雛形はもうできていることになります。追加の後処理がある場合、メソッドWindow_DeleteEventに追加するといいでしょう。

private void Window_DeleteEvent(object sender, DeleteEventArgs a)
{
	/*追加の後処理*/

	Application.Quit();/*この呼び出しがないと終了しない*/
}

GUIオブジェクトをGUIスレッドの外から操作

このキッチンタイマーアプリはGUIスレッドとタイマースレッドに分けて作成しますが、タイマースレッドが一秒刻むごとに表示用ラベル_label1の表示を書き換える必要があります。GtkSharpのプロパティ操作はスレッドセーフではないので、_label1の表示を巡ってタイマースレッドとGUIスレッドが競合します。これを避けるためにタイマースレッドからGUIスレッドに操作を依頼する必要があります。これをしてくれるのが、Gtk.Application.Invoke(System.EventHandler)です。

Application.Invoke ( delegate{
	/*GUIスレッドで実行する操作*/
} );

だいたいこんな感じの使い方になると思います(名前空間Gtkはusingされているので省略しています)。

最終的にこうなった

以上のことを踏まえて、外部スレッドで動くタイマーなどを作成し、出来上がったのがこちらです。

using System;
using Gtk;
using UI = Gtk.Builder.ObjectAttribute;

namespace KitchenTimer {
	class MainWindow: Window {
		[UI] private Label _label1 = null;
		[UI] private Button m_up_button = null;
		[UI] private Button m_down_button = null;
		[UI] private Button s_up_button = null;
		[UI] private Button s_down_button = null;
		[UI] private Button start_stop_button = null;
		TimerMain tm;
		bool counting;

		public MainWindow() : this(new Builder("MainWindow.glade")) { }

		private MainWindow(Builder builder) : base(builder.GetObject("MainWindow").Handle) {
			builder.Autoconnect(this);
			tm = new TimerMain();
			tm.updateLeftTime += updateLabelText;
			tm.setStat += setStat;

			start_stop_button.ButtonReleaseEvent += startTimer;

			DeleteEvent += Window_DeleteEvent;
			counting = false;

			m_up_button.ButtonReleaseEvent += incM;
			m_down_button.ButtonReleaseEvent += decM;
			s_up_button.ButtonReleaseEvent += incS;
			s_down_button.ButtonReleaseEvent += decS;

			updateLabelText();
		}

		private void Window_DeleteEvent(object sender, DeleteEventArgs a) {
			
			tm.exitThread();
			Application.Quit();
		}

		private void updateLabelText() {
			int
				sec = tm.getLeftTime(),
				show_min = sec / 60,
				show_sec = sec - (60 * show_min)
			;

			_label1.Markup = "<span font='32.0' weight='bold'>" + show_min.ToString() + "分\t" + show_sec.ToString() + "秒" + "</span>";
		}

		void startTimer(object sender, EventArgs e) {
			if(counting) {
				tm.pauseCountdown();
			}
			else {
				tm.resumeCountdown();
			}
		}

		void setStat(bool stat) {
			counting = stat;
			if(stat) {
				m_up_button.Sensitive = false;
				m_down_button.Sensitive = false;
				s_up_button.Sensitive = false;
				s_down_button.Sensitive = false;
			}
			else {
				m_up_button.Sensitive = true;
				m_down_button.Sensitive = true;
				s_up_button.Sensitive = true;
				s_down_button.Sensitive = true;
			}
		}

		void incM(object sender, EventArgs e) {
			tm.setLeftTime(tm.getLeftTime() + 60);
			updateLabelText();
		}

		void decM(object sender, EventArgs e) {
			int sec = tm.getLeftTime();

			sec -= 60;
			if(sec < 0) {
				sec += 60;
			}

			tm.setLeftTime(sec);
			updateLabelText();
		}

		void incS(object sender, EventArgs e) {
			tm.setLeftTime(tm.getLeftTime() + 1);
			updateLabelText();
		}

		void decS(object sender, EventArgs e) {
			int sec = tm.getLeftTime();

			sec -= 1;
			if(sec < 0) {
				sec = 0;
			}

			tm.setLeftTime(sec);
			updateLabelText();
		}
	}

	class TimerMain {
		bool exit, pause, sleeping;
		int left_sec;
		System.Threading.Thread th;
		System.Threading.SemaphoreSlim running_lock, pausing_lock, controll_lock;
		public delegate void LeftTimeUpdater();
		public event LeftTimeUpdater updateLeftTime;
		public delegate void CountingStatListener(bool stat);
		public event CountingStatListener setStat;

		void start(Object obj) {
			running_lock.Wait();
			sleeping = false;

			while(!exit) {
				if(pause) {
					pausing_lock.Wait();
					if(setStat != null) {
						Application.Invoke(delegate {
							setStat(false);
						});
					}

					running_lock.Release();

					try {
						sleeping = true;
						System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
					}
					catch(System.Threading.ThreadInterruptedException e) {
						sleeping = false;
					}

					running_lock.Wait();

					if(setStat != null) {
						Application.Invoke(delegate {
							setStat(true);
						});
					}
					pausing_lock.Release();
				}

				if(updateLeftTime != null) {
					Application.Invoke(delegate {
						updateLeftTime();
					});
				}

				if(left_sec <= 0) {
					pause = true;
				}
				else {
					System.Threading.Thread.Sleep(999);
					left_sec--;
				}
			}
		}

		public TimerMain() {
			exit = false;
			pause = true;
			th = new System.Threading.Thread(start);
			running_lock = new System.Threading.SemaphoreSlim(1, 1);
			pausing_lock = new System.Threading.SemaphoreSlim(1, 1);
			controll_lock = new System.Threading.SemaphoreSlim(1, 1);

			th.Start();
		}

		public bool setLeftTime(int sec) {
			using(var l1 = new GIHPLib.Locker(controll_lock)) {
				using(var l2 = new GIHPLib.Locker(running_lock, try_lock: true)) {
					if(l2.hasLock()) {
						left_sec = sec;
						return true;
					}

					return false;
				}
			}
		}

		public int getLeftTime() {
			return left_sec;
		}

		public bool resumeCountdown() {
			using(var l1 = new GIHPLib.Locker(controll_lock)) {
				using(var l2 = new GIHPLib.Locker(running_lock, try_lock: true)) {
					if(l2.hasLock()) {
						if(pause) {
							pause = false;
							th.Interrupt();
						}
						return true;
					}

					return false;
				}
			}
		}

		public bool pauseCountdown() {
			using(var l1 = new GIHPLib.Locker(controll_lock)) {
				using(var l2 = new GIHPLib.Locker(pausing_lock, try_lock: true)) {
					if(l2.hasLock()) {
						pause = true;
						return true;
					}

					return false;
				}
			}
		}

		public void exitThread() {
			using(var l = new GIHPLib.Locker(controll_lock)) {
				exit = true;
				System.Threading.Thread.Yield();
				while(!th.Join(0)) {
					if(sleeping) {
						th.Interrupt();
					}
					System.Threading.Thread.Yield();
				}
			}
		}
	}
}

GIHPLib.Lockerという見慣れないクラスがあるかと思いますが、これはC++std::unique_lockみたいなのが欲しかったので作ったものです。一つのSystem.Threading.SemaphoreSlimオブジェクトを管理対象とし、usingステートメントを抜けると管理対象Wait()済みであればRelease()します。nupkg化してあるので、このアプリのプロジェクトには含まれておらず、KitchenTimer.csprojに参照するパッケージとして記述してあります。

namespace GIHPLib {
	public class Locker: System.IDisposable {
		System.Threading.SemaphoreSlim semaphore;
		bool have_lock;

		public Locker(System.Threading.SemaphoreSlim init_semaphore, bool with_lock = true, bool try_lock = false) {
			semaphore = init_semaphore;
			if(with_lock && semaphore != null) {
				if(try_lock) {
					if(semaphore.Wait(0)){
						have_lock = true;
					}
					else{
						have_lock = false;
					}
				}
				else {
					semaphore.Wait();
					have_lock = true;
				}
			}
			else {
				have_lock = false;
			}
		}

		public void getLock(bool try_lock = false) {
			if(!have_lock && semaphore != null) {
				if(try_lock) {
					if(semaphore.Wait(0)){
						have_lock = true;
					}
					else{
						have_lock = false;
					}
				}
				else {
					semaphore.Wait();
					have_lock = true;
				}
			}
		}

		public void releaseLock() {
			if(have_lock && semaphore != null) {
				semaphore.Release();
				have_lock = false;
			}
		}

		public bool hasLock(){
			return have_lock;
		}

		public void Dispose() {
			if(have_lock && semaphore != null) {
				semaphore.Release();
				have_lock = false;
			}
		}
	}
}

実は、C#の経験は、ASP.NET CoreRaspberry Piを制御するための簡単なWebページを作ったことがあるという程度で、故に不慣れな感じのコードになっているかもしれません。

普段はC++QtGUIツールキット)で店頭イベント用の小さなアプリとか作っているのですが、C++std::mutexやstd::unique_lock等の単純で簡単なものが欲しくなりました。

screenshot
5分のカップ麺もOK

次はSDLでアラーム対応化

このアプリ、キッチンタイマーのくせにアラームがなりません。というわけで、次はSDL2_mixerで何かしら鳴らせるようにしてみます。

ニュートン・ラフソン法による逆数の求め方メモ

そもそもニュートン・ラフソン法ってなんだっけ?

ニュートン・ラフソン法(単にニュートン法ともいう)とは、方程式の解を近似値として反復的に求める手法です。

ニュートン・ラフソン法で逆数や平方根などを求める場合、普通はf(x)=0の解がその求めたい数になるようにします。

ということで最終目標は、f(x)=0を満たすときのxを求めることです。

大雑把な流れを書いていきます。


decide X0
x_0を決める

まずxとして有効な値を決め、初期値x_0とします。

左図の赤線をこのx_0とします。



tangent line of f(x) at X0
x=x_0におけるf(x)の接線

次にx=x_0で接するf(x)の接線を考えます。

左図の青い線をこの接線とします。



X1 decided
接線とx軸との交点のx座標がx_1

この接線とx軸との交点におけるx座標をx_1とします。



tangent line of f(x) at X1
x=x_1におけるf(x)の接線

今度は、x=x_1で接するf(x)の接線を考えます。



X2 decided
接線とx軸との交点のx座標がx_2

先ほどと同じように、接線とx軸との交点におけるx座標を、今度はx_2とします。


tangent line of f(x) at X2
x=x_2におけるf(x)の接線

以下、同様にx_3,x_4,x_5,\cdots

と、x_nを求めていきます。


X3 decided and compare with X0
接線とx軸との交点のx座標がx_3x_0との比較

すると、やがてx_nf(x)x軸との交点に近づいていきます。

左図でx_0x_3を比較すると、解であるf(x)x軸との交点に近づいていることがわかります。

これは、x_nf(x)=0の解に収束することを意味します。

以上のようにして、ニュートン・ラフソン法でf(x)=0の解を近似値として求めることができます。

逆数を求める

ニュートン・ラフソン法で逆数を求める際の式、f(x)

逆数を求めたい数を、SourceよりSrcとすることとします。f(x)=0を満たすときのxの値が解となるようにするので、

f(x)=\frac{1}{x}-Src
Srcは逆数を求めたい数)

となります。

x=x_nにおけるf(x)の接線

まず接線の傾きは、

f(x)

~=\frac{1}{x}-Src

~=x^{-1}-Src

f'(x)=-x^{-2}=-\frac{1}{x^2}

となります。

接線のy切片のy座標をYintとすると、x=x_nにおける接線を表す式(tangent lineよりt(x)とします)は、

t(x)=f'(x_n)x+Yint=-\frac{1}{x_n^2}x+Yint
t(x)x=x_nにおける接線)
Yintは接線のy切片のy座標)

となります。


cross
t(x)f(x)の交点

Yintを求めます。接点座標は、f(x_n)=\frac{1}{x_n}-Srcより、(x_n,\frac{1}{x_n}-Src)となります。これをt(x)に当てはめると、


\frac{1}{x_n}-Src=-\frac{1}{x_n^2}x_n+Yint

Yint=\frac{2}{x_n}-Src
Yintは接線のy切片のy座標)
Srcは逆数を求めたい数)

と、Yintが求まりました。

以上より、最終的にx=x_nにおける接線t(x)は、

t(x)=-\frac{1}{x_n^2}x+\frac{2}{x_n}-Src
t(x)x=x_nにおける接線)
Srcは逆数を求めたい数)

となります。

x_nからx_{n+1}を求める漸化式

x_nからx_{n+1}を求めるには、x_nにおける接線のx切片のx座標を求めます。

Xn to Xn+1
t(x)=0の解がx_{n+1}

要するに、先程求めた接線が0になる解を求めればいいのです。

t(x_{n+1})=0

-\frac{1}{x_n^2}x_{n+1}+\frac{2}{x_n}-Src=0

-x_{n+1}+2x_n-Src\,x_n^2=0

x_{n+1}=2x_n-Src\,x_n^2

x_{n+1}=x_n(2-Src\,x_n)

これが漸化式となります。

あとは、この漸化式を使ってx_nからx_{n+1}への変化が極僅かとなるまで収束させ続けます。

ニュートン・ラフソン法で逆数を求めるプログラムを書いてみる

Golang(Go言語、正式には単に「Go」らしいです)の入門も兼ねてGoで書いてみました。

A Tour of Go(日本語版)Packages - The Go Programming Languageに入門者向けの読み物と、パッケージ(API?)一覧がありますので、これを読みながら書きました。

x_0をどうするか

ニュートン・ラフソン法では、x_0が解に近いほど早く収束します。故にx_0はなるべく解に近づけます。

逆数を求める場合は、Src(逆数を求めたい数)がSrc>0のときx_0が解より大きすぎると、x_1<0となってしまい、収束しないはずです。故に0 < x_0 < \frac{1}{Src}を満たすようにします。(Src<0のときは不等号を逆にして考えてください。)

最善策がよくわからないので、まずx_0=Srcとし、Src \times x_01未満になるまでx_00.1をかけ続けるようにしました。

終了条件はどうするか

Src \times x_{n+1}1に近づき続ける間は、続けるようにしました。


ニュートン・ラフソン法で逆数を求める関数

func reciprocal(Src float64) float64 {
	if Src == 0.0 {
		return math.NaN()
	}

	var newdist /*終了判定用変数*/, newX /*Xn+1*/ float64
	var olddist /*終了判定用変数*/, oldX /*Xn*/ float64 = math.Inf(0), Src * 0.1

	//X0を決定。Src*X0が1より小さくなるまでX0を0.1倍し続ける。
	for {
		if Src*oldX <= 1.0 {
			break
		}
		oldX *= 0.1
	}

	for {
		//漸化式でXn+1を求める
		newX = oldX * (2.0 - oldX*Src)

		//前回よりもSrc*Xn+1が1に近づいたら続行
		newdist = math.Abs(1.0 - newX*Src)
		if newdist >= olddist {
			break
		}
		olddist = newdist

		//Xnを更新
		oldX = newX
	}

	return oldX
}

ニュートン・ラフソン法で逆数を求め除算をするプログラム

上記関数を使って除算をするプログラムです。

package main

import (
	"fmt"
	"math"
	"os"
	"strconv"
)

func main() {
	var v1, v2 float64
	var stat bool

	fmt.Printf("入力1>> ")
	stat, v1 = readValidValue()
	if !stat {
		fmt.Printf("数値をいれよ\n")
		return
	}

	fmt.Printf("入力は%Fであるな\n", v1)

	fmt.Printf("入力2>> ")
	stat, v2 = readValidValue()
	if !stat {
		fmt.Printf("数値をいれよ\n")
		return
	}

	fmt.Printf("入力は%Fであるな\n", v2)

	fmt.Printf("%F/%Fは%Fである\n", v1, v2, divide(v1, v2))
}

//数値入力(ふざけた入力は受け付けない)
func readValidValue() (bool, float64) {
	var buf /*直接入力するバッファ*/, num /*入力を溜め込み、後に[]byte→string→float64と変換*/ []byte
	var dotAppeared, invalid = false, false
	var ret float64

	buf = make([]byte, 1)
	num = make([]byte, 0, 127)

	for {
		//一文字(1byte)入力
		os.Stdin.Read(buf)
		if buf[0] == byte('\n') {
			//改行コードなら入力終了
			if len(num) == 0 {
				//このとき、入力が改行コードのみだった場合、不正な入力とする
				invalid = true
			}
			break
		} else if buf[0] == byte('-') && len(num) == 0 {
			//負の数を入力するため、先頭のみ「-」を許容
			num = append(num, buf[0])

		} else if buf[0] == byte('-') && len(num) != 0 {
			//先頭以外で-を入力したら不正な入力とする
			invalid = true
			readForNewLine()
			break

		} else if buf[0] == byte('.') && len(num) >= 1 && !dotAppeared {
			//少数入力のため、2文字目以降の初めての「.」のみ許容する
			dotAppeared = true
			num = append(num, buf[0])

		} else if buf[0] == byte('.') && (len(num) == 0 || dotAppeared) {
			//先頭または二度目の.なら不正な入力とする
			invalid = true
			readForNewLine()
			break

		} else if buf[0] >= byte('0') && buf[0] <= byte('9') {
			//0〜9は常に許容
			num = append(num, buf[0])

		} else {
			//あとはすべて不正な入力とする
			invalid = true
			readForNewLine()
			break

		}
	}

	ret, _ = strconv.ParseFloat(string(num), 64)

	return !invalid, ret
}

//読み捨て用の関数
func readForNewLine() {
	buf := make([]byte, 1)

	for {
		os.Stdin.Read(buf)
		if buf[0] == byte('\n') {
			break
		}
	}
}

//除算を逆数との掛け算として求める関数
func divide(a, b float64) float64 {
	if b == 0.0 {
		return math.NaN()
	}

	return a * reciprocal(b)
}

//ニュートン・ラフソン法で逆数を求める関数
func reciprocal(Src float64) float64 {
	if Src == 0.0 {
		return math.NaN()
	}

	var newdist /*終了判定用変数*/, newX /*Xn+1*/ float64
	var olddist /*終了判定用変数*/, oldX /*Xn*/ float64 = math.Inf(0), Src * 0.1

	//X0を決定。Src*X0が1より小さくなるまでX0を0.1倍し続ける。
	for {
		if Src*oldX < 1.0 {
			break
		}
		oldX *= 0.1
	}

	for {
		//漸化式でXn+1を求める
		newX = oldX * (2.0 - oldX*Src)

		//前回よりもSrc*Xn+1が1に近づいたら続行
		newdist = math.Abs(1.0 - newX*Src)
		if newdist >= olddist {
			break
		}
		olddist = newdist

		//Xnを更新
		oldX = newX
	}

	return oldX
}

実行結果

[アホのPC]$go run main.go
入力1>> 8
入力は8.000000であるな
入力2>> 32
入力は32.000000であるな
8.000000/32.000000は0.250000である

無事に計算できてますが、特にGolangでなくてもいいような出来になりました。