学ぶということ

学ぶという行為について知人と話していて賛同を得られたのでここに少し記す.

何を学ぶにしても順序というものがあり,その順序に従って学ばなければ本当の理解は得られないというのは広く知られている.しかし,どういう順序で学べばよいのかを知っているのは,既にその学問を修めた人間だけであるから,初学者は何らかの形で師に教わる必要がある,というのが私の考えの根幹である.

(特に私独自の考えを展開するわけではなく,今まで自分が触れてきた考えをまとめているだけなので,こんな記事は見飽きたという方がいたら申し訳ない)

学ぶ順序を知らないことによる弊害は,数学のような積み重ねの学問において非常に顕著に現れる.たとえば大学数学に興味を持った高校生が何も知らずに本屋に行ったとする.代数学解析学数学基礎論ガロア理論多様体論,圏論,様々な分野の本が並んでいる中で,最初に学ぶべきもの(他の分野の基礎となっており前提知識が必要でないもの)を引き当てることは難しいだろう.特に興味を持ったきっかけが例えば「リーマン予想についてのテレビ番組を見た」というものだったりしたら,まずタイトルにリーマン予想という単語が含まれている本から探そうとするのも仕方ない.リーマン予想について一般人向けに分かりやすく説明した書籍もあるだろうが,そういうものを読んでも結局内容を「ざっくりと知る」だけであって「理解を得る」ことは当然できない*1.かと言ってちゃんとした専門書を買っても,要求される前提知識が高すぎて何も理解することができない.誰もが一度は経験する失敗である.

面白いのは,間違った学習法で生半可な知識を得た素人と,大学などで講義を受けた玄人が会話したときに,素人の浅はかさがいとも簡単に露呈してしまうことである.そしてこのような会話は,前者の態度によっては後者を非常に苛立たせる.特に「色々なことに興味を持って楽しく学ぶだけで良い結果が得られる」という甘い考えを信じている者は相手の苛立ちに気付きにくい.

さてここで独学には大きな障害があることが分かる.自分の学習を自分で管理するため,やりたくないこと,あるいはやる必要が無いと判断したことはやらなくてよい.その結果抜けている部分が生じてしまい,特に基礎が疎かになりやすい.どのようなやり方で学べば理解を得られるのか知らない以上これは仕方ないことである.

しかしここまでのことを知っていれば話は別である.いきなり学びたい内容に手を付けるのではなく,その前にまず「学ぶ順序」から知ろうとすることは可能である.例えば数学ならば,これまでに同じような失敗をした人が多くいるからだろうか,Google検索をかければ学ぶ順序について書いてあるブログ記事などがいくつか見つかる.またSNSなどで上級者に聞くこともできる(ここでも態度に気を付ける必要がある).このようにして得た「学び方」に従えば,少しは独学の限界を引き上げることができるだろう*2.個人的な意見としては,書籍は最初から最後までの内容が連続しているためインターネット上に散在する記事よりも学習に適していると思う.

私は,数度の挫折を経てここまでの内容を悟るまでに長い時間がかかった.みな,長い時間をかけて理想の学び方を探してゆくのだろう.この記事がそんな誰かの助けになれば幸いである.

*1:最悪のパターンは,ざっくりと知った知識をもとに,明後日の方向に独自の考えを展開し始めることである.「菅数論」をご存知だろうか……

*2:もちろんこれができない学問もある.例えば文献の少ない古代言語を順序立てて学習するのは難しいだろうから,右も左も分からないまま茨の道を歩くことになる.え,そういうのが好き?ぼくも好き

【闇】【黒魔術】【C++】絶対にやってはいけない inf テク

皆さんこんばんは.今回はちょっと好まれなさそうなC++の使い方を紹介します.

たとえばある int 型配列の最小値が知りたいとき,皆さんはどうしていますか.

std::vector<int> a = something;
int min = 100000000; /* なんか大きな値 */
for(auto i : a){
    min = std::min(min, i);
}

のようにすると思います.この記事のテーマはこの「100000000; /* なんか大きな値 */」のところです.
いつだったかテレビに登場したソースコード内に 1145141919810893364364 という数値が登場して話題になったことがありました(2chのスレ)が,このように「なんか大きな値」はプログラムを書いているとしばしば必要になります.
多くの場合 inf といった名前の constexpr 変数に格納(あるいはdefine)されることが多いですが, 32bit整数型や64bit整数型,浮動小数点型などによって異なる値を使わなければならないため,linf,INF などと変数が増えていってしまいます.

それを † 闇 C + + † の力で解決してしまおう,というのが今回の記事です.つまり

int a = inf;    // int型に収まる大きな値が代入される
long long b = inf;    // long long型に収まる大きな値が代入される
double c = inf;    // double型に収まる大きな値が代入される

というように,代入式の左辺の型によって代入する値を変えたいわけです.

これはキャスト演算子オーバーロードすることによって可能になります.

struct Inf{
    constexpr operator int(){
        return INT_MAX;    //INT_MAXは<climits>で定義されている int の最大値
    }
};

としておけば,

Inf inf;
int a = inf;

とすることで inf::operator int() が呼ばれて a にはその返り値である INT_MAX が代入されるわけです.ちなみに inf::operator int() を static にすることはできないみたいですね.
struct Inf の定義と inf の宣言をまとめると,

struct{
    constexpr operator int(){
        return INT_MAX;
    }
} inf;

となります.またconstexprが付いているので

constexpr int a = inf;

ということも可能です.

こうして色々な型の最大値に化けられる inf を作ることができました.

今度はこれに負号を付けて -inf とすると最小値が得られるようにしてみましょう.つまり

int a = -inf;

としたときに,-INT_MAX = -(2^31 - 1) ではなく INT_MIN = -2^31 が代入されるようにしたいわけです.
今度は inf.operator-() をオーバーロードして

constexpr auto operator-(){
    struct{
        constexpr operator int(){ return INT_MIN; }
    } ret;
    return ret;
}

とすれば良いです.inf::operator-() が呼ばれて ret が返る → ret.operator int() が呼ばれて INT_MIN が返る → a に INT_MIN が代入されるという流れになります.

ちなみに各型の最大値は次のようになっています.

最大値 最小値 ヘッダ
unsigned char UCHAR_MAX climits
signed char SCHAR_MAX SCHAR_MIN climits
char CHAR_MAX CHAR_MIN climits
unsigned short USHRT_MAX climits
short SHRT_MAX SHRT_MIN climits
unsigned int UINT_MAX climits
int INT_MAX INT_MIN climits
unsigned long ULONG_MAX climits
long LONG_MAX LONG_MIN climits
unsigned long long ULLONG_MAX climits
long long LLONG_MAX LLONG_MIN climits
float FLT_MAX cfloat
double DBL_MAX cfloat
long double LDBL_MAX cfloat

(訂正 浮動小数点型の最小値のところにFLT_MIN,DBL_MIN,LDBL_MINを書いていましたがこれらは正の最小値でした)

また,cstdintヘッダのint32_tなどはこれらの型のエイリアスとして定義されるため,例えば using int32_t = int; となっていた場合 operator int() と operator int32_t() を別々に定義することはできません.

これらを全てまとめると次のようになります.

struct Inf{
	constexpr operator unsigned char(){ return UCHAR_MAX; }
	constexpr operator signed char(){ return SCHAR_MAX; }
	constexpr operator char(){ return CHAR_MAX; }
	constexpr operator unsigned short(){ return USHRT_MAX; }
	constexpr operator short(){ return SHRT_MAX; }
	constexpr operator unsigned int(){ return UINT_MAX; }
	constexpr operator int(){ return INT_MAX; }
	constexpr operator unsigned long(){ return ULONG_MAX; }
	constexpr operator long(){ return LONG_MAX; }
	constexpr operator unsigned long long(){ return ULLONG_MAX; }
	constexpr operator long long(){ return LLONG_MAX; }
	constexpr operator float(){ return FLT_MAX; }
	constexpr operator double(){ return DBL_MAX; }
	constexpr operator long double(){ return LDBL_MAX; }
	constexpr auto operator-(){
		struct{
			constexpr operator char(){ return CHAR_MIN; }
			constexpr operator signed char(){ return SCHAR_MIN; }
			constexpr operator short(){ return SHRT_MIN; }
			constexpr operator int(){ return INT_MIN; }
			constexpr operator long(){ return LONG_MIN; }
			constexpr operator long long(){ return LLONG_MIN; }
		} ret;
		return ret;
	}
} inf;

追記

何人かの方に std::numeric_limits の使用を勧められたためそれを使って書き換えてみます.

std::numeric_limitsを使ってmax,minを得る方法としては

std::numeric_limits<int>::max() // INT_MAX
std::numeric_limits<double>::lowest() // double型の最も低い値

などのようになります.(訂正,minではなくlowestでした.)
ただこれをそのまま使ってしまうと「inf + inf でオーバーロードしてしまったため inf の値を半分にする」ということが即座にできないため実際に使用する場合やはりキャスト演算子をもつ構造体を作っておく必要があります.

maxについては次のようにすれば長かった部分を短くすることができます.

struct{
	template<class T>
	constexpr operator T(){
		return std::numeric_limits<T>::max();
	}
} inf;

ただしこのようにtemplateを使おうとすると,負号を付けたときの処理を上のようにローカル構造体で片付けることができないため,負の inf についても構造体を作る必要があります.

struct{
	template<class T>
	constexpr operator T(){
		return std::numeric_limits<T>::lowest();
	}
} negative_inf;

そしてこれらにメンバ operator-() を持たせて,-inf が negative_inf に,-negative_inf が inf になるようにすれば良いです.
全体では次のようになります.

struct{
	template<class T>
	constexpr operator T(){
		return std::numeric_limits<T>::max();
	}
	constexpr auto operator-();
} inf;

struct{
	template<class T>
	constexpr operator T(){
		return std::numeric_limits<T>::lowest();
	}
	constexpr auto operator-();
} negative_inf;

constexpr auto decltype(inf)::operator-(){
	return negative_inf;
}

constexpr auto decltype(negative_inf)::operator-(){
	return inf;
}

雑記

人は考える。
自分は常に思考し続ける人間を理想像として掲げている。
考えるという行為は人間において本質的であり,最も重要である。
しかし実際はいつでも十分な思考を巡らすことができるわけではなく,思考を放棄することもある。これは悪である。
考え続けていると疲れてくる。考えること自体が嫌になってくる。
考えることは良いことだとしても,頭が狂ってしまうほど考え続けることは果たして良いことだろうか。
人間である以上思考は完全ではない。間違えることもある。
自分がどれだけ思考できるのか,その限界は知っておかねばならない。さらにそれを忘れてはならない。
しかし限界がどこにあるのか客観的に記述するのは難しい。長い経験が必要。
未来は予測できることもあるが,予測できないこともある。
自分がどれだけ未来を正確に予測できるか把握しておかねばならない。
自分に未来を予測する能力があると思い込んでしまったことがあった。
自分の予測が当たるとさらに能力を信頼するようになり,自分の予測が外れるとその経験が能力の精度を高めたと言って喜んだ。
実際は当然未来を正確に予測することはできない。能力を過信して油断し,思考を止めるなど言語道断である。
予測が外れる可能性も常に覚えておかねばならない。外れたときに焦りすぎないように。
また,予測が外れることを当てにするのも間違っている。予測は当たるとも外れるとも言えない。当たるときは当たるし外れるときは外れる,それだけである。
人は過去をもとに未来を予測している。では過去のことはどれだけ覚えているか。
人は過去を記憶する。しかしその記憶は本当に正確か。
記憶はよく勝手に書き換えられる。自分の記憶力を過信してはならない。
人は様々な手段によって知識を増やす。しかし知識が増える瞬間を全て記憶することはできない。
今の自分が知識として持っていることを,自分がいつ学んだのか,思い出せないことが多い。
故に過去の自分を見返したときに,現在の自分の知識を当時の自分の知識と混同することも多い。
知識が蓄積されていくとき,知識の変化を記録することは難しい。
記録しようと努力するのではなく,記録できないと割り切るべきである。
人は忘れる。
しかし現在の出来事を未来の自分がどれだけ覚えているか考えるのは難しい。
ずっと覚えていたいと思っていたのになぜかいつのまにか忘れていたり,どうせ忘れているだろうと思っていたらなぜかちゃんと記憶に残り続けていたりする。思い出せるときと思い出せないときがあったりもする。
注意,逆もまた然り。覚えていたいと思っていたことをずっと覚えていられることもあるし,忘れたいと思っていることを案外忘れられることもある。
また「記憶,覚えたこと」に対して「知識,知っていること」は消えないと思いがちだが,実際は時間が経てば知識そのものが失われることもある。
特に自分の思考によって生まれた知識の変化には気づきにくい。
人は常に自分が正しいと思っている。
自分の間違いを認めるときでさえ,その瞬間の自分(つまり間違いを認める自分)は正しいと思っている。
これは当然であり,それが正しいという言葉の本質である。
過去の自分が正しいと思っていたことが正しいかどうか疑うことは重要である。
さらに現在の自分が正しいと思っていることを未来の自分が正しくないと思う可能性も考えておく方が良いだろう。
人は精神状態を一定に保つことができない。
上述のような思考を永遠に続けることはできない。
どれだけ思考を続けることができるのか正確に判断し,適度な休息をとる必要がある。
人は感情を持つ。
ここから先,感情の存在を事実と認めた上で話を進める。
感情について考えたり,感情を制御したりしようとすることで,それまでの感情が失われる[ことがある]。
感情は人間社会の中で刷り込まれることによって生まれる[のではないだろうか]。
そういった意味で,何もない状態から発生する真実の感情は欲以外に存在しない[と思っている]。
一度失った感情も,同じように刷り込むことによって取り戻すことができる[気がする]。
それらを偽物の感情として否定することは必ずしもできない[可能性がある]。

Arch Linux インストールの流れ

BIOS/MBRUEFI/GPTのうち、より優れたUEFI/GPTを使う。


全体の流れ

1. インストール作業のための一時的な設定

2. パーティション分け

3. Linux自体のインストール

(chroot)

 4. 今後使う上で必要な設定

 5. 次回起動時のブートローダのインストールと設定

(exit)

6. 再起動

7. 諸設定

Arch Linuxのインストールメディアは牛肉屋を営む牛のようなものである。牛が仕入れて客に提供するのは牛肉だが、牛が自らを捌いて売るわけではない。同様に、Arch Linuxのインストールメディアはインターネット上からArch Linuxをダウンロードしてコンピュータにインストールするが、インストールメディアの中のArch Linuxがコピーされるわけではない。また、ユーザーは初めのうちは売る方の牛に指示を出すが、途中からは売られた方の牛を調理するようになる。この切り替えがchrootである。

インストール作業の中でテキストエディタviが登場する。慣れないうちは操作が大変なのでnanoを代わりに使ってもよい。ただ今後のためにviを使うことを推奨する。

以上のことを押さえた上でインストール作業に取り掛かる。

インストールメディアの入ったUSBメモリーを刺し、有線でインターネットに繋ぎ、PCの電源ボタンを押して即座にF2、Boot List Optionで[UEFI]、Boot Option #1で[UEFI: (USBメモリの名前)]を選択、Save Changes and Reset。表示される選択肢の中からArch Linux archiso x86_64 UEFI USBを選択して起動、Welcome to Arch Linux! に続いてコンソールに文字が流れる。

tty1でroot自動ログインが行われ、入力を受け付ける状態になる。

archiso login: root (automatic login)
root@archiso ~ # _

1. インストール作業のための一時的な設定

Dvorakユーザなのでloadkeysを使って切り替える。

# loadkeys dvorak-programmer.map

UEFIブートであることを確認する。

# ls /sys/firmware/efi

pingを使ってインターネット接続を一応確認する。

# ping -c3 www.google.com

2. パーティション分け

ディスク/dev/sdaのパーティションを決める。UEFIなのでgdiskまたはcgdiskを使う。私はどちらかというと対話型のgdiskの方が使いやすいように感じた。/dev/sdaはコマンドライン引数と標準入力のどちらでも指定できる。

# gdisk /dev/sda

コマンド入力を受け付けるようになる。

oで新しいパーティションテーブルを作成し、ここにパーティションを書いてゆく。

Command (? for help): o
This option deletes all partitions and creates a new protective MBR.
Proceed? (Y/N): Y

UEFIの場合まず/boot用のEFI System パーティション(ESP)を作る必要がある。Hex CodeとしてEF00を指定することでLinux filesystemではなくEFI systemとし、512M用意する。デフォルトのままのところは何も打たずにエンターを押せば良い。

Command (? for help): n
Partition number (1-128, default 1):
First sector (〜) or {+-}size{KMGTP}:
Last sector (〜) or {+-}size{KMGTP}: +512M
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): EF00
Changed type of partition to 'EFI System'

残りはすべて/用のrootパーティションとした。

Command (? for help): n
Partition number (1-128, default 2):
First sector (〜) or {+-}size{KMGTP}:
Last sector (〜) or {+-}size{KMGTP}:
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300):
Changed type of partition to 'Linux filesystem'

メモリが1GB以下だとswapも作ることが多いが、幸い4GB(十分!)もあったので作らなかった。swapを作るとしたらHex codeは8200になる。

そしてwriteで作成したパーティションテーブルをハードディスクに書き込む。

Command (? for help): w
Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING PARTITIONS!!
Do you want to proceed? (Y/N): Y

これで/dev/sda1(後の/boot)、/dev/sda2(後の/)の2つのパーティションが作られた。

ESPはF32でフォーマットする。

# mkfs.vfat -F32 /dev/sda1

他はext4でフォーマットする。

# mkfs.ext4 /dev/sda2

swapを作るならここでmkswap /dev/sdaXとswapon /dev/sdaXを行う。

/dev/sda2(後の/)を/mnt自体にマウントする。

# mount /dev/sda2 /mnt

/dev/sda1(後の/boot)は/mnt/bootを作ってマウントする。

# mkdir /mnt/boot
# mount /dev/sda1 /mnt/boot

3. Linux自体のインストール

viを起動してミラーリスト(どのサーバーから優先的に落とすか)を設定する。

# vi /etc/pacman.d/mirrorlist

日本在住の場合Japanを検索して一番上に持ってくる。キーボード操作としては「/Japan(エンター)」で検索、「2dd{p」でコピー&ペースト、「n」と「N」で次を検索&前を検索、終わったら「:wq(エンター)」で保存&終了。

pacstrapでLinuxをインストールする。

$ pacstrap -i /mnt base base-devel

エンターを押し続け、言われたものを全てインストールする。

fstabを生成する。

genfstab /mnt >> /mnt/etc/fstab

次回起動時に/mntが/、/mnt/bootが/bootになる。ここでfstabが書き込まれた/mnt/etc/fstabを確認する(cat /mnt/etc/fstab)と、/dev/sda2がext4で/に、/dev/sda1がvfatで/bootになるという趣旨の表があるはずだ。genfstabに-Uオプションを付けていると左の列がUUID=(数字とアルファベットの羅列)になる。

肉屋で牛肉を買い終えた。このあとは肉屋を出て、受け取った牛を調理する。

# arch-chroot /mnt

家に帰って牛肉を調理台に置いた。

4. 今後使う上で必要な設定

viで/etc/locale.genを開く。

# vi /etc/locale.gen

「/en_US」などとして「n」「N」を使いながら「# en_US.UTF-8 UTF-8」と書かれた行を探し、「0x」でコメントアウト。同様に「/ja_JP」などとして「# ja_JP.UTF-8 UTF-8」を探し、「0x」でコメントアウト。「:wq」で保存終了。そして変更を反映。

# locale-gen

locale.confで言語を設定する。

# echo LANG=en_US.UTF-8 > /etc/locale.conf
# export LANG=en_US.UTF-8


次回起動時のキーボードレイアウトとフォントを設定する。

# vi /etc/vconsole.conf

「i」で入力を開始、Escで入力を終了。一例:

KEYMAP=dvorak-programmer.map
FONT=Lat2-Terminus16

場所の設定。

# ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

時刻の設定。

# hwclock -u -w

ホスト名の設定。

# echo (ホスト名) > /etc/hostname

インターネット接続にはdhcpcdを使う。systemctlコマンドを使い、起動時にdhcpcdが自動でインターネットに接続するようにする。systemctlの後に続けてオプションを指定する:その場で立ち上げるにはstart、その場で止めるにはstopを使い、enable↔disableは起動したときに立ち上がるかどうかを選ぶ。次回起動時に繋がらなかった場合はまたsystemctlを使ってdhcpcdの状態を確認する。

# systemctl enable dhcpcd

パスワードを設定する。

# passwd

5. 次回起動時のブートローダのインストールと設定

Arch Linux自体の設定は完了した。現在Arch Linuxは起動した状態であり、ここからこのOSをずっと起動したままで使い続けることが可能である。いわば暖炉の中で薪が燃えている状態である。あなたはその火を使ってさまざまなことができる。

しかしあなたはまだマッチを持っていない。今燃えている火は、USBメモリのarchisoが点火してくれた火である。暖炉と薪は整ったが、マッチが無いことには火を付けることができない。

マッチとなるのはブートローダである。ブートローダには数種類あるが、今回はsystemd-boot(旧称gummiboot)を使う。インストール。

# bootctl --path=/boot install

この後いくつかの設定を行い、USBを抜いて再起動する。設定に失敗すると次の再起動ができなくなる。その場合は慌てずUSBを挿し直して起動し、/dev/sda1と/dev/sda2のマウントを上と同じように行い、ここからやり直す。

設定するのは/boot/loader/entriesディレクトリと/boot/loader/loader.confファイルの2つである。

まず/boot/loader/entriesに移動する。

# cd /boot/loader/entries

/boot/loader/entriesには起動モードの選択肢1つごとに1つのファイルを拡張子confで作る。arch.conf(普段)とarch-fallback.conf(Fallback、何かあったとき用)の2つを用意する。

ルートパーティション/dev/sda2の固有PARTUUIDを調べる。

# blkid -s PARTUUID -o value /dev/sda2

arch.confを編集して次のようにする。

title Arch Linux
linux /vmlinuz-linux
initrd /initramfs-linux.img
options root=PARTUUID=(PARTUUID) rw

PARTUUIDのところにblkidで調べたPARTUUIDを書く。長い文字列なので、メモするのが面倒ならblkidの最後に> arch.confと付け加えておくとPARTUUIDが表示される代わりにarch.confに出力される。

arch-fallback.confは

title Arch Linux Fallback
linux /vmlinuz-linux
initrd /initramfs-linux-fallback.img
options root=PARTUUID=(PARTUUID) rw

次に、既に存在する/boot/loader/loader.confを編集する。

# cd ..
# vi loader.conf

このファイルに一行ずつ設定を書いていく。必ず書かなければならないのは「editor no」の一行である。これを書かないと、「スペースキーを押しながら起動→eを押してinit=/bin/bashを書き加えた状態でArch Linuxを起動→パスワード無しでroot権限が手に入る!ガハハ!」なのでまずい*1。何も書かないとデフォルトでeditor yesになってしまう。

僕は、電源ボタンを押したら即座にArch Linux(fallbackでない方)が選択されて起動するようにした。そのためには「タイムアウト:0」と「デフォルト:Arch Linux」の2つを指定する。タイムアウトの方は何も書かなくても自動で0になる。書くとしたら「timeout 0」。デフォルトのローダは「default (ファイル名)」と書いて指定する。ファイル名のところにはさっき作ったファイル名arch.confの拡張子を除いた部分(つまりarch)を書く。「i」で入力開始、Escで入力終了。

最終的にloader.confの中身は次のようになる:

editor no
default arch

「:wq」で保存終了。他の設定例:

editor no
timeout 3
default arch

この場合,起動してから3秒間選択肢が表示され、その間に何もしないとArch Linuxが起動する。ここで表示される選択肢をtimeout 0でも表示するには起動時にスペースキーを押す。

6. 再起動

exitしてUSBを抜いて再起動。

# exit
# reboot

起動したら

login: root
password: (パスワード)

でログイン。

7. 諸設定

ユーザを追加する。

# useradd -m -G wheel (ユーザ名)

作ったユーザのパスワードを設定する。

# passwd (ユーザ名)

sudoersファイルを設定する。

# visudo

# %wheel ALL=(ALL) ALL と書かれた行を探し、コメントアウト

さらに一度入力したパスワードが一定時間有効なままなのが嫌なので

Defaults timestamp_timeout=0

を追記した。別に構わないならしなくて良い。

そうしたら今作ったユーザでログインし直す。

# exit

*1:やってみた。Welcome to Arch Linux! の代わりに[root@archlinux /]# と表示され、パスワードが変更できた。

X環境構築時に忘れてはいけない i3 のワナ集

ArchlinuxでX環境を構築。ウィンドウマネージャに i3 を使用。設定時に気をつけなければいけないことがたくさんあるのでまとめます。

Xをインストール

$ yay -S xorg-server xorg-apps xorg-xinit

この時点でstartxを打つとXが立ち上がり,即座に終了する。

i3をインストール

$ yay -S i3

ランチャー(ここではrofi),ターミナル(ここではterminator),フォント(ここではotf-ipafont)をインストール。フォントを扱うために必要なパッケージも依存で入る

$ yay -S rofi terminator otf-ipafont

xinitrcのコピー

$ cp /etc/X11/xinit/xinitrc .xinitrc

最後を消して exec i3 に書き換える。

ここからが問題。i3の設定ファイルを作る。

$ cd ~/.config/i3
$ vim config

最初に必ず

set $mod Mod4
bindsym $mod+Shift+e mode "exit"
mode "exit" {
    bindsym e exit
    bindsym Return mode "default"
}
mode "phony" {
    bindsym a workspace 1
}

と書く。exit するコマンドが無いとXを起動した後終了できなくなる。しかし bindsym $mod+Shift+e exit としてしまうと間違えてXを終了してしまうことがある。そのため mode "exit" を作ってミスを防ぐ。この場合 workspace に関する操作を含むコマンドを何か1つ書かないとi3-migrate-config-to-v4が実行されてエラーが出る。そのため mode "phony" の中にダミーのコマンドを書く。

ランチャーとして使う場合 rofi は次のように引数をつけて呼び出す。

$ rofi -show run

よって config には以下のように書く。

bindsym $mod+space exec rofi -show run

引数が無いと何も起こらない

modeを作る際の注意点。

set $resize resize [h:shrink width] [t:grow height] [n:shrink height] [s:grow width]
bindsym $mod+r mode "$resize"
mode "$resize" {
	bindsym Return mode "default"

	bindsym h resize shrink width 10 px or 10 ppt
	bindsym t resize grow height 10 px or 10 ppt
	bindsym n resize shrink height 10 px or 10 ppt
	bindsym s resize grow width 10 px or 10 ppt
}

必ず mode "default" に戻るためのコマンド(bindsym Return mode "default")を書き忘れないこと。これを忘れると mode "default" で定義されている exit コマンドが呼び出せず,終了できなくなる

workspace を作る際の注意点。例えば起動時にデフォルトとして workspace "1" が開かれる場合,workspace "1" へ移るコマンドを書き忘れないこと。これが無いと workspace "1" でアプリケーションを開いた後に他の workspace に移ると workspace "1" に戻れなくなる

デフォルトの config を使っているうちは問題無いが,書き換える人は以上のことに注意しなければいけない。

jackdaw開発日記(1) C言語でwavファイルを作る

jackdawというプロジェクトを開発しているのですが、後で自分の書いたコードを見返したときに分からなくならないように、いくつか書き残しておこうと思います。

今回はまずC言語でwavファイルを作る方法について書きます。

出力先ファイルを作る

まず空のファイルを作ります。open関数を使います。

#include <fcntl.h>

int open(const char *path, int oflag, ...);

第1引数 path にはファイル名を指定します。新しくファイルを作り、かつこの後プログラム内でファイルに対する読み込みと書き込みを行うので、第2引数 oflag には O_CREAT | O_RDWR を指定します(同様にファイルを作るcreat関数がありますが、O_RDWR の代わりに O_WRONLY が指定されてしまうため使えません)。第2引数 oflag が O_CREAT を含むとき、第3引数でファイルのアクセス権限を指定することができます。後でwavファイルを再生したり削除したりできるように S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH としましょう。返り値のファイルディスクリプタはこの後使うので変数に格納しておきます。エラーの場合 -1 が返ります。

ファイルを閉じるときにはclose関数を使います。

#include <unistd.h>

int close(int fildes);

第1引数 fildes にはファイルディスクリプタを渡します。

ここまでをまとめると以下のようになります。

#include <fcntl.h>
#include <unistd.h>

int main(void){
    char *out_filename = "a.wav"; //ファイル名は今のところこうしておきます
    int out = open(out_filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(out == -1) return -1;
    close(out);
}

空のファイルが作られます。ls -lで-rw-r--r--となっていることを確認して下さい。

マップする

次に、作ったファイルに対して書き込みを行う準備をします。このためには事前にファイルの大きさを知っておく必要があります。単位はバイトです。ビット深度/8 × チャンネル数 × サンプリング周波数 × 曲全体の秒数 + ヘッダサイズで計算できます。ヘッダサイズはこの後44バイトになります。

int samplerate;
short bitdepth, channel;
double length;
int filesize = bitdepth / 8 * channel * samplerate * length + 44;

現在ファイルは空であるため、中身を表すための領域がディスク上に確保されていません。まずこれを確保する必要があります。ftruncate関数を使います。

#include <unistd.h>

int ftruncate(int fildes, off_t length);

第1引数 fildes にファイルディスクリプタ、第2引数 length にファイルサイズを渡します。成功すると0、失敗すると-1が返ります。

次に、データを書き込むためにファイルをマップします。mmap関数を使います。

#include <sys/mman.h>

void *mmap(void *addr, size_t len, int prot, int flags, int fileds, off_t off);

第1引数 addr にはNULL、第2引数 len にはファイルサイズ、第3引数 prot には PROT_WRITE、第4引数 flags にはMAP_SHARED、第5引数 fildesにはファイルディスクリプタ、第6引数には 0 を渡します。返り値はマップした領域のアドレスです。何らかのポインタ型変数に格納します。今回はビット深度に合わせてshort *型にします。エラーの場合 MAP_FAILED が返ります。

ここまでをまとめると以下のようになります。サンプリング周波数は44100、ビット深度は16、チャンネルは2(ステレオ)とし、秒数は今のところ3秒にしておきます。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(void){
    char *out_filename = "a.wav";
    int out = open(out_filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(out == -1) return -1;

    int samplerate = 44100;
    short bitdepth = 16, channel = 2;
    double length = 3.;
    int filesize = bitdepth / 8 * channel * samplerate * length + 44;

    if(ftruncate(out, filesize) == -1) return -1;

    short *map = mmap(NULL, filesize, PROT_WRITE, MAP_SHARED, out, 0);
    if(map == MAP_FAILED) return -1;

    /* ここでデータを書き込む */

    close(out);
    munmap(map, filesize);
}

これで、map の参照先に代入した値がそのまま出力ファイルに書き込まれるようになります。

ヘッダーを書き込む

ヘッダーの中身はググると出てきます(私はwav ファイルフォーマットを見ました)
初めの4バイトは文字列"RIFF"
次の4バイトはファイルサイズ(filesize - 8)
次の4バイトは文字列"WAVE"
次の4バイトは文字列"fmt "
次の4バイトはヘッダーサイズ(16)
次の2バイトは波形フォーマット(1でいいらしい)
次の2バイトはチャンネル数(channel)
次の4バイトはサンプリング周波数(samplerate)
次の4バイトはデータ速度(samplerate * bitdepth / 8 * channel)
次の2バイトはブロックサイズ(bitdepth / 8 * channel)
次の2バイトはビット深度(bitdepth)
次の4バイトは文字列"data"
次の4バイトはデータサイズ(filesize - 44)
文字列の書き込みにはstring.hのmemcpy関数を使います。

memcpy(map, "RIFF", 4);
((unsigned int *)map)[1] = filesize - 8;
memcpy(map + 4, "WAVEfmt ", 8);
((unsigned int *)map)[4] = 16;
((unsigned short *)map)[10] = 1;
((unsigned short *)map)[11] = channel;
((unsigned int *)map)[6] = samplerate;
((unsigned int *)map)[7] = samplerate * bitdepth / 8 * channel;
((unsigned short *)map)[16] = channel * bitdepth / 8;
((unsigned short *)map)[17] = bitdepth;
memcpy(map + 18, "data", 4);
((unsigned int *)map)[10] = filesize - 44;

中身を書き込む

ここでは例として、440Hzの正弦波を書き込むことにします。math.hのsin関数を使います。コンパイル時に-lmを付けるのを忘れないで下さい。

for(int i = 0; i < samplerate * length; ++i)
    map[i * 2 + 22] =
    map[i * 2 + 23] =
        sin(2 * M_PI * 440 * i / samplerate) * (1 << 15);

プログラム全体は以下のようになります。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <math.h>

int main(void){
    char *out_filename = "a.wav";
    int out = open(out_filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(out == -1) return -1;

    int samplerate = 44100;
    short bitdepth = 16, channel = 2;
    double length = 3.;
    int filesize = bitdepth / 8 * channel * samplerate * length + 44;

    if(ftruncate(out, filesize) == -1) return -1;

    short *map = mmap(NULL, filesize, PROT_WRITE, MAP_SHARED, out, 0);
    if(map == MAP_FAILED) return -1;

    memcpy(map, "RIFF", 4);
    ((unsigned int *)map)[1] = filesize - 8;
    memcpy(map + 4, "WAVEfmt ", 8);
    ((unsigned int *)map)[4] = 16;
    ((unsigned short *)map)[10] = 1;
    ((unsigned short *)map)[11] = channel;
    ((unsigned int *)map)[6] = samplerate;
    ((unsigned int *)map)[7] = samplerate * bitdepth / 8 * channel;
    ((unsigned short *)map)[16] = channel * bitdepth / 8;
    ((unsigned short *)map)[17] = bitdepth;
    memcpy(map + 18, "data", 4);
    ((unsigned int *)map)[10] = filesize - 44;

    for(int i = 0; i < samplerate * length; ++i) map[i * 2 + 22] = map[i * 2 + 23] = sin(2 * M_PI * 440 * i / samplerate) * (1 << 15);

    close(out);
    munmap(map, filesize);
}

実行すると440Hzの正弦波が3秒間流れるwavファイルが作られます。

今回はここまでです。
この記事があなたの役に立てば幸いです。

関数を作る

関数を実行時に作る

今回は関数を作ってみましょう。作るといっても、コンパイル時に作るのではありません。実行時に作ります。

全体の流れとしては、「①関数を置く領域を作る」→「②そこに処理を書き込む」→「③呼び出す」となります。今回は②のところで「4バイト整数を受け取り、これに100を足した数を返す」とすることにしましょう。

①関数を置く領域を作る

変数の型には、「int」や「double」や「char *」などの「1つの値を保管するもの」と、「int[]」や「char[]」や構造体などの「複数の値を保管するもの」があります。関数のこなす処理はそのどちらで表されるしょうか。もちろん、複雑な処理が4バイトや8バイトで表されるわけがありませんから、後者になります。関数はたくさんの小さな処理の積み重ねでできていて、1つ1つの処理は1〜7バイト程度で表されます。関数を作るときには、これらを連続したデータ領域に書き込む必要があります。

ここで注意しなければいけないことがあります。普段連続したデータ領域を作るときには配列を作ったりmalloc/calloc関数を使ったりしますが、関数の処理を書き込む領域をこのやり方で確保しても、呼び出して実行することができません(※環境による)。ハッキング防止のためにOSが実行禁止にしているからです。

そこで、別のやり方でデータ領域を確保する必要があります。mmap関数というものを使います。

mmap関数は、malloc関数を使ってある程度の大きさのデータ領域を要求したときに内部的に呼び出される関数です。簡単に言うと、動的にデータ領域を確保する手段の1つということです。

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);

mmap関数の第3引数protで、そのデータ領域に対するどんなアクセスの仕方を許可するか指定できます。PROT_READは読み出しの許可、PROT_WRITEは書き込みの許可、PROT_EXECは実行の許可です。mallocを使うとこのPROT_EXECが指定されないため、実行できないのです。PROT_READ、PROT_WRITE、PROT_EXECのうち複数を指定したいときは | でビット和をとります。何も指定しないときはPROT_NONEとします(データにアクセスできなくなります)。

さて、今回は「関数の処理を書き込む」と「実行する」の両方が必要ですから第3引数protには「PROT_WRITE | PROT_EXEC」を指定します。

また、第4引数では実際にどの場所にメモリ領域を確保するのか指定します。例えばここで"hoge"という名前のファイルを作ってそのディスクリプタを渡すと、書き込んだ内容がhogeに出力されるのですが、今回は関数を作って使うだけでその中身をファイルに残しておく必要が無いので /dev/zero を使います。open("/dev/zero", O_RDONLY);の返り値を渡せばよいです。
2018/12/04追記:O_RDWRとしていましたがO_RDONLYで良いようです。

第1引数addrはNULL。第2引数lenは確保するバイト数。第4引数flagsはMAP_PRIVATE。第6引数offは0とします。

mmap関数は確保した領域の先頭アドレスを返すので、それをchar *型の変数に入れます。これで関数を置く領域が作れました。

また、malloc関数で確保した領域をfree関数で解放するのと同じように、mmap関数で確保した領域は使い終えた後にmunmap関数という関数を使って解放します。munmap関数の第1引数には確保する領域のアドレス、第2引数には確保していたバイト数を渡します。

ここまでの流れをまとめて書くと次のようになります。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(void){
    int fd, mapsize;
    char *map;
    fd = open("/dev/zero", O_RDONLY);
    mapsize = 100;
    map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    close(fd);
    munmap(map, mapsize);
    return 0;
}

確保する領域のサイズmapsizeは一時的に100としておきましたが、作る関数に合わせて後で変更します。

②処理を書き込む

確保した領域に実際に処理を書き込んで関数を作ります。

今回は4バイトの引数を1つ受け取り100を足して返す関数なので、「push %rbp」(55)「mov %rsp,%rbp」(48 89 e5)「mov %edi,-0x4(%rbp)」(89 7d fc)「mov -0x4(%rbp),%eax」(8b 45 fc)「add $0x64,%eax」(83 c0 64)「pop %rbp」(5d)「retq」(c3)となりますね。これをconst charの配列として用意しておいて、memcpy関数を使ってさっき作った領域にコピーします(memcpy関数を使うにはstring.hをインクルードします)。

const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
memcpy(map, data, sizeof data);

これで関数が作れました!!!

③呼び出す

mapはchar *型なので、そのままでは関数として呼び出すことができません。関数ポインタ型という型にキャストする必要があります。

fncという名前の関数ポインタ型の変数を宣言するときは次のようにします。

int型の引数を3つ受け取ってint型の値を返す関数へのポインタ:

int (*fnc)(int, int, int);

引数を受け取らずdouble *型の値を返す関数へのポインタ:

double *(*fnc)(void);

第1引数がchar[]型、第2引数がint型で、値を返さない関数へのポインタ:

void (*fnc)(char [], int);

今回はint型の引数を1つ受け取ってint型の値を返すので、int(*fnc)(int)となります。mapをキャストしてこれに代入します。

int (*fnc)(int);
fnc = (int(*)(int))map;

そのまま初期化するなら

int (*fnc)(int) = (int(*)(int))map;

のようになります。

(※mapを初めからvoid *型にしておけばキャスト演算子を書く必要は無くなります。またmapという変数を使わずに初めからfncだけを宣言しても構いません。今回は何をしているのか明確にするため書いています。)

さて、準備は整いました。いよいよ作った関数を呼び出します。

fnc(3);

試しに表示してみると、ちゃんと100が足されて返ってくるはずです。

printf("fnc(3) = %d\n", fnc(3));

出力:

fnc(3) = 103

ここまでをまとめると、コード全体は次のようになります:

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>

int main(void){
    int fd, mapsize;
    char *map;
    const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
    int (*fnc)(int);
    fd = open("/dev/zero", O_RDONLY);
    mapsize = sizeof data;
    map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    memcpy(map, data, sizeof data);
    fnc = (int(*)(int))map;
    printf("fnc(3) = %d\n", fnc(3));
    close(fd);
    munmap(map, mapsize);
    return 0;
}

ちょっと簡潔にしたバージョン:

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>

int main(void){
	const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
	int fd = open("/dev/zero", O_RDONLY);
	int (*fnc)(int) = mmap(NULL, sizeof data, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
	memcpy(fnc, data, sizeof data);
	printf("fnc(3) = %d\n", fnc(3));
	close(fd);
	munmap(fnc, sizeof data);
	return 0;
}

応用編

これを応用すると、「関数を返す関数」を作ることもできます。実質関数のカリー化(?)

「unsigned型変数nを受け取り、「2つのdouble型変数a, bを受け取り、aにbをn回かけた数を返す関数」を返す関数」を繰返2乗法で作ります。関数の宣言は次のようになります。

double (*make_pow(unsigned))(double, double);

C言語ってこんなこともできるんですね。でも考えてみれば結局返してるのは単なるポインタか。

実装は次のようになります。本当は確保したメモリを解放するための関数も別途作るべきなのですが面倒なので省きます。

double (*make_pow(unsigned n))(double, double){
    int fd = open("/dev/zero", O_RDONLY);
    const char data1[] = {0x55, 0x48, 0x89, 0xe5, 0xf2, 0x0f, 0x11, 0x45, 0xf8, 0xf2, 0x0f, 0x11, 0x4d, 0xf0}, data2[] = {0xf2, 0x0f, 0x10, 0x45, 0, 0xf2, 0x0f, 0x59, 0x45, 0xf0, 0xf2, 0x0f, 0x11, 0x45, 0}, data3[] = {0xf2, 0x0f, 0x10, 0x45, 0xf8, 0x5d, 0xc3};
    int mapsize = 6;
    for(unsigned i = n; i; i >>= 1) mapsize += (i & 1 ? 30 : 15);
    char *map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    memcpy(map, data1, sizeof data1);
    int cursor = sizeof data1;
    if(n & 1){
        memcpy(map + cursor, data2, sizeof data2);
        map[cursor + 4] = map[cursor + 14] = 0xf8;
        cursor += sizeof data2;
    }
    n >>= 1;
    while(n){
        memcpy(map + cursor, data2, sizeof data2);
        map[cursor + 4] = map[cursor + 14] = 0xf0;
        cursor += sizeof data2;
        if(n & 1){
            memcpy(map + cursor, data2, sizeof data2);
            map[cursor + 4] = map[cursor + 14] = 0xf8;
            cursor += sizeof data2;
        }
        n >>= 1;
    }
    memcpy(map + cursor, data3, sizeof data3);
    return (double(*)(double, double))map;
}

この関数に例えば11を渡すと、

double fnc(double a, double b){
    a *= b;
    b *= b;
    a *= b;
    b *= b;
    b *= b;
    a *= b;
    return a;
}

と同じ関数が返ります。実用性はそんなに無いんですがちょっと面白い。

この記事があなたの参考になれば幸いです。ではまた